为什么要学 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、接动态规则,整套东西才会稳。