零代码和可视化编排最近在 Java 圈子里确实火但市面上能直接落地的开源方案并不多。自己动手基于 Java 生态搭一个听起来工程量很大实际上把核心思路理清楚后反而比想象中简单。我当初做这个引擎主要就是为了解决团队里一个痛点每次接到新需求都要写一堆胶水代码去调用不同系统的接口改业务流程要发版测试回归周期长。做一个零代码可视化编排引擎的好处是业务人员或者后端同事直接在界面上拖拽节点、连线、配参数就能完成流程改造引擎执行的时候按图跑一遍就行不用动一行代码。这篇文章我会从架构选型、功能清单、核心实现、实操搭建、避坑记录五个方面完整拆解整个项目适合有一定 Java 基础、想自己搞一套流程编排或自动化工作流的读者参考。1. 项目概述与思路拆解1.1 这个引擎到底解决什么问题先聊清楚概念。零代码指的是业务配置过程中不需要编写源代码可视化编排指的是通过图形化界面拖拽节点、连接线来构建流程这两者结合起来的最终效果就是让一个流程的定义和执行从“写代码”变成“画图”。我这里说的编排引擎和传统的 BPM 工作流引擎有区别。BPM 侧重的是人机交互的审批流而编排引擎侧重的是系统之间的自动化调用和逻辑控制比如某个订单创建后自动调库存接口、调风控接口、写日志、发消息通知全部串起来。这个场景非常适合做成可视化编排。我当时选型时对比过 Flowable、Activiti 和自研轻引擎。Flowable 这类重量级工作流引擎功能确实强大但依赖活动表结构、用户体系、审批人策略对“系统间接口编排”这种纯自动化场景太重了。自研一个轻量级的编排引擎只关心“节点怎么组织、数据怎么流转、异常怎么处理”反而更可控、更好维护。我做这个引擎的定位是以 Java 后端为宿主面向 API 集成和业务自动化场景提供流程设计器、执行引擎、扩展节点体系三大部分。它适合作为独立服务部署也可以作为 SDK 嵌入现有 Spring Boot 项目。1.2 技术选型背后的考量整个引擎宿主框架选 Spring Boot这基本没有争议生态成熟、社区资料多、招人容易。持久层用 MyBatis-Plus主要是为了快速迭代流程定义、执行日志这些表结构都很简单用 MyBatis-Plus 的 CRUD 和 LambdaQueryWrapper 写起来很顺手。流程定义存储设计成 JSON 结构。我见过不少团队把流程定义存成 XML参考 BPMN但 JSON 对前端可视化编辑器的亲和度更高和后端对象模型互转也方便不需要引入 XML 绑定那一套复杂的东西。JSON 结构用 Jackson 序列化反序列化天然契合 Java 的 POJO。核心流程执行引擎没有用现成的规则引擎也没有引入复杂的图计算框架而是自己实现了一个基于节点模型的执行器。这里参考了 LiteFlash 和 Spring Statemachine 的思路把每个节点抽象成一个可执行单元节点之间通过边Edge连接执行器按图遍历。图遍历算法是基础中的基础但配合好数据结构设计能支撑非常复杂的业务场景。可视化设计器前端用 Vue3 GraphEditor 类组件库。这里不限定具体库主流选择是 AntV X6 或者 LogicFlow我最终选了 AntV X6因为它对 React/Vue 都友好节点自定义能力强缩放、拖拽、连线性能都很稳定。前端只负责“画图”和“生成 JSON”后端负责“解析 JSON”和“执行”这个边界划分很重要。2. 核心功能清单与模块划分2.1 可视化编排设计器设计器是整个引擎的门面用户感知最强的部分。我做的设计器主要功能如下节点面板左侧列出了所有可用的节点类型包括开始节点、结束节点、HTTP 请求节点、条件判断节点、脚本节点、延时节点、子流程节点以及自定义扩展节点。画布区支持拖拽节点到画布、节点之间连线、框选多节点、撤销重做、自动布局画布可以缩放和移动。配置面板点击画布上的节点右侧弹出属性配置面板比如 HTTP 节点的 URL、Method、Headers、Body 参数映射条件节点的表达式配置。校验与预览点击保存时前端对流程定义做空值校验、出口校验、环检测发现非法配置直接提示预览功能则根据 JSON 生成静态图的缩略展示。这里有个关键点设计器层面不需要关心节点内部执行逻辑只需要把用户配置的节点类型、参数、连接关系序列化成标准 JSON。比如一个最简单的流程开始节点 - HTTP 请求节点 - 结束节点对应的 JSON 结构大致是{ flowId: demo_flow, name: 示例流程, nodes: [ { id: start_001, type: START, name: 开始 }, { id: http_001, type: HTTP, name: 调用订单接口, config: { url: https://api.example.com/order, method: POST, timeout: 3000 } }, { id: end_001, type: END, name: 结束 } ], edges: [ { from: start_001, to: http_001 }, { from: http_001, to: end_001 } ] }前端在保存的时候做一次 JSON 标准化后端收到后做结构校验然后存储入库。这个 JSON 就是流程定义的唯一事实来源。2.2 执行引擎内核引擎内核是整个系统的心脏也是最考验功力的部分。我的引擎核心可以概括为“用消息驱动图遍历”。引擎启动时根据流程定义的 JSON 构建出一张有向图节点作为 Vertex边作为 Edge。执行时从起始节点开始将节点包装成执行上下文按深度优先或广度优先策略遍历。这里我选择的是广度优先配合队列好处是流程模型天然适合先横向处理同一层级的并行节点再纵向深入。节点的状态我定义了五种WAITING、RUNNING、SUCCESS、FAILED、SKIPPED。每次执行状态变化都记录审计日志。引擎执行时通过接口抽象屏蔽具体节点实现public interface FlowNode { NodeType getType(); NodeResult execute(FlowContext context); }这个接口是整个扩展体系的地基后面会细说怎么实现不同节点。2.3 扩展节点体系引擎的扩展能力决定了它能走多远。我只在核心模块里内置了最常用的节点类型——开始、结束、条件判断、HTTP 请求、Groovy 脚本、延时其余节点都通过 SPI 机制让业务方自行注册。SPI 注册的核心定义是一个 Spring 管理的组件注册中心Component public class NodeComponentRegistry { private final MapString, FunctionFlowContext, NodeResult nodeHandlers new ConcurrentHashMap(); public void register(String nodeType, FunctionFlowContext, NodeResult handler) { nodeHandlers.put(nodeType, handler); } public FunctionFlowContext, NodeResult getHandler(String nodeType) { return nodeHandlers.get(nodeType); } }业务方只需要实现一个 Bean 并在初始化时调用registry.register(MY_NODE, ctx - { ... })把自己的逻辑塞进引擎里就能在可视化设计器里拖出自己的专属节点。这里我踩过一个坑SPI 注册的节点类型如果和内置类型重名会造成覆盖或者只加载一个。解决方式是引入命名空间所有业务方自定义节点类型必须以业务编码做前缀比如order_、pay_用前缀来规避冲突。3. 引擎核心实现原理3.1 流程定义的数据建模聊完功能说说核心原理。流程要能被可视化配置还能被执行引擎正确解析好的数据建模是关键。我把流程定义拆成三个核心模型类FlowDefinition流程的元信息包括flowId、name、version、status草稿/发布/下线、content和createTime。FlowNode节点模型包含id、type、name、config这个节点特有的配置如 HTTP 配置、条件表达式。FlowEdge边模型包含id、sourceNodeId、targetNodeId、conditionExpression这个边被选中执行需要满足的条件。条件边的设计是整个条件分支实现的关键。我的做法是条件判断节点不再像传统写法那样在节点内部硬编码“走哪条线”而是让每条边自身携带表达式执行器遍历到一个节点后根据这个节点的出边列表逐条判断条件第一个条件满足的出边被选中继续执行。举个例子订单金额大于 100 走 vip 处理线小于等于 100 走普通线这个判断逻辑是挂在边上的{ id: edge_001, sourceNodeId: condition_001, targetNodeId: vip_node_001, conditionExpression: #amount 100 }条件表达式用 Spring 的SpelExpressionParser解析配合配置在上下文里的变量非常灵活。这个设计让判断逻辑和节点解耦新增分支只需要在界面上拖一条线、填一个表达式即可不用改后端代码。3.2 执行引擎的核心逻辑执行引擎的入口是一个execute(FlowDefinition def, FlowContext context)方法。整体流程如下校验FlowDefinition状态必须是“已发布”。构建执行图把节点列表和边列表转换成MapString, FlowNode和邻接表。从开始节点出发放入执行队列。循环处理队列弹出节点调用FlowNode.execute(context)。根据执行结果查询该节点的出边列表逐边判断条件符合条件的出边目标节点入队。循环直到队列为空或到达结束节点。这里有三个难点值得单独说。第一环检测。业务流程配置中很容易出现误操作把线连回前面的节点形成环如果执行器不加保护一旦进入环就会无限循环极端情况下把 JVM 跑挂。我的方案是在发布流程定义时做一次拓扑排序如果能完成拓扑排序则说明无环否则拒绝发布。这个校验放在发布入口不放在执行入口因为执行时的图已经经过校验可以直接信任。第二并行分支。某节点之后有两条出边如果两条边的条件都满足而且场景需要并行执行我通过给节点加parallel: true标志来实现。引擎检测到该节点并行标志后把多个出边目标节点放入一个并行任务组用线程池同时执行然后通过CountDownLatch等待所有并行分支完成后再执行后续节点。这里线程池的拒绝策略要设置成调用者执行避免流量高峰把任务池打满。第三上下文传递。流程执行过程中的数据存到哪我的设计是使用FlowContext携带一个MapString, Object data和MapString, Object variables。data存真实业务数据variables存流程级临时变量。节点与节点之间共享的是同一个FlowContext引用这样前一个节点写入的数据后一个节点能直接读取。需要注意并发场景的变量同步问题这里使用的是ConcurrentHashMap但流程变量本身是业务级的串行操作放在并发 Map 里问题不大。3.3 可视化端与后端的数据交互可视化前端和后端之间的数据交互模式值得单独说。整个链路是前端画布操作实现了节点增删改点击保存时先把画布上所有节点和边对象转换成 JSON发送 POST 请求到后端的/flow/save接口。后端接收后做结构校验然后存入flow_definition表。发布时前端调用/flow/publish接口后端先做环检测和配置完整性校验再生成版本号并更新状态。这里有一个重要设计流程定义保存后不会立即生效必须经过“发布”动作。每一次发布会生成一个新的版本号执行引擎永远只执行最新发布版本。这样一旦线上出问题可以随时调用接口回滚到上一个版本。版本字段我用了一个自动递增的version整数发布时1表结构里加上(flow_code, version)做联合唯一索引。前端画布保存的 JSON 会原样存进数据库的content字段避免在后端重新组装时丢失前端特有属性。后端新增关联字段时比如inputParams、outputMappings直接扩展 JSON 即可不需要改表结构。数据库选型的灵活性和 JSON 的动态特性在这里就体现出来了。4. 实操从零搭建一个最小可运行的编排引擎4.1 项目初始化与核心接口定义这一节我会把前面讲的理论落成代码带大家一步步搭一个可以跑起来的最小版本。不要怕这个最小版本的核心代码量不到 200 行。首先是工程结构。我用的是 Maven 多模块严格来说分三个模块flow-api对外接口、flow-engine核心引擎、flow-designer可视化服务。如果只是本地测试单模块也完全够用这里我用单模块演示。核心接口是NodeExecutorpublic interface NodeExecutor { String supportType(); void execute(FlowContext ctx); }所有节点执行器实现这个接口supportType()返回节点类型标识引擎通过类型找到对应的执行器。然后是FlowContextpublic class FlowContext { private final MapString, Object variables new ConcurrentHashMap(); private final String flowCode; private final String flowVersion; private final MapString, String headers; // getter/setter 略 }接着是引擎本体FlowEngineComponent public class FlowEngine { private final ListNodeExecutor executors; private final MapString, NodeExecutor executorMap new HashMap(); public FlowEngine(ListNodeExecutor executors) { this.executors executors; executors.forEach(e - executorMap.put(e.supportType(), e)); } public void execute(FlowDefinition def, FlowContext ctx) { MapString, FlowNode nodeMap def.getNodes().stream() .collect(Collectors.toMap(FlowNode::getId, Function.identity())); MapString, ListFlowEdge edgeMap def.getEdges().stream() .collect(Collectors.groupingBy(FlowEdge::getSourceNodeId)); // 寻找开始节点 FlowNode startNode nodeMap.values().stream() .filter(n - START.equals(n.getType())) .findFirst().orElseThrow(() - new RuntimeException(流程缺少开始节点)); // 通过队列执行 DequeFlowNode queue new LinkedList(); queue.add(startNode); while (!queue.isEmpty()) { FlowNode node queue.poll(); NodeExecutor executor executorMap.get(node.getType()); if (executor ! null) { executor.execute(ctx); } ListFlowEdge outEdges edgeMap.getOrDefault(node.getId(), Collections.emptyList()); for (FlowEdge edge : outEdges) { if (matchCondition(edge.getConditionExpression(), ctx)) { queue.add(nodeMap.get(edge.getTargetNodeId())); } } } } private boolean matchCondition(String expr, FlowContext ctx) { // 简单实现这里可以接入 SpEL return true; } }这里要注意的是执行过程中很多细节我为了演示做了简化比如没有处理节点执行异常、没有并行执行能力。生产用的话这两块是必须补上的。4.2 流程定义解析与执行流程定义我通过一个FlowParser来解析核心方法是把 JSON 字符串转成FlowDefinition对象Component public class FlowParser { private final ObjectMapper objectMapper new ObjectMapper(); public FlowDefinition parse(String json) throws JsonProcessingException { return objectMapper.readValue(json, FlowDefinition.class); } }实际项目中JSON 解析逻辑比这个复杂因为要对节点里的 config 做二次解析还要处理未知字段。但核心原理就是这样Jackson 的JsonProperty注解和JsonIgnoreProperties(ignoreUnknown true)一定要用好否则前端传多了字段直接解析失败。流程定义入库后发布服务会做一次解析校验确认 JSON 可解析为FlowDefinition、节点 id 不重复、边引用的节点都存在然后保存版本。执行服务加载时把最新的已发布版本从库里取出来解析成FlowDefinition交给FlowEngine执行。4.3 自定义节点扩展方式自定义节点是这套系统最有价值的部分。举个例子假设需要给流程添加一个“发送邮件”节点我们只需要三步第一步定义一个MailNodeExecutorComponent public class MailNodeExecutor implements NodeExecutor { Override public String supportType() { return MAIL; } Override public void execute(FlowContext ctx) { // 从 ctx.variables 里取收件人/主题/内容 // 调用邮件客户端发送 } }第二步在流程定义的 JSON 里使用这个类型{ id: mail_001, type: MAIL, name: 发送邮件通知 }第三步前端设计器节点面板中注册这个节点类型。你的前端代码里加一条映射const nodeTypes { MAIL: { label: 发送邮件, component: MailNode, configForm: MailConfigForm } }后续业务方要扩展带界面的自定义节点就走这三步后端注册执行器、前端注册展示器、JSON 里引用类型。整个配套机制四个字约定大于配置。只要遵循约定扩展极其方便。5. 常见问题与排查技巧实录5.1 流程图中的环检测问题环检测是我在项目上线后遇到的最典型的问题。之前说过发布入口做了一次拓扑排序校验但这个校验只覆盖纯结构层面的环。业务方在实际配置中经常造成的环是“条件环”比如 A 节点满足条件 C1 回到 A 节点从结构上它不是环因为只有特定条件下的边才会成环。这种条件环的检测复杂很多我目前的处理方式是在引擎执行阶段加上节点访问次数上限。每个节点的执行次数超过预设阈值比如 50 次就强制终止流程并抛出异常同时在日志里打出“疑似死循环”的警告。这是一种兜底策略虽然不完美但能防止真正的生产事故。5.2 节点执行超时与重试HTTP 请求节点是最容易出问题的节点类型。下游接口响应慢、网络抖动、对端服务重启都可能导致节点执行卡住。所有涉及外部调用的节点必须有超时时间。我在节点配置里默认给了 3 秒超时可配置并且支持重试机制。重试不能盲目重试。读接口可以放心的重试但写接口重试要非常小心可能造成重复下单、重复扣款。我在这块的处理是节点配置里区分“幂等重试”和“非幂等不重试”。幂等重试可以用简单的固定次数 退避间隔非幂等则直接报错进入异常分支。重试实现上用 Spring 的RetryTemplate配置RetryPolicy和BackOffPolicy重试时打印 warn 日志方便排查。5.3 幂等设计与重复请求谈到幂等执行引擎本身也面临重复请求的问题。比如某个流程是监听 MQ 消息触发的消费端重试会导致同一流程执行两次。我的处理方式是流程实例表flow_instance建立唯一索引(biz_key, flow_code)每次执行时先按bizKey查询是否已有成功记录有则直接跳过。bizKey由调用方传入比如订单 ID。这里补充一个细节如果流程是异步执行的从提交到执行完成还有时间窗口并发下同一bizKey可能出现两条执行记录。我的方案是引入 Redis 的SETNX做分布式锁锁的 key 是flow_execution:{flowCode}:{bizKey}获取锁成功才能创建流程实例流程执行完成后释放锁。5.4 可视化画布的性能问题最后聊聊可视化设计器的前端性能。我最初实现节点渲染时没有做懒加载和虚拟滚动当流程节点达到 200 个以上时拖拽明显卡顿缩放几乎不可用。排查后发现主要性能瓶颈不在渲染而在组件重绘。我的优化策略是两个方向第一AntV X6 的图数据加载改为增量更新避免全量重置第二右侧属性配置面板只在点击节点时渲染当个节点的配置表单而不是所有节点的配置都挂在页面上。做完这两个优化后200 个节点的流程操作流畅度基本和 50 个节点没有差别。另一个小技巧是画布和流程 JSON 的双向转换。前端维护一套自己的画布数据模型后端用另一套 JSON 模型两者之间的字段映射很容易因迭代维护出错。我后来直接让画布数据模型就是后端 JSON 模型前端不再做二次转换前端组件内部封装一层适配器消化画布特有字段。简单模型简单快乐复杂映射是 bug 的温床。6. 实操心得与个人经验这个项目从构思到第一版可用整个核心骨架搭建用了 4 周左右前两周主要花在数据模型设计和画布集成上后两周就是不断填坑和补充细节。我个人最大的体会是零代码编排引擎这类项目的复杂度不是高深算法而是边界情况太多流程定义不合法怎么办、节点执行失败退回哪一步、跨系统调用数据兼容、版本回滚页面配置冲突每一条都是业务方真实使用时必然踩到的坎。如果让我再选一次技术方案我依然会坚持用 JSON 做流程定义载体。BPMN 标准固然强大但适配可视化灵活拖拽的业务场景JSON 的开放性、可读性和开发效率优势太明显。实用技巧再补充一个在流程执行日志里每个节点执行前后各打一条带 traceId 的日志就能非常方便地还原整条调用链。我见过不少团队在开发阶段忽略这个细节等线上排查问题时抓瞎这个日志习惯一定要从开发第一天养成。如果后续继续扩展我建议把重点放在流程版本对比和回滚的可视化展示上。目前我的版本回滚支持是调接口手动回滚体验还是不够直观。理想状态是可视化设计器里能对比两个版本的操作差异一键回滚并且自动保存回滚前后的快照。这块涉及的数据结构和前端交互复杂度又会多一个量级但做好了对业务方体验提升很大。