WTF? Can Spring EL be a rules engine?

You read that right. Spring EL is not only a rules engine, but I’ve used it extensively in production environments.

Why use Spring EL as a rules engine?

Spring EL is lighter and cheaper to learn than other rules engines, and works better with functional programming than you might think!

My application in production

background

Before doing a financial system, financial system need to do settlement with the supplier, because we are in a weak position in the cooperation, leading to many suppliers cockiness requirements, we also need to satisfy plus the number of suppliers very much, and the rules of settlement changes very fast, so need to design is very flexible, if use hard-coded traditional way to do it, This can result in extremely complex code logic that needs to be changed very frequently.

To solve this problem, I decided to introduce a scripting engine to simplify development and reduce system complexity, and after reviewing it, I decided to use Spring EL.

How to do

First, let’s define a base object for settlement

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Order {
    private String userId;
    private Integer age;
    // Is it new
    private Boolean isNew;
    private LocalDate orderDate;
    private BigDecimal price;
}
Copy the code

If a supplier has a settlement rule that splits 80% of the order value, we can calculate it this way

    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);
    }
Copy the code

So we can figure out that we should settle 8 dollars, and of course this example is too simple, so let’s define a couple of complicated settlement rules, and see how we can do that

Rule 1: Settle new customers on Monday and Friday, the settlement amount is Price * 0.2

Rule 2: Settlement amount of (price-10) * 0.8 for those over 18 years of age and over 10 years of age

If we look at the above rule, it is actually divided into two steps, the first step is to filter out the non-settlement order, and the second step is to calculate the actual amount, so we can also divide the code into these two steps

Let me define a few test cases

	List<Order> orders = new ArrayList<Order>(){{
        Age 19, not new customer, order on Monday, amount 11
        add(new Order("Zhang".19.false,LocalDate.of(2020.11.9),new BigDecimal(11)));
        // Age 17, new customer, order on Friday, amount 19
        add(new Order("Bill".17.true,LocalDate.of(2020.11.13),new BigDecimal(19)));
        Age 17, not new customer, order on Saturday, amount 9
        add(new Order("Fifty".17.true,LocalDate.of(2020.11.14),new BigDecimal(9)));
    }};

Copy the code

The main logic is to filter out unwanted orders and then settle the remaining orders

    public static void settle(List
       
         orders, List
        
          filterRule, String settleRule, Map
         
           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));
    }
Copy the code

FunctionUtil. CacheFunction () is used to cache of Expression, because of the high costs of creating Expression, so the rules of the String as a key, Expression as the value cache, See this article on functional programming

Let’s do the code

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

        System.out.println("Settlement 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("Settlement rule2");
        List<String> filterRule2 = Arrays.asList("age > 18"."price > 10");
        String settleRule2 = "(price -10) * 0.8";
        settle(orders, filterRule2, settleRule2, expressionCache);
    }
Copy the code

The execution result

Settle RUle1, Li 4 3.8 settle Rule2, Zhang SAN 0.8Copy the code

As you can see, with Spring EL and functional programming, you can implement complex settlement logic only by writing rules.

Original is not easy, if reproduced please indicate the source, feel to you and help, trouble point a thumbs-up, thank you!