共计 2346 个字符,预计需要花费 6 分钟才能阅读完成。
问题背景
在 Java 应用中执行动态脚本(如 JavaScript、Groovy 等)的需求很常见,比如业务规则动态配置、插件系统实现等。传统的反射调用方式虽然灵活,但存在以下局限性:

- 性能开销大:每次执行都需要解析和编译脚本
- 类型转换复杂:Java 与脚本语言间的数据类型需要手动转换
- 安全性差:容易受到脚本注入攻击
- 维护困难:脚本变更需要重启应用
技术选型
Java 平台提供了两种主要的脚本执行方案:
- JSR-223 ScriptEngine
- 优点:标准 API,简单易用,支持多种脚本语言
-
缺点:性能一般,Nashorn 引擎已被弃用
-
GraalVM Polyglot
- 优点:高性能,支持多语言互操作
- 缺点:依赖 GraalVM,部署环境要求高
性能对比(基于 JMH 测试):
| 指标 | JSR-223 | GraalVM |
|---|---|---|
| 首次执行 (ms) | 120 | 80 |
| 重复执行 (ms) | 15 | 5 |
| 内存占用 (MB) | 50 | 30 |
核心实现
1. ScriptEngineManager 初始化
// 创建脚本引擎管理器
ScriptEngineManager manager = new ScriptEngineManager();
// 获取 JavaScript 引擎
ScriptEngine engine = manager.getEngineByName("javascript");
2. 变量交互示例
// 创建绑定对象
Bindings bindings = engine.createBindings();
// 设置 Java 变量
bindings.put("name", "张三");
// 执行脚本
engine.eval("print('Hello, '+ name);", bindings);
3. 完整代码示例
import javax.script.*;
public class ScriptDemo {public static void main(String[] args) {ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");
try {
// 安全执行脚本
String script = "function add(a, b) {return a + b;}";
engine.eval(script);
// 调用脚本函数
Invocable invocable = (Invocable) engine;
Object result = invocable.invokeFunction("add", 1, 2);
System.out.println(result);
} catch (ScriptException | NoSuchMethodException e) {e.printStackTrace();
} finally {
// 释放资源
if (engine instanceof AutoCloseable) {
try {((AutoCloseable) engine).close();} catch (Exception e) {e.printStackTrace();
}
}
}
}
}
高级优化
1. 预编译优化
if (engine instanceof Compilable) {Compilable compilable = (Compilable) engine;
CompiledScript compiledScript = compilable.compile(script);
// 重复执行时直接使用编译结果
compiledScript.eval();}
2. 脚本缓存实现
// 使用 SoftReference 缓存编译后的脚本
Map<String, SoftReference<CompiledScript>> scriptCache = new ConcurrentHashMap<>();
public CompiledScript getCompiledScript(ScriptEngine engine, String script) {
return scriptCache.computeIfAbsent(script, key -> {if (engine instanceof Compilable) {return new SoftReference<>(((Compilable) engine).compile(key));
}
return null;
}).get();}
避坑指南
1. 脚本注入防护
// 过滤危险操作
String safeScript = script.replace("java.lang.Runtime", "")
.replace("ProcessBuilder", "");
2. 线程安全策略
// 每个线程使用独立的引擎实例
private static final ThreadLocal<ScriptEngine> engineHolder = ThreadLocal.withInitial(() -> {ScriptEngineManager manager = new ScriptEngineManager();
return manager.getEngineByName("javascript");
});
3. Nashorn 迁移方案
- 方案一:切换到 GraalVM JavaScript 引擎
- 方案二:使用第三方引擎如 Rhino
- 方案三:考虑使用 Groovy 等其他 JVM 脚本语言
延伸思考
本文方案可以扩展到其他脚本语言支持:
- 对于 Python,可以使用 Jython 或 GraalVM Python
- 对于 Ruby,可以使用 JRuby
- 需要特别注意不同语言间的类型系统差异
在实际项目中,建议根据性能要求、部署环境和团队技术栈选择合适的方案。对于高性能场景,GraalVM 是更好的选择;对于简单的脚本需求,JSR-223 API 完全够用。
正文完
