为什么要学 Drools
规则一旦开始长到这个程度,普通 if/else 就会越来越难收:
1、条件组合越来越多
2、规则优先级开始互相影响
3、产品总想临时改条件、加条件、下线条件
4、线上出了问题,很难说清楚到底是哪条规则命中了
所以 Drools 真正解决的,不是“把判断写到配置里”,而是把规则判断从业务主流程里拆出来。
如果只是两三个固定分支,没必要上规则引擎;但一旦规则开始高频变更、组合变多,Drools 确实比长串分支更容易维护。
先认识几个核心概念
Drools 真正常用的概念其实不多,先把下面几个抓住就够用了:
1、Fact:送进规则引擎的一份事实数据
2、Rule:一条规则,包含条件和动作
3、KieBase:规则定义集合
4、KieSession:一次可执行的规则会话
5、AgendaGroup:规则分组,用来控制当前执行哪一组
6、Salience:规则优先级,数字越大越先执行
如果把它翻成更接近业务代码的话,其实就是:
1、先准备一份干净的输入
2、再挑一组规则执行
3、最后把命中结果收回来
执行时到底怎么跑
Drools 执行时,大致就是这条链路:
1、业务代码准备 Fact
2、把 Fact 放进 KieSession
3、引擎根据规则条件做模式匹配
4、命中的规则进入待执行队列
5、按分组、优先级、激活顺序依次执行
6、把规则结果回传给业务代码
如果只记一件事,我觉得最重要的是这句:
Drools 不是拿来直接写库、发消息、调 RPC 的,它更适合只做判断和产出结果。
什么场景值得上
我一般会在下面这些场景考虑 Drools:
1、营销规则、发券规则、定价规则这类组合条件多的地方
2、同一批事实会命中多条规则的地方
3、规则经常改,但主流程不想跟着频繁改代码的地方
4、需要把命中原因、命中规则编码沉淀出来的地方
如果只是简单状态流转,或者只有一两条明确分支,普通代码通常更直接。
先把 Fact 设计好
Drools 能不能好用,很多时候不取决于 rule 文件,而取决于你送进去的 Fact 是否稳定。
我更习惯单独定义一层规则对象,而不是直接把数据库实体丢进去。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Data public class OrderRuleFact {
private Long userId;
private String userType;
private String channel;
private BigDecimal payAmount;
private Integer skuStock;
private boolean firstOrder;
private LocalDateTime now; }
|
这样做有几个好处:
1、规则只依赖决策字段,不直接绑数据库表结构
2、业务实体怎么变,不会直接把规则搞碎
3、调试规则时,输入边界更清晰
结果也要先统一
如果规则命中后没有一个统一结果模型,后面主流程很容易被带乱。
1 2 3 4 5 6 7 8 9 10 11 12
| @Data @AllArgsConstructor public class RuleHitResult {
private String ruleCode;
private String actionCode;
private Integer priority;
private String reason; }
|
这样主流程只需要关心:
1、命中了哪条规则
2、下一步该执行什么动作
3、为什么会命中
先看一条最小规则
下面这条规则已经够说明 Drools 的基本写法了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| package rules.order;
import com.example.rule.OrderRuleFact; import com.example.rule.RuleHitResult;
global java.util.List resultList;
rule "first_order_reward" salience 100 agenda-group "ORDER" when $fact : OrderRuleFact( firstOrder == true, payAmount >= 100, skuStock > 0, channel == "APP" ) then resultList.add(new RuleHitResult("R_ORDER_001", "SEND_COUPON", 100, "首单且金额达标")); end
|
这条规则里真正值得记住的就几个点:
1、when 里写条件
2、then 里只产出结果
3、agenda-group 控制规则组
4、salience 控制优先级
Spring Cloud 里怎么接
如果项目本身就是 Spring Cloud 体系,我更建议把规则引擎收成一个独立规则服务,或者至少收成一个单独模块,而不是散在各个业务服务里。
比较常见的拆法是:
1、订单服务、营销服务这些业务服务负责准备 Fact
2、规则服务负责装载规则、执行规则、返回结果
3、规则文件可以跟代码一起发版,或者再往后接配置中心做动态刷新
先看最基础的依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| <dependency> <groupId>org.drools</groupId> <artifactId>drools-core</artifactId> <version>7.74.1.Final</version> </dependency> <dependency> <groupId>org.drools</groupId> <artifactId>drools-compiler</artifactId> <version>7.74.1.Final</version> </dependency> <dependency> <groupId>org.kie</groupId> <artifactId>kie-spring</artifactId> <version>7.74.1.Final</version> </dependency>
|
先把 Kie 容器配起来
如果只是本地 classpath 下的规则文件,Spring 配置可以先从最小版本开始。
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Configuration public class DroolsConfig {
@Bean public KieServices kieServices() { return KieServices.Factory.get(); }
@Bean public KieContainer kieContainer(KieServices kieServices) { return kieServices.getKieClasspathContainer(); } }
|
配一个 kmodule.xml:
1 2 3 4 5 6
| <?xml version="1.0" encoding="UTF-8"?> <kmodule xmlns="http://www.drools.org/xsd/kmodule"> <kbase name="ruleKBase" packages="rules.order"> <ksession name="ruleKSession"/> </kbase> </kmodule>
|
规则文件放在:
1
| src/main/resources/rules/order/order-rule.drl
|
执行代码怎么写
业务侧真正要做的事,其实是准备 Fact、执行 Session、收结果。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| @Service public class OrderRuleEngineService {
private final KieContainer kieContainer;
public OrderRuleEngineService(KieContainer kieContainer) { this.kieContainer = kieContainer; }
public List<RuleHitResult> execute(OrderRuleFact fact) { KieSession kieSession = kieContainer.newKieSession("ruleKSession"); List<RuleHitResult> resultList = new ArrayList<>(); try { kieSession.setGlobal("resultList", resultList); kieSession.getAgenda().getAgendaGroup("ORDER").setFocus(); kieSession.insert(fact); kieSession.fireAllRules(); return resultList; } finally { kieSession.dispose(); } } }
|
这段代码里最关键的不是 fireAllRules(),而是三件事:
1、输入对象先收干净
2、全局结果统一收口
3、Session 用完及时释放
Controller 层怎么接
如果你想在 Spring Cloud 服务里先快速验证,可以直接暴露一个调试接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @RestController @RequestMapping("/rule") public class RuleDebugController {
private final OrderRuleEngineService orderRuleEngineService;
public RuleDebugController(OrderRuleEngineService orderRuleEngineService) { this.orderRuleEngineService = orderRuleEngineService; }
@PostMapping("/order/evaluate") public List<RuleHitResult> evaluate(@RequestBody OrderRuleFact fact) { return orderRuleEngineService.execute(fact); } }
|
这样联调时会很方便,尤其适合先验证规则文件是否按预期命中。
规则怎么动态更新
规则一旦开始频繁变更,通常就不会满足于“改 drl 后重新发版”。
Spring Cloud 里更常见的做法一般是:
1、规则文件先放配置中心,比如 Nacos
2、规则服务监听配置变更
3、变更后重新构建 KieBase 或 KieContainer
4、新请求走新规则,旧 Session 自然释放
大致代码会像这样:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| @Component public class DynamicRuleLoader {
private volatile KieContainer kieContainer;
public void reload(String drlText) { KieServices kieServices = KieServices.Factory.get(); KieFileSystem kieFileSystem = kieServices.newKieFileSystem(); kieFileSystem.write("src/main/resources/rules/order/dynamic-rule.drl", drlText);
KieBuilder kieBuilder = kieServices.newKieBuilder(kieFileSystem); kieBuilder.buildAll();
Results results = kieBuilder.getResults(); if (results.hasMessages(Message.Level.ERROR)) { throw new IllegalStateException(results.getMessages().toString()); }
KieModule kieModule = kieBuilder.getKieModule(); this.kieContainer = kieServices.newKieContainer(kieModule.getReleaseId()); } }
|
这里最重要的不是热更新本身,而是更新失败时不能把线上正在用的旧规则一起弄挂。
容易踩的坑
1、把数据库实体直接塞进规则引擎
实体字段一改,规则文件就跟着一起脆。
2、在规则里直接写库、调 RPC、发消息
规则最好只做判断和产出结果,副作用还是留给业务代码。
3、规则拆得太碎
一条条件一个 rule,看起来灵活,后面优先级和执行顺序会越来越乱。
4、规则没有编码和命中原因
线上出了问题,只看“没命中”基本没法排查。
5、动态加载没有校验
规则文本一改就直接上线,语法错或逻辑错都很危险。
可以怎么落
如果让我在一个 Spring Cloud 项目里从零接 Drools,我更倾向按这个顺序来:
1、先挑一个规则多但副作用少的场景试水
2、先收 Fact 和结果模型,不急着做动态配置
3、先把规则执行服务跑顺,再考虑热更新
4、最后再做规则版本、灰度和回滚
这个顺序更稳,因为 Drools 最容易翻车的地方,通常不是规则语法,而是边界一下子拉太大。
收一下
Drools 真正值钱的地方,不是“把 if/else 写成规则文件”,而是把复杂判断从主流程里拆出来,变成可维护、可解释、可迭代的一层。
先把 Fact、规则分组、执行边界和结果收口这几件事理清,再接 Spring Cloud、接动态规则,整套东西才会稳。