拒绝if-else!小而美的规则引擎 Easy Rules 真不错!

开发 项目管理
Easy Rules 非常适合需要快速实现业务规则引擎的场景。对于中小型项目,Easy Rules 的简单性和灵活性是一大优势。如果项目规则复杂或者性能要求较高,可以考虑结合 Drools 等更强大的规则引擎使用。

Easy Rules是一个简单而强大的Java规则引擎,提供以下功能:

  • 轻量级框架和易于学习的API
  • 基于POJO的开发与注解的编程模型
  • 定义抽象的业务规则并轻松应用它们
  • 支持从简单规则创建组合规则的能力
  • 支持使用表达式语言(如MVEL和SpEL)定义规则的能力

为何选择Easy Rules规则引擎

1. 传统if - else编程的困境

案例一:电商满减规则频繁变更(真实生产场景重现)

在电商业务的实际运营中,促销规则的频繁变更是一个常见的问题。假设某电商平台有如下促销规则:

// 传统硬编码方式(噩梦般的代码片段)
if(user.isVip()){  
    if(order.getAmount() > 200){  
        if(order.getItems().stream().anyMatch(i -> i.isPromotion())){  
            order.applyDiscount(0.8); // 会员满200且含促销商品打8折  
        }  
    } else if(order.getCreateTime().isAfter(LocalDate.of(2023,11,1))){  
        order.applyDiscount(0.9); // 双十一期间会员专属9折  
    }  
} else {  
    // 普通用户规则嵌套层级更深...  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

这种传统的硬编码方式存在诸多痛点:

  • 维护困难:每当市场部调整规则时,开发者需要在大量的代码中艰难地寻找逻辑修改点,这不仅效率低下,还容易出错。
  • 发版风险高:发版频率极高,可能一个月需要进行6次规则修改和上线操作,每次上线都伴随着一定的风险,如代码冲突、功能异常等。
  • 协作问题:在多人协作开发时,由于代码结构复杂,很容易引发代码冲突,增加了开发和维护的难度。

案例二:物联网设备告警条件嵌套难题

在物联网设备监控系统中,复杂的告警条件嵌套也是一个常见的问题。某工厂设备监控系统需要进行如下判断:

if(temperature > 50 || humidity > 80) {  
    if(pressure < 100 && vibration > 5) {  
        if(deviceStatus != Status.MAINTENANCE) {  
            triggerAlarm(AlarmLevel.CRITICAL);  
        }  
    }  
} else if (runtimeHours > 1000 && !isMaintained) {  
    triggerAlarm(AlarmLevel.WARNING);  
}  
// 后续还有8个else if...
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

这种代码结构带来了以下问题:

  • 调试困难:在调试过程中,断点需要穿透10层条件判断,调试难度极大,耗费大量时间。
  • 扩展性差:当需要新增“电压波动 > 10%”这样的条件时,需要重构整个逻辑,开发成本高。
  • 知识传递困难:交接文档需要绘制3页流程图才能清晰说明规则逻辑,给知识传递带来了很大的困难。

可视化对比(代码量的显著优化)

代码量对比代码量对比

2. 轻量级规则引擎的优势

场景化演示:从复杂到简洁的转变

场景转变场景转变

核心优势解析

  • 解耦的智慧

规则与业务分离:规则与业务代码实现物理隔离,可以将规则存储在独立文件或数据库中,使代码结构更加清晰。这样,业务代码专注于业务逻辑的处理,而规则代码则负责规则的定义和管理。

动态加载规则:修改规则无需重新编译部署,支持动态加载规则。以下是一个动态加载规则的示例:

public void refreshRules() {
    List<Rule> newRules = ruleLoader.loadFromDB(); // 从数据库读取最新规则
    rulesEngine.fire(new Rules(newRules), facts);  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 可读性的提升

自描述性规则:规则具有自描述性,每个规则都可以看作是一个独立的文档,便于理解和维护。开发者可以通过规则的名称、描述和条件等信息,快速了解规则的用途和逻辑。

决策流程可视化:支持决策流程可视化,可以自动生成规则关系图。例如:

[用户类型] --> [VIP规则] --> [折扣计算]  
             \-> [普通用户规则] --> [满减计算]
  • 1.
  • 2.
  • 扩展性的保障

零侵入式扩展:新增规则对现有代码零侵入,只需添加新的Rule类即可。这使得系统的扩展性得到了极大的提升,开发者可以根据业务需求随时添加新的规则。

多规则源支持:支持混合多种规则源,例如数据库、YAML文件和注解。以下是一个YAML规则文件的示例:

# discount_rule.yml
name: "老用户回馈规则"
description: "注册超过3年的用户额外折扣"
condition: "user.registerYears >= 3"
actions:
  - "order.applyAdditionalDiscount(0.95)"
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.

定义规则

大多数业务规则可以由以下定义表示:

  • 名称:规则命名空间中的唯一规则名称
  • 说明:规则的简要说明
  • 优先级:相对于其他规则的规则优先级
  • 事实:去匹配规则时的一组已知事实
  • 条件:为了匹配该规则,在给定某些事实的情况下应满足的一组条件
  • 动作:当条件满足时要执行的一组动作(可以添加/删除/修改事实)

Easy Rules为定义业务规则的每个关键点提供了抽象。

在Easy Rules中,一个规则由Rule接口表示:

public interface Rule {

    /**
    * 改方法封装规则的条件(conditions)
    * @return 如果提供的事实适用于该规则返回true, 否则,返回false
    */
    boolean evaluate(Facts facts);

    /**
    * 改方法封装规则的操作(actions)
    * @throws 如果在执行过程中发生错误将抛出Exception
    */
    void execute(Facts facts) throws Exception;

    //Getters and setters for rule name, description and priority omitted.

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.

evaluate方法封装了必须求值为TRUE才能触发规则的条件。

execute方法封装了在满足规则条件时应执行的操作。条件和动作ConditionandAction接口表示。

规则可以用两种不同的方式定义:

  • 通过在POJO上添加注释,以声明方式定义
  • 通过RuleBuilder API,以编程方式定义

用注解定义规则

这些是定义规则的最常用方法,但如果需要,还可以实现Rulei接口或继承BasicRule类。

@Rule(name = "my rule", description = "my rule description", priority = 1)
publicclass MyRule {

    @Condition
    public boolean when(@Fact("fact") fact) {
        //my rule conditions
        returntrue;
    }

    @Action(order = 1)
    public void then(Facts facts) throws Exception {
        //my actions
    }

    @Action(order = 2)
    public void finally() throws Exception {
        //my final actions
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.

@Condition注解标记计算规则条件的方法。此方法必须是公共的,可以有一个或多个用@Fact注解的参数,并返回布尔类型。只有一个方法能用@Condition注解。

@Action注解标记要执行规则操作的方法。规则可以有多个操作。可以使用order属性按指定的顺序执行操作。默认情况下,操作的顺序为0。

2. 用RuleBuilder API定义规则

Rule rule = new RuleBuilder()
                .name("myRule")
                .description("myRuleDescription")
                .priority(3)
                .when(condition)
                .then(action1)
                .then(action2)
                .build();
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.

在这个例子中, Condition实例condition,Action实例是action1和action2。

定义事实

Facts API是一组事实的抽象,在这些事实上检查规则。在内部,Facts实例持有HashMap<String,Object>,这意味着:

  • 事实需要命名,应该有一个唯一的名称,且不能为空
  • 任何Java对象都可以充当事实

这里有一个实例定义事实:

Facts facts = new Facts();
facts.add("rain", true);
  • 1.
  • 2.

Facts 能够被注入规则条件,action 方法使用 @Fact 注解. 在下面的规则中,rain 事实被注入itRains方法的rain参数:

@Rule
class WeatherRule {

    @Condition
    public boolean itRains(@Fact("rain") boolean rain) {
        return rain;
    }

    @Action
    public void takeAnUmbrella(Facts facts) {
        System.out.println("It rains, take an umbrella!");
        // can add/remove/modify facts
    }

}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

Facts类型参数 被注入已知的 facts中 (像action方法takeAnUmbrella一样).

如果缺少注入的fact, 这个引擎会抛出 RuntimeException异常.

定义规则引擎

从版本3.1开始,Easy Rules提供了RulesEngine接口的两种实现:

  • DefaultRulesEngine:根据规则的自然顺序(默认为优先级)应用规则。
  • InferenceRulesEngine:持续对已知事实应用规则,直到不再应用规则为止。

创建一个规则引擎

要创建规则引擎,可以使用每个实现的构造函数:

RulesEngine rulesEngine = new DefaultRulesEngine();
// or
RulesEngine rulesEngine = new InferenceRulesEngine();
  • 1.
  • 2.
  • 3.

然后,您可以按以下方式触发注册规则:

rulesEngine.fire(rules, facts);
  • 1.

规则引擎参数

Easy Rules 引擎可以配置以下参数:

图片图片

  • skipOnFirstAppliedRule:告诉引擎规则被触发时跳过后面的规则。
  • skipOnFirstFailedRule:告诉引擎在规则失败时跳过后面的规则。
  • skipOnFirstNonTriggeredRule:告诉引擎一个规则不会被触发跳过后面的规则。
  • rulePriorityThreshold:告诉引擎如果优先级超过定义的阈值,则跳过下一个规则。版本3.3已经不支持更改,默认MaxInt。

可以使用RulesEngineParameters API指定这些参数:

RulesEngineParameters parameters = new RulesEngineParameters()
    .rulePriorityThreshold(10)
    .skipOnFirstAppliedRule(true)
    .skipOnFirstFailedRule(true)
    .skipOnFirstNonTriggeredRule(true);

RulesEngine rulesEngine = new DefaultRulesEngine(parameters);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.

如果要从引擎获取参数,可以使用以下代码段:

RulesEngineParameters parameters = myEngine.getParameters();
  • 1.

这允许您在创建引擎后重置引擎参数。

5分钟极速入门(Hello World版)

1. 环境搭建(手把手教学)

为什么选择Maven依赖?Easy Rules的核心库仅有 217KB,不会造成项目臃肿。只需在pom.xml中添加:

<dependency>
    <groupId>org.jeasy</groupId>
    <artifactId>easy-rules-core</artifactId>
    <version>4.1.0</version>
</dependency>
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

验证是否成功: 在IDE中新建RulesEngine engine = new DefaultRulesEngine();若无报错,则环境配置成功!

2. 第一个规则实战(带逐行解析)

场景背景: 假设我们正在开发智能家居系统,需要根据湿度传感器数据触发雨天提醒。

代码深度解读

图片图片

执行过程全解

public static void main(String[] args) {
    // 模拟传感器数据(真实项目从MQTT获取)
    Facts facts = new Facts();  // 事实对象(数据容器)
    facts.put("humidity", 85);  // 放入湿度值
    
    // 创建规则引擎(核心控制器)
    RulesEngine engine = new DefaultRulesEngine();
    
    // 装载规则并执行(点火!)
    engine.fire(new Rules(new RainRule()), facts); 
    
    // 执行结果:
    // 【智能家居】检测到湿度85%,建议关闭窗户带伞出门!
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.

3. 可视化规则执行流程(小白秒懂版)

完整执行链路图示

图片图片

关键点提醒

  • 一个Facts对象可承载多个数据:
facts.put("temperature", 28);  
facts.put("location", "上海");
  • 1.
  • 2.
  • 多个规则会按优先级顺序执行(默认优先级=0)
  • 使用
@Priority
  • 1.

注解调整执行顺序:

@Rule(priority = 1) // 数字越大优先级越高
  • 1.

新手常见问题QA规则没触发怎么办?

  • 检查@Fact名称是否与put时一致
  • 确认@Condition方法返回true
  • 添加日志打印调试:
@Action
public void remind() {
    System.out.println("规则触发!"); // 先确认是否执行到此
}
  • 1.
  • 2.
  • 3.
  • 4.

如何同时处理多个规则?

// 一次性加载多个规则
Rules rules = new Rules(new RainRule(), new TempRule(), new WindRule());
engine.fire(rules, facts);
  • 1.
  • 2.
  • 3.

需要我展示如何扩展这个案例,比如增加温度规则形成组合条件吗?比如"湿度>80%  温度>30℃"触发高温高湿预警?

6大经典场景深度解析

场景1:电商促销系统(组合优惠精算)

案例3进阶实现:VIP折扣与满减叠加计算

图片

避坑指南

  • 使用@Priority控制执行顺序(数值越大越先执行)
  • 折扣计算需采用乘法叠加而非减法,避免出现0元订单
  • 在动作中增加日志记录,审计实际优惠金额

场景2:物联网报警系统(多级联动)

案例4优化版:带设备状态判断的三级报警

图片图片

实战技巧

  1. 设备维护状态作为独立Fact传递
  2. 优先处理高风险规则(priority=3)
  3. 动作中集成多种通知渠道(短信/邮件/看板)

场景3:会员等级系统(混合规则源)

案例5增强方案:YAML+注解混合使用

图片图片

集成方法

// 加载YAML规则
RulesLoader loader = new YamlRuleLoader();
Rules yamlRules = loader.load(new File("promotion_rules.yml"));

// 加载注解规则
Rules annoRules = new Rules(new ShareRule());

// 合并执行
engine.fire(yamlRules, facts);
engine.fire(annoRules, facts);
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

场景4:工单分配系统(动态派单)

案例6增强版:基于值班表的动态分配

@Rule(name = "技术紧急工单")
publicclass TechEmergencyRule {
    @Condition
    public boolean isTechEmergency(
            @Fact("ticket") Ticket ticket,
            @Fact("dutyTable") DutyTable table) {
        return ticket.getType() == TECH 
            && ticket.getPriority() == HIGH
            && table.hasAvailableTechLead();
    }
    
    @Action
    public void assignToTechLead() {
        String techLead = dutyTable.getCurrentTechLead();
        ticket.setAssignee(techLead);
        dutyTable.markBusy(techLead); // 标记为忙碌状态
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

设计亮点

  • 值班表作为独立Fact,实时反映工程师状态
  • 自动标记工程师忙碌状态,避免重复分配
  • 可扩展支持轮询、负载均衡等分配策略

场景5:风控预警系统(时序检测)

案例7优化版:时间窗口滑动检测

图片图片

性能优化

  • 使用@Fact注入预处理的时序数据
  • 采用BloomFilter快速过滤低风险设备
  • 异步执行风险处理动作

场景6:游戏战斗系统(状态管理)

案例8增强版:连招技能状态机

@Rule(name = "龙卷风连击")
publicclass TornadoComboRule {
    @Condition
    public boolean checkComboSequence(
            @Fact("queue") CircularFifoQueue<Skill> queue) {
        return queue.size() >=3
            && queue.get(0) == Skill.A
            && queue.get(1) == Skill.B
            && queue.get(2) == Skill.C;
    }
    
    @Action
    public void releaseSuperSkill() {
        player.cast(Skill.SUPER_TORNADO);
        queue.clear(); // 清空连招队列
        effectPlayer.play("combo_success.wav");
    }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.

注意事项

  • 使用Apache Commons的CircularFifoQueue控制队列长度
  • 动作中重置状态避免重复触发
  • 集成音效/特效等游戏元素

架构师扩展包

  • 规则模板技术
public abstract class BasePromotionRule implements Rule {
    @Condition
    public abstract boolean matchCondition(Order order);
    
    @Action
    public void applyDiscountTemplate(@Fact("order") Order order) {
        order.applyDiscount(getDiscountRate());
        log.info("应用{}折扣", getRuleName());
    }
    
    protected abstract double getDiscountRate();
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 规则性能监控
engine.registerRuleListener(new RuleListener() {
    public void beforeExecute(Rule rule, Facts facts) {
        Monitor.startTimer(rule.getName());
    }
    
    public void afterExecute(Rule rule, Facts facts) {
        long cost = Monitor.stopTimer(rule.getName());
        if(cost > 100) {
            alertSlowRule(rule.getName(), cost);
        }
    }
});
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.

Spring Boot集成

配置自动加载:

@Configuration  
publicclass RuleEngineConfig {  

    @Bean
    public RulesEngine rulesEngine() {  
        returnnew DefaultRulesEngine(  
            new Parameters()  
                .skipOnFirstNonTriggeredRule(true)  
                .priorityThreshold(10)  
        );  
    }  

    @Bean
    public Rules ruleRegistry() throws IOException {  
        // 自动扫描带@Rule注解的Bean  
        returnnew Rules(  
            new AnnotationRuleFactory().create(  
                new ClasspathRuleDefinitionReader(),  
                new ClassPathResource("rules/").getFile()  
            )  
        );  
    }  

    @Bean
    public ApplicationRunner ruleInitializer(RulesEngine engine, Rules rules) {  
        return args -> {  
            // 启动时预加载验证规则  
            engine.fire(rules, new Facts());  
            logger.info("已成功加载{}条规则", rules.size());  
        };  
    }  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.
  • 32.

在controller中测试:

@RestController  
publicclass PromotionController {  
    @Autowired
    private RulesEngine rulesEngine;  
    @Autowired
    private Rules rules;  

    @PostMapping("/apply-rules")  
    public Order applyRules(@RequestBody Order order) {  
        Facts facts = new Facts();  
        facts.put("order", order);  
        rulesEngine.fire(rules, facts);  
        return order;  
    }  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.

在生产中我们还可以将规则配置设置为热更新,以@RefreshScope + Spring Cloud Config的方式,这样在配置更新时会自动加载。

总结

Easy Rules 非常适合需要快速实现业务规则引擎的场景。对于中小型项目,Easy Rules 的简单性和灵活性是一大优势。

如果项目规则复杂或者性能要求较高,可以考虑结合 Drools 等更强大的规则引擎使用。

责任编辑:武晓燕 来源: 码猿技术专栏
相关推荐

2024-11-13 16:32:21

aviatorJava表达式引擎

2023-11-01 11:04:12

Javaaviator

2023-10-11 08:32:13

2021-03-24 14:46:52

Python工具代码

2012-05-09 11:45:43

苹果

2023-06-02 07:30:24

If-else结构流程控制

2020-09-22 18:35:31

AI

2013-03-06 10:28:57

ifJava

2022-11-04 11:18:16

代码优化可读性

2020-10-22 09:20:22

SQLNoSQL 数据库

2021-04-13 06:39:13

代码重构code

2022-07-11 08:16:55

策略模式if-else

2021-03-10 07:20:43

if-else静态代码

2020-12-15 09:31:58

CTOif-else代码

2020-05-13 14:15:25

if-else代码前端

2021-04-14 10:47:12

区块链比特币以太坊

2021-11-04 08:53:00

if-else代码Java

2016-01-13 15:11:32

leangoo工具

2020-04-09 08:29:50

编程语言事件驱动
点赞
收藏

51CTO技术栈公众号