4. 复杂审批规则引擎:规则引擎选型与动态配置
说到审批流里的复杂规则,我见过不少团队一开始用硬编码写if-else,结果业务一变更,代码改得想哭。其实,规则引擎就是专门干这个的——把业务规则从代码里抽出来,让它们变成可配置、可热加载的独立模块。
这一章,咱们就聊聊规则引擎的选型、动态配置、热加载,还有那个容易踩坑的冲突检测。
4.1 规则引擎选型:Drools vs EasyRules
选型这事儿,说白了就是看你的场景有多复杂。我个人的经验是:别一上来就上重型引擎,也别啥都手写。
| 对比维度 | Drools | EasyRules |
|---|---|---|
| 规则复杂度 | 支持复杂推理、前向链、后向链 | 适合简单条件-动作规则 |
| 学习曲线 | 陡峭,需要理解DRL语法 | 平缓,纯Java注解即可 |
| 性能 | Rete算法优化,适合大量规则 | 线性匹配,规则少时够用 |
| 热加载 | 支持,但需处理KieSession | 简单,重新加载Rules即可 |
| 适用场景 | 风控、医疗诊断、复杂审批 | 简单审批、配置校验、小规则集 |
我在项目中遇到过这样一个情况:一个OA审批系统,刚开始只有十几条规则,用EasyRules跑得挺欢。结果业务扩张到几百条规则,还涉及多条件交叉判断,EasyRules就开始吃力了。后来迁移到Drools,虽然初期配置麻烦点,但Rete算法的效率确实高。
我的建议:
- 规则少于50条,且逻辑简单 → EasyRules
- 规则超过100条,或需要复杂推理 → Drools
- 团队有Java功底但没时间学DRL → EasyRules
- 需要规则版本管理、冲突检测 → Drools
4.2 动态规则配置:让业务人员也能改规则
动态配置的核心,就是把规则存到数据库或配置中心,而不是写在代码里。嗯,这里要注意:规则的结构设计决定了后续的灵活性。
我常用的做法是设计一个规则配置表:
-- 规则配置表
CREATE TABLE approval_rule (
id BIGINT PRIMARY KEY,
rule_name VARCHAR(100),
rule_condition TEXT, -- 条件表达式,如 "amount > 10000"
rule_action TEXT, -- 动作,如 "route_to:VP"
priority INT, -- 优先级,数字越小越优先
status VARCHAR(10), -- ENABLED / DISABLED
version INT,
create_time DATETIME
);
然后,在Java里用SPEL或MVEL解析这些条件。举个例子:
// 使用MVEL解析动态条件
public boolean evaluateCondition(String condition, Map<String, Object> facts) {
return MVEL.evalToBoolean(condition, facts);
}
// 调用示例
Map<String, Object> facts = new HashMap<>();
facts.put("amount", 15000);
facts.put("department", "Finance");
boolean result = evaluateCondition("amount > 10000 && department == 'Finance'", facts);
// 返回 true
避坑指南:我曾经把规则条件直接存成字符串,结果业务人员写了个语法错误的条件,整个审批流崩了。后来我加了一层语法校验,在保存规则时先用MVEL预编译一次,编译失败直接报错。
4.3 规则热加载:不停机更新规则
热加载,说白了就是让规则在运行时更新,不用重启应用。你想想看,一个OA系统白天几百人在用,你为了改一条规则重启服务,那不得被骂死?
实现热加载,我一般用两种方式:
- 轮询方式:每隔30秒检查数据库规则版本号,有变化就重新加载。
- 事件通知:用Redis的Pub/Sub或消息队列,规则变更时主动通知应用。
这里给个Drools热加载的示例:
public class DroolsRuleLoader {
private KieContainer kieContainer;
private volatile long lastLoadTime = 0;
public void reloadIfNeeded() {
// 检查数据库中的规则版本
long latestVersion = ruleRepository.getLatestVersion();
if (latestVersion > lastLoadTime) {
synchronized (this) {
// 重新构建KieContainer
KieServices ks = KieServices.Factory.get();
KieFileSystem kfs = ks.newKieFileSystem();
// 从数据库加载规则内容
List<Rule> rules = ruleRepository.findAllEnabled();
for (Rule rule : rules) {
String drlContent = buildDrl(rule);
kfs.write("src/main/resources/" + rule.getId() + ".drl",
drlContent);
}
KieBuilder kb = ks.newKieBuilder(kfs).buildAll();
this.kieContainer = ks.newKieContainer(kb.getKieModule().getReleaseId());
this.lastLoadTime = latestVersion;
System.out.println("规则已热加载,版本时间: " + latestVersion);
}
}
}
}
注意:热加载时,正在执行的审批实例怎么办?我建议用版本号隔离——旧实例继续用旧规则,新实例用新规则。别一刀切,否则审批到一半规则变了,业务人员会疯的。
4.4 冲突检测:别让规则打架
规则多了,难免有冲突。比如一条规则说“金额大于1万转给总监”,另一条说“金额大于5万转给CEO”。那3万块钱的审批到底给谁?
冲突检测,我把它分成两类:
- 直接冲突:相同条件,不同动作。比如条件都是“amount > 10000”,一个动作是“route_to:Manager”,另一个是“route_to:VP”。
- 间接冲突:条件有包含关系,动作不同。比如“amount > 10000”和“amount > 50000”,动作不同。
检测方法其实不复杂:
public class ConflictDetector {
public List<Conflict> detectConflicts(List<Rule> rules) {
List<Conflict> conflicts = new ArrayList<>();
for (int i = 0; i < rules.size(); i++) {
for (int j = i + 1; j < rules.size(); j++) {
Rule r1 = rules.get(i);
Rule r2 = rules.get(j);
// 检查条件是否重叠
if (conditionsOverlap(r1.getCondition(), r2.getCondition())) {
// 检查动作是否不同
if (!r1.getAction().equals(r2.getAction())) {
conflicts.add(new Conflict(r1, r2, "条件重叠但动作不同"));
}
}
}
}
return conflicts;
}
private boolean conditionsOverlap(String cond1, String cond2) {
// 这里可以用SAT求解器或简单的区间检测
// 简单场景:检查数值范围是否有交集
// 复杂场景:用Z3或Drools的冲突检测API
return true; // 简化示例
}
}
我的经验:冲突检测别全自动化,容易误报。我一般做法是:
- 保存规则时自动检测,列出潜在冲突
- 让业务人员手动确认是否真的冲突
- 如果确认冲突,用优先级解决——优先级高的规则胜出
这样既避免了漏检,又不会因为误报影响效率。
4.5 实战:一个完整的审批规则引擎示例
最后,我整合一下上面的思路,给个完整的示例。假设我们要实现一个费用报销审批规则引擎:
// 规则引擎核心类
public class ApprovalRuleEngine {
private RuleRepository ruleRepository;
private Map<String, RuleExecutor> executors = new ConcurrentHashMap<>();
public ApprovalResult evaluate(ApprovalRequest request) {
// 1. 加载当前生效的规则
List<Rule> rules = ruleRepository.findByStatus("ENABLED");
// 2. 按优先级排序
rules.sort(Comparator.comparingInt(Rule::getPriority));
// 3. 冲突检测(可选,生产环境建议异步做)
// conflictDetector.detectConflicts(rules);
// 4. 匹配规则
for (Rule rule : rules) {
if (matchCondition(rule.getCondition(), request)) {
// 5. 执行动作
return executeAction(rule.getAction(), request);
}
}
// 6. 默认路由
return ApprovalResult.defaultRoute();
}
private boolean matchCondition(String condition, ApprovalRequest request) {
// 使用MVEL或SPEL解析条件
Map<String, Object> context = new HashMap<>();
context.put("amount", request.getAmount());
context.put("department", request.getDepartment());
context.put("level", request.getApplicantLevel());
return MVEL.evalToBoolean(condition, context);
}
}
这个引擎看起来简单,但实际生产环境里,我还会加上规则缓存、执行日志、性能监控。毕竟,规则引擎一旦出问题,整个审批流就瘫了。
最后说一句:规则引擎不是银弹。如果你的规则逻辑极其简单,或者变更频率极低,用硬编码反而更稳。别为了用技术而用技术,这是我在项目里摔过跟头才明白的。