SpringBoot + 规则执行沙箱 + 超时熔断:防止脚本死循环拖垮整个服务
在后端服务开发中,动态规则执行是一个非常常见的场景------比如风控规则、定价规则、流程跳转规则等。为了提升灵活性,我们通常会允许业务人员通过脚本(如 Groovy、QLExpress)动态配置规则,而非每次都修改代码、重启服务。但随之而来的一个致命风险是:如果脚本中出现死循环、无限递归,或者执行时间过长,会直接耗尽服务线程池资源,导致整个服务雪崩,甚至宕机。
本文将分享一套基于 SpringBoot 的解决方案:通过「规则执行沙箱」实现脚本与主服务的隔离,结合「超时熔断机制」强制中断异常脚本,双重保障服务稳定性,彻底解决脚本异常拖垮服务的问题。方案兼顾实用性和可扩展性,可直接应用于生产环境。
一、核心痛点:为什么脚本异常会拖垮整个服务?
在没有隔离和熔断机制的情况下,脚本执行的风险主要集中在 3 个方面,最终都会指向服务不可用:
- 线程资源耗尽:SpringBoot 默认使用 Tomcat 线程池处理请求,线程数量有限(默认核心线程 10 个,最大线程 200 个)。如果脚本出现死循环,执行线程会一直被占用,无法释放;当这类异常请求增多时,线程池会快速被占满,新的请求无法处理,服务直接卡死。
- 资源泄露:异常脚本可能会频繁创建对象、占用 IO 资源,且无法正常释放,长期运行会导致 JVM 内存溢出(OOM),最终服务宕机。
- 无边界影响:脚本执行与主服务共用一个 JVM 进程,脚本中的恶意代码(或误写代码)可能会直接操作主服务的核心资源(如修改静态变量、调用危险方法),引发不可控的线上事故。
举个真实案例:某风控系统中,业务人员配置的 Groovy 脚本因逻辑疏漏出现死循环,单个请求执行时间超过 10 分钟,导致 Tomcat 线程池被占满,整个风控服务瘫痪,影响了核心交易流程,造成了严重的经济损失。
因此,对于动态脚本执行场景,「隔离」和「熔断」缺一不可------沙箱负责隔离,防止脚本影响主服务;熔断负责兜底,防止异常脚本长期占用资源。
二、方案设计:SpringBoot + 沙箱 + 超时熔断 三重保障
本次方案的核心思路是「分层隔离、超时兜底、异常熔断」,整体架构分为 3 层,各层职责清晰,协同工作:
2.1 架构分层说明
- **应用层(SpringBoot)**:负责接收请求、参数校验、结果返回,以及整合沙箱和熔断组件,提供统一的规则执行入口。
- **隔离层(规则执行沙箱)**:采用「轻量级沙箱框架」,为脚本执行提供独立的运行环境------包括独立的类加载器、线程池、资源限制,确保脚本执行不会影响主服务的 JVM 进程和线程资源。
- **兜底层(超时熔断)**:基于 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 中引入核心依赖,注意版本兼容性(已验证以下版本可正常运行):
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 配置沙箱,确保脚本执行在独立的环境中,禁止访问主服务的核心类和方法:
java
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 脚本引擎,提供脚本编译、执行、结果处理的一站式方法,并处理沙箱执行过程中的异常:
java
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 的超时和熔断参数,按需调整:
yaml
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 熔断服务封装
封装规则执行服务,添加超时和熔断注解,实现兜底逻辑(当熔断触发或执行超时时,返回降级结果):
java
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 接口开发:提供外部访问入口
开发一个接口,接收前端传递的规则脚本和参数,调用规则执行服务,返回执行结果:
java
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 调用接口:http://localhost:8080/rule/execute,请求体格式如下:
json
{
"script": "此处填写Groovy脚本",
"paramMap": {
"num1": 10,
"num2": 20
}
}
4.2 场景 1:正常脚本(验证基础功能)
脚本内容(计算两个数的和):
groovy
// 约定的main方法,接收paramMap参数
def main(Map paramMap) {
def num1 = paramMap.get("num1")
def num2 = paramMap.get("num2")
return num1 + num2
}
测试结果:接口返回 30,执行时间约 50ms,沙箱和熔断均未触发,服务正常。
4.3 场景 2:死循环脚本(验证超时熔断)
脚本内容(死循环,无法正常结束):
groovy
def main(Map paramMap) {
// 死循环,模拟异常脚本
while (true) {
println("死循环执行中...")
}
}
测试结果:
- 脚本执行 1000ms 后,超时熔断触发,接口返回兜底结果:「规则执行异常,请稍后重试(兜底返回)」。
- 查看日志:沙箱抛出超时异常,Resilience4j 触发熔断,死循环脚本被强制中断,沙箱线程池线程正常释放。
- 服务状态:主服务线程池无占用,接口可正常接收其他请求,服务稳定。
4.4 场景 3:频繁超时脚本(验证熔断降级)
连续调用 10 次死循环脚本(触发熔断阈值),测试结果:
- 前 5 次调用:触发超时,返回兜底结果,失败率 50%。
- 第 6 次调用:熔断触发(失败率超过 50%),直接返回兜底结果,不执行脚本(避免资源浪费)。
- 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 脚本预校验工具类(核心代码)
java
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 中,执行脚本前调用预校验方法,阻断异常脚本执行,修改后的核心代码如下(仅展示修改部分):
java
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 + 规则执行沙箱 + 超时熔断」方案,通过分层隔离、双重兜底,完美解决了这一痛点:
- 沙箱隔离:实现脚本与主服务的线程、类加载、资源隔离,防止脚本异常影响主服务。
- 超时熔断:对异常脚本进行超时中断和熔断降级,避免资源持续浪费,保障服务稳定性。
- 实操性强:方案基于主流框架,代码可直接复用,测试验证简单,适合快速落地到生产环境。
在实际生产中,可根据业务场景调整沙箱隔离级别、熔断参数、线程池配置,同时配合脚本预校验、日志监控,形成一套完整的动态规则执行安全体系。希望本文能为后端开发者提供参考,帮助大家在提升业务灵活性的同时,守住服务稳定性的底线。
关注我的CSDN:blog.csdn.net/qq_30095907...