SpringBoot + 规则执行沙箱 + 超时熔断:防止脚本死循环拖垮整个服务

在后端服务开发中,动态规则执行是一个非常常见的场景——比如风控规则、定价规则、流程跳转规则等。为了提升灵活性,我们通常会允许业务人员通过脚本(如 Groovy、QLExpress)动态配置规则,而非每次都修改代码、重启服务。但随之而来的一个致命风险是:如果脚本中出现死循环、无限递归,或者执行时间过长,会直接耗尽服务线程池资源,导致整个服务雪崩,甚至宕机。

本文将分享一套基于 SpringBoot 的解决方案:通过「规则执行沙箱」实现脚本与主服务的隔离,结合「超时熔断机制」强制中断异常脚本,双重保障服务稳定性,彻底解决脚本异常拖垮服务的问题。方案兼顾实用性和可扩展性,可直接应用于生产环境。

一、核心痛点:为什么脚本异常会拖垮整个服务?

在没有隔离和熔断机制的情况下,脚本执行的风险主要集中在 3 个方面,最终都会指向服务不可用:

  1. 线程资源耗尽​:SpringBoot 默认使用 Tomcat 线程池处理请求,线程数量有限(默认核心线程 10 个,最大线程 200 个)。如果脚本出现死循环,执行线程会一直被占用,无法释放;当这类异常请求增多时,线程池会快速被占满,新的请求无法处理,服务直接卡死。
  2. 资源泄露​:异常脚本可能会频繁创建对象、占用 IO 资源,且无法正常释放,长期运行会导致 JVM 内存溢出(OOM),最终服务宕机。
  3. 无边界影响​:脚本执行与主服务共用一个 JVM 进程,脚本中的恶意代码(或误写代码)可能会直接操作主服务的核心资源(如修改静态变量、调用危险方法),引发不可控的线上事故。

举个真实案例:某风控系统中,业务人员配置的 Groovy 脚本因逻辑疏漏出现死循环,单个请求执行时间超过 10 分钟,导致 Tomcat 线程池被占满,整个风控服务瘫痪,影响了核心交易流程,造成了严重的经济损失。

因此,对于动态脚本执行场景,「隔离」和「熔断」缺一不可——沙箱负责隔离,防止脚本影响主服务;熔断负责兜底,防止异常脚本长期占用资源。

二、方案设计:SpringBoot + 沙箱 + 超时熔断 三重保障

本次方案的核心思路是「分层隔离、超时兜底、异常熔断」,整体架构分为 3 层,各层职责清晰,协同工作:

2.1 架构分层说明

  1. ​**应用层(SpringBoot)**​:负责接收请求、参数校验、结果返回,以及整合沙箱和熔断组件,提供统一的规则执行入口。
  2. ​**隔离层(规则执行沙箱)**​:采用「轻量级沙箱框架」,为脚本执行提供独立的运行环境——包括独立的类加载器、线程池、资源限制,确保脚本执行不会影响主服务的 JVM 进程和线程资源。
  3. ​**兜底层(超时熔断)**​:基于 Resilience4j 实现超时控制和熔断机制,当脚本执行超时或异常频率过高时,直接中断执行并返回降级结果,避免资源浪费。

2.2 核心组件选型

选型原则:轻量、易集成、生产可用,避免引入过重的依赖导致服务性能损耗。

  • 基础框架​:SpringBoot 2.7.x(稳定版,兼容性好,生态完善)。
  • 脚本引擎​:Groovy(动态性强,语法接近 Java,与 SpringBoot 集成友好,适合业务规则编写)。
  • 规则沙箱​:Alibaba Sandbox4J(轻量级 Java 沙箱,无需修改 JVM 参数,支持类加载隔离、资源限制,性能损耗低)。
  • 超时熔断​:Resilience4j(轻量级熔断框架,基于 Java 8,支持超时、熔断、限流等功能,比 Hystrix 更轻量,更适合 SpringBoot 2.x 版本)。

三、实操实现:从零搭建可落地的解决方案

下面我们一步步实现整个方案,从环境搭建、核心代码开发,到测试验证,确保每一步都可复制、可落地。

3.1 环境搭建:引入依赖

在 SpringBoot 项目的 pom.xml 中引入核心依赖,注意版本兼容性(已验证以下版本可正常运行):

<!-- SpringBoot 基础依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- Groovy 脚本引擎 -->
<dependency>
    <groupId>org.codehaus.groovy</groupId>
    <artifactId>groovy-all</artifactId>
    <version>3.0.17</version>
    <type>pom</type>
</dependency>

<!-- Alibaba Sandbox4J 沙箱 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>sandbox4j-core</artifactId>
    <version>1.0.0</version>
</dependency>

<!-- Resilience4j 超时熔断 -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
    <version>1.9.0</version>
</dependency>

<!-- 测试依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

3.2 核心开发:沙箱配置与脚本执行封装

沙箱的核心作用是「隔离」,我们需要配置沙箱的运行环境,包括类加载隔离、线程池隔离、资源限制(如 CPU、内存),然后封装脚本执行的统一入口。

3.2.1 沙箱配置类

通过 Sandbox4J 的 API 配置沙箱,确保脚本执行在独立的环境中,禁止访问主服务的核心类和方法:

import com.alibaba.sandbox4j.api.Sandbox;
import com.alibaba.sandbox4j.api.SandboxConfig;
import com.alibaba.sandbox4j.api.SandboxFactory;
import com.alibaba.sandbox4j.enums.IsolationLevel;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * 规则执行沙箱配置类:实现脚本与主服务的隔离
 */
@Configuration
public class RuleSandboxConfig {

    /**
     * 配置沙箱:隔离级别为THREAD(线程隔离),并指定独立线程池
     */
    @Bean
    public Sandbox ruleSandbox() {
        // 1. 配置沙箱基础参数
        SandboxConfig config = SandboxConfig.builder()
                // 隔离级别:THREAD(线程隔离),支持类加载、线程、资源的完全隔离
                .isolationLevel(IsolationLevel.THREAD)
                // 禁止脚本访问主服务的核心包(可根据实际情况调整)
                .denyPackages("com.example.demo.service", "com.example.demo.mapper")
                // 允许脚本访问的基础包(如工具类)
                .allowPackages("java.lang", "java.util", "groovy.lang")
                // 脚本执行超时时间(默认1000ms,这里先配置,最终以熔断超时为准)
                .timeout(1000)
                .timeUnit(TimeUnit.MILLISECONDS)
                // 配置沙箱独立线程池(避免占用主服务线程池)
                .threadPool(ruleSandboxThreadPool())
                .build();

        // 2. 创建沙箱实例(单例,全局复用)
        return SandboxFactory.createSandbox(config);
    }

    /**
     * 沙箱独立线程池:与主服务线程池隔离,防止脚本异常占用主服务线程
     */
    private ThreadPoolExecutor ruleSandboxThreadPool() {
        return new ThreadPoolExecutor(
                5, // 核心线程数(根据脚本执行并发量调整)
                10, // 最大线程数
                60, // 空闲线程存活时间
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(20), // 任务队列(避免任务堆积)
                // 线程命名前缀(便于日志排查)
                r -> new Thread(r, "rule-sandbox-thread-"),
                // 任务拒绝策略:当线程池满时,拒绝任务并抛出异常(避免资源耗尽)
                new ThreadPoolExecutor.AbortPolicy()
        );
    }
}

3.2.2 脚本执行器封装

封装脚本执行的统一入口,整合沙箱和 Groovy 脚本引擎,提供脚本编译、执行、结果处理的一站式方法,并处理沙箱执行过程中的异常:

import com.alibaba.sandbox4j.api.Sandbox;
import com.alibaba.sandbox4j.exception.SandboxTimeoutException;
import groovy.lang.GroovyClassLoader;
import groovy.lang.GroovyObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 规则脚本执行器:封装沙箱执行逻辑,提供统一的脚本执行入口
 */
@Component
public class RuleScriptExecutor {

    @Autowired
    private Sandbox ruleSandbox;

    // Groovy类加载器(与沙箱类加载器隔离)
    private final GroovyClassLoader groovyClassLoader = new GroovyClassLoader();

    /**
     * 执行规则脚本
     * @param script 规则脚本内容(Groovy)
     * @param paramMap 脚本执行参数
     * @return 脚本执行结果
     * @throws Exception 执行异常(超时、语法错误、权限异常等)
     */
    public Object executeScript(String script, Map<String, Object> paramMap) throws Exception {
        try {
            // 1. 编译Groovy脚本(生成Class对象)
            Class<?> scriptClass = groovyClassLoader.parseClass(script);
            // 2. 创建脚本实例
            GroovyObject groovyObject = (GroovyObject) scriptClass.newInstance();

            // 3. 在沙箱中执行脚本(核心:脚本运行在沙箱隔离环境中)
            // 沙箱执行逻辑:将脚本执行任务提交到沙箱线程池,由沙箱控制超时和资源
            return ruleSandbox.execute(() -> {
                // 获取脚本的main方法(约定脚本必须有main方法,接收paramMap参数)
                return groovyObject.invokeMethod("main", new Object[]{paramMap});
            });
        } catch (SandboxTimeoutException e) {
            // 沙箱超时异常(会被熔断机制兜底,但这里提前捕获,便于日志排查)
            throw new Exception("规则脚本执行超时,已被沙箱中断", e);
        } catch (Exception e) {
            // 其他异常(语法错误、权限不足、死循环等)
            throw new Exception("规则脚本执行失败:" + e.getMessage(), e);
        }
    }
}

3.3 核心开发:超时熔断配置

沙箱虽然提供了超时控制,但熔断机制能提供更全面的兜底——比如当脚本频繁超时、异常时,直接触发熔断,拒绝执行后续请求,避免资源持续浪费。这里使用 Resilience4j 的 @TimeLimiter(超时控制)和 @CircuitBreaker(熔断)注解。

3.3.1 熔断配置文件

在 application.yml 中配置 Resilience4j 的超时和熔断参数,按需调整:

resilience4j:
  # 超时控制配置
  timelimiter:
    instances:
      # 规则脚本执行超时配置(与沙箱超时保持一致,双重保障)
      ruleScriptExecutor:
        timeoutDuration: 1000ms # 超时时间(核心:超过该时间直接中断执行)
        cancelRunningFuture: true # 超时后取消正在运行的任务(关键:中断死循环脚本)
  # 熔断配置
  circuitbreaker:
    instances:
      # 规则脚本熔断配置
      ruleScriptExecutor:
        slidingWindowSize: 10 # 滑动窗口大小(统计10个请求)
        failureRateThreshold: 50 # 熔断阈值:失败率超过50%触发熔断
        waitDurationInOpenState: 5000ms # 熔断开放时间:5秒后尝试恢复
        permittedNumberOfCallsInHalfOpenState: 3 # 半开状态允许的请求数:3个请求都成功则关闭熔断
        registerHealthIndicator: true # 注册健康指标,便于监控
        # 触发熔断的异常类型(超时、沙箱异常、脚本执行异常)
        recordExceptions:
          - java.lang.Exception
        # 不触发熔断的异常类型(按需配置)
        ignoreExceptions:
          - java.lang.IllegalArgumentException

3.3.2 熔断服务封装

封装规则执行服务,添加超时和熔断注解,实现兜底逻辑(当熔断触发或执行超时时,返回降级结果):

import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
 * 规则执行服务:整合熔断机制,提供熔断兜底
 */
@Service
public class RuleExecuteService {

    @Autowired
    private RuleScriptExecutor ruleScriptExecutor;

    /**
     * 执行规则脚本:添加超时熔断注解
     * @param script 规则脚本
     * @param paramMap 执行参数
     * @return 执行结果(CompletableFuture:支持异步执行,适配Resilience4j超时控制)
     */
    @TimeLimiter(name = "ruleScriptExecutor") // 关联超时配置
    @CircuitBreaker(
            name = "ruleScriptExecutor", // 关联熔断配置
            fallbackMethod = "executeScriptFallback" // 熔断/超时兜底方法
    )
    public CompletableFuture<Object> executeRule(String script, Map<String, Object> paramMap) {
        // 异步执行脚本(Resilience4j的超时控制基于异步任务)
        return CompletableFuture.supplyAsync(() -> {
            try {
                return ruleScriptExecutor.executeScript(script, paramMap);
            } catch (Exception e) {
                // 抛出异常,让熔断机制捕获并触发兜底
                throw new RuntimeException(e);
            }
        });
    }

    /**
     * 熔断/超时兜底方法:当脚本执行超时、熔断触发时,返回默认结果
     * 注意:方法参数、返回值必须与被熔断方法一致,最后添加一个Exception参数
     */
    public CompletableFuture<Object> executeScriptFallback(String script, Map<String, Object> paramMap, Exception e) {
        // 日志记录异常信息(便于排查)
        System.err.println("规则脚本执行异常(熔断/超时):" + e.getMessage());
        // 返回降级结果(可根据实际业务调整,比如返回默认规则结果、提示系统繁忙等)
        return CompletableFuture.completedFuture("规则执行异常,请稍后重试(兜底返回)");
    }
}

3.4 接口开发:提供外部访问入口

开发一个接口,接收前端传递的规则脚本和参数,调用规则执行服务,返回执行结果:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;
import java.util.concurrent.CompletableFuture;

/**
 * 规则执行接口:提供外部访问入口
 */
@RestController
@RequestMapping("/rule")
public class RuleExecuteController {

    @Autowired
    private RuleExecuteService ruleExecuteService;

    /**
     * 执行规则脚本接口
     * @param request 包含脚本内容和执行参数
     * @return 脚本执行结果
     */
    @PostMapping("/execute")
    public CompletableFuture<Object> executeRule(@RequestBody RuleExecuteRequest request) {
        // 参数校验(简化,实际生产需完善)
        if (request.getScript() == null || request.getParamMap() == null) {
            return CompletableFuture.completedFuture("脚本内容和执行参数不能为空");
        }
        // 调用规则执行服务
        return ruleExecuteService.executeRule(request.getScript(), request.getParamMap());
    }

    // 请求参数封装
    public static class RuleExecuteRequest {
        private String script; // 规则脚本(Groovy)
        private Map<String, Object> paramMap; // 执行参数

        // getter/setter 省略
        public String getScript() { return script; }
        public void setScript(String script) { this.script = script; }
        public Map<String, Object> getParamMap() { return paramMap; }
        public void setParamMap(Map<String, Object> paramMap) { this.paramMap = paramMap; }
    }
}

四、测试验证:模拟异常场景,验证方案有效性

方案搭建完成后,我们需要模拟 3 种异常场景,验证沙箱 + 熔断是否能有效保护服务:正常脚本、死循环脚本、频繁超时脚本。

4.1 测试准备

启动 SpringBoot 服务,使用 Postman 调用接口:,请求体格式如下:

{
  "script": "此处填写Groovy脚本",
  "paramMap": {
    "num1": 10,
    "num2": 20
  }
}

4.2 场景 1:正常脚本(验证基础功能)

脚本内容(计算两个数的和):

// 约定的main方法,接收paramMap参数
def main(Map paramMap) {
    def num1 = paramMap.get("num1")
    def num2 = paramMap.get("num2")
    return num1 + num2
}

测试结果:接口返回 30,执行时间约 50ms,沙箱和熔断均未触发,服务正常。

4.3 场景 2:死循环脚本(验证超时熔断)

脚本内容(死循环,无法正常结束):

def main(Map paramMap) {
    // 死循环,模拟异常脚本
    while (true) {
        println("死循环执行中...")
    }
}

测试结果:

  1. 脚本执行 1000ms 后,超时熔断触发,接口返回兜底结果:「规则执行异常,请稍后重试(兜底返回)」。
  2. 查看日志:沙箱抛出超时异常,Resilience4j 触发熔断,死循环脚本被强制中断,沙箱线程池线程正常释放。
  3. 服务状态:主服务线程池无占用,接口可正常接收其他请求,服务稳定。

4.4 场景 3:频繁超时脚本(验证熔断降级)

连续调用 10 次死循环脚本(触发熔断阈值),测试结果:

  1. 前 5 次调用:触发超时,返回兜底结果,失败率 50%。
  2. 第 6 次调用:熔断触发(失败率超过 50%),直接返回兜底结果,不执行脚本(避免资源浪费)。
  3. 5 秒后(熔断开放时间):尝试调用,若脚本恢复正常,则熔断关闭;若仍异常,则继续保持熔断状态。

结论:方案能有效处理频繁异常的脚本,避免服务资源被持续占用。

五、原理剖析:沙箱隔离与熔断机制的核心逻辑

很多开发者会疑惑:沙箱和熔断都有超时控制,为什么需要双重配置?两者的核心逻辑是什么?下面我们简单剖析,帮助大家理解方案的设计思路。

5.1 沙箱隔离的核心原理

本次使用的 Sandbox4J 沙箱,核心是「线程隔离 + 类加载隔离」:

  • 线程隔离​:沙箱使用独立的线程池执行脚本,与主服务的 Tomcat 线程池完全隔离,即使脚本死循环,占用的也是沙箱线程池的线程,不会影响主服务的请求处理。
  • 类加载隔离​:沙箱拥有独立的类加载器,脚本编译生成的 Class 对象只存在于沙箱的类加载器中,不会污染主服务的类加载器;同时,通过 allowPackages/denyPackages 配置,限制脚本的访问权限,防止脚本调用主服务的核心资源。
  • 资源限制​:沙箱可以限制脚本的 CPU、内存占用,避免脚本过度消耗服务器资源。

5.2 超时熔断的核心原理

Resilience4j 的超时和熔断机制,核心是「异步任务控制 + 失败率统计」:

  • 超时控制​:通过 @TimeLimiter 注解,将脚本执行任务封装为 CompletableFuture 异步任务,当任务执行时间超过配置的 timeoutDuration 时,自动取消任务(cancelRunningFuture=true),中断脚本执行。
  • 熔断机制​:通过滑动窗口统计脚本执行的失败率(超时、异常均视为失败),当失败率超过阈值时,触发熔断,进入开放状态;开放状态下,所有请求直接返回兜底结果,不执行脚本;经过指定时间后,进入半开状态,尝试执行少量请求,若全部成功则关闭熔断,否则继续保持开放状态。

5.3 为什么需要双重超时配置?

沙箱的超时是「沙箱层面的兜底」,Resilience4j 的超时是「应用层面的兜底」,两者协同工作:

  • 沙箱超时:防止沙箱线程被长期占用,即使 Resilience4j 出现异常,沙箱也能自行中断脚本。
  • Resilience4j 超时:触发熔断机制,实现请求级别的兜底,避免频繁调用异常脚本。

两者保持超时时间一致,确保无论哪一层先触发超时,都能快速中断脚本,释放资源。

六、生产环境优化建议

上述方案已能满足基础需求,但在生产环境中,还需要进行以下优化,提升稳定性和可维护性:

6.1 脚本预校验

在脚本执行前,添加预校验逻辑:

  • 语法校验​:使用 Groovy 的语法解析器,校验脚本语法是否正确,避免因语法错误导致的异常。
  • 危险方法校验​:禁止脚本使用 System.exit()、Runtime.getRuntime().exec()等危险方法,防止脚本恶意破坏服务。
  • 逻辑校验​:简单校验脚本是否存在明显的死循环(如 while(true)无退出条件),可通过静态代码分析实现。

以下是脚本预校验的具体实现代码,整合为工具类,可直接注入使用,校验失败直接抛出异常,阻断脚本执行:

6.1.1 脚本预校验工具类(核心代码)

import groovy.lang.GroovyCodeSource;
import groovy.lang.GroovyShell;
import org.codehaus.groovy.control.CompilationFailedException;
import org.codehaus.groovy.control.CompilerConfiguration;
import org.springframework.stereotype.Component;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 生产环境脚本预校验工具类:语法校验、危险方法校验、逻辑校验
 */
@Component
public class ScriptPreCheckUtil {

    // 危险方法正则:禁止System.exit、Runtime.exec等破坏服务的方法
    private static final Pattern DANGEROUS_METHOD_PATTERN = Pattern.compile(
            "(System\.exit\(|Runtime\.getRuntime\(\)\.exec\(|ProcessBuilder\()",
            Pattern.CASE_INSENSITIVE
    );

    // 死循环正则:匹配 while(true)、for(;;) 等明显死循环(简单校验,复杂场景需结合AST分析)
    private static final Pattern DEAD_LOOP_PATTERN = Pattern.compile(
            "(while\s*\(\s*true\s*\)|for\s*\(\s*;\s*;\s*\))",
            Pattern.CASE_INSENSITIVE
    );

    // Groovy脚本解析器(单例复用,提升性能)
    private static final GroovyShell GROOVY_SHELL;

    static {
        // 配置Groovy编译器:仅允许基础语法,禁止动态加载危险类
        CompilerConfiguration config = new CompilerConfiguration();
        config.setDisabledGlobalASTTransformations(null);
        GROOVY_SHELL = new GroovyShell(config);
    }

    /**
     * 脚本预校验入口:顺序执行语法校验、危险方法校验、逻辑校验
     * @param script 待校验的Groovy脚本
     * @throws IllegalArgumentException 校验失败抛出异常,包含具体失败原因
     */
    public void preCheckScript(String script) {
        // 1. 语法校验
        checkScriptSyntax(script);
        // 2. 危险方法校验
        checkDangerousMethod(script);
        // 3. 明显死循环校验
        checkDeadLoop(script);
    }

    /**
     * 语法校验:使用Groovy编译器解析脚本,判断是否存在语法错误
     */
    private void checkScriptSyntax(String script) {
        try {
            // 包装脚本为Groovy代码源,指定名称便于排查
            GroovyCodeSource codeSource = new GroovyCodeSource(script, "PreCheckScript.groovy", GroovyShell.DEFAULT_CODE_BASE);
            // 编译脚本,若语法错误会抛出CompilationFailedException
            GROOVY_SHELL.parse(codeSource);
        } catch (CompilationFailedException e) {
            throw new IllegalArgumentException("脚本语法校验失败:" + e.getMessage().split("\n")[0], e);
        }
    }

    /**
     * 危险方法校验:使用正则匹配禁止的危险方法,防止脚本破坏服务
     */
    private void checkDangerousMethod(String script) {
        Matcher matcher = DANGEROUS_METHOD_PATTERN.matcher(script);
        if (matcher.find()) {
            String dangerousMethod = matcher.group(1);
            throw new IllegalArgumentException("脚本包含禁止使用的危险方法:" + dangerousMethod);
        }
    }

    /**
     * 明显死循环校验:使用正则匹配无退出条件的死循环,简单拦截常见异常场景
     * 说明:复杂死循环(如while(flag)但flag始终为true)需结合AST分析,此处为基础校验
     */
    private void checkDeadLoop(String script) {
        Matcher matcher = DEAD_LOOP_PATTERN.matcher(script);
        if (matcher.find()) {
            String deadLoopCode = matcher.group(1);
            throw new IllegalArgumentException("脚本包含明显死循环,禁止执行:" + deadLoopCode);
        }
    }
}

6.1.2 校验工具类调用方式(整合到脚本执行器)

在之前实现的 RuleScriptExecutor 中,执行脚本前调用预校验方法,阻断异常脚本执行,修改后的核心代码如下(仅展示修改部分):

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;

/**
 * 规则脚本执行器:封装沙箱执行逻辑,提供统一的脚本执行入口
 */
@Component
public class RuleScriptExecutor {

    @Autowired
    private Sandbox ruleSandbox;

    // 注入脚本预校验工具类
    @Autowired
    private ScriptPreCheckUtil scriptPreCheckUtil;

    // Groovy类加载器(与沙箱类加载器隔离)
    private final GroovyClassLoader groovyClassLoader = new GroovyClassLoader();

    /**
     * 执行规则脚本(新增预校验步骤)
     * @param script 规则脚本内容(Groovy)
     * @param paramMap 脚本执行参数
     * @return 脚本执行结果
     * @throws Exception 执行异常(超时、语法错误、权限异常等)
     */
    public Object executeScript(String script, Map<String, Object> paramMap) throws Exception {
        try {
            // 新增:脚本预校验(校验失败直接抛出异常,不进入后续执行)
            scriptPreCheckUtil.preCheckScript(script);
            
            // 1. 编译Groovy脚本(生成Class对象)
            Class<?> scriptClass = groovyClassLoader.parseClass(script);
            // 2. 创建脚本实例
            GroovyObject groovyObject = (GroovyObject) scriptClass.newInstance();

            // 3. 在沙箱中执行脚本(核心:脚本运行在沙箱隔离环境中)
            // 沙箱执行逻辑:将脚本执行任务提交到沙箱线程池,由沙箱控制超时和资源
            return ruleSandbox.execute(() -> {
                // 获取脚本的main方法(约定脚本必须有main方法,接收paramMap参数)
                return groovyObject.invokeMethod("main", new Object[]{paramMap});
            });
        } catch (IllegalArgumentException e) {
            // 捕获预校验失败异常,单独处理(便于日志区分)
            throw new Exception("脚本预校验失败:" + e.getMessage(), e);
        } catch (SandboxTimeoutException e) {
            // 沙箱超时异常(会被熔断机制兜底,但这里提前捕获,便于日志排查)
            throw new Exception("规则脚本执行超时,已被沙箱中断", e);
        } catch (Exception e) {
            // 其他异常(语法错误、权限不足、死循环等)
            throw new Exception("规则脚本执行失败:" + e.getMessage(), e);
        }
    }
}

补充说明:

  • 校验工具类采用单例 GroovyShell,避免频繁创建编译器导致的性能损耗,适配生产环境高并发场景。
  • 危险方法校验可根据实际业务扩展正则表达式,比如禁止文件操作(new File())、网络请求等,按需调整。
  • 死循环校验为基础版本,若需拦截复杂死循环(如动态变量控制的循环),可引入 Groovy AST 分析框架(如 groovy-ast),进一步完善校验逻辑。
  • 校验失败会直接抛出异常,被脚本执行器捕获后,最终由 Resilience4j 熔断机制兜底,返回降级结果,形成完整的异常闭环。

6.2 日志与监控

添加完善的日志和监控,便于排查问题:

  • 日志记录:记录脚本执行的详细信息(脚本内容、参数、执行时间、结果、异常信息),尤其是熔断和超时场景的日志。
  • 监控指标:通过 Resilience4j 的监控功能,收集熔断状态、失败率、超时次数等指标;通过 Prometheus+Grafana 可视化监控,设置告警(如熔断触发、沙箱线程池满)。

6.3 线程池优化

根据生产环境的并发量,调整沙箱线程池参数:

  • 核心线程数和最大线程数:根据脚本执行的并发量调整,避免线程数过多导致资源浪费,或过少导致任务堆积。
  • 任务队列:使用有界队列,避免无界队列导致任务堆积,最终引发 OOM。
  • 拒绝策略:根据业务需求选择拒绝策略,如 AbortPolicy(直接拒绝)、CallerRunsPolicy(由调用线程执行,兜底)。

6.4 沙箱资源限制优化

在生产环境中,进一步限制沙箱的资源占用:

  • CPU 限制:通过 Sandbox4J 的 cpuQuota 配置,限制沙箱线程的 CPU 使用率(如 10%)。
  • 内存限制:限制沙箱执行脚本时的堆内存占用,避免脚本创建大量对象导致 OOM。

七、总结

动态规则执行虽然提升了业务灵活性,但也带来了脚本异常拖垮服务的风险。本文提出的「SpringBoot + 规则执行沙箱 + 超时熔断」方案,通过分层隔离、双重兜底,完美解决了这一痛点:

  1. 沙箱隔离​:实现脚本与主服务的线程、类加载、资源隔离,防止脚本异常影响主服务。
  2. 超时熔断​:对异常脚本进行超时中断和熔断降级,避免资源持续浪费,保障服务稳定性。
  3. 实操性强​:方案基于主流框架,代码可直接复用,测试验证简单,适合快速落地到生产环境。

在实际生产中,可根据业务场景调整沙箱隔离级别、熔断参数、线程池配置,同时配合脚本预校验、日志监控,形成一套完整的动态规则执行安全体系。希望本文能为后端开发者提供参考,帮助大家在提升业务灵活性的同时,守住服务稳定性的底线。

关注我的CSDN:blog.csdn.net/qq_30095907…

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:alixiixcom@163.com