简介:在Spring Boot项目中集成Drools规则引擎并实现规则动态重载,是服务端将业务规则从代码中解耦、提升维护效率的常见选择。这份资源面向已掌握Spring Boot基础、希望引入规则引擎的开发者,内容覆盖从Maven依赖配置、DRL规则文件编写,到KieContainer与KieSession初始化,再到通过KieScanner监听规则变更并自动刷新的全过程。资源还针对整合阶段容易出现的版本兼容、规则文件加载失败、KieSession无法更新等典型问题,提供了可参考的处理思路。无论是电商促销、风控策略还是审批流程,这类动态规则能力都能帮助团队快速试错和迭代。压缩包为ZIP格式,大小仅31KB,内容精简集中,便于快速查阅核心配置与示例代码。该主题已有429人学习下载,说明这一实践方式受到不少开发者关注。学习后可将Drools无缝嵌入Spring Boot应用,使规则调整无需重启即可生效,显著提升业务响应速度与系统灵活性。资源内容面向实战,适合作为快速上手的参考资料。
1. 为什么 Spring Boot 集成 Drools 后还要解决规则重新加载
把业务规则从 Java 代码里拆到 Drools 的 .drl 文件,几乎是规则引擎项目的入场动作。真正麻烦的是上线之后:产品经理调整一个阈值、风控加一条校验,是按老流程走一个发布窗口,还是让规则自己重新加载?如果是后者,Drools 带来的敏捷性才能真正兑现。Spring Boot 集成 Drools 的动态重载,核心不是拿到 KieSession 然后 fireAllRules,而是搞清楚 KieContainer、KieScanner 和 Maven 仓库之间如何配合。下面会从依赖选型、构建流程、KieScanner 监听,到手动刷新 KieBase 的兜底方案,把可落地的代码和参数讲透。适合正在用 Spring Boot 做规则引擎、或者被 DRL 热加载问题卡住的人。
2. 依赖选型与 DRL 组织:从 kie-spring 到 kjar
2.1 Maven 依赖与版本选型
在 Spring Boot 项目中引入 Drools,表面上是加一组 Maven 依赖,实际上要区分清楚:哪些是 API,哪些是 Spring 整合,哪些是规则编译时才会用到的实现。原资源里的kie-spring、kie-api、kie-internal是三块基础,但只靠它们可能不够,drools-compiler和drools-core也要进来。版本建议统一交给drools-bom管理,避免一堆 jar 各自带版本号。
<dependencyManagement> <dependencies> <dependency> <groupId>org.drools</groupId> <artifactId>drools-bom</artifactId> <version>${drools.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>这里${drools.version}替换成你的版本即可。我在实际项目里一般固定一个正式版,不用 SNAPSHOT,方便线上复现问题。BOM 引入后,依赖声明可以省略 version,版本由 BOM 统一约束。
| 依赖 | 作用 | 是否需要声明 |
|---|---|---|
org.kie:kie-api | KieServices、KieContainer、KieSession 等公开 API | 是 |
org.kie:kie-spring | 提供 Spring 整合类与命名空间解析 | 是 |
org.kie:kie-internal | 内部实现类,部分场景编译规则需要 | 是 |
org.drools:drools-compiler | 把 DRL 编译成可执行的规则包 | 是 |
org.drools:drools-core | 运行时核心,包含 Rete 引擎 | 是 |
声明位置:
<dependency> <groupId>org.kie</groupId> <artifactId>kie-spring</artifactId> </dependency> <dependency> <groupId>org.kie</groupId> <artifactId>kie-api</artifactId> </dependency> <dependency> <groupId>org.kie</groupId> <artifactId>kie-internal</artifactId> </dependency> <dependency> <groupId>org.drools</groupId> <artifactId>drools-compiler</artifactId> </dependency> <dependency> <groupId>org.drools</groupId> <artifactId>drools-core</artifactId> </dependency>注意:如果使用 Spring Boot 的 devtools 热重启,要把它对 Drools 类加载的影响考虑进去。drools-compiler里的规则编译会读取 TCCL,devtools 的双类加载机制可能导致ClassNotFoundException,我一般把 devtools 排除在规则执行模块之外。这是很多人忽略的一个点。
2.2 一条可用的 DRL 规则长什么样
DRL 是 Drools 规则的语言,核心是when和then。when描述触发条件,then描述触达后的动作。下面这条规则处理订单折扣,比只打印一行日志更能看出实际业务用法:
package com.example.rules import com.example.model.PurchaseOrder rule "high-value-order-discount" when $order : PurchaseOrder(totalAmount > 5000, level > 2) then $order.setDiscountRate(0.10); System.out.println("order " + $order.getId() + " discount 10%"); end关键点在$order这个变量绑定。PurchaseOrder(...)是 Drools 的屏蔽语法,等价于写一遍 getter 判断:totalAmount > 5000会被编译成getTotalAmount() > 5000的约束。属性名必须是 Java Bean 的标准命名,否则编译期会报Invalid rule。规则文件放在src/main/resources/rules/下,文件命名随意,但不建议在文件名里写中文或空格,部分 Windows 环境读取会出现编码问题。
2.3 kmodule.xml 与 kjar 的边界
DRL 文件写好后,还需要一个META-INF/kmodule.xml来声明 KieBase 和 KieSession:
<kmodule xmlns="http://www.drools.org/xsd/kmodule"> <kbase name="ruleKBase" packages="com.example.rules"> <ksession name="ruleSession" type="stateful"/> </kbase> </kmodule>packages必须对应该 KieBase 要加载的 DRL package,也就是 DRL 文件里的package声明的值。如果这里不一致,启动时看起来一切正常,fireAllRules却一条规则都不会执行。type="stateful"表示有状态会话,适合一个流程内多次insert并逐步匹配;如果有状态不需要保留,可以改成stateless,性能更好也更安全。
kjar 与普通 jar 的区别在于:kjar 除了 classes 和 resources,还包含编译后的规则包数据,以及kie-maven-plugin构建时写入的 KieModule 元信息。只有把规则项目打成 kjar 并发布到 Maven 仓库,KieScanner 才能按 ReleaseId 扫描到新版本。如果只是把 DRL 放在主项目 classpath 下,kieContainer一旦构建完,KieScanner 就没有可监听的目标。这是后续动态重载设计时的一个重要分界线。
3. 在 Spring Boot 中组装 KieContainer 与 KieSession
3.1 KieServices 到 KieContainer 的构建链路
Drools 的 API 入口是KieServices,所有 Kie 组件的创建都从它开始。构建一个可用于规则执行的 KieContainer,链路是:KieFileSystem 接收 DRL 和 pom 信息,KieBuilder 编译并校验,KieModule 保存编译产物,KieContainer 暴露访问入口,最后从中创建 KieSession。每一层都有独立职责,也都有对应检查点。
| 组件 | 职责 | 出错时看什么 |
|---|---|---|
KieServices | 工厂入口,创建其他组件 | 类冲突 |
KieFileSystem | 写入 DRL、pom、kmodule 文件 | 文件路径 |
KieBuilder | 编译规则,生成规则包 | getResults()错误 |
KieModule | 持有编译后的规则包 | 包元数据 |
KieContainer | 按 ReleaseId/KieBase 获取规则实例 | 找不到 kbase |
KieSession | 执行规则,插入 Fact | 线程安全 |
构建时最常见的错误是 DRL 里的 import 类不在当前模块的 classpath 上。KieBuilder 编译规则时,会根据 DRL 中的 import 解析类型,解析失败会在getResults()中列出多条错误,而不是直接抛异常。所以构建后必须检查hasMessages(Level.ERROR),否则最后创建 KieContainer 时才爆出ClassNotFoundException,排错成本翻倍。
3.2 Spring Boot 配置类完整实现
下面这个配置类从 classpath 下的rules/*.drl读取规则,构建一个 KieContainer,并暴露有状态会话。它和原资源的写法保持一致,补上了 ReleaseId 写入这一步,避免出现默认版本号导致的定位困难。
@Configuration public class DroolsConfig { @Value("classpath:rules/*.drl") private Resource[] drlResources; @Bean public KieContainer kieContainer() throws IOException { KieServices kieServices = KieServices.Factory.get(); KieFileSystem kfs = kieServices.newKieFileSystem(); ReleaseId releaseId = kieServices.newReleaseId( "com.example", "business-rules", "1.0.0"); kfs.generateAndWritePomXML(releaseId); for (Resource resource : drlResources) { String drlPath = "src/main/resources/" + resource.getFilename(); String drlContent = StreamUtils.copyToString( resource.getInputStream(), StandardCharsets.UTF_8); kfs.write(drlPath, drlContent); } KieBuilder kieBuilder = kieServices.newKieBuilder(kfs).buildAll(); if (kieBuilder.getResults().hasMessages(Level.ERROR)) { throw new IllegalStateException(kieBuilder.getResults().toString()); } KieModule kieModule = kieBuilder.getKieModule(); return kieServices.newKieContainer(kieModule.getReleaseId()); } @Bean(destroyMethod = "dispose") public KieSession kieSession(KieContainer kieContainer) { return kieContainer.newKieSession(); } }generateAndWritePomXML这一步容易被漏掉。不加这步,kieBuilder.getKieModule().getReleaseId()会返回默认的org.default:artifact:1.0.0,虽然也能用,但后面对接 KieScanner 时会失去对 GAV 的控制。StreamUtils.copyToString来自 Spring 的org.springframework.util,直接用资源输入流读取 DRL,避免手工关流的麻烦。KieSession Bean 声明了destroyMethod = "dispose",Spring 容器关闭时释放会话资源,注意dispose是 KieSession 的方法,和 KieContainer 的dispose都应当被调用。
3.3 KieSession 的作用域与线程安全
KieSession 不是线程安全的。像上面那样定义一个单例 Bean,多个请求同时insert同一个 session,会出现事实对象互相污染,甚至触发规则重复执行。并发量小的内部系统可能偶尔复现,线上压测时基本必现。
常见做法是保留 KieContainer 或 KieBase 的单例,由它在每次需要时创建短命会话:
@Service public class RuleExecutionService { private final KieContainer kieContainer; public RuleExecutionService(KieContainer kieContainer) { this.kieContainer = kieContainer; } public void execute(PurchaseOrder order) { KieSession session = kieContainer.newKieSession(); try { session.insert(order); session.fireAllRules(); } finally { session.dispose(); } } }如果只执行一轮规则、不需要跨多次调用保留 Fact,建议直接使用StatelessKieSession。无状态会话内部会帮你做 session 的创建和销毁,方法是session.execute(order),参数可以是单个 Fact 也可是集合。绝大多数 query 类、计算类场景,无状态会话够用且省心。只有像“给定一个 Fact 不断插入其他 Fact 继续推进”的长流程,才值得用有状态会话。这看起来是多写几行代码,实际上是把并发风险挡在门外。
4. 动态重载规则:KieScanner 与手动刷新 KieContainer
4.1 为什么只改 classpath 下的 DRL 不生效
很多团队初次集成时,习惯把规则文件直接放在主项目的resources/rules下,改完 DRL 后把新文件替换到服务器,然后期待 KieScanner 自动重载。这个期待并不能实现。KieScanner 的工作机制是:轮询 Maven 仓库中的 ReleaseId 对应产物,发现新版本后把 KieContainer 切换到新规则包。它监听的是仓库,不是本地文件。
换句话说,DRL 在主项目里,启动阶段就会被 KieFileSystem 读取并编译进 KieModule。运行期间即使你改了target/classes/rules下的 DRL,已构建的 KieBase 也不会感知到变化。要让动态重载跑通,规则代码必须拆成独立的 kjar 项目,并且主项目通过 GAV 从仓库解析规则。这个拆分不是架构上的洁癖,是 KieScanner 的设计使然。
4.2 KieScanner 的正确配置与参数说明
规则项目单独建立后,pom.xml要引入 kie-maven-plugin,把普通 jar 变成 kjar:
<build> <plugins> <plugin> <groupId>org.kie</groupId> <artifactId>kie-maven-plugin</artifactId> <version>${drools.version}</version> <extensions>true</extensions> </plugin> </plugins> </build>主项目的 pom 里声明规则项目的依赖。为了让 KieScanner 能发现更新,规则项目的版本建议使用 SNAPSHOT,或者每次变更递增 release 版本:
<dependency> <groupId>com.example</groupId> <artifactId>business-rules</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>Spring 配置类不再从 classpath 读 DRL,而是直接用 ReleaseId 创建 KieContainer,再把这个容器交给 KieScanner:
@Configuration public class DroolsConfig { @Value("${drools.scanner.interval:10000}") private long scannerInterval; @Bean(destroyMethod = "dispose") public KieContainer kieContainer() { KieServices kieServices = KieServices.Factory.get(); ReleaseId releaseId = kieServices.newReleaseId( "com.example", "business-rules", "LATEST"); return kieServices.newKieContainer(releaseId); } @Bean(destroyMethod = "dispose") public KieScanner kieScanner(KieContainer kieContainer) { KieScanner scanner = KieServices.Factory.get().newKieScanner(kieContainer); scanner.start(scannerInterval); return scanner; } }两点参数说明。第一,LATEST代表从 Maven 仓库拉取这个 groupId/artifactId 下的最新版本,适合开发环境快速验证;生产上我通常把版本号写到配置里,升级规则时改配置重启,或者使用 SNAPSHOT 让 scanner 自己拉新。第二,scanner.start(10000L)必须传一个大于 0 的毫秒数。原资源里的1000L是 1 秒一次,对本地 demo 没问题,但连的是公司私服时,频繁轮询会放大仓库压力。业务上几秒钟内的规则延迟通常可接受,我一般给到 10~30 秒。
KieScanner 发现新版本后,会替换 KieContainer 里的 KieModule,但已经创建并且正在运行的 KieSession 不会自动迁移。之前那些 session 持有的还是旧规则包,只有后续新建的 session 会使用新规则。所以涉及长会话的场景,要自己设计会话生命周期,规则更新后主动重建会话。
4.3 不依赖 Maven 仓库的手动刷新方案
如果公司没有私服,或者规则项目暂时拆不出去,还能走一条兜底方案:把规则文件放在一个可监控的目录,检测到变化后,用 KieFileSystem 重新编译并原子替换 KieContainer。完整思路如下:
@Component public class RuleRefreshHandler { private final AtomicReference<KieContainer> containerRef; public RuleRefreshHandler(KieContainer originalContainer) { this.containerRef = new AtomicReference<>(originalContainer); } public boolean refresh(Path drlPath) throws IOException { KieServices ks = KieServices.Factory.get(); ReleaseId releaseId = ks.newReleaseId( "com.example", "business-rules", UUID.randomUUID().toString()); KieFileSystem kfs = ks.newKieFileSystem() .generateAndWritePomXML(releaseId) .write("src/main/resources/" + drlPath.getFileName(), Files.readString(drlPath)); KieBuilder builder = ks.newKieBuilder(kfs).buildAll(); if (builder.getResults().hasMessages(Level.ERROR)) { return false; } KieModule module = builder.getKieModule(); KieContainer oldContainer = containerRef.getAndSet( ks.newKieContainer(module.getReleaseId())); if (oldContainer != null) { oldContainer.dispose(); } return true; } }核心思路是用一个AtomicReference保存当前生效的 KieContainer,每次需要执行规则时都从containerRef.get()获取。这样刷新操作和读取操作不会因为 container 替换出现空指针。随机 UUID 作为版本号,目的是让每个新容器都有唯一 ReleaseId,避免新旧容器元数据冲突。dispose要在getAndSet之后调用,先取走旧引用再释放,避免把刚换上的新容器释放掉。
| 方案 | 适用场景 | 重载生效范围 |
|---|---|---|
| KieScanner 监听 Maven 仓库 | 规则已独立成 kjar,有私服/公共仓库 | 新创建的 KieSession |
| 手动重建 KieContainer | 无私服,规则文件在本地或配置中心 | 新创建的 KieSession |
| 定时扫描 DRL 文件 | 单机 demo | 新创建的 KieSession |
无论哪种方式,都要记住同一个限制:老 session 不自动升级。想让规则变更彻底生效,最好在刷新时把长时间存活会话的地方同时重置,或者把有状态 session 化整为零,改成无状态。
5. 重载效果验证、日志排查与轮询参数调优
5.1 验证规则是否真正换掉
规则热加载完成后,不要只看日志里有没有输出,第一步应该从 KieBase 中列出当前规则名,确认新规则已经进入容器。下面这个接口可以直接对外暴露:
@RestController @RequestMapping("/rules") public class RuleController { private final KieContainer kieContainer; public RuleController(KieContainer kieContainer) { this.kieContainer = kieContainer; } @GetMapping("/list") public List<String> list() { return kieContainer.getKieBase("ruleKBase") .getKiePackages() .stream() .flatMap(pkg -> pkg.getRules().stream()) .map(Rule::getName) .collect(Collectors.toList()); } }调用curl http://localhost:8080/rules/list,对比刷新前后的规则名列表。如果列表没变化,说明重载没有真正发生,不用再继续查下游。另一个常见验证办法是在规则 RHS 里临时写一个自定义日志标记,例如log.info("rule marker {}", $order.getId()),刷新后请求一次,看标记是否出现。验证通过后再把日志删掉,避免把临时代码漏到生产。
5.2 扫不到新规则时先查这四个位置
KieScanner 一直扫不到更新,通常是下面四个原因之一。先检查规则项目有没有执行mvn install -DskipTests,并确认产物确实进了本地仓库;再检查主项目pom.xml里的依赖 GAV 和KieContainer中newReleaseId的 GAV 是否一致,这两个不一致时 scanner 监听的仓库坐标根本不存在;然后看 DRL 的 package 与kmodule.xml里kbase的 packages 是否匹配,不匹配时新版本被加载但规则条数为空;最后确认 scanner 启动时仓库能否访问,私服不可用时会静默重试,不是直接报错。
建议在 application.yml 里给 scanner 单独打开日志:
logging: level: org.kie.scanner: debug org.drools.compiler: infoorg.kie.scanner的 debug 日志会打印每次轮询的检查结果,能看到No change found或Update available这类关键信息,排查效率高很多。
5.3 轮询间隔与会话重建的经验值
上线初期可以用 10 秒间隔,先把链路跑通;如果规则变更不频繁,调大到 60 秒也不会让业务感知明显延迟。间隔太短,比如 1 秒,虽然满足“立刻生效”,但会让私服日志和网络连接数明显上升,尤其在多实例部署时,每台机器都在轮询。
最后提醒一个容易踩的坑:KieScanner 触发重载后,KieContainer 老版本的 dispose 不要手工调用,避免把正在使用的规则包提前卸载。如果你坚持手工管理容器,等流量切到新容器后再 dispose 旧对象。需要快速验证时,可以在部署脚本里加一条curl /rules/list,把规则名列表的变化作为发布是否成功的检查条件。
本文还有配套的精品资源,点击获取