在营销活动、风控规则、价格计算这类业务场景中,规则往往变化频繁。如果每次调整一个计算公式都要走一遍完整的开发、测试、打包、发布流程,效率会非常低。Groovy 是一门运行在 JVM 上的动态语言,语法与 Java 高度兼容,又支持运行时动态编译执行,非常适合嵌入 Spring Boot 项目中做动态脚本引擎。本文将手把手演示整合的完整过程。

一、引入依赖并搭建基础环境
Spring Boot 官方对 Groovy 有不错的支持,引入依赖非常简单。在 pom.xml 中添加 spring-boot-starter-groovy 即可,这个 starter 会自动带入 groovy 核心库以及与 Spring 集成所需的相关组件。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-groovy</artifactId>
</dependency>如果你使用的是 Gradle,对应的写法是 implementation 'org.springframework.boot:spring-boot-starter-groovy'。需要注意版本兼容性:Spring Boot 2.x 对应 Groovy 2.x 或 3.x,Spring Boot 3.x 则要求 Groovy 4.x,版本不匹配时容易出现编译异常,建议直接使用官方 starter 管理版本,避免手动指定造成冲突。
依赖引入后,你甚至可以直接把项目中的部分类写成 Groovy 文件(后缀为 .groovy),Spring Boot 的编译插件会自动识别并编译它们,Java 与 Groovy 可以混用,这为后续的脚本化改造提供了平滑过渡的路径。
二、使用 GroovyShell 快速执行脚本代码
GroovyShell 是最直接的脚本执行方式,它接收一段字符串形式的脚本源码,解释执行并返回结果。这种方式适合逻辑简单、即用即弃的场景,比如让运营人员在后台配置一个简单的计算公式。
GroovyShell shell = new GroovyShell();
Object result = shell.evaluate("return 3 * 7 + 2");
System.out.println(result); // 输出 23GroovyShell 还支持通过 Binding 对象向脚本中传递变量,这样外部数据就能参与脚本计算。例如把订单金额、用户等级传入脚本,脚本内部直接用变量名引用即可,无需解析参数字符串,既安全又方便。
Binding binding = new Binding();
binding.setVariable("amount", 500);
binding.setVariable("level", "VIP");
GroovyShell shell = new GroovyShell(binding);
Object discount = shell.evaluate(
"return level == 'VIP' ? amount * 0.8 : amount"
);
System.out.println(discount); // 输出 400.0需要注意的是,每次调用 evaluate 时如果脚本内容变化,都会触发一次动态编译。编译是有开销的,高频调用场景下不建议每次都新建 GroovyShell 去执行新字符串,可以考虑把编译后的 Script 对象缓存起来复用。
三、GroovyClassLoader 实现脚本热加载与缓存
生产环境中更推荐使用 GroovyClassLoader,它可以把脚本编译成真正的 Class,再实例化执行,性能接近原生 Java。同时配合文件时间戳或内容哈希判断,还能实现脚本热更新:脚本文件修改后自动重新编译,无需重启应用。
@Service
public class GroovyScriptService {
private final GroovyClassLoader classLoader = new GroovyClassLoader();
private final Map<String, Class<?>> cache = new ConcurrentHashMap<>();
private final Map<String, Long> fileStamps = new ConcurrentHashMap<>();
public Object runScript(File scriptFile, Map<String, Object> params) throws Exception {
long stamp = scriptFile.lastModified();
String key = scriptFile.getPath();
// 时间戳变化则重新编译,实现热加载
Class<?> clazz = cache.compute(key, (k, old) ->
old != null && fileStamps.get(k) == stamp ? old : null);
if (clazz == null) {
clazz = classLoader.parseClass(scriptFile);
cache.put(key, clazz);
fileStamps.put(key, stamp);
}
// 脚本类需实现统一接口,方便调用
ScriptAction action = (ScriptAction) clazz.getDeclaredConstructor().newInstance();
return action.execute(params);
}
}
public interface ScriptAction {
Object execute(Map<String, Object> params);
}对应的 Groovy 脚本文件只需要实现这个接口,写法非常自由:
class PriceRule implements ScriptAction {
@Override
Object execute(Map<String, Object> params) {
def amount = params.amount as double
if (amount > 1000) {
return amount * 0.85
}
return amount
}
}这种方案的关键点在于接口约定。脚本通过实现固定的 Java 接口,宿主程序不需要知道脚本内部逻辑,只面向接口编程,耦合度低、调用规范清晰。另外要提醒一点:parseClass 每次都会生成新的 Class 对象,旧的 Class 无法及时被回收,脚本更新非常频繁时可能产生元空间压力,生产上建议用内容哈希做缓存键,并在类加载器层面做好替换策略。
四、让脚本访问 Spring 容器中的 Bean
动态脚本如果只能做纯计算,能力就太有限了。很多时候脚本需要调用数据库、RPC 服务或者其他 Spring Bean。借助 ApplicationContext 就能把整个容器的能力注入脚本。
@Component
public class SpringBeanHolder implements ApplicationContextAware {
private static ApplicationContext context;
@Override
public void setApplicationContext(ApplicationContext ctx) {
context = ctx;
}
public static Object getBean(String name) {
return context.getBean(name);
}
}然后在执行脚本前,通过 Binding 把这个工具类传进去:
Binding binding = new Binding();
binding.setVariable("springBean", SpringBeanHolder.class);
GroovyShell shell = new GroovyShell(binding);
Object result = shell.evaluate(
"def userService = springBean.getBean('userService')\n" +
"return userService.getUserName(1001)"
);这样一来,脚本内部可以像普通代码一样获取并调用任意 Bean,能力边界被大大扩展。但也正因为如此,安全问题必须重视。如果脚本内容来源于用户输入,务必使用 CompileCustomizer 加上 SecureASTCustomizer,限制可调用的类和方法白名单,防止恶意脚本执行 System.exit、文件读写或反射调用危险 API。企业内部的规则脚本通常只开放给可信的管理后台,同时配合操作审计日志,形成完整的安全闭环。
五、性能与工程化建议
关于性能,有几个实践经验值得参考。第一,编译结果一定要缓存,同一脚本重复执行时直接复用 Class 或 Script 实例,性能差距可以达到几十倍。第二,Groovy 脚本的并发执行要注意 Script 对象是有状态的,多线程共享同一个 Script 实例可能产生变量覆盖问题,要么每次执行新建实例,要么通过 Binding 隔离上下文。第三,脚本数量多的时候,建议把脚本统一存储在数据库或配置中心,配合版本号管理,方便回滚和追溯。
在工程结构上,可以把脚本引擎抽象成独立的 starter 模块,对外提供 ScriptEngine.execute(scriptId, params) 这样的统一入口,内部封装加载、编译、缓存、监控等细节。再加上执行耗时统计和异常告警,一套轻量级的规则引擎雏形就完成了。相比直接引入 Drools 这类重型规则引擎,Groovy 方案更轻、学习成本更低,对绝大多数动态逻辑场景已经足够。
总结一下,Spring Boot 整合 Groovy 的核心思路是:引入 starter 建立环境,简单场景用 GroovyShell 快速执行,生产场景用 GroovyClassLoader 配合接口约定与缓存实现高性能热加载,再通过 ApplicationContext 打通 Spring 容器能力,最后用白名单机制守住安全底线。掌握这套组合拳,业务规则的动态化改造就不再是难题。
Spring BootGroovy动态脚本修改时间:2026-09-07 08:08:41