ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Spring Boot集成Drools规则热加载:从KieContainer到KieScanner

Spring Boot集成Drools规则热加载:从KieContainer到KieScanner 简介在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 到 kjar2.1 Maven 依赖与版本选型在 Spring Boot 项目中引入 Drools表面上是加一组 Maven 依赖实际上要区分清楚哪些是 API哪些是 Spring 整合哪些是规则编译时才会用到的实现。原资源里的kie-spring、kie-api、kie-internal是三块基础但只靠它们可能不够drools-compiler和drools-core也要进来。版本建议统一交给drools-bom管理避免一堆 jar 各自带版本号。dependencyManagement dependencies dependency groupIdorg.drools/groupId artifactIddrools-bom/artifactId version${drools.version}/version typepom/type scopeimport/scope /dependency /dependencies /dependencyManagement这里${drools.version}替换成你的版本即可。我在实际项目里一般固定一个正式版不用 SNAPSHOT方便线上复现问题。BOM 引入后依赖声明可以省略 version版本由 BOM 统一约束。依赖作用是否需要声明org.kie:kie-apiKieServices、KieContainer、KieSession 等公开 API是org.kie:kie-spring提供 Spring 整合类与命名空间解析是org.kie:kie-internal内部实现类部分场景编译规则需要是org.drools:drools-compiler把 DRL 编译成可执行的规则包是org.drools:drools-core运行时核心包含 Rete 引擎是声明位置dependency groupIdorg.kie/groupId artifactIdkie-spring/artifactId /dependency dependency groupIdorg.kie/groupId artifactIdkie-api/artifactId /dependency dependency groupIdorg.kie/groupId artifactIdkie-internal/artifactId /dependency dependency groupIdorg.drools/groupId artifactIddrools-compiler/artifactId /dependency dependency groupIdorg.drools/groupId artifactIddrools-core/artifactId /dependency注意如果使用 Spring Boot 的 devtools 热重启要把它对 Drools 类加载的影响考虑进去。drools-compiler里的规则编译会读取 TCCLdevtools 的双类加载机制可能导致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 和 KieSessionkmodule xmlnshttp://www.drools.org/xsd/kmodule kbase nameruleKBase packagescom.example.rules ksession nameruleSession typestateful/ /kbase /kmodulepackages必须对应该 KieBase 要加载的 DRL package也就是 DRL 文件里的package声明的值。如果这里不一致启动时看起来一切正常fireAllRules却一条规则都不会执行。typestateful表示有状态会话适合一个流程内多次insert并逐步匹配如果有状态不需要保留可以改成stateless性能更好也更安全。kjar 与普通 jar 的区别在于kjar 除了 classes 和 resources还包含编译后的规则包数据以及kie-maven-plugin构建时写入的 KieModule 元信息。只有把规则项目打成 kjar 并发布到 Maven 仓库KieScanner 才能按 ReleaseId 扫描到新版本。如果只是把 DRL 放在主项目 classpath 下kieContainer一旦构建完KieScanner 就没有可监听的目标。这是后续动态重载设计时的一个重要分界线。3. 在 Spring Boot 中组装 KieContainer 与 KieSession3.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 获取规则实例找不到 kbaseKieSession执行规则插入 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 disposeSpring 容器关闭时释放会话资源注意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 与手动刷新 KieContainer4.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 变成 kjarbuild plugins plugin groupIdorg.kie/groupId artifactIdkie-maven-plugin/artifactId version${drools.version}/version extensionstrue/extensions /plugin /plugins /build主项目的 pom 里声明规则项目的依赖。为了让 KieScanner 能发现更新规则项目的版本建议使用 SNAPSHOT或者每次变更递增 release 版本dependency groupIdcom.example/groupId artifactIdbusiness-rules/artifactId version1.0.0-SNAPSHOT/version /dependencySpring 配置类不再从 classpath 读 DRL而是直接用 ReleaseId 创建 KieContainer再把这个容器交给 KieScannerConfiguration 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 AtomicReferenceKieContainer 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 ListString 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把规则名列表的变化作为发布是否成功的检查条件。本文还有配套的精品资源点击获取
返回列表