使用Java/Spring的SpEL实现简单的规则引擎

在本文中,我们探讨了如何从头开始构建一个简单的基于 Java 的规则引擎。我们从基于 SpEL 的引擎开始,它在运行时评估动态规则,但维护和调试起来可能很困难,并且不提供编译时检查。接下来,我们探索了基于 POJO 的引擎,它提供了更多的类型安全性和清晰度;然而,它引入了更严格的规则体系。

在许多应用程序中,业务决策依赖于一组规则,这些规则评估数据以产生结果或得出结论。规则引擎支持动态定义和执行业务规则,同时将其与应用程序代码解耦,从而更轻松地维护、扩展和管理应用程序中复杂的决策逻辑。

规则引擎在执行以结构化格式定义的业务规则时,提供了关注点分离、灵活性和可重用性。虽然市面上有Drools或Easy Rules等成熟的库,以及其他可以处理复杂规则管理的库,但在某些情况下,更简单的方法就足够了。这使我们能够在满足特定业务需求的同时,将依赖关系保持在最低限度。

在本教程中,我们将使用两种方法构建一个简单的规则引擎。首先,使用 Spring 表达式语言 (SpEL) 来实现动态规则;然后,使用基于 POJO 的方法,这种方法可以提供更高的类型安全性。

设置
要使用 Spring 表达式语言,我们需要spring-expression依赖项:



    org.springframework
    spring-expression
    7.0.0-M7

我们现在将定义与规则引擎实现一起使用的模型类。

首先,让我们定义Customer类:


public class Customer {
    private String name;
    private int loyaltyPoints;
    private boolean firstOrder;
    // standard getters and setters
}
接下来,我们将定义Order类:

public class Order {
    private double amount;
    private Customer customer;
    // standard getters and setters
}

使用SpEL(Spring表达式语言)
Spring 表达式语言 (SpEL)是一种支持在运行时查询和更新对象图的表达式语言。

它支持多种操作符,易于使用,并且与 Spring 集成良好。

定义规则
我们将使用 SpEL 定义规则,它可以在配置文件中创建,也可以直接在代码本身中创建,它采用表达式及其描述:


public class SpelRule {
    private final String expression;
    private final String description;
    public SpelRule(String expression, String description) {
        this.expression = expression;
        this.description = description;
    }
    public boolean evaluate(Order order) {
        ExpressionParser parser = new SpelExpressionParser();
        StandardEvaluationContext context = new StandardEvaluationContext(order);
        context.setVariable("order", order);
        return parser.parseExpression(expression)
          .getValue(context, Boolean.class);
    }
    // standard getters and setters
}

评估方法接受一个Order 类型的参数并将其设置到上下文中。然后,系统根据定义的表达式评估上下文,以确定输入值是否满足规则。

测试
我们将使用 SpEL 引擎尝试以下两个简单规则:

忠诚度折扣:如果忠诚度积分大于 500,则客户有资格享受折扣
首单高额折扣:如果是首次购买且订单金额大于 500,则提供特别折扣
让我们定义并验证以下这些规则:


@Test
void whenLoyalCustomer_thenEligibleForDiscount() {
    Customer customer = new Customer("Bob", 730, false);
    Order order = new Order(200.0, customer);
    SpelRule rule = new SpelRule(
        "#order.customer.loyaltyPoints > 500",
        "Loyalty discount rule"
    );
    assertTrue(rule.evaluate(order));
}
@Test
void whenFirstOrderHighAmount_thenEligibleForSpecialDiscount() {
    Customer customer = new Customer("Bob", 0, true);
    Order order = new Order(800.0, customer);
    SpelRule approvalRule = new SpelRule(
        "#order.customer.firstOrder and #order.amount > 500",
        "First-time customer with high order gets special discount"
    );
    assertTrue(approvalRule.evaluate(order));
}
我们可以看到,已经添加了两个新的业务规则,并在提供的输入数据上进行了验证。

基于POJO的规则引擎
现在,让我们探索一个基于 Java 的规则引擎,与 SpEL 方法相比,它提供了更好的类型安全性。

定义规则
我们首先定义一个Rule接口,它将为所有业务规则提供契约。该契约类似于 SpEL 引擎的契约;引擎根据传递给规则的Order来评估表达式。evaluate方法提供了更高的类型安全性,因为它使用输入对象的属性来定义和执行规则:


public interface IRule {
    boolean evaluate(Order order);
    String description();
}
接下来,我们将根据业务需求定义规则。

首先,我们从“忠诚度折扣”规则开始:


public class LoyaltyDiscountRule implements IRule{
    @Override
    public boolean evaluate(Order order) {
        return order.getCustomer().getLoyaltyPoints() > 500;
    }
    @Override
    public String description() {
        return "Loyalty Discount Rule: Customer has more than 500 points";
    }
}
接下来我们定义“首单高额折扣”规则:

public class FirstOrderHighValueSpecialDiscountRule implements IRule {
    @Override
    public boolean evaluate(Order order) {
        return order.getCustomer()
          .isFirstOrder() && order.getAmount() > 500;
    }
    @Override
    public String description() {
        return "First Order Special Discount Rule: First Time customer with high value order";
    }
}
现在我们已经有了一些规则,让我们定义规则引擎,它将处理输入数据的这些规则:

public class RuleEngine {
    private final List rules;
    public RuleEngine(List rules) {
        this.rules = rules;
    }
    public List evaluate(Order order) {
        return rules.stream()
          .filter(rule -> rule.evaluate(order))
          .map(IRule::description)
          .collect(Collectors.toList());
    }
}
规则引擎有自己的评估方法,该方法接受输入参数。首先,它会评估所有规则,并最终返回满足条件的规则。

测试
我们已经根据业务需求定义了规则,并部署了规则引擎来处理这些规则。让我们运行一些测试来验证一下:


@Test
void whenTwoRulesTriggered_thenBothDescriptionsReturned() {
    Customer customer = new Customer("Max", 550, true);
    Order order = new Order(600.0, customer);
    RuleEngine engine = new RuleEngine(List.of(new LoyaltyDiscountRule(), new FirstOrderHighValueSpecialDiscountRule()));
    List results = engine.evaluate(order);
    assertEquals(2, results.size());
    assertTrue(results.contains("Loyalty Discount Rule: Customer has more than 500 points"));
    assertTrue(results.contains("First Order Special Discount Rule: First Time customer with high value order"));
}
@Test
void whenNoRulesTriggered_thenEmptyListReturned() {
    Customer customer = new Customer("Max", 50, false);
    Order order = new Order(200.0, customer);
    RuleEngine engine = new RuleEngine(List.of(new LoyaltyDiscountRule(), new FirstOrderHighValueSpecialDiscountRule()));
    List results = engine.evaluate(order);
    assertTrue(results.isEmpty());
}
我们在这里可以观察到,在第一个测试用例中,基于Customer和Order 输入,规则得到满足。另一方面,由于输入参数不满足业务需求,因此在第二个测试用例中,所有规则均不满足。

规则引擎方法比较
我们探讨了如何在两种实现中评估规则,并总结了每种方法的主要特点和权衡:


方面                 SpEL 方法                                                                           POJO 方法
编译时安全    没有编译时安全性——错误只能在运行时捕获    具有编译时安全性
重构               底层属性发生变化时重构成本高                                重构友好代码
调试              维护和调试复杂规则具有挑战性                                 易于调试
灵活性         灵活适应频繁的规则变化                                               规则频繁变化时灵活性较差
规则更新     规则更新无需更改代码                                                   添加或更新规则需要更改代码并重新部署