Java使用ScriptEngine执行JavaScript代码入门

作者:百宝袋时间:2026-10-30 17:59:51浏览:105

面向初学者的 Java 使用 ScriptEngine 执行 JavaScript 代码实用指南,详细讲解 JSR 223 核心架构、基础执行、双向数据传递、函数调用、JDK 版本演进辨析与生产优化要点。

在开发 Java 应用程序时,经常需要应对多变的计费规则、动态表达式求值或与前端统一校验逻辑等需求。Java 官方通过 JSR 223 规范提供了统一的脚本引擎接口体系,允许我们在 JVM 内部无缝嵌入并运行 JavaScript 代码。本文将带你从零建立认知,按步骤掌握如何初始化引擎、执行基础代码、双向传递变量、调用独立函数,并深入理清不同 JDK 版本中的引擎演变与生产优化策略。

一、理解 JSR 223 与 ScriptEngine 核心架构

JSR 223(Scripting for the Java Platform)是 Java 平台制定的脚本语言集成标准规范,位于标准库的 javax.script 包中。该规范将脚本执行抽象为一组标准接口,使得 Java 程序无需关注底层是哪一种具体脚本语言,都可以通过统一的接口进行加载与交互。

在该规范中,有几个核心角色相互协同:

  • ScriptEngineManager(引擎管理器):充当工厂角色,通过 Java 的服务发现机制(SPI)在类路径中自动搜索已注册的脚本引擎,并根据名称、文件扩展名或 MIME 类型返回对应的引擎实例。
  • ScriptEngine(脚本执行引擎):核心接口,负责维护执行状态、解析并执行脚本字符串,提供最基础的 eval() 执行能力。
  • Bindings 与 ScriptContext(上下文与变量作用域):用于存储在 Java 与脚本环境之间共享的键值对,决定变量对脚本的可见范围。
  • Invocable(可调用扩展接口):可选接口,由支持函数调用的引擎实现,允许 Java 绕过字符串解析,直接调用脚本中已经定义好的具体函数或对象方法。
Java使用ScriptEngine执行JavaScript代码入门
图 1:JSR 223 脚本引擎分层架构与各组件数据流转关系

二、快速上手:执行首行 JavaScript 脚本

执行一段 JavaScript 脚本通常仅需要三行核心代码。我们先看一个最直接的基础示例:在 Java 中计算一个数学表达式并输出字符串。

import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class QuickStartExample {
    public static void main(String[] args) {
        // 1. 创建脚本引擎管理器
        ScriptEngineManager manager = new ScriptEngineManager();

        // 2. 查找并获取 JavaScript 引擎实例
        ScriptEngine engine = manager.getEngineByName("javascript");
        if (engine == null) {
            System.err.println("当前运行环境未找到 JavaScript 引擎,请确认 JDK 版本或补充依赖!");
            return;
        }

        try {
            // 3. 执行简单的字符串表达式
            Object result = engine.eval("100 * (1 + 0.15) - 20");
            System.out.println("计算结果为:" + result);
            System.out.println("结果的数据类型为:" + result.getClass().getName());

            // 4. 执行控制台输出脚本
            engine.eval("var message = 'Hello from JavaScript'; print(message);");
        } catch (ScriptException e) {
            e.printStackTrace();
        }
    }
}

示例输出:

计算结果为:95.0
结果的数据类型为:java.lang.Double
Hello from JavaScript

在这段代码中,engine.eval() 会返回最后一个表达式的计算结果。需要注意,JavaScript 中的数值类型默认会被转换为 Java 的 java.lang.Double 或 java.lang.Integer。如果脚本中存在语法错误或执行期错误,引擎会抛出检查型异常 ScriptException,其中包含了具体的出错行号和错误原因。

三、核心操作步骤详解

为了让整个执行流程更具条理,我们将 Java 使用 ScriptEngine 执行脚本的过程归纳为标准四步法:

Java使用ScriptEngine执行JavaScript代码入门
图 2:Java 调用 ScriptEngine 的四个标准化执行步骤
  1. 查找与初始化引擎:调用 manager.getEngineByName("javascript")(也可以传入别名如 "js" 或 "nashorn")。务必判断返回的引擎是否为 null。
  2. 绑定上下文参数:绝大部分业务不可能只执行静态公式,通常需要传入外部输入(如订单金额、会员等级)。通过 engine.put("paramName", value) 将 Java 对象直接放入执行上下文。
  3. 解释或调用脚本:执行脚本内容。如果脚本是一整段流程脚本,可直接调用 eval();如果脚本中定义了函数库,先执行一次 eval() 加载函数定义,再利用 Invocable 按需多次调用函数。
  4. 捕获异常与提取结果:脚本执行成功时获取返回值对象,并转换为业务需要的类型;发生语法错误或脚本抛出未捕获异常时,通过 ScriptException 精准定位问题所在行。

四、Java 与 JavaScript 双向数据交互与函数调用

实际业务开发中,最核心的能力是数据在两种语言环境之间的流通。Java 可以向脚本注入基本类型和复杂对象;脚本执行后产生的变量也能被 Java 反向提取;此外,Java 还可以像调用本地方法一样调用 JavaScript 函数。

Java使用ScriptEngine执行JavaScript代码入门
图 3:Java 与 JavaScript 的数据流向与 Invocable 函数直接调用机制

1. 通过 put 注入变量并读取返回

通过 engine.put("name", value),你可以将任意 Java 原始数据类型甚至自定义 JavaBean 对象传递进脚本。在脚本中,这些变量如同全局变量一样可直接访问其属性或方法:

ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("javascript");

// 注入原始数据类型
engine.put("unitPrice", 85.5);
engine.put("quantity", 4);

// 执行脚本计算折扣总价,并在脚本中声明内部变量 finalAmount
String script = "var rawTotal = unitPrice * quantity;\n" +
                "var discount = rawTotal > 200 ? 0.9 : 1.0;\n" +
                "var finalAmount = rawTotal * discount;\n" +
                "finalAmount;"; // 最后一行表达式作为 eval 的返回值

Object result = engine.eval(script);
System.out.println("应付金额:" + result);

// 从引擎上下文反向获取脚本内部计算的中间变量
Object discountVal = engine.get("discount");
System.out.println("命中的折扣系数:" + discountVal);

2. 使用 Invocable 接口高效调用指定函数

如果业务脚本非常长,且需要在多次请求中反复调用同一个计算逻辑,频繁执行整段字符串脚本会造成严重的重复解析损耗。正确的做法是:只在启动时 eval() 一次脚本函数定义,后续通过 Invocable 接口直接传参调用。

import javax.script.Invocable;
import javax.script.NoSuchMethodException;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;

public class InvocableExample {
    public static void main(String[] args) throws ScriptException, NoSuchMethodException {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("javascript");

        // 一次性加载函数定义
        String jsFunction = "function computeTax(income, rate) {\n" +
                            "    if (income <= 5000) return 0;\n" +
                            "    return (income - 5000) * rate;\n" +
                            "}";
        engine.eval(jsFunction);

        // 判断引擎是否支持 Invocable 接口
        if (engine instanceof Invocable) {
            Invocable invocable = (Invocable) engine;
            
            // 传参直接调用已声明的函数
            Object tax1 = invocable.invokeFunction("computeTax", 8000, 0.03);
            Object tax2 = invocable.invokeFunction("computeTax", 15000, 0.1);

            System.out.println("第一笔个税:" + tax1);
            System.out.println("第二笔个税:" + tax2);
        }
    }
}

示例输出:

第一笔个税:90.0
第二笔个税:1000.0

五、常见认知误区与 JDK 版本演进辨析

许多初次接触该特性的开发者常存在一种误区:“Java 只要装了 JDK,就永远内置 JavaScript 引擎,直接调用绝对可用”。这种说法看似合理,因为在长期作为工业基准的 Java 8 中确实开箱即用,但在现代 JDK 环境下直接套用旧教程会导致严重的线上故障。

Java使用ScriptEngine执行JavaScript代码入门
图 4:JDK 平台 JavaScript 引擎演进路线与现代替代方案决策卡片

回顾 Java 的演进历史:

  • Java 6 与 7:内置基于 Mozilla Rhino 的实现。由于纯靠解释和早期字节码转换,性能较弱,仅支持旧版 ECMAScript 3/5 标准。
  • Java 8 到 14:Oracle 全面重写并推出了 Nashorn 引擎(JEP 174)。Nashorn 充分利用了 Java 7 引入的 invokedynamic 字节码指令,将 JS 直接编译为 JVM 字节码运行,性能提升数倍,并支持 ECMAScript 5.1 与部分 ES6 语法。
  • Java 15 及之后(包括 LTS 17 与 21):由于维护庞大的 ECMAScript 新语法跟进成本极高,OpenJDK 社区通过 JEP 372 彻底移除了 Nashorn 引擎及其相关模块。

避坑提示:如果你在 JDK 17 或 JDK 21 环境下直接运行 manager.getEngineByName("javascript"),其返回值必然为 null。如果盲目调用 engine.eval(),会直接触发 NullPointerException。

现代 JDK 17 / 21 下的解决方案

在现代 Java 项目中,继续使用 ScriptEngine 执行 JavaScript 代码有两种主流方案:

方案 1:引入独立维护的 Standalone Nashorn(适合存量代码平滑迁移)
OpenJDK 将 Nashorn 剥离为独立开源项目维护,只需在 Maven 的 pom.xml 中添加如下依赖,原有的 ScriptEngineManager 代码即可一字不改地在 JDK 17/21 上正常运行:

<dependency>
    <groupId>org.openjdk.nashorn</groupId>
    <artifactId>nashorn-core</artifactId>
    <version>15.4</version>
</dependency>

方案 2:引入 GraalVM JavaScript(推荐新项目采用)
如果需要支持现代 ES6+ 语法(如 let、const、箭头函数、Promise、解构赋值等),推荐引入 GraalVM 的 JavaScript 脚本引擎支持:

<dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js</artifactId>
    <version>23.0.0</version>
</dependency>
<dependency>
    <groupId>org.graalvm.js</groupId>
    <artifactId>js-scriptengine</artifactId>
    <version>23.0.0</version>
</dependency>

引入后,使用 manager.getEngineByName("graal.js") 或默认的 "javascript" 均能直接获取现代化的高性能执行引擎。

六、生产环境关键优化与安全注意事项

在将脚本引擎集成到企业生产系统前,还必须关注性能预编译、并发安全以及脚本沙箱三大关键环节:

1. 利用 Compilable 预编译消除解析损耗

在需要高频执行(例如每秒处理上千笔订单校验)的场景下,每次调用 eval(String) 都会重复触发词法解析与语法树构建。大多数现代引擎都实现了 Compilable 接口,可以将脚本预先编译为 CompiledScript 缓存起来:

import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.SimpleBindings;

public class CompiledExample {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("javascript");

        if (engine instanceof Compilable) {
            Compilable compilable = (Compilable) engine;
            // 一次性预编译
            CompiledScript compiled = compilable.compile("price * quantity * rate");

            // 多次快速执行,每次传入不同的参数上下文
            for (int i = 1; i <= 3; i++) {
                SimpleBindings bindings = new SimpleBindings();
                bindings.put("price", 100);
                bindings.put("quantity", i);
                bindings.put("rate", 0.95);

                Object res = compiled.eval(bindings);
                System.out.println("第 " + i + " 次求值结果:" + res);
            }
        }
    }
}

2. 线程安全性与作用域隔离

ScriptEngine 实例本身通常不是线程安全的。如果在多线程环境中共享同一个 ScriptEngine 并直接调用 engine.put(),后一个线程写入的变量会污染前一个线程的运行状态。

推荐实践:保持 CompiledScript 实例单例共享,但在每次执行时传入新创建的 SimpleBindings(或者利用 ThreadLocal<ScriptEngine> 为每个工作线程隔离独立的引擎实例)。

3. 严格防范未授权的 Java 类调用(沙箱限制)

Nashorn 引擎默认允许脚本直接与 Java 原生类交互,例如脚本中可以编写:

// 存在重大安全隐患的调用
var System = Java.type("java.lang.System");
System.exit(0); // 直接导致整台 JVM 进程崩溃退出

如果在系统中允许最终用户输入自定义脚本,必须启用安全过滤机制。在 Nashorn 中,可以通过传递 ClassFilter 参数彻底封锁危险类:

import org.openjdk.nashorn.api.scripting.NashornScriptEngineFactory;
import org.openjdk.nashorn.api.scripting.ClassFilter;

NashornScriptEngineFactory factory = new NashornScriptEngineFactory();
// 自定义类过滤器,拒绝一切不受信的 Java 类加载
ClassFilter classFilter = className -> {
    // 仅允许基础数据容器或完全禁止访问宿主类
    return className.startsWith("com.mycompany.model.safe.");
};

ScriptEngine safeEngine = factory.getScriptEngine(classFilter);

七、全文总结

Java 通过 javax.script 规范建立了一套标准的脚本宿主体系。在日常开发中掌握以下四个基本原则,即可稳妥高效地运用这一能力:

  1. 标准起步:通过 ScriptEngineManager 获取实例,并始终针对返回值执行非空校验。
  2. 双向流转:通过 put 与 Bindings 传递输入参数,通过 Invocable 高效复用并调用脚本具体函数。
  3. 版本感知:认清 JDK 8 与现代 JDK 17/21 的差异,JDK 15+ 务必显式引入 nashorn-core 或 graalvm-js。
  4. 生产加固:面对性能要求使用 Compilable 缓存编译结果,面对并发使用独立 Bindings 隔离作用域,面对不可信脚本务必配置 ClassFilter 沙箱拦截。
作者最新文章
相关文章
更多