camunda feel - modric2jeff/archive GitHub Wiki
好——把问题拆开说清楚,给出可用的解决办法与安全提醒。
- Camunda 存 process variable 时,会按已注册的 variable serializers 来选择序列化格式;如果没有合适的 serializer,默认会退回到 Java 序列化(java
Serializable)。这导致在引擎尝试持久化脚本/表达式生成的 POJO/Map 时会走 Java 序列化路径而失败或被拒绝。([docs.camunda.org]1) - 推荐做法是把复杂对象以 JSON(或 XML)格式 存入变量,这通常通过 Camunda Spin(或 Variables.objectValue + 指定 serializationDataFormat) 来实现,从而避免 Java 序列化问题并提高互操作性。([docs.camunda.org]2, [Camunda]3)
- 你可以明确把变量设置为 JSON 类型:
Variables.objectValue(obj).serializationDataFormat("application/json").create()(或使用SerializationDataFormats.JSON常量),这样引擎会用 JSON serializer 而不是 Java 序列化。([Camunda Forum]4, [docs.camunda.org]5) - 引擎默认会禁止 Java 序列化(安全考虑);可以通过配置
javaSerializationFormatEnabled打开,但通常不推荐(有安全风险)。更稳妥的是改用 JSON serializer 或注册自定义 serializer。([Camunda Forum]6) - 如果需要更细粒度行为,可以通过注册自定义
TypedValueSerializer(或在引擎配置中添加customPreVariableSerializers/customPostVariableSerializers)来处理特定类型的序列化/反序列化。([谷歌组]7, [GitHub]8)
- 当表达式(JUEL / 脚本任务中的 JS/ Groovy 等)构造了一个
java.util.Map、List 或任意 POJO 并把它setVariable时,Camunda 在找不到更合适的序列化器(比如 JSON serializer)时,会尝试以 Java 序列化保存该对象。若对象不可序列化或引擎禁用了 java 序列化,就会抛异常或失败。最终结果就是“表达式生成 JSON 对象,但上下文序列化失败”的症状。([docs.camunda.org]1)
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)
在脚本任务(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)
在 process engine 配置中允许 Java 序列化:
<property name="javaSerializationFormatEnabled">true</property>注意:这会引入反序列化攻击面 —— 仅在完全信任的环境下并了解风险时才考虑。([Camunda Forum]6)
如果你有特定类型需要自定义序列化(例如用 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)
- 在出问题的表达式/脚本位置,把对象改为 JSON 字符串或使用
Variables.objectValue(...).serializationDataFormat(JSON)存入。 - 在需要脚本层创建复杂对象时,优先使用 Spin(若已在项目中引入)。
-
不要轻易打开
javaSerializationFormatEnabled,除非你理解并接受风险。 - 如果你的系统对序列化有统一需求(比如用 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)
思路:实现 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 会一次性访问多个字段,先做一次批量拉取)。
若你希望更“引擎级” 的集成(例如把外部对象映射为 FEEL 原生值,或支持显式函数 fetch(orderId).customer.name),可以:
-
实现 Value Mapper SPI(CustomValueMapper) —— 控制如何把 Java 变量转换为 FEEL 值(ValueMapper 文档说明可以定制变量到 FEEL 数据模型的转换)。这能在 FEEL 访问前拦截并返回自定义的
ValContext/ValObject,从而实现按需加载。实现后通过META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper或在创建 FEEL 引擎时注入来注册。([camunda.github.io]3) -
或实现 FeelCustomFunctionProvider / CustomFunction —— 注册自定义函数,在 FEEL 中以函数形式调用(例如
getCustomer(orderId).name)。相对简单,但不能保留.原生属性访问语法(除非你在函数内部返回一个能被 FEEL 识别为 object 的值)。([camunda.github.io]4, [Camunda 7 文档]5)
优点:更整洁、可控、可重用;能把复杂类型映射到 FEEL 数据模型。 缺点:实现复杂、需要注册插件并测试 FEEL 类型映射,跨 Java/Scala 类型转换可能繁琐(feel-scala 的实现细节需要注意)。([Camunda Forum]6)
不要在 FEEL 里做远程调用;在进入决策前(service task / execution listener)统一拉取所需数据,序列化成 JSON(或 Spin / objectValue(JSON)),传入 FEEL。这样保留了 DMN 的确定性与可测试性。若数据量大可做分页或只拉所需字段。
优点:决策评估快、可重放、容易审计。 缺点:如果不易预知访问字段,可能需要额外查询/更复杂查询逻辑(但通常是更安全与可维护的选择)。
- DMN/FEEL 的职业建议:DMN/FEEL 本质上用于“决定”——推荐表达式是纯函数式、可重复的。把网络/数据库调用放入 FEEL 会破坏可重复性、测试性以及可能造成性能/超时/阻塞问题。尽量把 side-effect / IO 放在流程的 service layer(在评估决策之前做数据准备)。(这是最佳实践建议。)([Camunda 7 文档]7)
- 性能:FEEL 在评估复杂表达式时可能多次访问同一属性,必须在 wrapper 里做缓存或实现批量加载 API。
- 事务性:如果你在加载数据时依赖 DB 事务,请注意 FEEL 评估线程和事务边界(transient 变量只在当前事务内有效)。([Camunda 7 文档]2)
- 序列化:若你的 wrapper 被引擎持久化(非 transient),会遇到 Java 序列化或 JSON 序列化问题。要么把 wrapper 标记为 transient,要么实现可序列化 / Jackson friendly 的 DTO 并在需要时把数据展开成 JSON 存储。([Camunda 7 文档]2, [docs.camunda.io]8)
- LazyMap(见上面)。
- 在流程里注入 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
}
}- (可选)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(例如ValContext、ValString等)。Camunda 的 Spin 已有现成SpinValueMapper处理 JSON/XML;自定义类型可以通过实现CustomValueMapper/ 继承JavaCustomValueMapper来接入。([GitHub]1, [Camunda 7 文档]2)
- 定义一个标记/代理类型(例如
LazyEntityRef),它保存 id 和一个 loader(service)用于按需查询属性。 - 实现一个
JavaCustomValueMapper子类(例如LazyEntityValueMapper),在toValue(...)中识别LazyEntityRef,并 返回一个ValContext。 - 为
ValContext提供一个 自定义的 FEELContext实现(Scala trait,在 Java 中实现),该实现将在 FEEL 访问a.b时触发对LazyEntityRef的按属性加载。 - 把 mapper 注册到 SPI(
META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper),FEEL 引擎启动时会用ServiceLoader自动发现。([Jar Download]3)
下面是一个尽量完整但简化的 Java 示例(假设项目已经引入 feel-engine / camunda dmn feel-scala 对应依赖)。示例演示懒加载属性并把它暴露为 FEEL context。
注意:FEEL engine 的 Context 是 Scala trait,因此 Java 里要引用 scala.Option、scala.collection 等。以下代码已按这种互操作写出(需要 feel-engine 在 classpath)。
// 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);
}
}// 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;
}
}在 src/main/resources/META-INF/services/org.camunda.feel.valuemapper.CustomValueMapper 中写入你的实现类全名:
com.example.feel.LazyEntityValueMapper
(这样 FEEL 引擎启动时会通过 ServiceLoader 自动发现)
- 将上面的类打包入你的 Spring Boot 可执行 JAR(
feel-engine相关依赖需在 classpath)。把META-INF/services/...文件放在src/main/resources。 - FEEL-Scala 的 Context 是 Scala trait;Java 实现时需要引入
scala-library/feel-engine,并使用scala.Option、scala.collection互操作工具(示例中用了JavaConverters)。 -
toValue中返回ValContext后,FEEL 在表达式里访问x.foo就会去调用你实现的Context.get("foo")—— 也就是会触发你的懒加载逻辑(从 DB / HTTP 查询)并把结果按内置规则映射为 FEELVal(你也可以在get里返回 Java 对象,让 FEEL 的其他 ValueMapper 进一步转换)。 -
priority()用来控制多个 mapper 的顺序——如果你的 mapper 要优先处理你的类型,确保返回较高值(例如 100)。文档与已有SpinValueMapper的实现可以参考。([Camunda 7 文档]2)
-
为什么我之前会遇到“默认 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) -
懒加载的副作用和性能
- 每次 FEEL 访问某个属性都会触发
get,因此建议在LazyEntityRef内实现本地缓存(示例已示范)。 - 如果表达式里会遍历很多元素或多次访问同一对象属性,优先做合批查询或预加载以减少 DB/网络开销。
- 每次 FEEL 访问某个属性都会触发
-
安全与隔离
- FEEL 表达式是在引擎内部执行的,避免在
get中做非常危险的操作(重试、长时间阻塞)——如果必须,做好超时和熔断。 - 在并发场景下,
LazyEntityRef的缓存要线程安全(示例里使用简单HashMap,生产要用并发结构或同步)。
- FEEL 表达式是在引擎内部执行的,避免在
-
调试
- 可以在
toValue打 log,确认 FEEL 在什么时候把你的 Java 对象交给了 mapper。 - 使用 FEEL Playground(或单元测试)验证表达式行为。([camunda.github.io]5)
- 可以在
- 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 基类;并给出预编译与缓存的建议。
思路:把一个 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,脚本可以直接调用。
思路:为目标对象包一层 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如果你能控制对象类(或给对象 mixin),可以直接在类中实现 propertyMissing(String name),Groovy 在找不到属性时会回调它,从而实现懒加载/远程访问。
示例(在类里实现):
class Domain {
def propertyMissing(String name) {
// 自定义取值逻辑
return RemoteLoader.fetch(name, this.id)
}
}注意:这需要在创建 Domain 对象时使用这个类;并且 method/property missing 的开销要考虑。
你可以在应用启动时修改某个类的 metaClass,为类增加方法或覆盖属性访问。这能让所有脚本自动获得这些扩展,但它是全局性的,要非常小心线程安全与副作用。
示例(启动时执行一次):
// 在 Spring @PostConstruct 中执行
MyDomainClass.metaClass.getCustomerName = { ->
// delegate 是实例
RemoteLoader.fetch("customerName", delegate.id)
}脚本中直接使用:
def name = myDomainInstance.customerName风险:改动会影响 JVM 中所有同类实例;升级/调试时容易出问题。
通过 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")Groovy 脚本在频繁评估时会有编译/创建引擎成本。推荐做法:
- 使用
Compilable接口预编译脚本并缓存CompiledScript(如果使用 JSR-223); - 或者在 Java Delegate 中使用 GroovyShell + CompilerConfiguration 预编译并重用(要注意线程安全:GroovyShell 不是线程安全,编译后可缓存 Class 并用不同 Binding 执行)。
- 避免在每次条件判断里创建新脚本引擎。
- 脚本有完整 JVM 访问能力 —— 不可信任的脚本可能造成数据泄露或被滥用。
- 如果需要外部可编辑脚本(业务可改),请增加限制:超时、并发限制、白名单 API、输入校验。
- 考虑在脚本中使用只读/受限的 helper API(不要把整个 Spring 上下文或敏感 bean 注入到脚本)。
- 捕获异常并在脚本外侧(Java Delegate)做降级处理与超时保护。
- 优先方案:把复杂逻辑写成 Java/Spring 服务(可单元测试),在流程里把服务以
utils/svc变量注入脚本绑定;脚本只做简单调用。 - 需要按属性懒加载的,使用 Wrapper(覆盖
getProperty/propertyMissing)并注入该 wrapper。 - 需要给所有脚本提供隐式函数的,考虑自定义 Script 基类或在应用启动时用
metaClass注入全局方法(理解风险后再用)。 - 性能关键路径:预编译脚本并缓存,避免每次创建脚本引擎。
- 安全关键路径:只把受限 helper 注入、避免暴露整个应用上下文,考虑超时/调用限制。
如果你愿意,我可以立刻给出 与你当前项目匹配的具体实现示例(例如:Spring Boot + Camunda7,演示如何在启动时把 MyUtils 注入到所有脚本的 binding,或展示一个 RemotePropertyWrapper 的完整 Java/Groovy + 流程示例),我会把完整可运行的代码贴出来。想看哪一个例子?