为了监控集团各业务线的资金来源和去向,资金部需每天分析所有账户出金和入金情况。为此,我们提供了资金管理平台,该平台拥有账户收支流水和账单拉取等功能,以及现金流打标能力,为资金部提供更加精准的现金流分析。
资金管理平台作为发起方,以账户维度请求支付系统下载渠道账单(不同渠道传参不同),解析流水落库后做现金流打标。
上述需求中资金平台请求支付系统下载账单功能这一点,考虑到不同渠道的账户,请求传参不同,该场景如何做功能设计?
方案 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 "处理中";
}
}
上述两种设计似乎对参数处理能力的抽象力度还不够,是否能将其抽象为一个领域能力,以实现参数处理的动态化或可配置化,而不再依赖于硬编码的参数处理逻辑。
基于这个设计思路,可以进行以下步骤:
通过以上设计思路,可以实现一个可配置的领域能力,提高代码的可维护性和扩展性,同时降低了开发和部署的工作量。配置表的维护也提供了更大的灵活性,使得系统可以快速响应和适应不同渠道的变化和需求。
为了实现不同渠道参数的动态化配置,我们引入了 Spring 表达式语言(SpEL)。通过使用 SpEL,我们可以将参数处理逻辑表达为字符串表达式,并在运行时动态地解析和执行表达式,从而实现对不同渠道参数的处理。使用 SpEL 不仅提高了处理参数的灵活性和可配置性,还能更好地遵循面向对象设计原则和领域驱动设计思想,将参数处理视为一个具有独立职责的领域模型。
SpEL 即 Spring 表达式语言,是一种强大的表达式语言,可以在运行时评估表达式并生成值。SpEL 最常用于 Spring Framework 中的注解和 XML 配置文件中的属性,也可以以编程方式在 Java 应用程序中使用。
总的来说,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 的几个概念:
处理流程:
维护渠道和其对应参数处理策略的关联关系:
渠道表
渠道 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 数据解析为相应的实体对象。
而使用 SpEL 方式解析 Excel 则具有更加动态和灵活的特性,避免了显式创建和维护大量的实体类。以下是使用 SpEL 方式动态解析 Excel 的一般步骤:
配置表中维护的关联关系:(表达式中 #source.column 变量表示列与 Excel Sample 列相对应)
Excel Sample:
总的来说,SpEL 表达式语言具备动态性、灵活性、可扩展性等优点。结合具体业务需求和系统设计,其可应用于很多系统场景:
*文/金橙五
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。