资讯动态

条件工作流无类型写法:把 if 判断变成可配置数据

发布时间:2026/8/30 6:15:33 来源:尧图企业网站定制
最近接到一个很典型的业务需求订单金额超过 1000 元的走总监审批低于 1000 元的自动通过。放在两三年前我会直接在 Java 代码里写一个if (order.getAmount() 1000)然后调用审批流程接口。但现在再这么写问题就大了。原因是这类条件判断不是只出现一次。今天加一个“会员等级为 VIP 且下单次数大于 5 次走专属客服”明天加一个“风控评分小于 60 走人工复核”后天还要调整 1000 这个阈值。每一次改动都要改代码、走发布流程、运维上线流程和需求互相卡脖子业务方等不起后端也烦。条件工作流的无类型写法解决的就是这个痛点把“条件判断逻辑”从代码里抽出来变成一份可配置、可热更新、可审计的数据。我们用 JSON 描述条件用表达式引擎动态求值业务主干代码不用再跟着规则频繁变动。这是本系列的第 140 个主题今天我会把这种写法的原理、实现和坑一次性讲清楚。一个比较关键的理解是无类型写法的核心价值不是省去类型声明那么表面而是把“条件”从代码资产变成了数据资产。但代价也很明显——编译期类型检查没了跨系统传参更容易出错表达式执行还有安全风险。所以这篇文章不只要给你能跑的代码还会把该补的工程手段一起说清楚。1. 这篇文章真正要解决的问题先说一个真实的开发场景。你负责一个审批平台上游业务方提交订单后系统要根据订单属性决定审批路径。最朴素的做法是写一个 Service 方法public String decideApprovalPath(Order order) { if (order.getAmount() 1000) { return director_approval; } if (VIP.equals(order.getUserLevel()) order.getOrderCount() 5) { return vip_service_approval; } return auto_pass; }这段代码本身没有问题问题出在它进入了业务频繁变动的链路里。业务方的需求是“这个分支条件能不能由我们自己配”而后端的痛苦是“每条规则都意味着一次代码变更”。随着规则越来越多这个方法会变成一长串 if/else可读性下降测试用例膨胀版本发布频率也被迫提高。条件工作流的无类型写法就是为了把这类“条件分支”从硬编码中解放出来。它的本质是条件是一段字符串数据是一张 Map执行引擎负责把两者结合起来求值。生产环境里即使规则变化也不用重新编译 Java 代码只要更新配置或数据库里的规则记录。这篇文章适合以下读者正在做审批流、工单流、规则引擎的后端开发想把业务规则从代码中剥离、交给配置中心管理的团队遇到“规则天天变、发版跟不上”问题的同学想学习表达式引擎、动态条件判断写法的入门者。读完你会得到三样东西一套可直接运行的极简条件工作流示例一份无类型写法的设计模型一份表达式的安全与性能排查清单。2. 基础概念与核心原理2.1 什么是条件工作流工作流里最常见的节点有三种开始节点、审批任务节点、条件分支节点。条件分支节点就是根据上下文数据判断“下一步走哪条路”。比如订单金额大走总监审批金额小自动通过这就是一个最简条件工作流。在流程引擎里条件节点通常有一个表达式属性引擎根据表达式求值结果决定流转到哪个后续节点。表达式越灵活流程的可编排性越强表达式越死板流程就越容易变成写死的状态机。2.2 什么是无类型写法无类型写法指的是在定义和执行条件时不预先声明字段的数据类型数据统一用 Map、JSON 这类结构承载表达式的参数类型在运行期动态匹配。例如{ field: amount, operator: , value: 1000 }这段配置里的amount不需要在 Java 类里定义对应字段value也不需要在编译期指定是 Integer、Long 还是 BigDecimal。运行时把订单数据转成MapString, Object取amount的值与value比较即可。对比强类型写法同样是“金额大于 1000”这个条件对比维度强类型写法无类型写法示例order.getAmount() 1000amount 1000类型检查编译期检查类型安全运行期动态匹配变更成本改代码、重新编译、发版改配置可立即生效可读性需要懂 Java 语法业务人员也能看懂表达式可测试性单元测试覆盖需要表达式测试用例和规则校验安全性无表达式注入问题需防范表达式注入与恶意脚本性能方法调用开销低表达式解析有额外开销需缓存这里要澄清一个容易误解的点无类型不代表“没有类型”而是“类型在运行期才确定”。amount 1000这个表达式如果amount传进来的是字符串abc求值时会报类型错误。所以无类型写法必须配套“入参校验”和“表达式语法校验”否则问题会被推迟到线上才暴露。2.3 无类型写法的三个典型场景第一类是规则引擎和条件工作流也就是本文的主场景。第二类是 MyBatis 动态 SQL 中的if test...它在 Mapper XML 里写的status ! null并不会声明 status 的类型底层也是从参数对象或 Map 里动态取值判断。第三类是 JavaScript 这类动态类型语言中的箭头函数写法比如list.filter(item item.amount 1000)参数 item 不需要声明类型。这三类场景的共同点是一致的判断条件的字段不预先绑定强类型而是在运行时从上下文动态获取。这正是无类型写法在不同技术栈里的不同面貌。3. 环境准备与前置条件下面示例采用 Java 8 Maven 工程表达式引擎使用 Aviator。Aviator 是一个轻量级 Java 表达式引擎对 Map 类型支持很好表达式写法接近自然语言适合做无类型条件求值。版本请以实际项目依赖为准本文重点演示通用思路不要直接拷贝一个不确定的版本号用于生产。在 pom.xml 中引入依赖dependency groupIdcom.googlecode.aviator/groupId artifactIdaviator/artifactId version5.3.3/version /dependency如果 Maven 拉取失败或团队不使用 Aviator也可以换成 Spring 的 SpEL 或阿里巴巴 QlExpress核心思路一致只是表达式语法和 API 略有差异。版本准备说明JDK 8 及以上版本均可不需要额外安装数据库示例使用本地文件加载规则需要 Lombok 的话可以自行引入本文为了避免额外依赖统一使用手写 getter/setter推荐使用 IDEA 或 Eclipse普通命令行编译也能运行。4. 核心设计把条件工作流拆成数据在设计条件工作流前先明确目标我们希望流程条件不要写在 Java 代码里而是通过配置文件或数据库记录来维护。因此需要一套数据结构来描述“条件”和“分支”。4.1 规则模型设计最简的三张表模型如下规则 ID标识一条分支条件条件表达式一段字符串例如amount 1000 status PAID目标节点编码条件命中后流转到哪个节点。落到 Java 对象上public class RuleNode { private String ruleId; private String expression; private String targetNode; // getter / setter 省略 }如果需要在多个条件之间做多分支匹配就使用ListRuleNode引擎按顺序匹配命中第一条就返回对应分支。4.2 完整的条件节点 JSON 示例下面是一个典型的多分支条件工作流配置。以订单审批为例包含三个分支自动通过、总监审批、人工复核。{ processId: order_approval, rules: [ { ruleId: rule_auto_pass, expression: amount 1000 status PAID, targetNode: auto_pass }, { ruleId: rule_director_approval, expression: amount 1000 userLevel VIP, targetNode: director_approval }, { ruleId: rule_manual_review, expression: riskScore 60, targetNode: manual_review } ], defaultNode: auto_pass }这一段 JSON 就是“无类型”的直观体现amount、userLevel、riskScore都没有在 Java 里声明类型它们只是表达式中的变量名运行时我们传入一个MapString, Object引擎会把自己能取到的变量填充进表达式。4.3 表达式设计规范表达式不是 SQL也不是 Java 完整语法它只需要解决“判断”这一个问题。因此建议收敛表达式能力集合避免让规则编写者写出过于复杂的逻辑。个人经验只允许比较运算、、、、、!只允许逻辑运算、||、!变量名统一使用驼峰命名避免和 Java 关键字冲突字符串字面量使用单引号Aviator 中单引号和双引号都支持但统一规范更利于排查禁止在表达式里调用自定义方法除非你明确知道自己在做什么。5. 完整示例与代码实现下面给出一个可运行的极简条件工作流。整个工程只需要三个文件规则加载器、流程引擎、测试入口。5.1 工程结构src/main/java/com/example/flow/ ├── FlowEngine.java ├── RuleLoader.java └── Main.java src/main/resources/rules/order_approval.json5.2 条件规则加载器// 文件路径src/main/java/com/example/flow/RuleLoader.java package com.example.flow; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONObject; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.util.List; public class RuleLoader { public static ListRuleNode loadRules(String resourcePath) throws Exception { InputStream is RuleLoader.class.getClassLoader() .getResourceAsStream(resourcePath); if (is null) { throw new IllegalArgumentException(Cannot find rule file: resourcePath); } String content new String(is.readAllBytes(), StandardCharsets.UTF_8); JSONObject obj JSON.parseObject(content); return obj.getJSONArray(rules).toJavaList(RuleNode.class); } }这段代码负责把 JSON 规则文件解析成 Java 对象列表。如果你不想引入 fastjson2也可以换成 Jackson 或 Gson只要能把rules数组映射成ListRuleNode即可。5.3 条件节点对象// 文件路径src/main/java/com/example/flow/RuleNode.java package com.example.flow; public class RuleNode { private String ruleId; private String expression; private String targetNode; public String getRuleId() { return ruleId; } public void setRuleId(String ruleId) { this.ruleId ruleId; } public String getExpression() { return expression; } public void setExpression(String expression) { this.expression expression; } public String getTargetNode() { return targetNode; } public void setTargetNode(String targetNode) { this.targetNode targetNode; } }5.4 核心流程引擎这是最关键的部分。引擎接收业务数据和规则列表将业务对象转成无类型的MapString, Object然后逐条计算表达式命中即返回目标节点。// 文件路径src/main/java/com/example/flow/FlowEngine.java package com.example.flow; import com.googlecode.aviator.AviatorEvaluator; import com.googlecode.aviator.Expression; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; public class FlowEngine { private static final MapString, Expression CACHE new ConcurrentHashMap(); public String decide(MapString, Object bizData, java.util.ListRuleNode rules, String defaultNode) { for (RuleNode rule : rules) { if (evaluate(rule.getExpression(), bizData)) { return rule.getTargetNode(); } } return defaultNode; } private boolean evaluate(String expression, MapString, Object bizData) { try { Expression compiled CACHE.computeIfAbsent(expression, AviatorEvaluator::compile); return Boolean.TRUE.equals(compiled.execute(bizData)); } catch (Exception e) { // 生产环境请换成标准日志框架 System.err.println(evaluate error, expression expression , data bizData); return false; } } }代码里做了两件重要的事一是把表达式编译结果缓存起来避免每次求值都重新解析语法树性能损失会小很多二是求值异常时不要直接抛出先记录日志再返回默认分支这与“流程不能因单条规则异常而中断”的容错思路有关。5.5 业务数据与测试入口// 文件路径src/main/java/com/example/flow/Main.java package com.example.flow; import java.util.HashMap; import java.util.List; import java.util.Map; public class Main { public static void main(String[] args) throws Exception { ListRuleNode rules RuleLoader.loadRules(rules/order_approval.json); FlowEngine engine new FlowEngine(); String defaultNode auto_pass; // 场景1普通订单金额 500已支付 MapString, Object order1 new HashMap(); order1.put(amount, 500); order1.put(status, PAID); order1.put(userLevel, NORMAL); order1.put(riskScore, 80); System.out.println(order1 - engine.decide(order1, rules, defaultNode)); // 场景2VIP 用户大额订单金额 5000 MapString, Object order2 new HashMap(); order2.put(amount, 5000); order2.put(status, PAID); order2.put(userLevel, VIP); order2.put(riskScore, 90); System.out.println(order2 - engine.decide(order2, rules, defaultNode)); // 场景3风控分低需要人工复核 MapString, Object order3 new HashMap(); order3.put(amount, 800); order3.put(status, PAID); order3.put(userLevel, NORMAL); order3.put(riskScore, 40); System.out.println(order3 - engine.decide(order3, rules, defaultNode)); } }这里传入的是一个普通的HashMap没有专门定义Order类这就是无类型写法的落地形态业务数据以 Map 传递表达式中出现的变量名必须和 Map 的 key 对得上。5.6 对照MyBatis 动态 SQL 的无类型写法条件工作流的无类型写法和 MyBatis 动态 SQL 里if test...的写法底层思想是相通的。比如一个订单查询接口筛选条件可能有状态、最小金额这些条件不是固定的用 Java 拼接 SQL 很容易出错MyBatis 的写法是select idselectOrders resultTypemap SELECT order_id, amount, status, user_level FROM orders WHERE 1 1 if teststatus ! null and status ! AND status #{status} /if if testminAmount ! null AND amount gt; #{minAmount} /if /select这里的status、minAmount来自查询参数可能是 POJO 的属性也可能是 Map 的 key。MyBatis 并不要求在编译期声明这些参数的类型而是运行时从参数对象中反射取值作用机制与无类型条件表达式的思路一致。这里有一个新手常踩的坑XML 的test属性里写amount #{minAmount}是错的因为 XML 中如果不转义会被解析成标签闭合符。需要写成amount gt; #{minAmount}或者用amount ge #{minAmount}这类 OGNL 支持的形式。5.7 进一步IDEA 等环境中如何运行在 IDEA 中直接运行Main类即可。如果你使用 Maven 命令行mvn compile exec:java -Dexec.mainClasscom.example.flow.Main也可以把工程打包成可执行 jar 后再运行这取决于你的项目构建方式。6. 运行结果与效果验证运行Main后预期输出如下order1 - auto_pass order2 - director_approval order3 - manual_review如何判断运行成功场景 1 金额 500不满足amount 1000也不满足riskScore 60所以进入默认分支auto_pass场景 2 金额 5000 且是 VIP命中amount 1000 userLevel VIP进入director_approval场景 3 风控分 40命中riskScore 60进入manual_review。建议再增加两个验证角度第一验证“条件顺序影响结果”。如果把第一条规则改成riskScore 60场景 3 先被这条规则匹配返回manual_review前其他规则不会再执行这是顺序匹配的预期行为。第二验证“表达式中变量缺失”时的容错。把场景 1 的riskScore去掉再运行因为表达式涉及riskScore 60引擎会抛变量缺失异常。生产环境里这种情况很常见所以引擎兜底返回默认节点但日志里必须能追踪到。如果运行失败第一件事先看控制台是否出现evaluate error日志。出现错误后检查三个方面JSON 文件路径是否正确、表达式变量名是否和 Map 的 key 一致、表达式语法是否合法。Aviator 对空值比较敏感amount如果是字符串类型直接和数字比较会报错。7. 常见问题与排查思路问题现象可能原因排查方式解决方案表达式解析异常表达式语法错误查看异常堆栈和规则 ID使用 Aviator 官方语法校验工具或写单元测试变量缺失报错Map 中缺少表达式里的变量打印本次求值 bizData统一在入口处校验必填变量数字比较结果异常amount 是 String 类型打印变量实际类型入参转换为同类型后再求值命中错误分支条件顺序不对查看规则优先级明确规则按序匹配把最严格条件放前面XML 动态 SQL 报错未转义打开 XML 查看是否被解析成标签使用gt;或ge性能下降表达式每次都重新编译查看是否使用缓存使用 Expression 缓存每条规则都返回默认节点所有表达式都没命中开启引擎日志逐条规则打印表达式与求值结果真正麻烦的是类型比较问题。比如金额在 JSON 里写的是1000解析时可能变成 Integer从数据库查出来的 SUM 结果可能是 BigDecimal前端传过来的金额又可能是 String1000.00。无类型写法把这些差异全部暴露在运行期所以入参规范转换在实践里比表达式本身更重要。8. 最佳实践与工程建议8.1 表达式安全是第一优先级无类型写法引入了一个风险点如果把表达式配置暴露给不可信用户对方可以构造恶意表达式。即使只开放比较和逻辑运算也要防止变量名注入、长表达式拖垮内存。生产环境建议遵循规则配置只允许后台管理员修改表达式长度限制在 200 字符以内不在表达式中开放env、sys这类内置变量使用独立账号或安全组限制规则读写的接口权限对表达式下发做变更审批保留操作审计日志。8.2 参数校验与数据转换引擎内部虽然是无类型 Map但入口处必须有明确的参数契约。建议在业务调用方先把业务对象转成 Map并做一层“字段是否缺失、类型是否合法”的校验。例如public MapString, Object toContext(Order order) { MapString, Object ctx new HashMap(); ctx.put(amount, order.getAmount() null ? BigDecimal.ZERO : order.getAmount()); ctx.put(status, order.getStatus()); ctx.put(userLevel, order.getUserLevel()); ctx.put(riskScore, order.getRiskScore()); return ctx; }这一步看起来多写了几行代码但能避免大量运行期类型异常。8.3 规则版本管理与回滚条件工作流的无类型写法让配置可以热更新但也意味着“一个坏配置可能让全量流程走错分支”。所以必须给规则加上版本号。推荐方案规则表增加version字段和effective_time生效时间每次修改不覆盖旧记录而是新增一条新版本引擎加载时优先读取生效时间最新的版本发布新版本后观察一段时间发现问题可秒级回滚到旧版本。8.4 日志与链路追踪表达式求值失败时只打印“evaluate error”是不够的。生产环境里你根本不知道是哪一笔订单、哪条规则出了问题。建议日志里至少包含processId、ruleId、expression、bizData的关键摘要、耗时。如果要全量打印 Map注意脱敏避免把手机号、身份证号写进日志。8.5 性能优化表达式编译是有开销的务必缓存编译结果。Aviator 的AviatorEvaluator.compile返回的Expression是线程安全的可以像示例一样放入ConcurrentHashMap。另外规则数量控制在几百条内单次请求遍历求值的耗时基本可以忽略如果规则上十万条就要考虑用规则树或索引结构来剪枝。8.6 团队协作约定无类型写法降低了后端介入成本也提高了规则编写自由度。团队里最好定一份简单的“规则编写规范”变量命名统一、布尔值不要写入表达式、字符串用单引号、表达式不允许包含分号。这些约定能在团队扩大后减少大量排错时间。9. 总结与后续学习方向条件工作流的无类型写法把所有条件判断从 Java 代码中迁移到 JSON 配置和表达式字符串里。它改变的不只是代码结构更是流程的变更模式以前改规则要发版现在改配置即生效以前规则逻辑散落在多个 Service 中现在集中在一份配置里可以审计、可以回滚、可以对比版本。需要注意的是无类型写法适合“条件变化频繁、业务人员参与配置”的场景并不是所有地方都该用。如果一段条件的业务逻辑极其稳定、性能和类型安全要求极高老老实实写 Java 代码仍然是最优解。所谓无类型本质上是一种取舍用一部分运行期风险换取流程配置化的敏捷性。下一步建议按这个顺序深入把本文示例接一个真实场景比如审批流或工单流转了解 Aviator 的变量绑定、函数注册、异常处理细节研究规则引擎的成熟方案比如 Drools对比表达式配置和规则文件的差异如果你的项目已经在用 MyBatis 动态 SQL可以顺手把if test的 OGNL 原理搞明白两者底层思维很接近。条件工作流的本质是“让条件成为数据”。理解这一点后你会发现很多看起来复杂的技术——规则引擎、流程编排、DSL 设计——底层都是同一个思路把代码里的判断逻辑慢慢变成可描述、可传递、可变化的数据。这也是本文想传递的核心判断。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价