皇家塔楼防御战斗
35.56M · 2026-02-13
在后端服务开发中,动态规则执行是一个非常常见的场景——比如风控规则、定价规则、流程跳转规则等。为了提升灵活性,我们通常会允许业务人员通过脚本(如 Groovy、QLExpress)动态配置规则,而非每次都修改代码、重启服务。但随之而来的一个致命风险是:如果脚本中出现死循环、无限递归,或者执行时间过长,会直接耗尽服务线程池资源,导致整个服务雪崩,甚至宕机。
本文将分享一套基于 SpringBoot 的解决方案:通过「规则执行沙箱」实现脚本与主服务的隔离,结合「超时熔断机制」强制中断异常脚本,双重保障服务稳定性,彻底解决脚本异常拖垮服务的问题。方案兼顾实用性和可扩展性,可直接应用于生产环境。
在没有隔离和熔断机制的情况下,脚本执行的风险主要集中在 3 个方面,最终都会指向服务不可用:
举个真实案例:某风控系统中,业务人员配置的 Groovy 脚本因逻辑疏漏出现死循环,单个请求执行时间超过 10 分钟,导致 Tomcat 线程池被占满,整个风控服务瘫痪,影响了核心交易流程,造成了严重的经济损失。
因此,对于动态脚本执行场景,「隔离」和「熔断」缺一不可——沙箱负责隔离,防止脚本影响主服务;熔断负责兜底,防止异常脚本长期占用资源。
本次方案的核心思路是「分层隔离、超时兜底、异常熔断」,整体架构分为 3 层,各层职责清晰,协同工作:
选型原则:轻量、易集成、生产可用,避免引入过重的依赖导致服务性能损耗。
下面我们一步步实现整个方案,从环境搭建、核心代码开发,到测试验证,确保每一步都可复制、可落地。
在 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>
沙箱的核心作用是「隔离」,我们需要配置沙箱的运行环境,包括类加载隔离、线程池隔离、资源限制(如 CPU、内存),然后封装脚本执行的统一入口。
通过 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()
);
}
}
封装脚本执行的统一入口,整合沙箱和 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);
}
}
}
沙箱虽然提供了超时控制,但熔断机制能提供更全面的兜底——比如当脚本频繁超时、异常时,直接触发熔断,拒绝执行后续请求,避免资源持续浪费。这里使用 Resilience4j 的 @TimeLimiter(超时控制)和 @CircuitBreaker(熔断)注解。
在 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
封装规则执行服务,添加超时和熔断注解,实现兜底逻辑(当熔断触发或执行超时时,返回降级结果):
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("规则执行异常,请稍后重试(兜底返回)");
}
}
开发一个接口,接收前端传递的规则脚本和参数,调用规则执行服务,返回执行结果:
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 种异常场景,验证沙箱 + 熔断是否能有效保护服务:正常脚本、死循环脚本、频繁超时脚本。
启动 SpringBoot 服务,使用 Postman 调用接口:,请求体格式如下:
{
"script": "此处填写Groovy脚本",
"paramMap": {
"num1": 10,
"num2": 20
}
}
脚本内容(计算两个数的和):
// 约定的main方法,接收paramMap参数
def main(Map paramMap) {
def num1 = paramMap.get("num1")
def num2 = paramMap.get("num2")
return num1 + num2
}
测试结果:接口返回 30,执行时间约 50ms,沙箱和熔断均未触发,服务正常。
脚本内容(死循环,无法正常结束):
def main(Map paramMap) {
// 死循环,模拟异常脚本
while (true) {
println("死循环执行中...")
}
}
测试结果:
连续调用 10 次死循环脚本(触发熔断阈值),测试结果:
结论:方案能有效处理频繁异常的脚本,避免服务资源被持续占用。
很多开发者会疑惑:沙箱和熔断都有超时控制,为什么需要双重配置?两者的核心逻辑是什么?下面我们简单剖析,帮助大家理解方案的设计思路。
本次使用的 Sandbox4J 沙箱,核心是「线程隔离 + 类加载隔离」:
Resilience4j 的超时和熔断机制,核心是「异步任务控制 + 失败率统计」:
沙箱的超时是「沙箱层面的兜底」,Resilience4j 的超时是「应用层面的兜底」,两者协同工作:
两者保持超时时间一致,确保无论哪一层先触发超时,都能快速中断脚本,释放资源。
上述方案已能满足基础需求,但在生产环境中,还需要进行以下优化,提升稳定性和可维护性:
在脚本执行前,添加预校验逻辑:
以下是脚本预校验的具体实现代码,整合为工具类,可直接注入使用,校验失败直接抛出异常,阻断脚本执行:
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);
}
}
}
在之前实现的 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);
}
}
}
补充说明:
添加完善的日志和监控,便于排查问题:
根据生产环境的并发量,调整沙箱线程池参数:
在生产环境中,进一步限制沙箱的资源占用:
动态规则执行虽然提升了业务灵活性,但也带来了脚本异常拖垮服务的风险。本文提出的「SpringBoot + 规则执行沙箱 + 超时熔断」方案,通过分层隔离、双重兜底,完美解决了这一痛点:
在实际生产中,可根据业务场景调整沙箱隔离级别、熔断参数、线程池配置,同时配合脚本预校验、日志监控,形成一套完整的动态规则执行安全体系。希望本文能为后端开发者提供参考,帮助大家在提升业务灵活性的同时,守住服务稳定性的底线。
关注我的CSDN:blog.csdn.net/qq_30095907…