欢迎光临
我们一直在努力

Groovy 动态 Tool 热加载:源码编译、缓存和 ClassLoader 回收

本文结合项目中的真实代码,介绍完整实现:

  • 数据库存储动态 Tool 源码;
  • 使用 GroovyClassLoader 动态编译;
  • 通过 IDynamicAgentTool 约束脚本接口;
  • 使用源码 MD5 作为缓存键;
  • 使用 LRU 限制缓存大小;
  • 使用 CompletableFuture 避免重复编译;
  • 使用独立 ClassLoader 支持版本隔离;
  • 淘汰缓存时释放 ClassLoader;
  • 分析动态执行带来的安全边界。

本文不虚构性能提升数据,重点说明代码本身解决了哪些问题,以及当前方案没有解决哪些问题。


一、为什么需要动态 Tool

Agent 的工具通常有两类。

第一类是内置工具,例如项目中的:

com.hxh.apboa.core.tool.builtins.GetCurrentTimeTool
com.hxh.apboa.core.tool.builtins.ThesisFigureTool

这类工具由项目源码实现,启动时可以直接注册。

第二类是业务人员配置的自定义工具。例如在后台配置一个“数学计算工具”,不希望每次修改计算逻辑都重新发布服务。这时可以将工具源码保存到数据库,运行时动态加载。

项目中的工具配置表包含这些字段:

tool_id
tool_type
input_schema
class_path
language
code
need_confirm
enabled
version

其中:

  • tool_type=BUILTIN 表示内置工具;
  • tool_type=CUSTOM 表示自定义工具;
  • input_schema 描述工具入参;
  • code 保存动态源码;
  • enabled 控制工具是否启用;
  • need_confirm 控制调用前是否需要用户确认。

项目中 ToolkitFactory 根据工具类型选择不同的注册方式:

toolService.listByIds(toolIds)
.stream()
.filter(ToolConfig::getEnabled)
.forEach(toolConfig -> {
if (toolConfig.getToolType() == ToolType.BUILTIN) {
toolkit.registerTool(
ToolsRegister.getTool(toolConfig.getClassPath())
);
} else {
toolkit.registerTool(new DynamicAgentTool(toolConfig));
}
});

这里的 DynamicAgentTool 并不是具体业务工具,而是一个代理对象。真正的脚本实例会在工具被调用时动态创建或从缓存中获取。


二、动态 Tool 的接口约束

项目没有允许脚本任意返回对象,而是统一要求实现 IDynamicAgentTool:

public interface IDynamicAgentTool {

Object execute(Object… args);

default String toJsonString(Object obj) {
return JsonUtils.toJsonStr(obj);
}

default JsonNode parse(String string) {
return JsonUtils.parse(string);
}

default <T> T parse(String string, Class<T> clazz) {
return JsonUtils.parse(string, clazz);
}
}

动态源码只需要实现一个方法:

public class MathCalculator implements IDynamicAgentTool {

@Override
public Object execute(Object… args) {
return "tool result";
}
}

这样做有几个好处:

  • 加载器只需要判断是否实现统一接口;
  • Agent 侧不需要知道脚本的具体类名;
  • 所有动态工具都有统一的执行入口;
  • 可以在接口中提供公共 JSON 转换能力;
  • 未来可以增加其他脚本语言的加载器。
  • 项目中的加载器校验逻辑如下:

    Object instance = getObject(clazz);

    if (!(instance instanceof IDynamicAgentTool)) {
    throw new IllegalArgumentException(
    "Glue script class needs to implement "
    + IDynamicAgentTool.class.getName()
    );
    }

    如果源码没有实现接口,加载阶段直接失败,而不是等到 Agent 调用时才出现类型错误。


    三、动态工具如何接入 AgentScope

    动态工具的入口是 DynamicAgentTool。

    它实现了 AgentScope 的 AgentTool 接口:

    public class DynamicAgentTool implements AgentTool {
    private final ToolConfig toolConfig;

    public DynamicAgentTool(ToolConfig toolConfig) {
    this.toolConfig = toolConfig;
    }

    @Override
    public String getName() {
    return toolConfig.getToolId();
    }

    @Override
    public String getDescription() {
    return toolConfig.getDescription();
    }
    }

    它还会根据数据库中的 input_schema 生成工具参数定义:

    @Override
    public Map<String, Object> getParameters() {
    JsonNode inputSchema = toolConfig.getInputSchema();

    if (inputSchema == null || inputSchema.isNull()) {
    return Map.of();
    }

    Map<String, Object> properties = new HashMap<>();
    List<String> required = new ArrayList<>();

    for (JsonNode jsonNode : inputSchema) {
    String name = JsonUtils.getStringValue(jsonNode, "name", true);
    String type = JsonUtils.getStringValue(jsonNode, "type", true);
    String description =
    JsonUtils.getStringValue(jsonNode, "description", false);

    boolean requiredArg =
    JsonUtils.getBooleanValue(jsonNode, "required", false);

    properties.put(name, Map.of(
    "type", type,
    "description", description == null ? name : description
    ));

    if (requiredArg) {
    required.add(name);
    }
    }

    return Map.of(
    "type", "object",
    "properties", properties,
    "required", required
    );
    }

    例如,项目中数学工具的入参配置可以是:

    [
    {
    "name": "num1",
    "description": "第一个数字",
    "type": "string",
    "required": true
    },
    {
    "name": "num2",
    "description": "第二个数字",
    "type": "string",
    "required": true
    },
    {
    "name": "operator",
    "description": "运算符",
    "type": "string",
    "required": true
    }
    ]

    AgentScope 看到的是标准化工具描述,而不是数据库表结构。


    四、调用动态工具时加载源码

    真正的动态加载发生在 callAsync 方法中:

    @Override
    public Mono<ToolResultBlock> callAsync(ToolCallParam param) {
    return Mono.fromCallable(() -> {
    try {
    List<Object> args = getObjects(param);

    IDynamicAgentTool dynamicAgentTool =
    ToolInstanceLoadFactory
    .getInstanceLoader(toolConfig.getLanguage())
    .loadInstance(toolConfig.getCode());

    Object result = dynamicAgentTool.execute(args.toArray());

    return ToolResultBlock.of(
    param.getToolUseBlock().getId(),
    param.getToolUseBlock().getName(),
    TextBlock.builder()
    .text(JsonUtils.toJsonStr(R.data(result)))
    .build()
    );
    } catch (Exception e) {
    return ToolResultBlock.of(
    param.getToolUseBlock().getId(),
    param.getToolUseBlock().getName(),
    TextBlock.builder()
    .text(JsonUtils.toJsonStr(R.fail(e.getMessage())))
    .build()
    );
    }
    });
    }

    调用流程是:

    AgentScope 调用 Tool
    |
    v
    DynamicAgentTool.callAsync()
    |
    v
    根据 language 获取加载器
    |
    v
    GroovyToolInstanceLoader.loadInstance(code)
    |
    v
    获取缓存或动态编译
    |
    v
    IDynamicAgentTool.execute(args)
    |
    v
    包装为 ToolResultBlock

    参数顺序由 input_schema 决定:

    private List<Object> getObjects(ToolCallParam param) {
    List<Object> args = new LinkedList<>();

    Map<String, Object> input = param.getInput();
    JsonNode inputSchema = toolConfig.getInputSchema();

    if (input != null
    && !input.isEmpty()
    && inputSchema != null
    && !inputSchema.isEmpty()) {

    inputSchema.forEach(jsonNode -> {
    if (jsonNode.has("name")
    && !jsonNode.get("name").asText().trim().isEmpty()) {

    String name = jsonNode.get("name").textValue();
    args.add(input.getOrDefault(name, null));
    }
    });
    }

    return args;
    }

    这意味着脚本接收到的是按配置顺序排列的参数:

    args[0] -> num1
    args[1] -> num2
    args[2] -> operator

    这也是为什么 input_schema 的顺序必须稳定,否则脚本参数可能错位。


    五、加载器工厂如何选择实现

    项目通过 ToolInstanceLoadFactory 管理不同语言的加载器:

    public class ToolInstanceLoadFactory {

    private static final Map<CodeLanguage, ToolInstanceLoader> loaders =
    new HashMap<>();

    public static void registerLoader(ToolInstanceLoader loader) {
    loaders.put(loader.getLanguage(), loader);
    }

    public static ToolInstanceLoader getInstanceLoader(
    CodeLanguage language) {

    ToolInstanceLoader loader = loaders.get(language);

    if (loader == null) {
    throw new RuntimeException(
    "No loader found for language " + language
    );
    }

    return loader;
    }
    }

    加载器通过 Spring 初始化阶段自动注册:

    public interface ToolInstanceLoader
    extends InstanceLoader<IDynamicAgentTool> {

    @Override
    default void afterSingletonsInstantiated() {
    ToolInstanceLoadFactory.registerLoader(this);
    }
    }

    项目当前实现的是 GroovyToolInstanceLoader。

    需要说明一个容易混淆的地方:项目中的 CodeLanguage 枚举字段目前使用 JAVA 表示这类动态 Java/Groovy 代码配置,但具体实现类使用的是 GroovyClassLoader。文章中更准确的表述应该是:

    项目当前通过 GroovyClassLoader 编译动态 Java 语法源码,数据库中的语言枚举沿用了 JAVA 配置值。

    不能简单描述为“系统同时支持 Java、Groovy、Kotlin 三种脚本语言”,因为目前代码中没有对应的其他加载器实现。


    六、源码编译:GroovyClassLoader

    核心加载器位于:

    core/src/main/java/com/hxh/apboa/core/tool/dynamices/loader/GroovyToolInstanceLoader.java

    源码编译逻辑如下:

    private LoadedTool doLoad(String codeSource) {
    GroovyClassLoader classLoader =
    new GroovyClassLoader(getClass().getClassLoader());

    try {
    Class<?> clazz = classLoader.parseClass(codeSource);

    if (clazz == null) {
    throw new IllegalArgumentException(
    "Dynamic tool source is empty"
    );
    }

    Object instance = getObject(clazz);

    if (!(instance instanceof IDynamicAgentTool)) {
    throw new IllegalArgumentException(
    "Dynamic tool must implement IDynamicAgentTool"
    );
    }

    dependencyInjection(instance);

    return new LoadedTool(
    classLoader,
    (IDynamicAgentTool) instance
    );
    } catch (RuntimeException | Error e) {
    closeQuietly(classLoader);
    throw e;
    }
    }

    这里有三个关键点。

    1. 每次编译创建独立 ClassLoader

    new GroovyClassLoader(getClass().getClassLoader())

    没有复用同一个 ClassLoader,而是每个源码版本创建独立加载器。

    这样做是为了实现版本隔离:

    旧源码 -> ClassLoader A -> OldTool
    新源码 -> ClassLoader B -> NewTool

    当数据库中的 Tool 源码发生变化时,源码 MD5 也会变化,系统会编译出新的类,不会继续复用旧版本 Class。

    2. 校验源码是否实现约定接口

    if (!(instance instanceof IDynamicAgentTool)) {
    throw new IllegalArgumentException(…);
    }

    动态语言本身比较灵活,但平台必须在边界处建立约束,否则错误会延迟到真正执行时才暴露。

    3. 加载失败时立即清理

    catch (RuntimeException | Error e) {
    closeQuietly(classLoader);
    throw e;
    }

    如果编译失败、实例化失败或依赖注入失败,ClassLoader 不应该继续保留。


    七、为什么不能每次调用都重新编译

    如果每次 Agent 调用工具都执行:

    new GroovyClassLoader(…).parseClass(code);

    会有几个问题:

  • 编译开销重复产生;
  • 工具调用延迟增加;
  • 高频调用会产生大量 ClassLoader;
  • 旧 Class 可能长期占用 Metaspace;
  • 并发调用时会重复编译同一份源码。
  • 因此项目在加载器中增加了源码缓存。


    八、源码 MD5 作为缓存键

    加载器首先校验源码:

    if (FuncUtils.isEmpty(codeSource)) {
    throw new IllegalArgumentException(
    "Dynamic tool source is empty"
    );
    }

    然后生成源码身份:

    String identity = codeIdentity(codeSource);

    codeIdentity 定义在公共的 InstanceLoader 接口中,本质上根据源码内容生成 MD5:

    default String codeIdentity(String codeSource) {
    try {
    MessageDigest md5 =
    MessageDigest.getInstance("MD5");

    byte[] digest = md5.digest(
    codeSource.getBytes(StandardCharsets.UTF_8)
    );

    return HexFormat.of().formatHex(digest);
    } catch (NoSuchAlgorithmException e) {
    return codeSource;
    }
    }

    缓存键使用源码内容,而不是工具 ID:

    toolId + 相同源码 -> 命中同一个缓存
    toolId + 源码修改 -> 生成新的缓存键

    这样可以自然支持热加载:

    第一次源码:
    class Demo implements IDynamicAgentTool { … }
    MD5 = A

    修改源码:
    class Demo implements IDynamicAgentTool { … }
    MD5 = B

    由于 A != B,第二次调用会自动加载新版本。


    九、LRU 缓存限制 ClassLoader 数量

    项目使用一个带访问顺序的 LinkedHashMap:

    private final Map<String, LoadedTool> toolObjCache =
    Collections.synchronizedMap(
    new LinkedHashMap<>(16, 0.75f, true) {

    @Override
    protected boolean removeEldestEntry(
    Map.Entry<String, LoadedTool> eldest) {

    if (size() <= Math.max(1, maxCacheSize)) {
    return false;
    }

    releaseQuietly(
    eldest.getValue().classLoader()
    );

    return true;
    }
    }
    );

    构造函数中的第三个参数:

    new LinkedHashMap<>(16, 0.75f, true)

    true 表示按照访问顺序排序,而不是插入顺序。

    因此它实现的是 LRU:

    最近使用的源码保留
    长时间没有使用的源码优先淘汰

    缓存容量由配置控制:

    @Value("${agentOri.groovy.script-cache.max-size:256}")
    private int maxCacheSize = DEFAULT_MAX_CACHE_SIZE;

    默认值是 256。

    这里的 256 是项目中的默认配置,不是压测得出的最优值。实际部署时应根据:

    • 动态工具数量;
    • 单个脚本的类数量;
    • JVM 堆和 Metaspace;
    • 工具更新频率;
    • 并发调用量;

    进行调整。


    十、并发加载为什么使用 CompletableFuture

    仅仅加一个缓存还不够。

    假设同一份源码第一次被 100 个请求同时调用:

    请求 A: 缓存未命中 -> 开始编译
    请求 B: 缓存未命中 -> 开始编译
    请求 C: 缓存未命中 -> 开始编译

    如果没有并发控制,同一份源码可能被编译很多次。

    项目使用:

    private final ConcurrentMap<String,
    CompletableFuture<LoadedTool>> loading =
    new ConcurrentHashMap<>();

    加载逻辑如下:

    CompletableFuture<LoadedTool> mine =
    new CompletableFuture<>();

    CompletableFuture<LoadedTool> existing =
    loading.putIfAbsent(identity, mine);

    if (existing != null) {
    return await(existing).instance();
    }

    完整过程是:

    LoadedTool cached = toolObjCache.get(identity);

    if (cached != null) {
    return cached.instance();
    }

    CompletableFuture<LoadedTool> mine =
    new CompletableFuture<>();

    CompletableFuture<LoadedTool> existing =
    loading.putIfAbsent(identity, mine);

    if (existing != null) {
    return await(existing).instance();
    }

    含义是:

    • 第一个线程成功放入 loading,负责编译;
    • 后续线程发现已有 Future,直接等待;
    • 编译完成后,所有等待线程拿到同一个结果;
    • 同一份源码只进行一次编译。

    真正执行编译的线程:

    try {
    LoadedTool loaded = doLoad(codeSource);

    toolObjCache.put(identity, loaded);
    mine.complete(loaded);

    return loaded.instance();
    } catch (RuntimeException | Error e) {
    mine.completeExceptionally(e);
    throw e;
    } finally {
    loading.remove(identity, mine);
    }

    这里先写入缓存,再唤醒等待者:

    toolObjCache.put(identity, loaded);
    mine.complete(loaded);

    这样等待线程被唤醒后,可以直接命中缓存。


    十一、异常时如何避免并发请求永久等待

    如果编译失败,不能只抛出异常,还要结束 Future:

    mine.completeExceptionally(e);

    否则其他线程会一直等待:

    await(existing)

    项目中的 await 会把 CompletionException 还原成原始异常:

    private LoadedTool await(
    CompletableFuture<LoadedTool> future) {

    try {
    return future.join();
    } catch (CompletionException e) {
    Throwable cause = e.getCause();

    if (cause instanceof RuntimeException runtimeException) {
    throw runtimeException;
    }

    if (cause instanceof Error error) {
    throw error;
    }

    throw e;
    }
    }

    这个处理很重要。

    如果不还原异常,调用方看到的可能只是:

    CompletionException

    而不是实际的:

    Groovy compilation error

    恢复原始异常后,日志和工具返回结果更容易定位。


    十二、ClassLoader 为什么需要回收

    动态编译最大的风险之一是 ClassLoader 生命周期。

    每个 ClassLoader 都可能持有:

    • 动态生成的 Class;
    • 方法元数据;
    • 常量池;
    • 反射信息;
    • 相关对象引用。

    如果源码版本不断变化,却始终把旧版本保留在缓存中,可能导致 Metaspace 持续增长。

    项目在缓存淘汰时执行:

    private static void releaseQuietly(
    GroovyClassLoader classLoader) {

    if (classLoader == null) {
    return;
    }

    try {
    classLoader.clearCache();
    } catch (Exception e) {
    log.warn(
    "Failed to clear GroovyClassLoader cache: {}",
    e.getMessage()
    );
    }
    }

    加载失败时则执行更彻底的清理:

    private static void closeQuietly(
    GroovyClassLoader classLoader) {

    if (classLoader == null) {
    return;
    }

    releaseQuietly(classLoader);

    try {
    classLoader.close();
    } catch (Exception e) {
    log.warn(
    "Failed to close GroovyClassLoader: {}",
    e.getMessage()
    );
    }
    }

    缓存中保存的不是单独的工具实例,而是:

    private record LoadedTool(
    GroovyClassLoader classLoader,
    IDynamicAgentTool instance
    ) {
    }

    这样淘汰工具时可以同时找到对应 ClassLoader。

    项目没有在工具刚执行完就关闭 ClassLoader,因为缓存中的实例还可能被继续使用。正确时机是:

    缓存淘汰
    -> 清理 ClassLoader 缓存
    -> 移除旧版本引用
    -> 后续由 JVM 回收

    需要注意,clearCache() 和 close() 并不等于立即触发 Full GC。它们只是解除框架层面的引用,最终是否回收还由 JVM 决定。


    十三、一个真实的动态工具示例

    项目初始化 SQL 中包含自定义数学工具,代码结构类似:

    import com.hxh.apboa.core.tool.dynamices.IDynamicAgentTool;
    import java.util.*;

    public class MathCalculator
    implements IDynamicAgentTool {

    @Override
    public Object execute(Object… args) {
    if (args == null || args.length < 3) {
    return Map.of(
    "error",
    "需要三个参数:数字1、数字2、运算符"
    );
    }

    String num1Text = args[0].toString();
    String num2Text = args[1].toString();
    String operator = args[2].toString();

    double num1 = Double.parseDouble(num1Text);
    double num2 = Double.parseDouble(num2Text);

    double result;

    switch (operator) {
    case "+":
    result = num1 + num2;
    break;
    case "-":
    result = num1 – num2;
    break;
    case "*":
    result = num1 * num2;
    break;
    case "/":
    if (num2 == 0) {
    throw new ArithmeticException("除数不能为0");
    }
    result = num1 / num2;
    break;
    default:
    return Map.of(
    "error",
    "不支持的运算符: " + operator
    );
    }

    return Map.of(
    "num1", num1Text,
    "num2", num2Text,
    "operator", operator,
    "result", result
    );
    }
    }

    调用时的真实过程:

    Agent 识别需要调用 math_calculator
    |
    v
    DynamicAgentTool 获取输入参数
    |
    v
    GroovyToolInstanceLoader.loadInstance(code)
    |
    v
    源码 MD5 计算
    |
    +– 命中缓存 -> 直接取实例
    |
    +– 未命中 -> GroovyClassLoader 编译
    |
    v
    MathCalculator.execute(args)
    |
    v
    封装成 ToolResultBlock 返回 Agent

    这段流程没有依赖虚构的性能数据,完全对应项目的运行时结构。


    十四、热加载是如何发生的

    假设数据库中原来的源码是:

    return num1 + num2;

    修改为:

    return num1 * num2;

    调用流程如下:

    旧源码 -> MD5-A -> 缓存中存在 -> 使用旧版本
    新源码 -> MD5-B -> 缓存中不存在 -> 编译新版本

    系统不需要额外调用:

    reload()
    refresh()
    restart()

    因为缓存键由源码内容决定,源码发生变化后自然会产生新版本。

    但这里也有一个边界:

    代码热加载只针对已经传入加载器的源码内容,不代表数据库配置修改后所有已经创建的 Agent 会自动重新构建。

    如果 Agent 或 Tool 配置在某个运行上下文中已经被读取,是否立即生效还取决于上层 Agent 的创建时机和缓存策略。

    因此面试时不能说“修改数据库后所有在线 Agent 立即热更新”,更准确的说法是:

    动态 Tool 加载器能够根据源码内容自动识别新版本,首次调用新源码时重新编译;上层 Agent 是否立即使用新配置,还取决于 Agent 的重新装配时机。


    十五、当前方案解决了什么问题

    当前实现已经解决:

    1. 不重启服务即可更新动态 Tool

    源码存储在数据库中,运行时读取并编译。

    2. 相同源码不会重复编译

    源码 MD5 作为缓存键。

    3. 并发首次加载不会重复编译

    ConcurrentHashMap + CompletableFuture 实现单飞加载。

    4. 不同版本的源码相互隔离

    每个版本使用独立的 GroovyClassLoader。

    5. 缓存不会无限增长

    LRU 缓存限制了缓存容量。

    6. 旧版本具备释放路径

    缓存淘汰和编译失败时会清理 ClassLoader。

    7. AgentScope 不需要感知脚本细节

    DynamicAgentTool 对外提供标准的 AgentTool 接口。


    十六、当前方案没有解决什么问题

    动态执行代码本身风险很高,当前实现不能自动解决以下问题。

    1. 没有真正的沙箱隔离

    动态脚本理论上可以访问:

    • 文件系统;
    • 网络;
    • JVM 系统属性;
    • 环境变量;
    • 反射 API;
    • 进程执行能力。

    因此不能把普通后台用户提交的代码直接当作可信脚本执行。

    更安全的生产方案通常需要:

    • 独立进程执行;
    • 容器隔离;
    • 限制网络访问;
    • 限制文件系统;
    • CPU 和内存配额;
    • 超时强制终止;
    • 代码审核和发布审批。

    2. 没有源码版本回滚机制

    缓存能够区分不同源码版本,但当前加载器没有实现:

    版本列表
    版本审核
    版本回滚
    灰度发布

    这些属于配置中心或发布系统能力。

    3. 没有分布式缓存

    当前缓存位于单个 JVM 进程内。

    多实例部署时:

    节点 A 有一份缓存
    节点 B 有另一份缓存

    同一份源码可能在不同节点分别编译。这不影响功能正确性,但会增加各节点的编译开销。

    4. 没有统计编译耗时和缓存命中率

    当前代码有日志和异常处理,但没有完整的指标体系。因此不能在文章中写“缓存命中率达到多少”或“编译耗时下降多少”。

    5. tool_config 中的 language 还不是完整多语言体系

    项目虽然通过 ToolInstanceLoadFactory 抽象了加载器,但目前主要实现是 GroovyToolInstanceLoader。未来如果增加 JavaScript、Python 等语言,还需要增加对应的:

    ToolInstanceLoader

    并注册到工厂。


    十七、为什么不用简单的反射加载

    反射加载通常要求类已经编译并存在于应用 Classpath 中:

    Class.forName("com.example.MyTool");

    它适合加载项目内置类,不适合数据库中刚修改的源码。

    动态编译方案的区别是:

    反射加载:
    已编译 Class -> Class.forName() -> 实例

    动态加载:
    源码字符串 -> GroovyClassLoader.parseClass() -> 实例

    AgentOri 同时支持两种工具:

    if (toolConfig.getToolType() == ToolType.BUILTIN) {
    toolkit.registerTool(
    ToolsRegister.getTool(toolConfig.getClassPath())
    );
    } else {
    toolkit.registerTool(
    new DynamicAgentTool(toolConfig)
    );
    }

    这是一种比较实用的混合方案:

    • 稳定能力使用内置工具;
    • 需要频繁调整的能力使用动态工具;
    • AgentScope 侧统一看到 AgentTool。

    十八、总结

    AgentOri 的动态 Tool 不是简单地执行一段字符串,而是完成了一个相对完整的运行时链路:

    数据库 ToolConfig
    |
    v
    DynamicAgentTool
    |
    v
    ToolInstanceLoadFactory
    |
    v
    GroovyToolInstanceLoader
    |
    +– 源码校验
    +– MD5 生成版本身份
    +– LRU 缓存查询
    +– CompletableFuture 并发去重
    +– GroovyClassLoader 编译
    +– IDynamicAgentTool 类型校验
    +– 依赖注入
    +– ClassLoader 回收
    |
    v
    IDynamicAgentTool.execute()
    |
    v
    AgentScope ToolResultBlock

    这套设计解决的是“工具需要动态配置和更新”的问题,同时通过缓存和并发控制避免重复编译,通过独立 ClassLoader 和 LRU 降低动态类长期占用 JVM 资源的风险。

    但它仍然不是一个完整的脚本安全平台。生产环境中还需要补充沙箱隔离、代码审核、版本管理、执行超时、资源限制和监控指标。

    赞(0)
    未经允许不得转载:171主机测评 » Groovy 动态 Tool 热加载:源码编译、缓存和 ClassLoader 回收
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址