尧图建网站 尧图建网站 YAOTU WEB BUILD 免费咨询
ARTICLE DETAIL

资讯详情

深耕网站建设与建站编程的一线实战洞察。

基于状态模式与Drools规则引擎的复杂业务规则实现方案

基于状态模式与Drools规则引擎的复杂业务规则实现方案 最近在整理本地项目时遇到一个挺有意思的场景一个核心业务模块在特定条件下比如“本区”、“单日”、“首秀”需要触发一系列复杂的联动操作“双鸟”、“开门红”并且这些操作之间还有严格的执行顺序和状态依赖。这让我想起了游戏里那种“达成特定条件解锁隐藏成就”的机制只不过我们是在代码里实现。这种业务逻辑如果直接用一堆if-else嵌套代码会迅速变得臃肿且难以维护。今天我们就来系统性地探讨如何优雅地实现这种“条件触发式”的复杂业务规则引擎。本文将从一个高度简化的“中二”业务场景出发逐步拆解最终落地一套基于状态模式State Pattern和规则引擎Drools的可扩展解决方案。无论你是正在处理类似多条件、多动作业务流的后端开发者还是对设计模式与规则引擎结合应用感兴趣的同行都能从中获得可直接复用的代码和架构思路。1. 业务场景与核心概念拆解首先我们需要把那个有点“中二”的标题翻译成程序员能懂的语言。这实际上描述了一个基于多重条件判断的业务规则执行系统。本区 代表一个数据过滤维度或上下文范围。例如用户所属的区域、当前处理的业务分区等。在代码中这通常是一个上下文对象Context的属性。单日 代表一个时间窗口条件。规则只在某个特定时间周期内如当天有效。首秀 代表一个状态条件。指某个主体如用户、活动首次进行某项操作或处于某种初始状态。双鸟 代表两个需要并行或顺序执行的关联动作。在业务中可能是发送两条消息、更新两个状态、调用两个服务接口等。开门红 代表一个最终的核心动作或状态结果。通常是业务上最重要的一个操作比如创建订单、发放大奖、记录重要日志等。所以完整的业务逻辑是当且仅当满足“本区”、“单日”、“首秀”这三个前提条件时系统需要自动执行“双鸟”动作并最终完成“开门红”动作。为什么不能用简单的 if-else假设我们只有这三个条件伪代码如下if (isInTargetZone(context) isFirstShowToday(context) isWithinSingleDay(context)) { // 执行双鸟动作A doActionA(context); // 执行双鸟动作B doActionB(context); // 执行开门红动作 doFinalAction(context); }看起来清晰但业务是变化的明天产品经理说“单日”要改成“首周”“首秀”要增加一个“VIP用户”的判断。下个月另一个场景需要复用“双鸟”动作但前提条件是“跨区”和“非首秀”。“双鸟”的两个动作可能有依赖关系A成功才能执行B或者需要并行执行提升性能。很快代码里会散落着各种相似但又不完全相同的条件判断块修改一处逻辑可能会引发意想不到的BUG。我们需要一个将条件判断、动作执行与业务逻辑解耦的架构。2. 技术选型与环境准备我们将探索两种由浅入深的实现方案方案一基于状态模式State Pattern的设计- 适合规则相对固定、条件与状态强相关的场景强调代码的可读性和对状态转换的控制。方案二基于Drools规则引擎- 适合规则频繁变化、条件组合复杂、且希望由非技术人员如产品、运营维护规则的场景。环境说明本文示例基于 Java 17 和 Spring Boot 3.x 框架。构建工具使用 Maven。IDE 不限。方案一状态模式所需依赖仅需 Spring Boot Starter Web 即可主要用于创建简单的 REST 接口进行测试。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency方案二Drools所需依赖需要在 Spring Boot 项目中集成 Drools 规则引擎。dependency groupIdorg.drools/groupId artifactIddrools-engine/artifactId version7.73.0.Final/version !-- 请根据实际情况使用稳定版本 -- /dependency dependency groupIdorg.drools/groupId artifactIddrools-mvel/artifactId version7.73.0.Final/version /dependency !-- Spring 集成支持 (可选但推荐) -- dependency groupIdorg.kie/groupId artifactIdkie-spring/artifactId version7.73.0.Final/version exclusions exclusion groupIdorg.springframework/groupId artifactIdspring-tx/artifactId /exclusion /exclusions /dependency注意Drools版本请以官方仓库如 Maven Central最新稳定版为准不同版本间 API 可能有细微差异。3. 方案一使用状态模式实现规则引擎状态模式的核心思想是允许一个对象在其内部状态改变时改变它的行为对象看起来好像修改了它的类。在我们的场景中可以将整个规则执行流程看作一个状态机。初始是“待检查”状态依次通过各个条件检查后状态变迁并触发相应的动作。3.1 定义上下文与状态接口首先定义业务执行上下文RuleContext它包含了规则判断所需的所有数据和最终执行结果。// 文件路径src/main/java/com/example/rules/state/context/RuleContext.java import lombok.Data; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; Data public class RuleContext { /** 用户ID */ private String userId; /** 区域编码 */ private String zoneCode; /** 用户首次操作日期 */ private LocalDate firstShowDate; /** 当前操作日期 */ private LocalDate currentDate; /** 是否VIP */ private boolean isVip; // 执行过程记录 private ListString actionLogs new ArrayList(); // 最终结果 private String finalResult; public void logAction(String action) { this.actionLogs.add(action); System.out.println([ACTION LOG] action); } }接着定义状态接口RuleState。每个状态负责检查一部分条件并决定下一个状态。// 文件路径src/main/java/com/example/rules/state/core/RuleState.java public interface RuleState { /** * 执行当前状态的处理逻辑并返回下一个状态。 * param context 规则上下文 * return 下一个状态如果为null表示流程结束。 */ RuleState process(RuleContext context); }3.2 实现具体状态类我们按照“本区 - 单日 - 首秀 - 双鸟 - 开门红”的顺序设计状态。1. 检查区域状态 (CheckZoneState):// 文件路径src/main/java/com/example/rules/state/impl/CheckZoneState.java public class CheckZoneState implements RuleState { private static final String TARGET_ZONE SU-XI-CHANG; // 目标区域苏锡常 Override public RuleState process(RuleContext context) { context.logAction(开始检查区域条件...); if (TARGET_ZONE.equals(context.getZoneCode())) { context.logAction(✓ 区域条件满足: context.getZoneCode()); return new CheckDateState(); // 进入下一状态检查日期 } else { context.logAction(✗ 区域条件不满足流程终止。当前区域: context.getZoneCode()); return null; // 条件不满足终止流程 } } }2. 检查日期状态 (CheckDateState):// 文件路径src/main/java/com/example/rules/state/impl/CheckDateState.java public class CheckDateState implements RuleState { Override public RuleState process(RuleContext context) { context.logAction(开始检查单日条件...); // 假设“单日”指的是操作发生在当天 if (context.getCurrentDate().isEqual(LocalDate.now())) { context.logAction(✓ 单日条件满足); return new CheckFirstShowState(); // 进入下一状态检查是否首秀 } else { context.logAction(✗ 不在有效日期内流程终止。); return null; } } }3. 检查首秀状态 (CheckFirstShowState):// 文件路径src/main/java/com/example/rules/state/impl/CheckFirstShowState.java public class CheckFirstShowState implements RuleState { Override public RuleState process(RuleContext context) { context.logAction(开始检查首秀条件...); // 首次操作日期为空或者首次操作日期就是今天则认为是首秀 if (context.getFirstShowDate() null || context.getFirstShowDate().isEqual(context.getCurrentDate())) { context.logAction(✓ 首秀条件满足); return new ExecuteDoubleBirdState(); // 进入下一状态执行双鸟动作 } else { context.logAction(✗ 非首秀用户流程终止。); return null; } } }4. 执行双鸟动作状态 (ExecuteDoubleBirdState):// 文件路径src/main/java/com/example/rules/state/impl/ExecuteDoubleBirdState.java public class ExecuteDoubleBirdState implements RuleState { Override public RuleState process(RuleContext context) { context.logAction( 开始执行【双鸟】动作...); // 模拟动作A context.logAction( 执行双鸟动作A: 发送站内信...); // 实际业务中可能是notificationService.sendMsg(userId, typeA); // 模拟动作B context.logAction( 执行双鸟动作B: 更新用户积分...); // 实际业务中可能是pointService.addPoints(userId, 100); context.logAction( 【双鸟】动作执行完毕。); return new ExecuteFinalActionState(); // 进入最终状态 } }5. 执行最终动作状态 (ExecuteFinalActionState):// 文件路径src/main/java/com/example/rules/state/impl/ExecuteFinalActionState.java public class ExecuteFinalActionState implements RuleState { Override public RuleState process(RuleContext context) { context.logAction( 开始执行【开门红】最终动作...); // 模拟核心业务操作如创建订单 String orderId ORDER- System.currentTimeMillis(); context.logAction( 创建订单成功订单号: orderId); context.setFinalResult(开门红成功订单号: orderId); context.logAction( 【开门红】动作执行完毕。); return null; // 流程结束 } }3.3 创建状态机与客户端调用创建一个状态机来驱动整个流程// 文件路径src/main/java/com/example/rules/state/core/StateMachine.java public class StateMachine { private RuleState currentState; public StateMachine(RuleState initialState) { this.currentState initialState; } public void execute(RuleContext context) { while (currentState ! null) { currentState currentState.process(context); } context.logAction(规则引擎状态机执行完毕。); } }最后创建一个 Spring Boot 控制器进行测试// 文件路径src/main/java/com/example/rules/controller/StateRuleController.java import com.example.rules.state.context.RuleContext; import com.example.rules.state.core.StateMachine; import com.example.rules.state.impl.CheckZoneState; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; RestController public class StateRuleController { PostMapping(/api/rule/execute/state) public String executeRuleByState(RequestBody RuleContext requestContext) { // 设置当前日期模拟 requestContext.setCurrentDate(LocalDate.now()); // 初始化状态机从第一个状态检查区域开始 StateMachine machine new StateMachine(new CheckZoneState()); machine.execute(requestContext); return requestContext.getFinalResult() ! null ? requestContext.getFinalResult() : 规则未完全满足无最终动作执行。; } }测试与结果使用 Postman 或 curl 发送 POST 请求到http://localhost:8080/api/rule/execute/stateBody 为 JSON{ userId: user123, zoneCode: SU-XI-CHANG, firstShowDate: null, isVip: false }控制台会输出清晰的日志展示状态流转过程最终返回“开门红成功订单号: ORDER-...”。方案一总结状态模式将复杂的条件分支拆解到各个独立的类中符合单一职责原则。新增或修改一个条件只需增加或修改一个状态类不会影响其他逻辑。但它仍然需要编码实现当规则数量爆炸式增长时维护成本会变高。4. 方案二使用Drools规则引擎实现Drools 是一个业务规则管理系统BRMS它允许我们将业务规则从应用程序代码中分离出来并用一种接近自然语言的格式.drl文件来编写。4.1 配置Drools与Spring Boot集成首先创建 Drools 配置类用于加载规则文件并创建KieContainer。// 文件路径src/main/java/com/example/rules/drools/config/DroolsConfig.java import org.kie.api.KieServices; import org.kie.api.builder.KieBuilder; import org.kie.api.builder.KieFileSystem; import org.kie.api.builder.KieModule; import org.kie.api.runtime.KieContainer; import org.kie.internal.io.ResourceFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class DroolsConfig { private static final String RULES_PATH rules/; private static final KieServices KIE_SERVICES KieServices.Factory.get(); Bean public KieContainer kieContainer() { KieFileSystem kieFileSystem KIE_SERVICES.newKieFileSystem(); // 加载 classpath:rules/ 目录下所有 .drl 规则文件 kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH business-rule.drl, UTF-8)); KieBuilder kieBuilder KIE_SERVICES.newKieBuilder(kieFileSystem); kieBuilder.buildAll(); KieModule kieModule kieBuilder.getKieModule(); return KIE_SERVICES.newKieContainer(kieModule.getReleaseId()); } }4.2 定义事实对象Facts在 Drools 中传入引擎进行规则匹配的 Java 对象称为“事实Fact”。我们复用之前的RuleContext但为了更好配合 Drools可以为其添加一些注解和方法。// 文件路径src/main/java/com/example/rules/drools/fact/RuleFact.java import lombok.Data; import java.time.LocalDate; import java.util.ArrayList; import java.util.List; Data public class RuleFact { private String userId; private String zoneCode; private LocalDate firstShowDate; private LocalDate currentDate; private boolean isVip; // 规则执行结果和过程 private ListString actionLogs new ArrayList(); private String finalResult; // 一个标志位用于控制规则执行流程例如满足条件后不再执行其他无关规则 private boolean ruleMatched false; public void log(String message) { this.actionLogs.add(message); System.out.println([DROOLS LOG] message); } }4.3 编写业务规则.drl文件在src/main/resources/rules/目录下创建business-rule.drl文件。// 文件路径src/main/resources/rules/business-rule.drl package com.example.rules import com.example.rules.drools.fact.RuleFact import java.time.LocalDate // 规则1: 检查“本区”条件 rule Rule: Check Target Zone salience 100 // 优先级值越大越先执行 when $fact: RuleFact(zoneCode SU-XI-CHANG, ruleMatched false) then $fact.log(✓ 区域条件满足: $fact.getZoneCode()); modify($fact) { // 修改事实可能会触发其他规则重新匹配 setRuleMatched(true); // 简单起见这里先标记实际可能更复杂 } end // 规则2: 检查“单日”条件 (依赖于规则1通过) rule Rule: Check Single Day salience 90 when $fact: RuleFact(ruleMatched true, currentDate ! null, currentDate LocalDate.now()) then $fact.log(✓ 单日条件满足); // 可以设置另一个标志位或者使用更复杂的议程组(agenda-group)控制流程 end // 规则3: 检查“首秀”条件 rule Rule: Check First Show salience 80 when $fact: RuleFact(ruleMatched true, (firstShowDate null || firstShowDate currentDate)) then $fact.log(✓ 首秀条件满足); $fact.log( 开始执行【双鸟】动作...); $fact.log( 执行双鸟动作A: 发送站内信...); $fact.log( 执行双鸟动作B: 更新用户积分...); $fact.log( 【双鸟】动作执行完毕。); end // 规则4: 执行“开门红”最终动作 (依赖于以上条件) rule Rule: Execute Final Action salience 70 when $fact: RuleFact(ruleMatched true) // 这里理想情况下应该检查前面所有条件都已满足的某种聚合状态 // 为了示例简化我们假设前面规则都触发了log这里直接执行最终动作 then $fact.log( 开始执行【开门红】最终动作...); String orderId DROOLS-ORDER- System.currentTimeMillis(); $fact.log( 创建订单成功订单号: orderId); $fact.setFinalResult(Drools引擎驱动-开门红成功订单号: orderId); $fact.log( 【开门红】动作执行完毕。); end // 一个兜底规则用于处理条件不满足的情况 rule Default: No Rule Matched salience -100 // 低优先级 when $fact: RuleFact(ruleMatched false) then $fact.log(✗ 初始条件不满足无规则触发。); $fact.setFinalResult(规则未触发); end注意上述规则文件是一个简化示例。在实际复杂场景中通常会使用agenda-group、activation-group或ruleflow-group来更精确地控制规则执行顺序和生命周期而不是简单依赖salience和ruleMatched标志。4.4 创建服务层与控制器创建服务来封装 Drools 引擎的调用。// 文件路径src/main/java/com/example/rules/drools/service/DroolsRuleService.java import com.example.rules.drools.fact.RuleFact; import lombok.RequiredArgsConstructor; import org.kie.api.runtime.KieContainer; import org.kie.api.runtime.KieSession; import org.springframework.stereotype.Service; Service RequiredArgsConstructor public class DroolsRuleService { private final KieContainer kieContainer; public RuleFact executeRules(RuleFact fact) { // 从容器中获取一个新的 KieSession会话它是规则执行的运行时 KieSession kieSession kieContainer.newKieSession(); try { // 将事实对象插入到会话的工作内存中 kieSession.insert(fact); // 触发所有匹配的规则 int rulesFired kieSession.fireAllRules(); System.out.println(触发了 rulesFired 条规则。); } finally { // 非常重要必须释放会话资源防止内存泄漏 kieSession.dispose(); } return fact; } }创建控制器提供接口。// 文件路径src/main/java/com/example/rules/controller/DroolsRuleController.java import com.example.rules.drools.fact.RuleFact; import com.example.rules.drools.service.DroolsRuleService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import java.time.LocalDate; RestController public class DroolsRuleController { private final DroolsRuleService ruleService; public DroolsRuleController(DroolsRuleService ruleService) { this.ruleService ruleService; } PostMapping(/api/rule/execute/drools) public String executeRuleByDrools(RequestBody RuleFact requestFact) { requestFact.setCurrentDate(LocalDate.now()); RuleFact resultFact ruleService.executeRules(requestFact); return resultFact.getFinalResult(); } }测试与结果发送 POST 请求到http://localhost:8080/api/rule/execute/droolsBody 为 JSON{ userId: user456, zoneCode: SU-XI-CHANG, firstShowDate: null, isVip: true }控制台将输出由 Drools 引擎触发的规则日志并返回最终结果。方案二总结Drools 将业务规则外部化、声明化。产品经理或业务分析师可以在不修改 Java 代码、不重启服务的情况下结合 Drools Workbench 或数据库存储规则通过修改.drl文件来调整业务逻辑。它极大地提升了复杂规则系统的可维护性和灵活性。5. 两种方案对比与常见问题排查5.1 方案对比特性状态模式 (State Pattern)Drools 规则引擎学习成本低纯面向对象设计模式中高需学习 DRL 语法、引擎 API维护性规则修改需改代码、重新部署规则可外部化热更新可能需额外设计灵活性一般规则逻辑硬编码极高规则可动态组合、复杂度高性能高直接方法调用中需引擎匹配、推理但优化后很好适用场景规则相对稳定、流程清晰、状态驱动规则频繁变化、组合复杂、由非技术人员维护5.2 常见问题与排查思路状态模式常见问题状态类过多如果条件组合爆炸会导致状态类数量激增。可以考虑使用表驱动或组合模式来管理简单状态。状态流转复杂在process方法中返回null或下一个状态如果流程复杂容易出错。可以引入一个状态机配置器用 XML 或 JSON 定义状态图然后由引擎驱动。共享上下文污染RuleContext被所有状态访问需注意线程安全和不恰当的修改。尽量设计为不可变对象或深度拷贝。Drools 集成常见问题规则不触发检查 Fact 对象确保插入到 KieSession 的对象属性与规则 LHS (when) 部分的条件完全匹配包括类型、值。检查包路径.drl文件中的package声明需与import的类路径对应。检查规则语法DRL 语法严格注意拼写和符号。查看日志启用 Drools 日志 (KieServices.Factory.get().getLoggers().newConsoleLogger(kieSession)) 查看规则加载和匹配过程。性能问题避免在 LHS 中使用复杂的 Java 方法调用这会影响匹配性能。合理使用salience、agenda-group控制规则执行顺序避免不必要的全量匹配。及时dispose()KieSession防止内存泄漏。规则更新不生效默认情况下KieContainer是静态的规则文件修改后需要重启应用或动态刷新容器。生产环境通常结合KieScanner或从数据库读取规则实现动态更新。6. 最佳实践与工程建议明确边界按需选择如果业务规则简单、稳定且开发团队对 Drools 不熟悉状态模式或策略模式是更轻量、更直观的选择。如果规则数量多几十上百条、变动频繁、且希望业务人员参与维护Drools 等规则引擎的优势非常明显。设计可测试的规则状态模式为每个RuleState编写单元测试模拟不同的RuleContext输入验证状态转换和动作执行。Drools为每个重要的规则或规则组编写测试用例。可以创建一个专门的测试类初始化KieSession插入测试 Fact触发规则然后断言 Fact 的状态或执行日志。确保规则修改后原有测试用例依然通过。规则版本管理与回滚无论是代码中的状态类还是.drl文件都必须纳入 Git 等版本控制系统。对于 Drools在生产环境更新规则时必须有完善的灰度发布和快速回滚机制。可以考虑将规则文件存储在数据库或配置中心通过版本号进行管理。监控与告警在关键的状态节点或规则执行前后添加监控点记录执行次数、成功率、耗时等指标。对于 Drools监控KieSession的创建销毁频率、规则触发次数有助于发现性能问题或规则循环。文档化使用注释或单独的文档清晰描述每个状态/规则的意图、触发条件和执行动作。这对于后续维护和交接至关重要。通过本文的两种实现方案我们从简单的条件判断出发逐步构建了一个健壮、可扩展的业务规则执行框架。状态模式提供了清晰的代码结构和控制流而 Drools 则提供了无与伦比的业务灵活性和解耦能力。在实际项目中你可以根据团队的技术栈、业务复杂度和变更频率选择最适合的那把“钥匙”来优雅地解开“中二节奏”般的复杂业务逻辑之锁。下次再遇到类似“在A条件下做B和C然后一定要做D”的需求时不妨试试这两种方法。
返回列表