基于Spring EL的规则引擎

·  阅读 3437

WTF?Spring EL还能做规则引擎?

你没有看错,Spring EL不仅能做规则引擎,我还在生产环境大范围的使用了。

为什么要用Spring EL做规则引擎?

相对于别的规则引擎,Spring EL更加轻量级,学习成本更低,与函数式编程配合表现可能超乎你的想象!

我在生产中的应用

背景

之前做过一个财务系统,财务系统需要跟供应商做结算,由于我方在合作中处于弱势地位,导致供应商很多非分的要求我们也需要去满足,再加上供应商的数量非常多,并且结算规则的变化非常的快,因此需要设计的非常的灵活,如果用传统硬编码的方式去做,可能导致代码逻辑异常复杂,而且需要改动得非常频繁。

为了解决这个问题,我打算引入脚本引擎去简化开发,降低系统复杂度,在考察过后,最终决定用Spring EL。

如何做

首先,我们先来定义一个结算的基础对象

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private String userId;
    private Integer age;
    //是否是新客
    private Boolean isNew;
    private LocalDate orderDate;
    private BigDecimal price;
}
复制代码

如果有一个供应商有一条结算规则是分成订单金额的80%,我们可以这样计算

    public static void main(String[] args){
        ExpressionParser expressionParser = new SpelExpressionParser();
        Expression expression = expressionParser.parseExpression("price * 0.8");
        Order order = new Order();
        order.setPrice(BigDecimal.TEN);
        BigDecimal value = expression.getValue(order, BigDecimal.class);
        System.out.println(value);
    }
复制代码

这样就能算出应该结算8元,当然这个例子太简单了,我们定义几个复杂的结算规则,看看应该如何去做

规则1:周一周五新客结算,结算金额为 price * 0.2

规则2: 年龄大于18岁、金额大于10的结算,结算金额为(price - 10) * 0.8

我们来看上面的规则,实际上是分成了两步,第一步是过滤掉不结算的订单,第二步是真正的计算金额,因此代码我们也可以分这两步走

我先定义几个测试用例

	List<Order> orders = new ArrayList<Order>(){{
        //年龄19,不是新客,周一下单,金额11
        add(new Order("张三",19,false,LocalDate.of(2020,11,9),new BigDecimal(11)));
        //年龄17,是新客,周五下单,金额19
        add(new Order("李四",17,true,LocalDate.of(2020,11,13),new BigDecimal(19)));
        //年龄17,不是新客,周六下单,金额9
        add(new Order("王五",17,true,LocalDate.of(2020,11,14),new BigDecimal(9)));
    }};

复制代码

主要逻辑如下,先过滤掉不需要的订单,然后对剩下的订单进行结算

    public static void settle(List<Order> orders, List<String> filterRule,
                              String settleRule, Map<String, Expression> expressionCache) {
        Stream<Order> stream = orders.stream();
        for (String rule : filterRule) {
            Expression expression = FunctionUtil
                    .cacheFunction(s -> expressionParser.parseExpression(s), rule, expressionCache);
            stream = filter(stream, expression);
        }
        Expression expression = FunctionUtil
                .cacheFunction(s -> expressionParser.parseExpression(s), settleRule, expressionCache);
        stream.forEach(o -> System.out.println(o.getUserId() + expression.getValue(o)));
    }

    public static <T> Stream<T> filter(Stream<T> stream, Expression expression) {
        return stream.filter(s -> expression.getValue(s, Boolean.class));
    }
复制代码

FunctionUtil.cacheFunction()的作用是对Expression进行缓存,因为创建Expression的代价比较高,因此把String规则作为key,Expression作为value 缓存一下,关于函数式编程可以看这篇

执行一下代码

    public static void main(String[] args) {
        Map<String, Expression> expressionCache = new HashMap<String, Expression>();

        System.out.println("结算rule1");
        List<String> filterRule1 =
                Arrays.asList("orderDate.getDayOfWeek().getValue() == 1 || orderDate.getDayOfWeek().getValue() == 5", "isNew");
        String settleRule1 = "price * 0.2";
        settle(orders, filterRule1, settleRule1, expressionCache);

        System.out.println("结算rule2");
        List<String> filterRule2 = Arrays.asList("age > 18", "price > 10");
        String settleRule2 = "(price - 10) * 0.8";
        settle(orders, filterRule2, settleRule2, expressionCache);
    }
复制代码

执行结果

结算rule1
李四3.8
结算rule2
张三0.8
复制代码

可以看到,通过Spring EL和函数式编程,我们只需要编写规则就可以实现复杂的结算逻辑。

原创不易,如果转载请注明出处,觉得对你又帮助,麻烦点个赞,谢谢!

分类:
后端
标签: