camunda feel - modric2jeff/archive GitHub Wiki

好——把问题拆开说清楚,给出可用的解决办法与安全提醒。

关键点(先结论)

  1. Camunda 存 process variable 时,会按已注册的 variable serializers 来选择序列化格式;如果没有合适的 serializer,默认会退回到 Java 序列化(java Serializable。这导致在引擎尝试持久化脚本/表达式生成的 POJO/Map 时会走 Java 序列化路径而失败或被拒绝。([docs.camunda.org]1)
  2. 推荐做法是把复杂对象以 JSON(或 XML)格式 存入变量,这通常通过 Camunda Spin(或 Variables.objectValue + 指定 serializationDataFormat) 来实现,从而避免 Java 序列化问题并提高互操作性。([docs.camunda.org]2, [Camunda]3)
  3. 你可以明确把变量设置为 JSON 类型:Variables.objectValue(obj).serializationDataFormat("application/json").create()(或使用 SerializationDataFormats.JSON 常量),这样引擎会用 JSON serializer 而不是 Java 序列化。([Camunda Forum]4, [docs.camunda.org]5)
  4. 引擎默认会禁止 Java 序列化(安全考虑);可以通过配置 javaSerializationFormatEnabled 打开,但通常不推荐(有安全风险)。更稳妥的是改用 JSON serializer 或注册自定义 serializer。([Camunda Forum]6)
  5. 如果需要更细粒度行为,可以通过注册自定义 TypedValueSerializer(或在引擎配置中添加 customPreVariableSerializers / customPostVariableSerializers)来处理特定类型的序列化/反序列化。([谷歌组]7, [GitHub]8)

为什么表达式会触发 Java 序列化并失败

  • 当表达式(JUEL / 脚本任务中的 JS/ Groovy 等)构造了一个 java.util.Map、List 或任意 POJO 并把它 setVariable 时,Camunda 在找不到更合适的序列化器(比如 JSON serializer)时,会尝试以 Java 序列化保存该对象。若对象不可序列化或引擎禁用了 java 序列化,就会抛异常或失败。最终结果就是“表达式生成 JSON 对象,但上下文序列化失败”的症状。([docs.camunda.org]1)

具体可用的修复/改进方案(按推荐顺序)

1) 最简单且推荐:把变量显式存为 JSON(程序端)

Java delegate / service task 中:

import static org.camunda.bpm.engine.variable.Variables;
import org.camunda.bpm.engine.variable.value.ObjectValue;

Map<String,Object> data = ...; // 你构造的对象/map
ObjectValue jsonValue = Variables
    .objectValue(data)
    .serializationDataFormat(Variables.SerializationDataFormats.JSON) // 或 "application/json"
    .create();
execution.setVariable("myData", jsonValue);

这样读取时 REST / External Task / 脚本都能以 JSON(可读)方式访问。([docs.camunda.org]5, [Camunda Forum]4)

2) 在脚本/表达式里用 Spin API(如果已经引入 camunda-spin)

在脚本任务(JavaScript/Groovy)或表达式里构造 JSON value:

// 伪示例:Nashorn/JS 脚本中
var spin = S(someJsObject); // 需要 spin 上下文可用
execution.setVariable("myJson", spin);

或者把对象先转成 JSON 字符串再以 ObjectValue 标记为 JSON。Spin 提供很多便捷方法来操作 JSON,并且注册了 JSON serializer。([docs.camunda.org]2, [Camunda]3)

3) 如果必须使用 Java 序列化(不推荐,安全风险)

在 process engine 配置中允许 Java 序列化:

<property name="javaSerializationFormatEnabled">true</property>

注意:这会引入反序列化攻击面 —— 仅在完全信任的环境下并了解风险时才考虑。([Camunda Forum]6)

4) 更高级:注册自定义 TypedValueSerializer

如果你有特定类型需要自定义序列化(例如用 Jackson),可以实现 TypedValueSerializer 并通过引擎配置(customPreVariableSerializers / customPostVariableSerializers)或在 ProcessEnginePlugin 中注册。这样可以完全控制序列化/反序列化行为。适合需要跨进程/跨语言兼容的场景。([谷歌组]7, [GitHub]8)


对表达式/业务的具体影响(要点)

  • 可读性:JSON 存储后可通过 REST/API 直观看到;Java 序列化存储内容不可读。([docs.camunda.org]1)
  • 互操作性:外部 worker(external task)、非 Java 客户端或前端查看变量时,JSON 更安全、兼容。([Camunda Forum]4)
  • 性能/大小:Java 序列化通常比 JSON 更紧凑,但带来可移植性与安全问题。
  • 安全性:启用 Java 序列化增加反序列化攻击风险(所以默认禁用/慎用)。([Camunda Forum]6)

建议的短期操作(你可以现在做的)

  1. 在出问题的表达式/脚本位置,把对象改为 JSON 字符串或使用 Variables.objectValue(...).serializationDataFormat(JSON) 存入。
  2. 在需要脚本层创建复杂对象时,优先使用 Spin(若已在项目中引入)。
  3. 不要轻易打开 javaSerializationFormatEnabled,除非你理解并接受风险。
  4. 如果你的系统对序列化有统一需求(比如用 Jackson),考虑实现并注册一个自定义 serializer。

如果你愿意,我可以:

  • 帮你把出问题的表达式 / 脚本贴出来(或者贴出你如何 setVariable 的代码片段),我可以直接给出改成 JSON / Spin 的改写示例;或者
  • 给出一个演示项目的小片段(含依赖和代码)来展示 Variables.objectValue(...).serializationDataFormat(JSON) 在多实例/External Task 场景下的用法。

想看哪个?

好——这事我懂你的意图:你想让 FEEL 表达式保留「点语法」访问(例如 order.customer.name),但当 FEEL 访问某个属性时,自动触发从数据库 / 网络拉取该对象属性(或按需加载)。下面把可行方案、实现要点、陷阱与示例代码都给你——并说明在哪些场景下 不应该 这样做(DMN 可重复性/性能/安全性考量)。


快速结论(先看要点)

  • 最实用、最简单的做法:把 FEEL 在决策时要访问的变量放成一个延迟加载(lazy)包装对象(实现 java.util.Map 或标准 JavaBean getter),当 FEEL 通过点语法访问属性时,包装对象的 get(...) / getter 触发 DB/HTTP 查询并缓存结果。FEEL 本身会把 Map / JavaBean 当作上下文对象访问。([Camunda 7 文档]1)
  • 为避免序列化问题,把这样的延迟对象以 transient typed variable 注入(typed API),或仅在内存中短期传递,避免把不可序列化对象持久化到数据库。Camunda 支持通过 Variables.objectValue(obj, true) 标记为 transient。([Camunda 7 文档]2)
  • 更底层/优雅的办法:实现 FEEL 的 Value Mapper SPI(或 Feel 的 custom value-mapper / custom function provider),在 FEEL 层将变量解析为自定义 FEEL 值(可按需触发 load)。这个方案对引擎集成更整洁,但实现复杂度更高,需要编写并注册 SPI/plugin。([camunda.github.io]3)

方案 A —— 推荐:Lazy Map / Lazy Bean (最少侵入,开发快)

思路:实现 Map<String,Object>(或 JavaBean),把它放入流程变量(最好是 transient),FEEL 在解析 obj.prop 时会调用 map.get("prop") 或 getter,从而触发加载。

优点:实现简单、可以在业务代码里完全控制缓存/并发/超时/降级;不必改 FEEL 引擎。 缺点:如果把对象持久化,会遇到序列化问题;FEEL 多次访问会触发多次远程调用(需缓存)。

示例(伪代码,便于落地):

public class LazyPropertyMap implements Map<String,Object> {
    private final String id; // 比如 orderId
    private final Map<String,Object> cache = new ConcurrentHashMap<>();
    private final DataLoader dataLoader; // 你的 DB/HTTP 客户端

    public LazyPropertyMap(String id, DataLoader loader) {
        this.id = id;
        this.dataLoader = loader;
    }

    @Override
    public Object get(Object key) {
        String k = String.valueOf(key);
        // 先缓存
        return cache.computeIfAbsent(k, kk -> {
            try {
                // 从 DB/网络加载单个属性(建议后端提供按属性拉取 API)
                return dataLoader.loadProperty(id, kk);
            } catch (Exception e) {
                // 处理超时/异常,返回 null 或抛出包裹异常 — FEEL 会看到 null 或失败
                return null;
            }
        });
    }

    // 其余 Map 方法简单代理或实现(size, containsKey, keySet 等)
}

把它作为 transient 变量注入(示例):

Object lazy = new LazyPropertyMap(orderId, myLoader);
TypedValue transientObject = Variables.objectValue(lazy, true).create();
execution.setVariable("order", transientObject);

然后在 DMN/FEEL 中就能写: order.customer.name,第一次访问会触发 load。参考 Camunda transient 变量说明。([Camunda 7 文档]2)

注意点:

  • 一定要在 Variables.objectValue(..., true) 标记为 transient(如果你不希望被序列化到 DB)。
  • 在 long-running 流程中 transient 会在事务边界丢失(transient 只在当前事务内有效),所以这必须发生在同一事务/同步流程中(例如,在决策评估所在的 service task 中)。([Camunda 7 文档]2)
  • 内部应做缓存避免重复远程调用,防止 FEEL 表达式重复求值引起 N+1 问题。建议实现批量预取接口(如果 FEEL 会一次性访问多个字段,先做一次批量拉取)。

方案 B —— 在 FEEL 层做扩展(ValueMapper / Custom Function Provider)

若你希望更“引擎级” 的集成(例如把外部对象映射为 FEEL 原生值,或支持显式函数 fetch(orderId).customer.name),可以:

  1. 实现 Value Mapper SPI(CustomValueMapper) —— 控制如何把 Java 变量转换为 FEEL 值(ValueMapper 文档说明可以定制变量到 FEEL 数据模型的转换)。这能在 FEEL 访问前拦截并返回自定义的 ValContext/ValObject,从而实现按需加载。实现后通过 META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper 或在创建 FEEL 引擎时注入来注册。([camunda.github.io]3)

  2. 或实现 FeelCustomFunctionProvider / CustomFunction —— 注册自定义函数,在 FEEL 中以函数形式调用(例如 getCustomer(orderId).name)。相对简单,但不能保留 . 原生属性访问语法(除非你在函数内部返回一个能被 FEEL 识别为 object 的值)。([camunda.github.io]4, [Camunda 7 文档]5)

优点:更整洁、可控、可重用;能把复杂类型映射到 FEEL 数据模型。 缺点:实现复杂、需要注册插件并测试 FEEL 类型映射,跨 Java/Scala 类型转换可能繁琐(feel-scala 的实现细节需要注意)。([Camunda Forum]6)


方案 C —— 把数据提前加载到 JSON 变量(最安全、推荐用于生产)

不要在 FEEL 里做远程调用;在进入决策前(service task / execution listener)统一拉取所需数据,序列化成 JSON(或 Spin / objectValue(JSON)),传入 FEEL。这样保留了 DMN 的确定性与可测试性。若数据量大可做分页或只拉所需字段。

优点:决策评估快、可重放、容易审计。 缺点:如果不易预知访问字段,可能需要额外查询/更复杂查询逻辑(但通常是更安全与可维护的选择)。


风险 & 设计建议(很重要)

  1. DMN/FEEL 的职业建议:DMN/FEEL 本质上用于“决定”——推荐表达式是纯函数式、可重复的。把网络/数据库调用放入 FEEL 会破坏可重复性、测试性以及可能造成性能/超时/阻塞问题。尽量把 side-effect / IO 放在流程的 service layer(在评估决策之前做数据准备)。(这是最佳实践建议。)([Camunda 7 文档]7)
  2. 性能:FEEL 在评估复杂表达式时可能多次访问同一属性,必须在 wrapper 里做缓存或实现批量加载 API。
  3. 事务性:如果你在加载数据时依赖 DB 事务,请注意 FEEL 评估线程和事务边界(transient 变量只在当前事务内有效)。([Camunda 7 文档]2)
  4. 序列化:若你的 wrapper 被引擎持久化(非 transient),会遇到 Java 序列化或 JSON 序列化问题。要么把 wrapper 标记为 transient,要么实现可序列化 / Jackson friendly 的 DTO 并在需要时把数据展开成 JSON 存储。([Camunda 7 文档]2, [docs.camunda.io]8)

参考实现片段(综合示例)

  1. LazyMap(见上面)。
  2. 在流程里注入 transient 变量(Java delegate):
public class PrepareDecisionDelegate implements JavaDelegate {
  @Override
  public void execute(DelegateExecution execution) {
    LazyPropertyMap orderProxy = new LazyPropertyMap(orderId, myLoader);
    TypedValue typedTransient = Variables.objectValue(orderProxy, true).create();
    execution.setVariable("order", typedTransient); // FEEL 可以直接读 order.customer.name
  }
}
  1. (可选)ValueMapper 插件示意(注册点与文档见 FEEL-Scala Value Mapper SPI):实现并在 META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper 注册类名。详见 Value Mapper 文档。([camunda.github.io]3)

我能帮你做的下一步

  • 告诉我你用的是 Camunda 7 还是 Camunda 8(以及具体版本),以及运行方式(Spring Boot / 独立容器 / embedded)。不同版本的 FEEL 引擎和 SPI 注册方式略有差别,我会给出可直接复制粘贴的 Maven 依赖、完整 Java 类与 plugin 注册文件,或一个 Spring Boot 示例。
  • 如果愿意,我可以把上面的 LazyPropertyMap 写成完整、线程安全、带超时/缓存/批量预取的实现,并演示如何在 DMN 输入变量里使用(包含测试用例)。

想先要哪个:A)完整 LazyMap + transient 注入示例代码,还是 B)FEEL ValueMapper 插件的完整实现与注册示例?或者把你的 Camunda 版本发来,我直接给出针对你环境的代码。

好 — 我给你一份可直接参考、在 Camunda 7 + Spring Boot 下扩展 FEEL 的完整示例思路与代码骨架,重点说明 如何用 ValueMapper 把自定义对象(比如需要按属性从 DB/网络懒加载的实体)暴露给 FEEL,以及如何注册它。先说明关键点,然后给出 Java 代码(可放在 Spring Boot 项目里),最后给出注意事项和替代方案建议。

重要背景(参考):FEEL 引擎允许通过 ValueMapper SPI 将 Java/Spin 对象映射为 FEEL 的 Val(例如 ValContextValString 等)。Camunda 的 Spin 已有现成 SpinValueMapper 处理 JSON/XML;自定义类型可以通过实现 CustomValueMapper / 继承 JavaCustomValueMapper 来接入。([GitHub]1, [Camunda 7 文档]2)


概览(思路)

  1. 定义一个标记/代理类型(例如 LazyEntityRef),它保存 id 和一个 loader(service)用于按需查询属性。
  2. 实现一个 JavaCustomValueMapper 子类(例如 LazyEntityValueMapper),在 toValue(...) 中识别 LazyEntityRef,并 返回一个 ValContext
  3. ValContext 提供一个 自定义的 FEEL Context 实现(Scala trait,在 Java 中实现),该实现将在 FEEL 访问 a.b 时触发对 LazyEntityRef 的按属性加载。
  4. 把 mapper 注册到 SPI(META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper),FEEL 引擎启动时会用 ServiceLoader 自动发现。([Jar Download]3)

下面是一个尽量完整但简化的 Java 示例(假设项目已经引入 feel-engine / camunda dmn feel-scala 对应依赖)。示例演示懒加载属性并把它暴露为 FEEL context。


代码示例(简化、可直接放入 Spring Boot 工程)

注意:FEEL engine 的 Context 是 Scala trait,因此 Java 里要引用 scala.Optionscala.collection 等。以下代码已按这种互操作写出(需要 feel-engine 在 classpath)。

1) 标记/代理对象(你的实体代理)

// src/main/java/com/example/feel/LazyEntityRef.java
package com.example.feel;

import java.util.*;
import java.util.function.Function;

public class LazyEntityRef {
  private final String id;
  // a function that when given (id, propertyName) returns the property value (could call DB or HTTP)
  private final BiPropertyLoader loader;
  private final Map<String, Object> cache = new HashMap<>();

  public LazyEntityRef(String id, BiPropertyLoader loader) {
    this.id = id;
    this.loader = loader;
  }

  public String getId() { return id; }

  public Object getProperty(String property) {
    if (!cache.containsKey(property)) {
      Object v = loader.load(id, property);
      cache.put(property, v);
    }
    return cache.get(property);
  }

  // optionally list known keys if your service can
  public Set<String> knownKeys() {
    return Collections.emptySet(); // 或从 loader 请求属性列表
  }

  public interface BiPropertyLoader {
    Object load(String id, String propertyName);
  }
}

2) 自定义 ValueMapper(关键)

// src/main/java/com/example/feel/LazyEntityValueMapper.java
package com.example.feel;

import org.camunda.feel.valuemapper.JavaCustomValueMapper;
import org.camunda.feel.syntaxtree.Val;
import org.camunda.feel.syntaxtree.ValContext;
import org.camunda.feel.context.Context;
import scala.Option;
import scala.collection.JavaConverters;

import java.util.Optional;
import java.util.function.Function;

/**
 * 把 LazyEntityRef 映射为 ValContext(FEEL 的 context/object)。
 * 当 FEEL 读取某个键时,会触发 LazyEntityRef#getProperty。
 */
public class LazyEntityValueMapper extends JavaCustomValueMapper {

  @Override
  public Optional<Val> toValue(Object x, Function<Object, Val> innerValueMapper) {
    if (x instanceof LazyEntityRef) {
      LazyEntityRef ref = (LazyEntityRef) x;

      // 实现 org.camunda.feel.context.Context(Scala trait),在 get / keys 调用时触发加载
      Context ctx = new Context() {
        private static final long serialVersionUID = 1L;

        @Override
        public Option<Object> get(String name) {
          // 如果属性不存在则返回 scala.Option.empty()
          Object v = ref.getProperty(name);
          return Option.apply(v); // 如果 v == null,Option.apply(null) -> None(FEEL 中视为 null)
        }

        @Override
        public scala.collection.Iterable<String> keys() {
          // 将 Java Set 转 scala Iterable;如果你不能提前知道 keys,可以返回 empty
          return JavaConverters.asScalaIteratorConverter(ref.knownKeys().iterator()).asScala().toIterable();
        }
      };

      Val v = new ValContext(ctx);
      return Optional.of(v);
    }
    return Optional.empty();
  }

  @Override
  public Optional<Object> unpackValue(Val value, Function<Val, Object> innerValueMapper) {
    // 如需将 FEEL 值反序列化为 Java 对象,可在这里实现
    return Optional.empty();
  }

  @Override
  public int priority() {
    // 优先级:较高的 priority 会先被尝试
    return 100;
  }
}

3) SPI 注册

src/main/resources/META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper 中写入你的实现类全名:

com.example.feel.LazyEntityValueMapper

(这样 FEEL 引擎启动时会通过 ServiceLoader 自动发现)


在 Spring Boot / Camunda 中的集成要点

  • 将上面的类打包入你的 Spring Boot 可执行 JAR(feel-engine 相关依赖需在 classpath)。把 META-INF/services/... 文件放在 src/main/resources
  • FEEL-Scala 的 Context 是 Scala trait;Java 实现时需要引入 scala-library/feel-engine,并使用 scala.Optionscala.collection 互操作工具(示例中用了 JavaConverters)。
  • toValue 中返回 ValContext 后,FEEL 在表达式里访问 x.foo 就会去调用你实现的 Context.get("foo") —— 也就是会触发你的懒加载逻辑(从 DB / HTTP 查询)并把结果按内置规则映射为 FEEL Val(你也可以在 get 里返回 Java 对象,让 FEEL 的其他 ValueMapper 进一步转换)。
  • priority() 用来控制多个 mapper 的顺序——如果你的 mapper 要优先处理你的类型,确保返回较高值(例如 100)。文档与已有 SpinValueMapper 的实现可以参考。([Camunda 7 文档]2)

常见问题与建议

  1. 为什么我之前会遇到“默认 Java 序列化导致 FEEL/JSON 失败”的问题? Camunda 默认把复杂变量以 Java 序列化(Serializable) 存入 DB 时,某些场景下 FEEL/DMN 在把变量映射为 FEEL 值时会期望 JSON/Map/Spin 类型;如果你把原始对象以 Java 序列化存储,FEEL 的默认 mapper 可能不能正确转换。解决办法是:使用 Spin(JSON)存储复杂对象,或实现 ValueMapper 来把你的对象转换为 FEEL 能理解的 Val(例如 ValContext)。参见 Spin 与 FEEL 集成文档以及 SpinValueMapper 实现。([Camunda 7 文档]4)

  2. 懒加载的副作用和性能

    • 每次 FEEL 访问某个属性都会触发 get,因此建议在 LazyEntityRef 内实现本地缓存(示例已示范)。
    • 如果表达式里会遍历很多元素或多次访问同一对象属性,优先做合批查询或预加载以减少 DB/网络开销。
  3. 安全与隔离

    • FEEL 表达式是在引擎内部执行的,避免在 get 中做非常危险的操作(重试、长时间阻塞)——如果必须,做好超时和熔断。
    • 在并发场景下,LazyEntityRef 的缓存要线程安全(示例里使用简单 HashMap,生产要用并发结构或同步)。
  4. 调试

    • 可以在 toValue 打 log,确认 FEEL 在什么时候把你的 Java 对象交给了 mapper。
    • 使用 FEEL Playground(或单元测试)验证表达式行为。([camunda.github.io]5)

参考(阅读与 API)

  • FEEL-Scala 的 “Custom Object Mapper / ValueMapper” 概念(SPI)说明。([GitHub]1)
  • SpinValueMapper(Camunda 平台源码 Javadoc)— 查看 Spin 到 Val 的现成映射实现,作为实现参考。([Camunda 7 文档]2)
  • FEEL 引擎使用与 Bootstrapping(如何把 engine 加入 classpath / 启动参数等)。([camunda.github.io]5)

如果你愿意,我可以:

  • 把上面的示例改成可直接编译的 Maven/Gradle 完整小工程(含 pom.xml、依赖)并验证(我会给出最小依赖和 classpath 要点);或
  • 根据你现有的实体/接口(把你的 LazyEntityRef 的 loader 接口/返回结构贴出来)把示例改写为生产级(加并发缓存、超时、批量加载支持),并给出性能/异常处理建议。

你想要我继续把它做成可直接运行的 Spring Boot demo(包含依赖版本和 build 文件)吗?

好 — 下面给你一套可直接实操的办法与示例,覆盖在 Camunda + Groovy 脚本中扩展自定义函数、实现自定义对象属性访问逻辑,以及性能/安全/可维护性的注意点。按从简单到进阶排序:优先用绑定 helper / wrapper;需要全局行为再考虑 metaClass 或自定义 Script 基类;并给出预编译与缓存的建议。

1) 最简单且推荐:把“工具/服务对象”注入到脚本的绑定(Binding)里

思路:把一个 Java/Spring helper(封装 DB/网络/缓存/复杂逻辑)放到脚本上下文里,脚本只做声明式调用。优点:可测试、易维护、安全性高。

示例(先在流程前/一个 Java Delegate 中注入):

// JavaDelegate 在流程中执行一次,或在启动时把 bean 放入流程变量
public class InjectUtilsDelegate implements JavaDelegate {
  private final MyScriptUtils utils; // Spring 注入

  public InjectUtilsDelegate(MyScriptUtils utils){ this.utils = utils; }

  @Override
  public void execute(DelegateExecution execution) {
    execution.setVariable("utils", utils); // 将 helper 放到脚本变量中
  }
}

Groovy 脚本(在 Script Task / InputMapping / Gateway 条件里):

// scriptFormat="groovy"
def user = utils.fetchUserById(userId)   // utils 来自绑定
if (user.active && utils.checkRisk(user)) {
  // ...
}

说明:Camunda 在执行脚本时会把流程变量(如 utils)放入脚本 binding,脚本可以直接调用。

2) 用“Wrapper / Proxy”实现按需的自定义属性访问(最灵活)

思路:为目标对象包一层 wrapper,覆盖 getProperty / propertyMissing,在访问属性时触发 DB/HTTP 调用或自定义逻辑;wrapper 本身当作脚本变量注入。

示例(Groovy 风格 wrapper):

import groovy.lang.GroovyObjectSupport

class RemotePropertyWrapper extends GroovyObjectSupport {
  private final Object target
  RemotePropertyWrapper(Object target){ this.target = target }

  @Override
  Object getProperty(String name) {
    // 先在 target 上取
    try {
      return target."$name"
    } catch (MissingPropertyException e) {
      // 未命中 -> 自定义加载逻辑(比如从 DB/HTTP 获取)
      return RemoteLoader.fetchProperty(target, name)
    }
  }
}

在 Java Delegate 中注入:

execution.setVariable("wrappedOrder", new RemotePropertyWrapper(order));

脚本中使用:

def customerName = wrappedOrder.customerName   // 若 target 无该属性,会触发 RemoteLoader.fetchProperty

3) 用 Groovy 的 propertyMissing / methodMissing 在对象上动态解析属性/方法

如果你能控制对象类(或给对象 mixin),可以直接在类中实现 propertyMissing(String name),Groovy 在找不到属性时会回调它,从而实现懒加载/远程访问。

示例(在类里实现):

class Domain {
  def propertyMissing(String name) {
    // 自定义取值逻辑
    return RemoteLoader.fetch(name, this.id)
  }
}

注意:这需要在创建 Domain 对象时使用这个类;并且 method/property missing 的开销要考虑。

4) 全局扩展:ExpandoMetaClass / 修改 metaClass(慎用)

你可以在应用启动时修改某个类的 metaClass,为类增加方法或覆盖属性访问。这能让所有脚本自动获得这些扩展,但它是全局性的,要非常小心线程安全与副作用。

示例(启动时执行一次):

// 在 Spring @PostConstruct 中执行
MyDomainClass.metaClass.getCustomerName = { ->
  // delegate 是实例
  RemoteLoader.fetch("customerName", delegate.id)
}

脚本中直接使用:

def name = myDomainInstance.customerName

风险:改动会影响 JVM 中所有同类实例;升级/调试时容易出问题。

5) 自定义 Script 基类(给脚本提供隐式函数)

通过 Groovy CompilerConfiguration#setScriptBaseClass 或用 GroovyShell/Compiler 在你自行 eval 脚本时设置“基类”,可以让脚本直接调用你定义的工具方法(无需显式 utils. 前缀)。在 Camunda 默认的 JSR-223 执行流里不容易插入,但如果你自己在 Java Delegate 中执行 Groovy(而不是让 Camunda 自动执行),这是很强的方式。

示例(Java 中用 GroovyShell):

CompilerConfiguration cc = new CompilerConfiguration();
cc.setScriptBaseClass("com.example.BaseScript"); // BaseScript 提供 fetchProp(...)
GroovyShell shell = new GroovyShell(binding, cc);
Object result = shell.evaluate(scriptText);

BaseScript:

abstract class BaseScript extends Script {
  def fetchProp(obj, name) { RemoteLoader.fetch(name, obj.id) }
}

脚本可以直接:

def v = fetchProp(order, "customerName")

6) 提升性能:预编译 & 缓存脚本 / 复用 ScriptEngine

Groovy 脚本在频繁评估时会有编译/创建引擎成本。推荐做法:

  • 使用 Compilable 接口预编译脚本并缓存 CompiledScript(如果使用 JSR-223);
  • 或者在 Java Delegate 中使用 GroovyShell + CompilerConfiguration 预编译并重用(要注意线程安全:GroovyShell 不是线程安全,编译后可缓存 Class 并用不同 Binding 执行)。
  • 避免在每次条件判断里创建新脚本引擎。

7) 安全性与健壮性(重要)

  • 脚本有完整 JVM 访问能力 —— 不可信任的脚本可能造成数据泄露或被滥用。
  • 如果需要外部可编辑脚本(业务可改),请增加限制:超时、并发限制、白名单 API、输入校验。
  • 考虑在脚本中使用只读/受限的 helper API(不要把整个 Spring 上下文或敏感 bean 注入到脚本)。
  • 捕获异常并在脚本外侧(Java Delegate)做降级处理与超时保护。

8) 在 Camunda 中的实操建议(总结)

  1. 优先方案:把复杂逻辑写成 Java/Spring 服务(可单元测试),在流程里把服务以 utils/svc 变量注入脚本绑定;脚本只做简单调用。
  2. 需要按属性懒加载的,使用 Wrapper(覆盖 getProperty / propertyMissing)并注入该 wrapper。
  3. 需要给所有脚本提供隐式函数的,考虑自定义 Script 基类或在应用启动时用 metaClass 注入全局方法(理解风险后再用)。
  4. 性能关键路径:预编译脚本并缓存,避免每次创建脚本引擎。
  5. 安全关键路径:只把受限 helper 注入、避免暴露整个应用上下文,考虑超时/调用限制。

如果你愿意,我可以立刻给出 与你当前项目匹配的具体实现示例(例如:Spring Boot + Camunda7,演示如何在启动时把 MyUtils 注入到所有脚本的 binding,或展示一个 RemotePropertyWrapper 的完整 Java/Groovy + 流程示例),我会把完整可运行的代码贴出来。想看哪一个例子?

⚠️ **GitHub.com Fallback** ⚠️