一、布景

资金途径概述

为了监控集团各事务线的资金来源和去向,资金部需每天剖析一切账户出金和入金情况。为此,咱们供给了资金管理途径,该途径具有账户收支流水和账单拉取等功用,以及现金流打标才能,为资金部供给愈加精准的现金流剖析。

需求场景

资金管理途径作为建议方,以账户维度恳求付出体系下载途径账单(不同途径传参不同),解析流水落库后做现金流打标。

体系交互简图

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运用实战

三、SpEL运用实战

装备表规划

保护途径和其对应参数处理战略的相相联系:

途径表

SpEL运用实战

途径 API 表

SpEL运用实战

说明: 每新增一个途径接入时不需求进行代码开发,只需在装备表中保护相相联系。依据 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 列相对应)

SpEL运用实战

Excel Sample:

SpEL运用实战

五、总结

总的来说,SpEL 表达式言语具有动态性、灵敏性、可扩展性等长处。结合详细事务需求和体系规划,其可运用于很多体系场景:

  • Excel 解析:SpEL 能够用于解析 Excel 表格中的数据。能够运用 SpEL 表达式来指定需求解析的单元格、行、列等等,提取数据并运用相应的逻辑。这使得解析进程愈加灵敏和可扩展。
  • 规矩引擎:在运用规矩引擎时,SpEL 能够用于界说规矩条件和履行动作。经过 SpEL 表达式,能够动态地依据特定的条件对数据进行处理和决策。这使得规矩引擎能够依据实际情况在运行时进行灵敏的判别和决策。
  • 模板引擎:SpEL 能够用于填充模板数据。经过 SpEL 表达式,能够在模板中引证目标的特点、办法或函数。这使得模板引擎能够依据目标的特点动态地生成内容。
  • 装备文件解析:SpEL 能够用于解析装备文件中的动态值。经过 SpEL 表达式,能够在装备文件中引证其他特点或办法的值。这使得装备文件具有动态性,能够依据实际情况进行动态的装备和调整。
  • 验证规矩:在数据验证的场景中,SpEL 能够用于界说验证规矩。经过 SpEL 表达式,能够对数据进行杂乱的验证和处理。这使得验证进程愈加灵敏和可装备。

*文/金橙五

本文属得物技能原创,更多精彩文章请看:得物技能官网

未经得物技能答应严禁转载,不然依法追究法律责任!