三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

Solon Flow:Java轻量级流程编排框架,从规则引擎到AI链路的统一范式

Solon Flow:Java轻量级流程编排框架,从规则引擎到AI链路的统一范式

1. 从“硬编码”到“编排”:为什么我们需要一个新的流程引擎?

如果你是一个Java后端开发者,大概率遇到过这样的场景:产品经理拿着一个复杂的业务流程图来找你,上面画满了各种判断、分支、并行任务和异步调用。你的任务,就是把这些图翻译成代码。于是,你开始写if-else,写switch-case,调用各种Service,处理异常,记录日志。一个流程下来,几百行代码是家常便饭。更头疼的是,当业务规则变了,比如“用户等级大于V3且订单金额超过1000元才送积分”变成了“用户等级大于V2或订单金额超过500元就送积分”,你就得在一堆业务逻辑里小心翼翼地修改条件判断,生怕改错了其他地方。这种开发模式,我们称之为“硬编码”的业务流程。

硬编码的问题显而易见:维护成本高、灵活性差、可读性低、难以复用。业务流程的逻辑和业务代码深度耦合,任何一点变动都需要开发、测试、上线全套流程。为了解决这个问题,业界诞生了工作流引擎,比如老牌的JBPM、Activiti,以及现在依然活跃的Flowable、Camunda。它们通过BPMN(业务流程模型与标记)标准,将流程可视化、配置化,实现了业务与流程逻辑的解耦。这无疑是一个巨大的进步。

然而,在实际项目中,尤其是面对快速迭代、规则多变的业务时(比如营销活动、风控决策、订单处理),传统的工作流引擎有时会显得“太重”了。启动一个完整的Flowable引擎,需要数据库、需要一套复杂的运行时服务,对于只是想做一个轻量级规则判断或者任务串联的场景,有点杀鸡用牛刀的感觉。另一方面,AI应用的爆发式增长,又带来了新的编排需求:如何将大语言模型(LLM)的调用、向量数据库检索、条件判断、数据格式化等步骤,像搭积木一样灵活地组合成一个智能体(Agent)或应用?这需要一种更轻量、更灵活、更能覆盖从简单规则到复杂工作流,再到AI链路的统一编排范式。

正是在这样的背景下,Solon Flow进入了我的视野。它不是另一个BPMN标准的实现,而是一个面向Java开发者的、声明式的流程编排框架。它的核心设计理念是“一个引擎,七种节点”,用极简的API和模型,试图覆盖规则编排、任务编排、工作流乃至AI编排的全场景。简单来说,它想让你用写配置(或少量代码)的方式,来定义和执行任何复杂的业务流程,并且足够轻量,可以嵌入到任何Spring Boot或Solon应用中。接下来,我将结合我的实际使用和踩坑经验,为你深入拆解Solon Flow是如何做到这一点的。

2. Solon Flow 核心架构:七种节点如何构建万物?

Solon Flow 的威力,完全体现在其精心设计的七种节点类型上。这七种节点就像七种基本的乐高积木,通过不同的组合,几乎可以搭建出任何你想要的流程结构。理解它们,是掌握Solon Flow的关键。

2.1 七种节点深度解析

1. 开始节点 (StartNode)这是每个流程的入口,没有前置条件。它主要用来初始化流程上下文(Context),你可以在这里注入一些全局参数。在实践中,我通常用它来设置流程的批次号、触发用户信息等元数据。

2. 结束节点 (EndNode)流程的终点。一个流程可以有多个结束节点,代表不同的结束状态(如成功、失败、审批拒绝)。它负责收尾工作,比如清理资源、发送最终通知、更新主业务状态。这里有个关键技巧:务必在结束节点明确设置流程的最终输出结果,因为后续的调用方很可能依赖这个结果。

3. 动作节点 (ActionNode)这是最常用、最灵活的节点。它代表一个具体的业务动作或计算。你需要实现一个Action接口,在execute方法中编写你的业务逻辑。

// 示例:一个简单的积分计算动作 @Component public class CalculatePointsAction implements Action { @Override public Object execute(Context context) throws Exception { Order order = context.get("order"); User user = context.get("user"); // 业务逻辑:根据订单金额和用户等级计算积分 int points = order.getAmount() / 100; if ("VIP".equals(user.getLevel())) { points *= 2; } context.put("calculatedPoints", points); return points; // 返回值也会被放入上下文 } }

注意:动作节点的执行是同步的。对于耗时操作(如调用外部HTTP接口),建议在节点内自行处理异步,或者考虑使用后续会提到的异步子流程

4. 条件节点 (ConditionNode)用于流程的分支决策。它基于流程上下文中的数据,执行一个判断(实现Condition接口),并返回下一个要执行的节点ID。这是实现“IF-ELSE”逻辑的核心。

@Component public class AmountCheckCondition implements Condition { @Override public String evaluate(Context context) throws Exception { Order order = context.get("order"); // 如果金额大于1000,跳转到节点`highAmountProcess`,否则跳转到`normalProcess` return order.getAmount() > 1000 ? "highAmountProcess" : "normalProcess"; } }

5. 分支节点 (ForkNode) & 合并节点 (JoinNode)这是一对用来实现并行任务的节点。

  • 分支节点 (ForkNode):将流程分成多个并行的分支,每个分支独立执行一系列节点。在定义流程时,你需要指定分支的数量和每个分支的子流程链。这在处理可以同时进行的独立任务时非常高效,比如“同时发送短信和邮件通知”、“并行调用多个风控数据源”。
  • 合并节点 (JoinNode):等待所有由同一个分支节点创建的分支都执行完毕后,再继续向下执行。它支持不同的合并策略,比如“全部成功才继续”、“任一成功即继续”、“多数成功”等。这里有一个大坑:如果并行分支中某个分支执行失败或阻塞,合并节点的默认策略(全部完成)可能会导致流程永远挂起。务必根据业务场景设置合理的超时和异常处理策略。

6. 子流程节点 (SubProcessNode)用于流程的嵌套和复用。你可以将一个已定义的、复杂的流程作为一个节点嵌入到另一个流程中。这极大地提升了模块化能力。例如,你可以定义一个“风控审核”子流程,然后在“贷款申请”主流程和“大额提现”主流程中重复使用它。子流程节点支持同步和异步调用。

2.2 “一个引擎”的智慧:统一调度与上下文管理

有了这七种积木,谁来搭?谁来确保它们按正确的顺序执行?这就是Solon Flow引擎的核心职责。这个引擎非常轻量,它的核心是一个流程执行器 (FlowExecutor)

引擎的工作流程可以概括为:

  1. 加载流程定义:流程定义可以通过Java代码构建(FlowDefinitionBuilder),也可以通过JSON/YAML等配置文件描述。后者更适合可视化编辑和动态部署。
  2. 创建执行实例:传入流程定义和初始参数,引擎创建一个流程实例(FlowInstance)和共享的上下文(Context)。
  3. 调度执行:引擎从开始节点出发,根据每个节点的执行结果和路由逻辑(尤其是条件节点的判断),驱动流程一步步向后执行。对于动作节点,它调用你写的Action;对于分支节点,它创建并行任务。
  4. 上下文传递Context是整个流程的“粘合剂”。它是一个类似Map的数据结构,贯穿流程始终。每个节点都可以从中读取数据,也可以写入新的数据供下游节点使用。这里有一个重要经验:为了避免键名冲突和混乱,建议为放入上下文的数据定义清晰的、带有命名空间风格的键,例如risk:score,order:finalAmount

这种设计的好处是,无论你要编排的是简单的三步规则校验,还是包含几十个并行、循环步骤的复杂工作流,抑或是调用大模型、处理向量数据的AI链,使用的都是同一套引擎、同一套API。学习成本一次投入,多处受益。

3. 实战:从规则编排到复杂工作流

理论说再多,不如看实战。我们通过三个由简到繁的场景,来看看Solon Flow如何落地。

3.1 场景一:轻量级订单折扣规则引擎

假设我们有一个电商订单折扣规则:1)新用户首单打9折;2)订单金额满200减20;3)以上优惠可叠加。用硬编码写if-else很容易,但用Solon Flow,我们可以做得更清晰、更易维护。

首先,我们定义三个动作节点:

  • CheckNewUserAction: 检查是否为新用户,在上下文中设置isNewUser=true/false
  • CalculateBaseDiscountAction: 计算满减折扣。
  • CalculateFinalAmountAction: 汇总所有折扣,计算最终金额。

然后,用条件节点和动作节点串联:

开始 -> [检查新用户] -> (是否新用户?) --是--> [计算新用户折扣] -> [计算满减] -> [计算最终金额] -> 结束 --否--> [计算满减] ------------┘

在Solon Flow中,我们可以用代码这样定义这个流程:

FlowDefinition flow = FlowDefinitionBuilder.start() .then(new ActionNode("checkUser", checkNewUserAction)) .then(new ConditionNode("isNewUserCond", isNewUserCondition) .when("true", new ActionNode("newUserDiscount", newUserDiscountAction)) .otherwise(null) // 否则直接跳过 ) .then(new ActionNode("fullReduce", calculateBaseDiscountAction)) .then(new ActionNode("finalCalc", calculateFinalAmountAction)) .end("success") .build();

这样做的好处:每个规则都是一个独立的Action,修改“满200减20”为“满300减30”时,你只需要修改CalculateBaseDiscountAction,流程结构完全不用动。新的规则(比如“会员日额外95折”)可以很容易地以插入新节点的方式加入。

3.2 场景二:并行任务与异步处理——审核通知流

现在考虑一个更复杂的场景:用户提交内容后,需要1)进行AI内容安全检测(耗时),2)同时发送站内信通知审核员,3)等待AI检测结果和人工审核结果(任一通过即可),4)根据结果通知用户。

这个流程包含了并行任务(1和2同时进行)和异步等待(等1和3的结果)。用Solon Flow实现如下:

  1. 定义节点

    • AiCheckAction: 调用AI审核API,这是一个异步动作(内部使用CompletableFuture)。
    • NotifyReviewerAction: 发送站内信,同步动作。
    • WaitForReviewCondition: 一个“等待”条件节点,它会轮询或监听,直到AI结果或人工审核结果到达。
    • NotifyUserAction: 根据结果通知用户。
  2. 构建流程

开始 -> [分支节点] |--> [AI内容检测] ----------------------| |--> [通知审核员] -> (等待审核结果) --|--> [合并节点] -> [通知用户] -> 结束

这里的关键是分支节点合并节点的使用。分支节点创建两个并行分支。合并节点的策略可以设置为“任一成功即继续”,因为只要有一个审核通过,流程就可以继续往下走通知用户。

踩坑记录:在这个场景中,AiCheckAction是异步的,它启动后立即返回,实际结果稍后才存入上下文。WaitForReviewCondition需要能够获取到这个未来的结果。我的做法是让AiCheckAction返回一个Future或一个任务ID,WaitForReviewCondition去查询这个异步任务的状态。这要求上下文能够存储非即时结果,对设计有一定挑战。

3.3 场景三:动态可配的营销工作流

对于运营人员来说,他们希望不经过开发就能配置营销活动流程:比如“用户签到 -> 判断连续签到天数 -> 大于7天抽大奖,否则抽小奖 -> 发放奖励 -> 记录日志”。Solon Flow的流程定义支持从JSON或数据库加载,这使得动态配置成为可能。

你可以开发一个可视化编辑器,让运营拖拽节点(对应七种类型),配置每个动作节点的具体实现类(如SignInAction,LotteryDrawAction)和参数。流程定义以JSON格式保存。当活动创建或修改时,只需要将新的JSON定义加载到FlowExecutor中即可,无需重启应用。

{ "id": "daily_checkin_flow", "name": "每日签到流程", "startNodeId": "start", "nodes": [ {"id": "start", "type": "START"}, {"id": "signIn", "type": "ACTION", "bean": "signInAction"}, {"id": "checkDays", "type": "CONDITION", "bean": "continuousDaysCondition", "routes": [ {"expression": ">7", "target": "bigLottery"}, {"expression": "default", "target": "smallLottery"} ] }, {"id": "bigLottery", "type": "ACTION", "bean": "bigLotteryDrawAction"}, {"id": "smallLottery", "type": "ACTION", "bean": "smallLotteryDrawAction"}, {"id": "end", "type": "END"} ] }

这种模式将业务流程的变更权交给了业务人员,实现了真正的业务敏捷。注意事项:动态加载的节点bean必须是在Spring或Solon容器中已经管理好的Bean,且需要做好类安全隔离和热加载机制,避免生产环境的内存泄漏。

4. 高阶应用:拥抱AI Agent编排

随着AI应用深入,我们经常需要编排多个LLM调用和工具使用。例如,一个智能客服的流程可能是:“理解用户问题 -> 查询知识库 -> 根据答案生成友好回复 -> 如果知识库没有,则转交人工”。这本质上也是一个流程编排问题。

Solon Flow的七种节点同样适用:

  • 动作节点 (ActionNode):可以封装“调用ChatGPT API”、“查询向量数据库”、“执行SQL”、“调用内部函数”等操作。
  • 条件节点 (ConditionNode):判断“用户意图是否为投诉”、“知识库检索结果置信度是否大于阈值”。
  • 开始/结束节点:定义对话的启动和结束。
  • 子流程节点:将“查询知识库并生成回复”这个通用能力封装成一个子流程,供多个主流程复用。

我们可以构建一个AI流程:

开始 -> [意图识别] -> (是否为查询?) --是--> [知识库检索] -> (置信度>0.8?) --是--> [组织答案] -> 结束 --否--> [通用对话] ------------------------------┘ --(转人工)--> [创建工单] -> 结束

在这个流程中,每个方框都是一个ActionNode,可能背后调用的是不同的AI模型或工具。菱形都是ConditionNode,做路由判断。Solon Flow负责以清晰可控的方式执行这条“AI链”,并记录每个节点的输入输出,这对于AI应用的调试和效果追踪至关重要。相比一些专用的AI编排框架(如LangChain的LCEL),Solon Flow的优势在于它更通用、更轻量,并且能和你现有的Java业务系统无缝集成,共用同一套运维、监控、事务管理机制。

5. 性能、监控与踩坑指南

引入任何框架,性能和可观测性都是必须考虑的问题。

性能考量: Solon Flow引擎本身非常轻量,开销主要在于节点的执行逻辑和上下文管理。对于高性能场景,我有以下建议:

  1. 避免在上下文中存储大对象Context在流程中传递,存储过大的对象(如完整的订单详情DTO)会增加内存和序列化开销。应该只传递必要的标识ID,节点按需从缓存或数据库查询。
  2. 合理使用异步:对于IO密集型节点(网络调用、数据库查询),务必将其设计为异步,防止阻塞整个流程线程。可以利用CompletableFuture或响应式编程模型。
  3. 流程定义缓存:频繁从数据库或配置中心解析JSON/YAML定义是耗时的。务必在内存中缓存编译好的FlowDefinition对象。
  4. 分支节点的代价:创建大量并行分支会消耗线程资源。需要根据业务负载合理设置线程池参数。

监控与调试: 流程编排让业务逻辑清晰,但也让调用链变得更长。完善的监控必不可少。

  1. 链路追踪:为每个流程实例生成唯一Trace ID,并传递到每一个节点动作中。这样可以在日志或APM工具(如SkyWalking, Zipkin)中完整还原一次请求的完整路径。
  2. 上下文快照:在关键节点(尤其是出错时),记录当前上下文的快照。这对于复现和调试复杂流程中的问题有奇效。Solon Flow的Context可以方便地序列化为JSON进行记录。
  3. 节点执行度量:收集每个ActionNode的执行时间、成功/失败次数。这能帮你快速定位性能瓶颈和故障点。

常见“坑”与解决方案

  • 坑1:条件节点路由死循环。条件节点的evaluate方法逻辑错误,导致流程在两个节点间来回跳转。解决:在流程定义阶段加入简单的环路检测,或为流程执行设置最大步数限制。
  • 坑2:并行分支中的异常处理。一个分支失败,是导致整个流程失败,还是忽略它继续其他分支?解决:在ForkNodeJoinNode上仔细定义异常处理策略。通常,我会在分支动作内部做好异常捕获,将错误信息作为结果放入上下文,而不是直接抛出异常中断分支,最后由合并节点根据策略决定流程走向。
  • 坑3:上下文数据污染。多个节点读写同一个上下文键,可能造成意外覆盖。解决:建立严格的上下文数据命名规范,如使用阶段:实体:属性validate:order:status)的格式。对于关键数据,考虑使用不可变对象或进行深拷贝。
  • 坑4:事务边界模糊。一个业务流程可能涉及多个数据库写操作,分布在不同的节点中。如何保证事务?解决:Solon Flow本身不管理分布式事务。对于需要强一致性的场景,可以将一个事务内的所有数据库操作放在同一个ActionNode中。对于跨节点的最终一致性场景,需要结合消息队列和补偿机制(Saga模式)来实现。

经过多个项目的实践,Solon Flow以其“一个引擎,七种节点”的极简设计,确实为Java后端提供了一种新颖而强大的流程编排范式。它不像传统工作流引擎那样沉重,却提供了媲美工作流引擎的表现力;它足够灵活,能从简单的规则判断扩展到复杂的并行工作流,甚至触及前沿的AI Agent编排。对于正在被复杂业务逻辑和频繁变更所困扰的团队,尝试引入这样一种声明式的编排思想,或许能带来意想不到的提效和清晰度。当然,它也不是银弹,对于需要严格遵循BPMN标准、有复杂人工审批场景的需求,传统的BPM引擎可能仍是更成熟的选择。但在微服务架构下,处理服务内部或服务间可自动化的业务逻辑流,Solon Flow无疑是一个值得放入工具箱的利器。

← 返回列表