ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Java低代码平台动态引擎设计实践:从表单到规则的热部署方案

Java低代码平台动态引擎设计实践:从表单到规则的热部署方案 1. Liquor 立项Java 低代码平台的引擎“动态化”非做不可“你们这个东西一个审批结果改了三十次规则每次都要我们把整个服务停掉重新构建打包上线时间从两小时变成两天还能叫低代码吗”这是好几年前某位客户架构师在评审会上拍着桌子跟我说的话。当时我们确实已经有了一套完整的表单可视化拖拽、数据模型设计器和流程画布但在最关键的“规则变动”上平台是死的只要审批金额阈值变了、部门审批链调整了、某个字段联动方式改了研发就得重新拉分支改 Java 代码走完整个发布流程。Java 是静态类型语言这种“配置完之后还要能热变化”的能力恰好是跑在 JVM 上的低代码平台最难补齐的一块短板。于是就有了 Liquor。Liquor 这个名字刚开始本来叫 Liquid意思是“让规则像液体一样流动”但命名冲突太多后来改成 Liquor蒸馏烈酒也暗含“从静态代码里蒸馏出动态能力”的意思。团队内部没有把它叫成“规则引擎”而是一直叫“动态引擎”。原因很简单低代码平台需要的并不是单点规则判断而是一整层运行时的动态执行能力。这层能力具体拆开是四件事页面结构和控件在运行时能根据元数据渲染字段校验、默认值、联动逻辑能按配置执行流程节点在提交、回退、驳回时能动态决定下一个任务以及用户配置的表达式、脚本能被安全地编译和运行。哪一块是死的整个低代码产品都会被业务嘲笑。Liquor 不是我入职时就在的项目而是我们在被业务反复教育之后立项的新内核。它定位在平台服务和业务服务之间不关心某个业务表长什么样也不关心某个特定按钮绑定什么操作它只负责一件事把表单、规则、流程统一描述成可执行的动态模型然后在运行时解释和编排这些模型。写这篇复盘的时候我尽量把自己踩过的细节都还原出来包括选型比较、类加载器问题、热部署缓存失效以及生产环境拿不到日志时有多绝望。1.1 动态执行不是一个新需求而是低代码平台的“最后一公里”先还原一下我们早期平台的实现路径。最初版本没有一个像样的表单元数据描述所有的输入框、下拉框、日期选择器都是模板代码用 Freemarker 根据数据库字段动态拼出 HTML再配合一段段硬编码的 JS 做校验。这种做法的优点是页面渲染快适合内部后台系统。缺点也很明显新增一个控件类型要改模板字段联动要写 JS 和对应的后端接口复杂表单完全脱离“配置化”运营和产品经理根本没法自助变更。为了节省成本我们又走入了第二条路元数据 CRUD。也就是让用户通过界面定义表结构、字段控件、校验规则然后平台启动时扫描这些元数据生成通用的增删改查接口和页面。这条路跑通后简单的用户管理、订单查询基本不需要写代码产品交付速度明显提高。但它很快就暴露了新的问题页面能动态生成交互却无法动态描述——为什么这个字段隐藏了另一个字段还必填为什么某个用户提交订单后要多走一道主管复核为什么金额大于某个值要触发另外一套库存逻辑这些需求用纯 CRUD 表达不出来必须处理事件、条件、分支、流程事务之间的关系。而“事件 条件 动作”恰恰是所有低代码平台中容易超出模板能力的地方。当时可选方案有几种引入成熟的规则引擎、使用脚本引擎嵌入 Groovy、基于 Java 表达式语言实现自定义 DSL。我们没有立刻拍板而是花了两周做了场景试跑把最常见的 20 类业务需求分别用三种方案实现统计代码量、开发复杂度和运行性能这才有了 Liquor 的技术选型雏形。1.2 Liquor 的职责边界先定义不做什么再定义做什么立项会议上我们定了 Liquor 的第一份边界声明至今仍然有效。它做四件事表单控件树的运行时解析、字段级表达式计算、动作节点的事件编排、流程节点选择器的结果返回。它不做的也包括四个方面不做数据库表结构维护、不直接对接前端 UI 框架、不提供完整的 BPMN 流程引擎、不承担业务服务本身的原子逻辑。这套边界非常关键。很多团队在自研低代码平台动态能力时失败原因不是功能做少了而是把引擎越做越重最后变成“用低代码方式又写了一个业务中台”。Liquor 的定位更像是一个解释器你给它一份结构化的 JSON 描述它解析后生成可执行模型然后通过统一的上下文和规则运行器返回结果。只有保持轻量才能让上层业务和平台业务都能快速接进来。2. Liquor 的运行时模型把表单、规则、流程装进同一套“可执行树”任何动态引擎第一件事就是定义自己的运行时数据结构。我们一开始参考的是编译原理里的 AST 思想但很快就意识到不需要做词法分析器级的抽象重点应该放在“可解释的元数据模型”上。Liquor 使用了一个统一的节点模型所有表单、规则、流程在最终解析后都会变成一棵节点树引擎遍历节点时根据类型决定动作。public class LiquorDefinition { private String definitionId; private int version; private DefinitionType type; // FORM / RULE / FLOW private LiquorNode root; public static class LiquorNode { private NodeType type; // COMPONENT / ACTION / BRANCH / EXPRESSION / SUB_FLOW private String name; private String expression; // 条件或执行片段 private MapString,Object config new HashMap(); private ListLiquorNode children new ArrayList(); } }这段代码看起来不复杂但它是一个很重要的收敛。早期各处实现方式不统一表单引擎把元数据放在 JSON 里流程引擎有自己的流程结构规则服务又是一个 Redis 缓存的条件集。三个模块各自为政导致“一个规则联动流程跳转”这类需求要写大量胶水代码。Liquor 想把它们统一就必须允许节点嵌套节点允许子节点是另一个规则或子流程。2.1 表单模型运行时控件树不是简单 json 渲染一般团队接触低代码第一个功能往往是把“字段 JSON”渲染成页面。但动态表单的真正复杂度在于控件之间的依赖关系而不是控件本身的展示。例如用户选择“企业客户”付款方式下拉框必须变成“银行转账、承兑汇票、赊销”输入金额大于 100 万备注框高亮并提示需要解释资金来源。依赖关系在 Liquor 中被描述成组件节点的子节点用表达式驱动显隐和联动。每份表单定义落库之后会被 Liquor 的 form builder 编译成一个 ComponentTree。渲染时前端拿到的是包含组件属性、布局参数、校验规则、联动事件的完整树不再单独查数据库拼凑页面。后端做提交校验时也复用同一棵树的校验节点实现前后端规则一致。常见的问题在于前端校验和后端校验代码各写一份最后状态不一致。Liquor 的解法是把校验表达式本身放在同一份配置里前端负责即时反馈后端负责强制校验两边执行的是同一条formula字段。举个最简单的例子充值金额必须大于 0 且小于单日限额规则配置会生成这样的执行节点{ type: VALIDATION, expression: amount 0 amount dailyLimit, message: 充值金额超出当日限额 }引擎执行校验节点时并不直接读数据库而是从 LiquorContext 中获取上下文。上下文里保存了当前表单所有字段值、当前登录人、时间、组织等运行时信息。设计上下文时我们遵循一个约定业务值必须全量传入引擎不主动查库。这样做是为了减少执行过程中的外部依赖也让所有规则都具有纯函数特征方便测试和回放。2.2 规则与表达式把条件判定收敛成同一种执行结果规则模型是 Liquor 最核心的一层。它的定义源有两种一种由低代码平台的规则编辑器生成另一种由技术人员直接在 JSON 配置里编写。无论来源如何最终都会变成RULE类型的 LiquorNode。规则节点的expression字段支持运行时可配置的表达式引擎执行到该节点时只负责把表达式放入表达式求值器执行拿到布尔值或一个枚举结果。一个常见的动态审批规则长这样当订单金额超过 5000 时进入主管审批否则自动通过。在 Liquor 中它会被编译为if (order.amount 5000) { ctx.setResult(need_manager_approval); } else { ctx.setResult(auto_pass); }不过实际配置不会让业务人员写这样的 Java 片段而是通过规则界面选择字段、比较符和结果值。Liquor 把界面配置翻译成一个内部执行模型每个比较条件变成元素多个条件用 AND/OR 组合再挂上结果映射。执行器不会去解释一整段 Java 代码而是对 AST 化的条件树做短路求值这样复杂度可控安全风险也小得多。2.3 流程模型让“流程画布”和“规则引擎”能对话如果只有一个表单和一堆规则还很难称之为“低代码平台”业务真正需要的是流程。Liquor 的流程模型没有重复造一个完整的工作流引擎而是提供一个流程选择器桥接层。流程编排仍然交给平台内已有的流程服务去做但流程的节点跳转条件和动态审批人统一通过 Liquor 解析。比如用户发起一个请假申请流程到达部门审批节点时流程引擎调用 Liquor 询问当前任务应该分给部门经理、项目主管还是直接跳过。Liquor 根据上下文中的申请假期天数、申请人的部门、项目状态得出结果返回一个 taskOwner 表达式。流程引擎拿到结果后动态创建对应审批任务。这个模式对业务的好处很明显——改审批策略时不用重画流程图只需要改规则版本然后发布流程绑定的动态规则即可。3. 内核落地的关键实现表达式编译、脚本安全和版本化类加载Liquor 最核心的代码不到一万行但五脏俱全。真正的难点不在“写一个 evaluate”而在工程化控制表达式和脚本的执行耗时、热更新风险、内存泄漏、异常可读性、并发热点。下面这段是我从 Liquor 源码里简化出来的主引擎调用逻辑保留了实际主干。public class LiquorEngine { private final DefinitionRepository repository; private final ScriptCompiler scriptCompiler; private final LiquorScriptCache scriptCache; public ExecutionResult execute(ExecutionRequest request) { LiquorDefinition definition repository.findVersion( request.getDefinitionId(), request.getVersion() ); if (definition null) { throw new DefinitionNotFoundException(request.getDefinitionId()); } LiquorContext context LiquorContext.create(request.getVariables()); Object result walk(definition.getRoot(), context); return ExecutionResult.success(result, context.getSnapshot()); } private Object walk(LiquorNode node, LiquorContext context) { if (context.isInterrupted()) { throw new ScriptTimeoutException(执行超时已中断节点 node.getName()); } switch (node.getType()) { case EXPRESSION: return scriptCompiler.eval(node.getExpression(), context); case BRANCH: for (LiquorNode child : node.getChildren()) { if (Boolean.TRUE.equals(scriptCompiler.eval(child.getExpression(), context))) { return walk(child.get(thenTarget), context); } } return walk(node.get(elseTarget), context); case SUB_FLOW: return execute(ExecutionRequest.fromNode(node, context)); default: return walkChildren(node, context); } } }主循环能看明白后我重点说几个工程细节。它们分别是表达式与脚本的边界、不可信脚本沙箱、类加载器生命周期设计。这三个问题决定了 Liquor 能不能从 Demo 走到生产环境。3.1 表达式引擎到底选哪种SpEL、Aviator 还是 Groovy低代码平台不可避免要跟表达式打交道。我根据不同场景给 Liquor 定了三个档位策略。简单求值比如比较大小、字符串拼接、三元运算、基本日期计算不需要引入重量级脚本。比较合适的方案是 Spring 的 SpEL 或 AviatorScript。SpEL 的优势是 Spring 生态自带的熟悉 Java 的开发者容易上手语法也比较自然。AviatorScript 性能高语法贴近 Java而且设计上就是为表达式场景服务的。但 AviatorScript 社区活跃度和文档丰富度都不如 SpEL所以如果团队没有特殊历史包袱我建议优先 SpEL。这里要提醒的是SpEL 的StandardEvaluationContext非常强大强大到如果不做控制表达式里可以反射调用任意类和静态方法这是很危险的。Liquor 的做法不是直接把用户表达式塞给 SpEL 执行而是先做 AST 扫描把涉及方法调用、类型引用和非白名单属性的节点直接拦截只允许算术、比较、三目、逻辑以及预置的上下文属性访问。实现代码不必展开核心思想就是表达式执行前必须经过安全校验不能把执行器裸露给表单配置者。对于更复杂的多步逻辑比如创建集合、遍历数据、调用外部接口、循环计算未出库数量SpEL 会非常痛苦。Liquor 在这一档嵌入 Groovy 脚本作为补充能力。Groovy 和 Java 的桥接非常自然Java 对象可以无缝传入 Groovy 脚本团队学习成本低。但 Groovy 每次编译脚本都相当“重”——它需要生成独立的类并加载到独立的 ClassLoader 中如果没有管理好生命周期频繁发布规则会导致 JVM 元空间不断增长。还有一个场景是表达式复用和调试。我们在 Liquor 的调试器里增加了一个“分步执行”能力任何一条规则执行失败或结果不对都可以复制执行链路里的上下文变量在本地模拟环境里重新跑一遍。这个能力让平台配置人员不需要看 Java 堆栈也能定位到是哪一个字段为空导致表达式空指针。实际上这是我把 Liquor 推上线之后最后悔没早点做的功能。3.2 不可信脚本怎么跑才安全白名单与 SecureASTCustomizer低代码平台的规则配置者往往是业务人员或实施顾问不是百分之百可信的开发者。动态执行他们的表达式和脚本本质上是在运行一段不可信代码。Groovy 引擎提供了SecureASTCustomizer可以在编译阶段限制可访问的类、拦截器和导入范围。Liquor 的默认配置是禁止 import java.lang.Runtime、System、ProcessBuilder禁止访问 Class 对象禁止调用 System.exit变量只能来自 LiquorContext。即使有这套保护我依然建议把“脚本隔离”和“表达式隔离”分清楚而不是把所有动态能力都塞进 Groovy。因为任何脚本引擎都可能存在绕过的漏洞一旦用户能执行任意反射就等价于在你的服务里开了一个后门。Liquor 的兜底策略是核心链路里的比较和流程分支绝对不允许使用 Groovy 语法Groovy 只开放给后台管理员级别的“自定义动作”运行时还要配合独立的线程池和超时控制。除了编译期限制运行时也需要隔离。Liquor 执行 Groovy 脚本时不是直接在请求线程里跑而是提交到一个专门的动态脚本线程池。线程池有固定大小、拒绝策略和任务超时。这样即使某条脚本发生了死循环或阻塞 IO也不会拖垮主业务线程。我们压测过最坏情况某条写坏的脚本在 while 循环里不退出通过 Future.get 设置 3 秒超时可以让该次调用超时失败但其他线程的流程不受影响。3.3 类加载器生命周期热发布规则后不重启不泄漏这部分是 Liquor 踩坑最深的地方值得写清楚。Groovy 会为每次脚本编译创建 ClassLoader一个常规低代码平台一天可能要发布几十上百次规则变更如果每个版本的 ClassLoader 都被长期引用JVM 永久代或元空间迟早撑爆。Liquor 的早期版本就吃过一次元空间溢出的亏。当时规则上线第三天一个负责处理合同审批的服务直接 OOM排查发现频繁变更规则导致 GroovyClassLoader 实例堆积。JVM 卸载类需要 ClassLoader 不再被任何对象引用但 Liquor 脚本缓存里始终持有 CompiledScript 对象而 CompiledScript 又反向引用了自己的 ClassLoader导致旧版本类永远无法回收。修复方案是版本化类加载器模型。每个规则版本对应一个独立的脚本 ClassLoader 实例当规则从发布版本 A 变更为版本 B 时Liquor 会主动把版本 A 的所有 cached 脚本从缓存中移除并调用对应 ClassLoader 的clearCache()方法。同时执行链路单次请求依然可以持有旧版本的 ClassLoader保证正在执行的流程不受影响。等请求结束后不再有人引用旧 ClassLoader 时JVM 才会安全回收这批类。public class LiquorScriptCache { private final CacheString, VersionedEntry cache; public void invalidateVersion(String definitionId, int oldVersion) { ListVersionedEntry entries cache.asMap() .entrySet().stream() .filter(e - e.getValue().definitionId.equals(definitionId) e.getValue().version oldVersion) .map(Map.Entry::getValue) .collect(Collectors.toList()); for (VersionedEntry entry : entries) { entry.classLoader.clearCache(); cache.invalidate(entry.cacheKey); } } }需要注意的是clearCache()只能清理类对象之间的引用真正让 ClassLoader 被 GC 回收还需要其他引用全部断开。最容易忽略的地方是 Spring 的 SpEL 表达式或者核心上下文里间接持有类的静态引用以及线程池的 ThreadLocal 中遗留的 ClassLoader。所以我们专门在每次请求结束后统一清理 ThreadLocal 的上下文快照。4. 一个接入实例把“订单超阈值自动审批”跑通并发布纯讲架构很容易让人失去手感这里用一个完整实例串联 Liquor 的用法。假设客户要求订单申请提交后如果金额大于 5000 元且该客户信用等级为普通审批流程需要多一级财务主管审核否则系统自动通过并发送消息。先定义一张规则。规则编辑器会生成类似下面的 JSON 配置。这里我为了让描述直观用了伪 Java 语法实际存储的是结构化的条件节点。definitionId: order_approval_v2 type: RULE root: type: BRANCH children: - expression: order.amount 5000 customer.level NORMAL thenTarget: type: ACTION config: action: CREATE_TASK taskOwner: finance_supervisor taskName: 订单财务复核 - expression: order.amount 5000 thenTarget: type: ACTION config: action: AUTO_PASS sendMessage: true业务服务提交订单时并不需要关心这条规则是哪个版本。它只需要从 Liquor 的 SDK 中拿到引擎入口构建好环境上下文然后执行规则。PostMapping(/order/submit) public Response submit(RequestBody OrderCreateRequest request) { Order order orderService.create(request); MapString, Object vars new HashMap(); vars.put(order, order); vars.put(customer, customerService.findById(order.getCustomerId())); ExecutionRequest exec ExecutionRequest.builder() .definitionId(order_approval_v2) .version(4) .variables(vars) .build(); ExecutionResult result liquorEngine.execute(exec); if (CREATE_TASK.equals(result.getAction())) { workflowService.createTaskFromConfig(result.getTaskConfig(), order); return Response.accepted(订单已进入财务复核); } if (AUTO_PASS.equals(result.getAction())) { if (result.needSendMessage()) { notifyService.sendOrderPassed(order); } return Response.accepted(订单自动通过); } throw new UnsupportedOperationException(未知的引擎动作); }这段代码本身平平无奇但它把一个重要的模式体现出来了业务接口层不写任何规则判断只作为引擎结果的下游执行方。以后客户把 5000 改成 8000、给信用等级增加一个“VIP 快速通道”或者把一级财务复核改成二级复核我们只需要在平台的规则配置页发布一个新版本订单服务的代码一行都不用改。4.1 版本发布后的“老会话”怎么处理接入过程中最容易被忽略的是正在进行的业务流程。假设某个订单在 A 版本规则下“超过 5000 元进入财务复核”业务人员点击提交前管理员把阈值调整成 8000发布 B 版本。如果只用最新版本执行刚才那个用户提交时到底按 5000 还是 8000 算从业务一致性讲一个流程一旦开始路径应该尽量稳定。Liquor 在设计上区分了“规则快照”和“规则版本”。每次发布都会生成一份新的快照可以看作一个不可变对象。流程实例启动时在流程上下文里记录当前使用的快照 ID后续步骤都按这个快照执行。新提交的流程则默认使用最新发布版本。这样既保证了在线流程不被突发规则变更打断也方便后查审计这条订单的审批链路为什么长这样都可以回溯到当时的规则快照。4.2 引擎结果如何回写上下文避免二次查询很多低代码平台实现联动时喜欢在业务代码里手动修改数据比如根据规则给某字段赋值后再查一遍数据库。Liqur 更推荐把规则中产出的临时结果先放回 LiquorContext而不是直接写库。例如规则触发后要设置“当前单据需要风险备注”就把ctx.set(needRiskRemark, true)。流程结束后业务服务一次性把上下文中需要持久化的字段写进业务表。这样做的收益是让规则执行具有可观测性。调试时最痛苦的问题就是字段值不知道是怎么变过来的。Liquor 提供了上下文快照审计记录每一步对上下文的修改值、修改节点和执行时间。出问题的时候实施人员可以直接在平台上查看一次流程执行的完整数据流而不是去数据库里翻变更日志。这也是后来客户满意度上升的关键原因。5. 生产环境的四个“真坑”以及我们堵住它们的方法Liquor 从可运行到可放心上生产经历了一个很长的打磨期。有很多问题不在代码逻辑本身而藏在环境条件里。下面这几个坑每个都值得写成一篇单独的故障复盘但核心心得可以概括在几条里。5.1 规则明明改了执行结果却还是旧的动态引擎上线后实施的同事反馈的第一个严重故障是修改规则并发布后有部分请求仍然走旧逻辑且没有任何报错。一开始怀疑是缓存但清掉 Redis 后现象依旧说明问题不在配置读取层。排查后定位到脚本缓存 key 设计得不够严谨。Liquor 当时用 definitionId 加版本号做缓存 key第一次发布时缓存了规则 A 的编译结果。第二次规则内容从 A 改到 B但定义 ID 没有变化版本号也没有递增到新值因为发布接口在特定场景下会“原地覆盖”一个草稿版本。缓存对照 key 时发现 version 没变直接返回了旧脚本。修复方式很直接缓存 key 必须加入规则正文的摘要值只依赖定义 ID 和版本号是不够的。5.2 元空间以肉眼可见的速度上涨这个问题在文章前面已经提到过核心原因就是 Groovy 的 ClassLoader 生命周期没有被回收。这里再补充一个容易被忽略的细节即使你使用了独立 ClassLoader 并主动清理只要有一个线程池或 ThreadLocal 持有该类加载器的引用GC 就永远不会回收对应的类。JVM 的元空间不会因为你不再使用旧规则就自动降下来它只认 ClassLoader 是否可达。我们在每个规则版本发布时增加了一个“现场体检”任务主动检查活跃 ClassLoader 数量、缓存条目数量和元空间已提交大小。如果一天内发布次数超过预设阈值监控系统会报警提醒管理员关注规则变更频率。最高峰时我们把一天两百多次的规则发布压缩到每周集中发布ClassLoader 数量稳定回到正常水平所有规则上线后不需要重启节点的目标才算真正达成。5.3 动态脚本线程池被写坏的规则拖垮Groovy 脚本确实灵活但不是所有灵活都应该无限制开放。有一次线上事故是某个实施顾问在脚本中写了一个while(true)的循环用于等待一个永远不会满足的条件。该调用进入线程池后长时间不返回导致动态脚本线程池的线程全部被占满其他规则请求排队等待最终整个业务服务无法处理任何审批动作。解决办法分两层。第一层是脚本执行时内置最大循环次数检查严格限制单条脚本执行时间。Liquor 的脚本上下文里维护了一个执行时钟超过阈值后主动抛异常中断。第二层是线程池的隔离不能让业务流程的公共线程和脚本运行线程混在一起。把不可信、不确定时长的代码放入独立线程池超时和拒绝策略都设为不阻塞主线程。第一次实现时我们还是心存侥幸觉得业务人员不会写出死循环后来证明一定要对“最笨的使用者”做防御设计。5.4 错误信息太“Java”使用者根本没法排查低代码平台的使用者不一定懂 Java。NullPointerException、ClassCastException 这类异常输出给页面用户不仅没用还会让人对整个平台失去信心。Liquor 对异常做了包装所有内部异常在抛出边界时都必须附上规则名、定义 ID、触发节点和上下文中的关键字段值。效果是什么样以前报错用户看到的是Cannot get property amount on null object完全摸不着头脑。现在看到的是「客户等级规则 执行失败字段 customer.level 为空当前订单号SO20240101001」。有了这个信息实施人员可以直接去查数据为什么缺失而不是先找研发看堆栈。我们甚至把异常的英文信息全部改写成中文并保留一个排查 code方便去日志系统聚合。这个细节看似不高级但它决定了动态引擎能不能真正交给一线人员使用。技术上强大但报错难懂最终只会变成研发人员自己的噩梦。5.5 一定要暴露内部执行指标不要黑盒运行Liquor 在核心调用链路上埋了 Micrometer 指标包括规则执行次数、表达式编译耗时、脚本执行耗时分布、缓存命中率、失败率、当前活跃脚本数量等。这些指标通过 Actuator 暴露给监控系统。不要认为自己的引擎很轻量就不做指标采集动态引擎一旦生产运行最需要的就是可观测性。我见过不少低代码平台内部执行逻辑完全是个黑盒出了问题只能重启或加日志。所以 Liquor 的要求很明确任何规则都必须能追踪、能回放、能量化耗时。对于执行时间超过 1 秒的慢规则我们会自动把上下文快照存为一条慢日志。这样后续做性能优化时就有真实样本可以分析而不是靠猜。我自己在 Liquor 上线后养成一个习惯每次要给动态引擎加一个新特性先问三个问题——这条新能力如果写错会造成什么后果执行超时时怎么中断使用者看到什么错误才够友好。把这三个问题想清楚再动代码比写完再补防御要省力得多。动态引擎不是银弹也不可能让所有业务都不写代码。但我个人觉得Java 后端团队如果认真做一次这样的项目对整个平台的架构能力提升很大因为你不只是写接口而是在设计一套“让业务规则自己生长的运行环境”。
返回列表