SpEL应用实战

文章介绍了资金管理平台如何通过策略模式处理不同渠道的账单请求,以及引入SpringExpressionLanguage(SpEL)实现动态参数配置,降低代码冗余和维护成本,提高系统灵活性和扩展性。
摘要由CSDN通过智能技术生成

一 背景
资金平台概述

为了监控集团各业务线的资金来源和去向,资金部需每天分析所有账户出金和入金情况。为此,我们提供了资金管理平台,该平台拥有账户收支流水和账单拉取等功能,以及现金流打标能力,为资金部提供更加精准的现金流分析。

需求场景

资金管理平台作为发起方,以账户维度请求支付系统下载渠道账单(不同渠道传参不同),解析流水落库后做现金流打标。

系统交互简图
在这里插入图片描述
抛出问题
上述需求中资金平台请求支付系统下载账单功能这一点,考虑到不同渠道的账户,请求传参不同,该场景如何做功能设计?
实现方

方案 1(简写):无脑堆 if else
缺点:每新增一个渠道,都要在原有代码基础上添加参数处理逻辑,导致代码臃肿,难以维护,难以支持系统的持续演进和扩展。违反开闭原则,修改会对原有功能产生影响,增加了引入错误的风险。
方案 2:策略模式优化
优点:符合开闭原则,新增渠道接入时,只需创建新的具体策略实现类并实现接口即可,无需修改原有代码,系统灵活性和可扩展性较好。

缺点:每接入一个新渠道,还是存在代码开发和部署的工作量,且随着渠道接入数量的增加,策略类数量增多,代码维护成本变高。

public interface IChannelApplyFileStrategy {
    /**
     * 渠道匹配策略
     *
     * @param instCode 渠道名
     * @return 是否匹配
     */
    boolean match(String instCode);

    /**
     * 入参组装
     *
     * @param instAccountNo 账户
     * @return 请求支付入参
     */
    FileBillReqDTO assembleReqData(String instAccountNo);
}

// 不同渠道具体策略类


```java
@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可以提供更大的灵活性和可配置性,使得应用程序的参数配置和逻辑处理更为动态和可扩展。它的强大表达能力和运行时求值特性可以在很多场景下发挥作用,简化开发和维护工作。

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 的结构,从根节点开始,逐级向下遍历并对每个节点进行求值。求值过程可能涉及递归操作,直到所有节点都被求值。

结果返回:表达式求值的结果作为最终结果返回给调用者。返回结果可以是任何类型,包括基本类型、对象、集合等

说明:每新增一个渠道接入时不需要进行代码开发,只需在配置表中维护关联关系。根据 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);
    }
}

Spring Boot 利用AOP解析SPEL,实现更强大的日志记录
SPEL表达式
SpEL 使用 #{…} 作为定界符 , 所有在大括号中的字符都将被认为是 SpEL , SpEL 为 bean 的属性进行动态赋值提供了便利;

通过 SpEL 可以实现:
通过 bean 的 id 对 bean 进行引用。
调用方式以及引用对象中的属性。
计算表达式的值
正则表达式的匹配。
在业务代码上的注解加上表达式

注意 #{#webLoginPojo.userName}这就是spel表达式
   /**
     * web用户登录
     * @param webLoginPojo 账号  密码  验证码
     * @param
     * @return
     */
    @OperLog(message = "登录:#{#webLoginPojo.userName}",operation = OperationType.LOGIN)
    @RequestMapping(value = "/webLogin")
    @ResponseBody
    public ResultInfo<Object> webLogin(@RequestBody WebLoginPojo webLoginPojo){
        try {
            System.out.println("欢迎登录");
            return new ResultInfo<Object>(ConnUtil.SUCCESS,"登录成功");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return new ResultInfo<Object>(ConnUtil.ERROR,"服务器异常");
    }
   /**
     * 解析SPEL
     * @param message
     * @param joinPoint
     * @return
     */
    private String executeTemplate(String message, ProceedingJoinPoint joinPoint)throws Exception{

        ExpressionParser parser = new SpelExpressionParser();

        LocalVariableTableParameterNameDiscoverer discoverer = new LocalVariableTableParameterNameDiscoverer();
        Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();

        String[] params = discoverer.getParameterNames(method);

        Object[] args = joinPoint.getArgs();

        EvaluationContext context = new StandardEvaluationContext();
        for (int len = 0; len < params.length; len++) {
            context.setVariable(params[len], args[len]);
        }
        return parser.parseExpression(message, new TemplateParserContext()).getValue(context, String.class);
    }

//获取操作

 OperLog myLog = method.getAnnotation(OperLog.class);
    if (myLog != null) {
        String message = myLog.message();
        String description = executeTemplate(message, proceedingJoinPoint);
        sysLog.setMessage(description);//保存日志备注
    }
  • 7
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值