SpEL应用实战
一、背景
资金平台概述
为了监控集团各业务线的资金来源和去向,资金部需每天分析所有账户出金和入金情况。为此,我们提供了资金管理平台,该平台拥有账户收支流水和账单拉取等功能,以及现金流打标能力,为资金部提供更加精准的现金流分析。
需求场景
资金管理平台作为发起方,以账户维度请求支付系统下载渠道账单(不同渠道传参不同),解析流水落库后做现金流打标。
系统交互简图
抛出问题
上述需求中资金平台请求支付系统下载账单功能这一点,考虑到不同渠道的账户,请求传参不同,该场景如何做功能设计?
实现方案
方案 1(简写):无脑堆 if else
缺点:每新增一个渠道,都要在原有代码基础上添加参数处理逻辑,导致代码臃肿,难以维护,难以支持系统的持续演进和扩展。违反开闭原则,修改会对原有功能产生影响,增加了引入错误的风险。
/** * 资金系统请求支付系统下载渠道账单 * * @param instCode 渠道名 * @param instAccountNo 账户 * @return 同步结果 */ public String applyFileBill(String instCode, String instAccountNo) { // 不同渠道入参组装 FileBillReqDTO channelReq = new FileBillReqDTO(); if ("支付宝".equals(instCode)) { channelReq.setBusinessCode("ALIPAY_" + instAccountNo + "_BUSINESS"); channelReq.setPayTool(4); channelReq.setTransType(50); } else if ("微信".equals(instCode)) { channelReq.setBusinessCode("WX_" + instAccountNo); channelReq.setPayTool(3); channelReq.setTransType(13); } else if ("通联".equals(instCode)) { channelReq.setBusinessCode("TL_" + instAccountNo); channelReq.setPayTool(5); channelReq.setTransType(13); } // ... 可以继续添加其他渠道的处理逻辑 // 请求支付系统拉取账单文件,同步返回处理中,异步MQ通知下载结果 BaseResult<FileBillResDTO> result = cnRegionDataFetcher.applyFileBill(channelReq, "资金账单下载"); return "处理中"; }
方案 2:策略模式优化
优点:符合开闭原则,新增渠道接入时,只需创建新的具体策略实现类并实现接口即可,无需修改原有代码,系统灵活性和可扩展性较好。
缺点:每接入一个新渠道,还是存在代码开发和部署的工作量,且随着渠道接入数量的增加,策略类数量增多,代码维护成本变高。
// 定义策略接口 public interface IChannelApplyFileStrategy { /** * 渠道匹配策略 * * @param instCode 渠道名 * @return 是否匹配 */ boolean match(String instCode); /** * 入参组装 * * @param instAccountNo 账户 * @return 请求支付入参 */ FileBillReqDTO assembleReqData(String instAccountNo); } // 不同渠道具体策略类 @Component public class AlipayChannelApplyFileStrategy implements IChannelApplyFileStrategy { @Override public boolean match(String instCode) { return "支付宝".equals(instCode); } @Override public FileBillReqDTO assembleReqData(String instAccountNo) { FileBillReqDTO channelReq = new FileBillReqDTO(); channelReq.setBusinessCode("ALIPAY_" + instAccountNo + "_BUSINESS"); channelReq.setPayTool(4); channelReq.setTransType(50); return channelReq; } } @Component public class WechatChannelApplyFileStrategy implements IChannelApplyFileStrategy { @Override public boolean match(String instCode) { return "微信".equals(instCode); } @Override public FileBillReqDTO assembleReqData(String instAccountNo) { FileBillReqDTO channelReq = new FileBillReqDTO(); channelReq.setBusinessCode("WX_" + instAccountNo); channelReq.setPayTool(3); channelReq.setTransType(13); return channelReq; } } @Component public class TlbChannelApplyFileStrategy implements IChannelApplyFileStrategy { @Override public boolean match(String instCode) { return "通联".equals(instCode); } @Override public FileBillReqDTO assembleReqData(String instAccountNo) { FileBillReqDTO channelReq = new FileBillReqDTO(); channelReq.setBusinessCode("TL_" + instAccountNo); channelReq.setPayTool(5); channelReq.setTransType(13); return channelReq; } } // 调用类 @Component public class ChannelApplyFileClient { // IOC属性自动注入策略实现类集合 @Resource private List<IChannelApplyFileStrategy> iChannelApplyFileStrategies; @Resource private CNRegionDataFetcher cnRegionDataFetcher; public String applyFileBill(String instCode, String instAccountNo) { // 不同渠道入参组装 IChannelApplyFileStrategy strategy = iChannelApplyFileStrategies.stream().filter(item -> item.match(instCode)).findFirst().orElse(null); FileBillReqDTO channelReq = strategy.assembleReqData(instAccountNo); // 请求支付系统拉取账单文件,同步返回处理中,异步MQ通知下载结果 BaseResult<FileBillResDTO> result = cnRegionDataFetcher.applyFileBill(channelReq, "资金账单下载"); return "处理中"; } }
思考
上述两种设计似乎对参数处理能力的抽象力度还不够,是否能将其抽象为一个领域能力,以实现参数处理的动态化或可配置化,而不再依赖于硬编码的参数处理逻辑。
基于这个设计思路,可以进行以下步骤:
-
定义领域模型:确定需要处理的领域对象和领域操作。在这个场景中,领域对象表示不同渠道,领域操作表示参数处理和接口调用。
-
创建配置表:设计一个配置表,用于存储不同渠道和其对应的参数处理策略,该表可以包含渠道名称和策略标识等字段。
-
实现动态参数处理策略:根据配置表的信息,在系统运行时动态加载和执行参数处理策略。可以使用 SpEL 表达式解析和反射的方式来实现。
-
配置关联关系:通过配置表维护渠道和其对应参数处理策略的关联关系。在新增渠道时,只需要在配置表中添加一条新的配置记录,指明渠道名称和对应的策略标识。
通过以上设计思路,可以实现一个可配置的领域能力,提高代码的可维护性和扩展性,同时降低了开发和部署的工作量。配置表的维护也提供了更大的灵活性,使得系统可以快速响应和适应不同渠道的变化和需求。
方案选用
为了实现不同渠道参数的动态化配置,我们引入了 Spring 表达式语言(SpEL)。通过使用 SpEL,我们可以将参数处理逻辑表达为字符串表达式,并在运行时动态地解析和执行表达式,从而实现对不同渠道参数的处理。使用 SpEL 不仅提高了处理参数的灵活性和可配置性,还能更好地遵循面向对象设计原则和领域驱动设计思想,将参数处理视为一个具有独立职责的领域模型。
二、引入SpEL
介绍
SpEL 即 Spring 表达式语言,是一种强大的表达式语言,可以在运行时评估表达式并生成值。SpEL 最常用于 Spring Framework 中的注解和 XML 配置文件中的属性,也可以以编程方式在 Java 应用程序中使用。
SpEL的应用场景
-
动态参数配置:可以通过 SpEL 将应用程序中的各种参数配置化,例如配置文件中的数据库连接信息、业务规则等。通过动态配置,可以在运行时根据不同的环境或需求来进行灵活的参数设置。
-
运行时注入:使用SpEL,可以在运行时动态注入属性值,而不需要在编码时硬编码。这对于需要根据当前上下文动态调整属性值的场景非常有用。
-
条件判断与业务逻辑:SpEL支持复杂的条件判断和逻辑计算,可以方便地在运行时根据条件来执行特定的代码逻辑。例如,在权限控制中,可以使用SpEL进行资源和角色的动态授权判断。
- 表达式模板化:SpEL支持在表达式中使用模板语法,允许将一些常用的表达式作为模板,然后在运行时通过填充不同的值来生成最终的表达式。这使得表达式的复用和动态生成更加方便。
总的来说,SpEL可以提供更大的灵活性和可配置性,使得应用程序的参数配置和逻辑处理更为动态和可扩展。它的强大表达能力和运行时求值特性可以在很多场景下发挥作用,简化开发和维护工作。
简单举例
/** * 验证数字是否大于10 * * @param number 数字 * @return 结果 */ public String spELSample(int number) { // 创建ExpressionParser对象,用于解析SpEL表达式 ExpressionParser parser = new SpelExpressionParser(); String expressionStr = "#number > 10 ? 'true' : 'false'"; Expression expression = parser.parseExpression(expressionStr); // 创建EvaluationContext对象,用于设置参数值 StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("number", number); // 求解表达式,获取结果 return expression.getValue(context, String.class); }
处理过程分析
给定一个字符串最终解析成一个值,这中间至少经历:字符串->语法分析->生成表达式对象->添加执行上下文->执行此表达式对象->返回结果。
关于 SpEL 的几个概念:
-
表达式(“干什么”):SpEL 的核心,所以表达式语言都是围绕表达式进行的。
-
解析器(“谁来干”):用于将字符串表达式解析为表达式对象。
-
上下文(“在哪干”):表达式对象执行的环境,该环境可能定义变量、定义自定义函数、提供类型转换等等。
-
Root 根对象及活动上下文对象(“对谁干”):Root 根对象是默认的活动上下文对象,活动上下文对象表示了当前表达式操作的对象。
处理流程:
-
表达式解析:首先,SpEL 对表达式进行解析,将其转换为内部表示形式即抽象语法树(AST)或者其他形式的中间表示。
-
上下文设置:在表达式求值之前,需要设置上下文信息。上下文可以是一个对象,它包含了表达式中要引用的变量和方法。通过将上下文对象传递给表达式求值引擎,表达式可以访问并操作上下文中的数据。
-
表达式求值:一旦表达式被解析和上下文设置完成,SpEL 开始求值表达式。求值过程遵循 AST 的结构,从根节点开始,逐级向下遍历并对每个节点进行求值。求值过程可能涉及递归操作,直到所有节点都被求值。
-
结果返回:表达式求值的结果作为最终结果返回给调用者。返回结果可以是任何类型,包括基本类型、对象、集合等。
三、SpEL应用实战
配置表设计
维护渠道和其对应参数处理策略的关联关系:
渠道表
渠道 API 表
说明: 每新增一个渠道接入时不需要进行代码开发,只需在配置表中维护关联关系。根据 inst_code 匹配对应策略标识 channel_code,根据策略标识找到具体参数处理策略表达式。
实现动态参数处理策略
// 定义解析工具类 @Slf4j @Service @CacheConfig(cacheNames = CacheNames.EXPRESSION) public class ExpressionUtil { private final ExpressionParser expressionParser = new SpelExpressionParser(); // 创建上下文对象,设置自定义变量、自定义函数 public StandardEvaluationContext createContext(String instAccountNo){ StandardEvaluationContext context = new StandardEvaluationContext(); context.setVariable("instAccountNo", instAccountNo); // 注册自定义函数 this.registryFunction(context); return context; } // 注册自定义函数 private void registryFunction(StandardEvaluationContext context) { try { context.addPropertyAccessor(new MapAccessor()); context.registerFunction("yuanToCent", ExpressionHelper.class.getDeclaredMethod("yuanToCent", String.class)); context.registerFunction("substringBefore", StringUtils.class.getDeclaredMethod("substringBefore",String.class,String.class)); } catch (Exception e) { log.info("SpEL函数注册失败:", e); } } // 开启缓存,使用解析器解析表达式,返回表达式对象 @Cacheable(key="'getExpressionWithCache:'+#cacheKey", unless = "#result == null") public Expression getExpressionWithCache(String cacheKey, String expressionString) { try { return expressionParser.parseExpression(expressionString); } catch (Exception e) { log.error("SpEL表达式解析异常,表达式:[{}]", expressionString, e); throw new BizException(ReturnCode.EXCEPTION.getCode(),String.format("SpEL表达式解析异常:[%s]",expressionString),e); } } } // 定义解析类: @Slf4j @Service public class ExpressionService { @Resource private ExpressionUtil expressionUtil; public FileBillReqDTO transform(ChannelEntity channel, String instAccountNo) throws Exception { // 获取上下文对象(变量设置、函数设置) StandardEvaluationContext context = expressionUtil.createContext(instAccountNo); // 获取支付请求类对象 FileBillReqDTO target = ClassHelper.newInstance(FileBillReqDTO.class); // t_channel_api表配置的api映射表达式 for (ChannelApiEntity api : channel.getApis()) { // 通过反射获取FileBillReqDTO类属性名对象 Field field = ReflectionUtils.findField(FileBillReqDTO.class, api.getFieldCode()); // 表达式 String expressionString = api.getFieldExpression(); // 开启缓存,使用解析器解析表达式,返回表达式对象 Expression expression = expressionUtil.getExpressionWithCache(api.fieldExpressionKey(), expressionString); // 通过表达式对象获取解析后的结果值 Object value = expression.getValue(context, FileBillReqDTO.class); // 将结果通过反射赋值给FileBillReqDTO对象中指定属性字段 field.setAccessible(true); field.set(target, value); } // 返回解析赋值后的完整对象 return target; } } // 调用类 @Component public class ChannelApplyFileClient { @Resource private CNRegionDataFetcher cnRegionDataFetcher; @Resource private ExpressionService expressionService; @Resource private ChannelRepository channelRepository; public String applyFileBill(String instCode, String instAccountNo) { // 根据渠道码查询t_channel、t_channel_api表,返回ChannelEntity对象 ChannelEntity channel = channelRepository.findByInstCode(instCode); // 通过SpEL解析t_channel_api表中表达式,并将值赋值给对应属性中,返回完整请求对象 FileBillReqDTO channelReq = expressionService.transform(channel, instAccountNo); // 请求支付系统拉取账单文件,同步返回处理中,异步MQ通知下载结果 BaseResult<FileBillResDTO> result = cnRegionDataFetcher.applyFileBill(channelReq, "资金账单下载"); return "处理中"; } }
优点:通过领域能力抽象和 SpEL 的运用,实现参数处理的动态化或可配置化,不再依赖于硬编码的参数处理逻辑,提高代码的可维护性和扩展性,同时降低了开发和部署的工作量,更好地遵循面向对象设计原则和领域驱动设计思想,成为一个具有独立职责的领域模型。
四、扩展-其他应用-Excel解析
需求
资金平台需从不同的渠道下载账单,并对账单进行解析,解析后的数据落入流水表。注意不同渠道的账单的头字段和格式存在差异。
方案
传统的方式中,解析 Excel 通常需要通过创建实体类来映射 Excel 的结构和数据。每个实体类代表一个 Excel 行或列,需要手动编写代码来将 Excel 数据解析为相应的实体对象。
而使用 SpEL 方式解析 Excel 则具有更加动态和灵活的特性,避免了显式创建和维护大量的实体类。以下是使用 SpEL 方式动态解析 Excel 的一般步骤:
-
使用 Apache POI 等工具读取 Excel 数据表。
-
根据配置表,将 Excel 中的列与 SpEL 表达式进行关联。
-
使用 SpEL 解析器,在运行时解析这些 SpEL 表达式。
-
将解析后的结果做数据清洗后落表,应用于现金流打标业务。
配置表中维护的关联关系:(表达式中 #source.column 变量表示列与 Excel Sample 列相对应)
Excel Sample:
五、总结
总的来说,SpEL 表达式语言具备动态性、灵活性、可扩展性等优点。结合具体业务需求和系统设计,其可应用于很多系统场景:
-
Excel 解析:SpEL 可以用于解析 Excel 表格中的数据。可以使用 SpEL 表达式来指定需要解析的单元格、行、列等等,提取数据并应用相应的逻辑。这使得解析过程更加灵活和可扩展。
-
规则引擎:在使用规则引擎时,SpEL 可以用于定义规则条件和执行动作。通过 SpEL 表达式,可以动态地根据特定的条件对数据进行处理和决策。这使得规则引擎可以根据实际情况在运行时进行灵活的判断和决策。
-
模板引擎:SpEL 可以用于填充模板数据。通过 SpEL 表达式,可以在模板中引用对象的属性、方法或函数。这使得模板引擎可以根据对象的属性动态地生成内容。
-
配置文件解析:SpEL 可以用于解析配置文件中的动态值。通过 SpEL 表达式,可以在配置文件中引用其他属性或方法的值。这使得配置文件具备动态性,可以根据实际情况进行动态的配置和调整。
-
验证规则:在数据验证的场景中,SpEL 可以用于定义验证规则。通过 SpEL 表达式,可以对数据进行复杂的验证和处理。这使得验证过程更加灵活和可配置。
*文/金橙五
本文属得物技术原创,更多精彩文章请看:得物技术官网
未经得物技术许可严禁转载,否则依法追究法律责任!

低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
NGINX Agent 的可观测性和远程配置
原文作者:Prabhat Dixit of F5 原文链接:NGINX Agent 的可观测性和远程配置 转载来源:NGINX 开源社区 NGINX 唯一中文官方社区 ,尽在nginx.org.cn 在 NGINX Sprint 2022 大会上,我们承诺实现 NGINX 开源版项目管理和社区互动方式的现代化。为此,我们宣布后续将推出 NGINX Agent — 该守护进程会作为伴侣软件来管理各个 NGINX 部署,提供可观测性和配置 API。今天,我们非常自豪能够在 Apache 2 许可下推出 NGINX Agent,成功兑现了这一承诺。 F5 NGINX 致力于构建一个涵盖应用部署和管理方方面面的生态系统。NGINX Agent 通过为开发和平台运维团队提供细粒度控制以及用于配置、监控和管理 NGINX 实例的附加功能,在这一愿景中扮演了重要角色。 NGINX Agent 有何作用? NGINX Agent 是一个轻量级守护进程,可与您的 NGINX 开源版或 NGINX Plus 实例一同部署。值得注意的是,NGINX Agent 具备一些 NGINX 开源版没有的功能: NG...
- 下一篇
BentoML:如何使用 JuiceFS 加速大模型加载
BentoML 是一个开源的大语言模型(LLM) AI 应用的开发框架和部署工具,致力于为开发者提供最简单的构建大语言模型 AI 应用的能力,其开源产品已经支持全球数千家企业和组织的核心 AI 应用。 当 BentoML 在 Serverless 环境中部署模型时,其中一个主要挑战是冷启动慢,尤其在部署大型语言模型时更为明显。由于这些模型体积庞大,启动和初始化过程耗时很长。此外,由于 Image Registry 的带宽较小,会让大体积的 Container Image 进一步加剧冷启动缓慢的问题。为了解决这一问题,BentoML引入了JuiceFS。 JuiceFS 的 POSIX 兼容性和数据分块使我们能够按需读取数据,读取性能接近 S3 能提供的性能 的上限,有效解决了大型模型在 Serverless 环境中冷启动缓慢的问题。**使用 JuiceFS 后,模型加载速度由原来的 20 多分钟缩短至几分钟。**在实施 JuiceFS 的过程中,我们发现实际模型文件的读取速度与预期基准测试速度存在差异。通过一系列优化措施,如改进数据缓存策略和优化读取算法,我们成功解决了这些挑战。在本文...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS7,CentOS8安装Elasticsearch6.8.6
- CentOS关闭SELinux安全模块
- Docker快速安装Oracle11G,搭建oracle11g学习环境
- Docker使用Oracle官方镜像安装(12C,18C,19C)
- Springboot2将连接池hikari替换为druid,体验最强大的数据库连接池
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- CentOS6,CentOS7官方镜像安装Oracle11G
- Docker安装Oracle12C,快速搭建Oracle学习环境
- SpringBoot2整合MyBatis,连接MySql数据库做增删改查操作