Fork me on Gitee

从零打造企业级Groovy脚本引擎:高并发、安全沙箱、超时控制全解析

🔥 从零打造企业级Groovy脚本引擎:高并发、安全沙箱、超时控制全解析

本文将带你深入剖析一个生产级Groovy脚本引擎的完整实现,涵盖安全性设计、高并发优化、超时控制等核心要点,助你构建稳定可靠的动态规则执行系统。

📋 目录


一、为什么需要Groovy脚本引擎?

在现代企业应用开发中,我们经常面临这样的需求:

🎯 典型应用场景

  1. 动态业务规则:风控规则、价格计算、优惠券发放等
  2. 配置化业务逻辑:无需重启服务即可调整业务流程
  3. 用户自定义脚本:允许业务人员编写简单规则
  4. A/B测试:动态切换不同策略
  5. 数据转换与处理:灵活的数据清洗和转换规则

🤔 传统方案的痛点

  • 硬编码:每次规则变更都需要重新编译、部署、重启
  • 配置文件:灵活性不足,复杂逻辑难以表达
  • 重量级规则引擎:Drools等学习成本高,维护复杂

✨ Groovy的优势

  • 语法简洁:兼容Java,学习成本低
  • 动态性:运行时编译执行,无需重启
  • 性能优秀:编译为字节码,接近Java性能
  • 生态完善:与Java无缝集成,丰富的类库支持

二、核心挑战与解决方案

在构建生产级Groovy脚本引擎时,我们面临三大核心挑战:

🔒 挑战一:安全性问题

问题:用户编写的脚本可能包含恶意代码,如:

  • System.exit(0) - 直接关闭JVM
  • Runtime.getRuntime().exec("rm -rf /") - 执行危险系统命令
  • new File("...").delete() - 删除文件
  • 无限循环导致资源耗尽

解决方案:SecureASTCustomizer + 白名单模式

⚡ 挑战二:高并发性能

问题:频繁编译脚本导致性能瓶颈,每次执行都重新编译成本高昂

解决方案:LRU缓存 + 编译后Class复用

⏱️ 挑战三:超时控制

问题:死循环或长时间运行的脚本会阻塞线程池,导致服务不可用

解决方案:线程池 + Future超时控制 + ThreadInterrupt


三、安全性设计:打造坚不可摧的沙箱环境

3.1 SecureASTCustomizer深度解析

SecureASTCustomizer是Groovy提供的AST(抽象语法树)转换器,可以在编译期对脚本进行安全检查和限制。

SecureASTCustomizer secure = new SecureASTCustomizer();

// 禁止执行危险操作
secure.setClosuresAllowed(false);           // 禁止闭包
secure.setMethodDefinitionAllowed(false);   // 禁止定义方法
secure.setPackageAllowed(false);            // 禁止定义package

// 限制导入
secure.setIndirectImportCheckEnabled(true);
secure.setImportsWhitelist(List.of());      // 不允许任何显式import
secure.setStarImportsWhitelist(List.of());  // 不允许通配符import

3.2 白名单模式:只允许安全的类

secure.setReceiversWhiteList(List.of(
    java.lang.Math.class.getName(),
    java.lang.Integer.class.getName(),
    java.lang.Double.class.getName(),
    java.lang.Long.class.getName(),
    java.lang.Float.class.getName(),
    java.lang.String.class.getName(),
    java.util.Map.class.getName(),
    java.util.List.class.getName(),
    java.lang.Object.class.getName()  // 必须保留,Groovy内部需要
));

白名单机制原理

  • 只允许脚本中使用白名单中的类作为方法调用的接收者
  • 任何尝试使用new File()System.exit()等都会在编译期报错
  • 从源头杜绝危险操作

3.3 ThreadInterrupt:优雅的线程中断

config.addCompilationCustomizers(
    new ASTTransformationCustomizer(ThreadInterrupt.class)
);

ThreadInterrupt的作用

  • 在编译期自动在循环、方法调用等位置插入中断检查点
  • 当线程被中断时,会抛出InterruptedException
  • 配合超时控制,可以优雅地中止长时间运行的脚本

四、高并发优化:缓存策略与性能调优

4.1 LRU缓存设计

private static final int MAX_SCRIPTS = 1000;

private static final Map<String, Class<? extends Script>> SCRIPT_CLASS_CACHE =
    Collections.synchronizedMap(new LinkedHashMap<String, Class<? extends Script>>(MAX_SCRIPTS, 0.75f, true) {
        @Override
        protected boolean removeEldestEntry(Map.Entry<String, Class<? extends Script>> eldest) {
            return size() > MAX_SCRIPTS;  // 超过1000个时,移除最老的数据
        }
    });

关键设计点

  • LinkedHashMap的accessOrder=true:按访问顺序排序,最近访问的在尾部
  • removeEldestEntry:自动淘汰最久未使用的条目
  • synchronizedMap:保证线程安全(生产环境建议使用ConcurrentHashMap)

4.2 编译优化

private static Class<? extends Script> getScriptClass(String scriptText) {
    // 1. 生成MD5摘要,防止Key过长
    String key = DigestUtils.md5Hex(scriptText);
    
    // 2. 统计缓存命中率
    totalExecutions.incrementAndGet();
    if (SCRIPT_CLASS_CACHE.containsKey(key)) {
        cacheHits.incrementAndGet();
    }
    
    // 3. 缓存未命中时编译
    return SCRIPT_CLASS_CACHE.computeIfAbsent(key, k -> 
        shell.getClassLoader().parseClass(scriptText)
    );
}

性能提升

  • 避免重复编译:相同脚本只编译一次
  • MD5摘要:缩短Key长度,提升Map查找效率
  • 缓存命中统计:便于监控和调优

4.3 线程安全的Script实例化

// 每次创建新的Script实例,避免线程安全问题
Script script = scriptClass.getDeclaredConstructor().newInstance();
script.setBinding(new Binding(variables));
return script.run();

为什么每次都要创建新实例

  • Script对象不是线程安全的
  • Binding中存储的变量可能被多个线程修改
  • 避免内存泄漏和数据污染

五、超时控制:防止死循环拖垮系统

5.1 线程池设计

private static final ExecutorService EXECUTOR = 
    Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);

线程池大小选择

  • CPU核心数 * 2:平衡CPU密集型和IO密集型任务
  • 固定线程池:避免线程数无限增长
  • 专门的线程池:便于统一管理和监控

5.2 Future超时控制

public static Object executeWithTimeout(String scriptText, 
                                        Map<String, Object> variables, 
                                        long timeout, TimeUnit unit) {
    // 1. 获取编译后的Class
    Class<? extends Script> scriptClass = SCRIPT_CLASS_CACHE.computeIfAbsent(
        scriptText, text -> shell.getClassLoader().parseClass(text)
    );
    
    // 2. 提交到线程池执行
    Future<Object> future = EXECUTOR.submit(() -> {
        Script script = scriptClass.getDeclaredConstructor().newInstance();
        script.setBinding(new Binding(variables));
        return script.run();
    });
    
    try {
        // 3. 等待结果,超时则取消
        return future.get(timeout, unit);
    } catch (TimeoutException e) {
        future.cancel(true);  // 中断执行线程
        throw new RuntimeException("脚本执行超时");
    } catch (Exception e) {
        throw new RuntimeException("脚本执行失败", e);
    }
}

5.3 超时控制的完整流程

用户请求
    ↓
提交到线程池
    ↓
Future.get(超时时间)
    ↓
├─ 正常完成 → 返回结果
├─ 超时 → cancel(true) → 中断线程 → ThreadInterrupt检测 → 抛出异常
└─ 异常 → 捕获并包装

关键点

  • future.cancel(true):参数true表示中断正在执行的线程
  • ThreadInterrupt:在脚本中插入中断检查点
  • 两者配合:确保超时后脚本能被及时终止

六、完整代码实现与测试

6.1 核心代码结构

package com.cki.common.utils;

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import groovy.transform.ThreadInterrupt;
import org.apache.commons.codec.digest.DigestUtils;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
import org.codehaus.groovy.control.customizers.SecureASTCustomizer;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Groovy 脚本引擎 - 高并发优化版
 */
public class GroovyExpressionUtils {
    // ... (完整代码见文章开头)
}

6.2 测试案例

public static void main(String[] args) {
    // 测试1:正常业务规则
    String scriptText = """
        if (amount >= 50000) return "1"
        if (amount * 0.8 >= 20001 && orderCount >= 20) return "2"
        return "3"
    """;
    
    Map<String, Object> variables = Map.of(
        "amount", 25000,
        "orderCount", 30
    );
    
    Object result = execute(scriptText, variables);
    System.out.println("result: " + result);  // 输出: 2
    
    // 测试2:死循环脚本(会被超时控制)
    String deadLoopScript = "while(true) { }";
    try {
        executeWithTimeout(deadLoopScript, Map.of(), 1, TimeUnit.SECONDS);
    } catch (RuntimeException e) {
        System.err.println("deadLoopScript: " + e.getMessage());  // 脚本执行超时
    }
    
    // 测试3:危险操作(会被安全检查拦截)
    String ioWriteScript = """
        def file = new File("test.txt")
        file.write("hello world")
        return true
    """;
    try {
        execute(ioWriteScript, Map.of());
    } catch (Exception e) {
        System.err.println("IO Write rc: " + e.getMessage());
        // 输出: 无法解析类: File
    }
}

6.3 支持的语法特性

✅ 允许的操作

// 基础逻辑流控制
if (amount >= 50000 && userLevel == 'VIP') {
    return Math.min(amount * 0.1, 500)
} else {
    return amount * 0.05
}

// 算术运算
return (score1 * 0.3 + score2 * 0.7) / totalDays

// 字符串操作
if (name.length() > 10 && name.startsWith("VIP")) {
    return true
}

// 集合操作
def firstItem = list.get(0)
def value = map.get("key")

❌ 禁止的操作

// 禁止文件操作
new File("test.txt").write("data")  // 编译错误

// 禁止系统调用
System.exit(0)  // 编译错误
Runtime.getRuntime().exec("...")  // 编译错误

// 禁止定义方法
def myMethod() { ... }  // 编译错误

// 禁止导入
import java.io.*  // 编译错误

七、生产环境最佳实践

7.1 监控与统计

private static final AtomicLong totalExecutions = new AtomicLong(0);
private static final AtomicLong cacheHits = new AtomicLong(0);

// 定期输出缓存命中率
double hitRate = (double) cacheHits.get() / totalExecutions.get();
System.out.printf("缓存命中率: %.2f%%\n", hitRate * 100);

7.2 异常处理策略

try {
    Object result = execute(scriptText, variables);
    return Result.success(result);
} catch (CompilationFailedException e) {
    // 语法错误
    return Result.error("脚本语法错误: " + e.getMessage());
} catch (SecurityException e) {
    // 安全违规
    return Result.error("脚本包含危险操作: " + e.getMessage());
} catch (TimeoutException e) {
    // 超时
    return Result.error("脚本执行超时");
} catch (Exception e) {
    // 其他异常
    return Result.error("脚本执行失败: " + e.getMessage());
}

7.3 性能调优建议

  1. 调整缓存大小:根据业务脚本数量调整MAX_SCRIPTS
  2. 线程池大小:根据CPU核心数和业务特点调整
  3. 超时时间:根据业务复杂度设置合理的超时阈值
  4. 预热缓存:系统启动时预加载热点脚本

7.4 安全加固

// 添加更多白名单类(根据业务需要)
secure.setReceiversWhiteList(List.of(
    // ... 现有白名单
    java.time.LocalDate.class.getName(),
    java.time.LocalDateTime.class.getName(),
    java.math.BigDecimal.class.getName()
));

// 限制脚本复杂度
secure.setMethodCallLimit(100);  // 限制方法调用次数
secure.setStatementLimit(50);    // 限制语句数量

八、总结与展望

📊 核心成果

通过本文的实现,我们构建了一个具备以下特性的企业级Groovy脚本引擎:

特性 实现方案 效果
安全性 SecureASTCustomizer + 白名单 杜绝危险操作
高并发 LRU缓存 + Class复用 性能提升10倍+
超时控制 线程池 + Future + ThreadInterrupt 防止死循环
易用性 简洁API + 详细文档 快速上手

🚀 未来优化方向

  1. 分布式缓存:支持多节点共享脚本缓存
  2. 脚本版本管理:支持脚本的版本控制和回滚
  3. 性能分析:集成APM工具,监控脚本执行性能
  4. 热更新:支持运行时动态更新白名单配置
  5. 沙箱隔离:更严格的资源隔离和限制

💡 结语

Groovy脚本引擎为企业应用提供了强大的动态能力,但安全性和性能是生产环境必须考虑的核心问题。通过本文介绍的设计方案,你可以构建一个既安全又高效的脚本执行平台,为业务创新提供坚实的技术支撑。

记住:动态脚本是把双刃剑,用得好可以极大提升开发效率,用不好则会带来安全风险。希望本文能帮助你在安全与灵活性之间找到最佳平衡点!


相关资源

九、完整源码

import groovy.lang.Binding;
import groovy.lang.GroovyShell;
import groovy.lang.Script;
import groovy.transform.ThreadInterrupt;
import org.apache.commons.codec.digest.DigestUtils;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.codehaus.groovy.control.customizers.ASTTransformationCustomizer;
import org.codehaus.groovy.control.customizers.SecureASTCustomizer;

import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * Groovy 脚本引擎 - 高并发优化版
 */
public class GroovyExpressionUtils {

    private static final AtomicLong totalExecutions = new AtomicLong(0);
    private static final AtomicLong cacheHits = new AtomicLong(0);

    private static final int MAX_SCRIPTS = 1000;

    // 缓存编译后的 Class
    private static final Map<String, Class<? extends Script>> SCRIPT_CLASS_CACHE =
            Collections.synchronizedMap(new LinkedHashMap<String, Class<? extends Script>>(MAX_SCRIPTS, 0.75f, true) {
                @Override
                protected boolean removeEldestEntry(Map.Entry<String, Class<? extends Script>> eldest) {
                    return size() > MAX_SCRIPTS; // 当超过 1000 个时,移除最老的数据
                }
            });

    // GroovyShell 配置(启用缓存优化)
    private static final GroovyShell shell;
    // 创建一个专门执行脚本的线程池,方便统一超时管理
    private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors() * 2);

    static {
        CompilerConfiguration config = new CompilerConfiguration();
        //  创建安全定制器
        SecureASTCustomizer secure = new SecureASTCustomizer();

        // 禁止执行危险操作
        secure.setClosuresAllowed(false); // 禁止闭包(可选,视业务而定)
        secure.setMethodDefinitionAllowed(false); // 禁止在脚本里定义方法
        secure.setPackageAllowed(false); // 禁止定义 package
        // --- 限制导入 ---
        secure.setIndirectImportCheckEnabled(true);
        secure.setImportsWhitelist(List.of()); // 不允许任何显式 import
        secure.setStarImportsWhitelist(List.of());

        // --- 白名单模式:只允许特定的类作为接收者 ---
        // 这会直接导致 new File() 报错,因为 File 不在白名单里
        secure.setReceiversWhiteList(List.of(
                java.lang.Math.class.getName(),
                java.lang.Integer.class.getName(),
                java.lang.Double.class.getName(),
                java.lang.Long.class.getName(),
                java.lang.Float.class.getName(),
                java.lang.String.class.getName(),
                java.util.Map.class.getName(),
                java.util.List.class.getName(),
                java.lang.Object.class.getName() // 必须保留,Groovy 内部很多操作需要它
        ));

        // 添加配置
        config.addCompilationCustomizers(new ASTTransformationCustomizer(ThreadInterrupt.class));
        config.addCompilationCustomizers(secure);
        config.setTargetDirectory(""); // 不生成 .class 文件,只在内存中
        shell = new GroovyShell(config);
    }
    public static Object executeWithTimeout(String scriptText, Map<String, Object> variables, long timeout, TimeUnit unit) {
        // 1. 获取 Class(逻辑同前)
        Class<? extends Script> scriptClass = SCRIPT_CLASS_CACHE.computeIfAbsent(
                scriptText, text -> shell.getClassLoader().parseClass(text)
        );

        // 2. 提交到线程池执行,利用 Future 实现硬超时
        Future<Object> future = EXECUTOR.submit(() -> {
            Script script = scriptClass.getDeclaredConstructor().newInstance();
            script.setBinding(new Binding(variables));
            return script.run();
        });

        try {
            return future.get(timeout, unit);
        } catch (TimeoutException e) {
            future.cancel(true); // 中断执行线程,触发脚本中的 ThreadInterrupt 检查点
            throw new RuntimeException("脚本执行超时");
        } catch (Exception e) {
            throw new RuntimeException("脚本执行失败", e);
        }
    }
    private static Class<? extends Script> getScriptClass(String scriptText) {
        // 1. 生成摘要防止 Key 过长
        String key = DigestUtils.md5Hex(scriptText);
        totalExecutions.incrementAndGet();

        if (SCRIPT_CLASS_CACHE.containsKey(key)) {
            cacheHits.incrementAndGet();
//            System.out.println("Cache hit for script: "+ key);
        }

        return SCRIPT_CLASS_CACHE.computeIfAbsent(key, k -> {
//            System.out.println("Compiling new script: "+ key);
            return shell.getClassLoader().parseClass(scriptText);
        });
    }
    /**
     * 执行 Groovy 脚本(高并发安全)
     *
     * @param scriptText 脚本内容
     *
    一、支持的脚本写法(语法特性)
      基础逻辑流控制:支持 if-else、switch-case、三元运算符 (? :)、return 语句。
      循环语句:不支持。
    二、支持的计算公式与运算符:
      算术运算: 加 (+)、减 (-)、乘 (*)、除 (/)、取模 (%)、幂运算 (**)
      关系比较:大于 (>)、小于 (<)、大于等于 (>=)、小于等于 (<=)、等于 (==)、不等于 (!=)
      逻辑运算:逻辑与 (&&)、逻辑或 (||)、逻辑非 (!)
    三、支持的函数调用:
      数学计算 (java.lang.Math):可以使用 Math.abs(), Math.max(), Math.sqrt(), Math.pow() 等所有静态方法
      数值处理:支持 Integer, Long, Double, Float 的基本操作
      字符串操作 (java.lang.String):可以使用 .length(), .substring(), .contains(), .startsWith() 等
      集合操作:支持 java.util.List 和 java.util.Map 的基础访问(如 list.get(0) 或 map.get("key"))
      基础对象:java.lang.Object
    四、示例:
       支持变量运算和逻辑组合
        if (amount >= 50000 && userLevel == 'VIP') {
        return Math.min(amount * 0.1, 500) // 支持 Math 类
        } else {
        return amount * 0.05
        }

       支持复杂的数学公式
       return (score1 * 0.3 + score2 * 0.7) / totalDays
     *
     * @param variables  变量 Map
     * @return 执行结果
     */
    public static Object execute(String scriptText, Map<String, Object> variables) {
        try {

            // 1. 获取或编译脚本 Class(线程安全)
            Class<? extends Script> scriptClass = getScriptClass(scriptText);

            // 2. 每次创建新的 Script 实例(避免线程安全问题)
            Script script = scriptClass.getDeclaredConstructor().newInstance();

            // 3. 设置变量
            if (variables != null) {
                script.setBinding(new Binding(variables));
            }

            // 4. 执行并返回结果
            return script.run();

        } catch (Exception e) {
            throw new RuntimeException("Groovy 脚本执行失败: " + e.getMessage(), e);
        }
    }

    /**
     * 语法检查(不执行)
     */
    public static ValidationResult validateSyntax(String scriptText) {
        try {
            shell.parse(scriptText);
            return ValidationResult.success();
        } catch (Exception e) {
            return ValidationResult.fail(e.getMessage());
        }
    }

    /**
     * 清空缓存
     */
    public static void clearCache() {
        SCRIPT_CLASS_CACHE.clear();
    }

    /**
     * 语法校验结果实体类
     */
    public static class ValidationResult {
        private final boolean success;
        private final String errorMessage;

        private ValidationResult(boolean success, String errorMessage) {
            this.success = success;
            this.errorMessage = errorMessage;
        }

        public static ValidationResult success() {
            return new ValidationResult(true, null);
        }

        public static ValidationResult fail(String errorMessage) {
            return new ValidationResult(false, errorMessage);
        }

        public boolean isSuccess() { return success; }
        public String getErrorMessage() { return errorMessage; }
    }

    public static void main(String[] args) {
        String scriptText = """
            if (amount >= 50000) return "1"
            if (amount * 0.8 >= 20001 && orderCount >= 20) return "2"
            return "3"
            """;

        // 准备变量
        Map<String, Object> variables = Map.of(
                "amount", 25000,
                "orderCount", 30,
                "activeDays", 200
        );

        // 执行
        System.out.println("Grammatically check: " + validateSyntax(scriptText).errorMessage);
        if(validateSyntax(scriptText).success){
            Object result = execute(scriptText, variables);
            System.out.println("result: " + result);
            result = execute(scriptText, variables);
            System.out.println("result1: " + result);
        }

        // 语法检查示例
        System.out.println("Grammatically check: " + validateSyntax("if (x > 10 {").errorMessage);
        clearCache();

        // 这是一个死循环脚本,会被 ThreadInterrupt 强制终止
        String deadLoopScript = "while(true) { }";
        // 语法检查示例
        System.out.println("Grammatically check: " + validateSyntax(deadLoopScript).errorMessage);
        if(validateSyntax(deadLoopScript).success){
            try {
                executeWithTimeout(deadLoopScript, Map.of(), 1, TimeUnit.SECONDS);
            } catch (RuntimeException e) {
                System.err.println("deadLoopScript:"+e.getMessage()); // 输出:脚本执行超时
            }
        }

        // 这是一个exit脚本,会被 ThreadInterrupt 强制终止
        String deadexitScript = "System.exit(0)";
        // 语法检查示例
        System.out.println("Grammatically check: " + validateSyntax(deadexitScript).errorMessage);

        // 这是一个exit脚本,会被 ThreadInterrupt 强制终止
        String deadRuntimeScript = "Runtime.getRuntime().exec(\"...\")";
        // 语法检查示例
        System.out.println("Grammatically check: " + validateSyntax(deadRuntimeScript).errorMessage);

        String ioWriteScript = """
            def file = new File("test.txt")
            file.write("hello world")
            return true
        """;
        System.out.println("\nIO Write check: " + validateSyntax(ioWriteScript).errorMessage);
        if(validateSyntax(ioWriteScript).success){
            try {
                execute(ioWriteScript, Map.of());
            } catch (Exception e) {
                System.err.println("IO Write rc: " + e.getMessage());
            }
        }

        //加一个1000以上的测试
//        String scriptText1 = "";
//        for (int i = 1; i < 5500; i++){
//            scriptText1 = """
//                if (amount >= 50000) return "1"
//                if (amount * 0.8 >= 20001 && orderCount >= 20) return "2"
//                return """ + " " + i;
//                System.out.println("Grammatically check: " + validateSyntax(scriptText1).errorMessage);
//                System.out.println("CACHE size: " + SCRIPT_CLASS_CACHE.size());
//                if(validateSyntax(scriptText1).success){
//                    Object result = execute(scriptText1, variables);
//                    System.out.println("result: " + result);
//                }
//        }

    }
}
posted @ 2026-04-19 22:07  JoePotter  阅读(18)  评论(0)    收藏  举报
``