关于java:SpEL应用实战

58次阅读

共计 9426 个字符,预计需要花费 24 分钟才能阅读完成。

一、背景

资金平台概述

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

需要场景

资金治理平台作为发起方,以账户维度申请领取零碎下载渠道账单 (不同渠道传参不同),解析流水落库后做现金流打标。

零碎交互简图

抛出问题

上述需要中资金平台申请领取零碎下载账单性能这一点,思考到不同渠道的账户,申请传参不同,该场景如何做功能设计?

实现计划

计划 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 表达式,能够对数据进行简单的验证和解决。这使得验证过程更加灵便和可配置。

* 文 / 金橙五

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

未经得物技术许可严禁转载,否则依法追究法律责任!

正文完
 0