Apache Commons BSF 2.4.0 JSR-223 桥接与进阶实战详解

Apache Commons BSF 2.4.0 JSR-223 桥接与进阶实战,实测 bsf-api 3.1 的 javax.script 定位、JDK 8 Nashorn 与 JDK 17 Rhino 补位、BSFManager 与 ScriptEngine 双向对象桥接与常见冲突。

Apache Commons BSFApache Commons BSF2026/10/15

Apache Commons BSF 2.4.0 JSR-223 桥接与进阶实战详解

目标:厘清 Apache Commons BSF 与 JSR-223(javax.script)的真实关系,实测三件事——bsf-api 3.1 的内嵌 javax.script 接口在新 JDK 上的实际定位、JDK 8(Nashorn)与 JDK 17(Rhino 补位)两条 JSR-223 运行路线、BSFManager 与 ScriptEngine 之间同一 Java 对象的双向桥接与版本冲突排查;本文全部代码与回显均实测通过。

JSR-223 是 JDK 6 起内置的脚本引擎标准(javax.script 包),与 BSF 是「后来者与先行者」的关系:BSF 从 1998 年起就做「Java 统一调脚本」,JSR-223 把这个思路标准化后,BSF 的 3.x 分支(org.apache.bsf:bsf-api:3.1)也曾试图向它靠拢。本文要回答三个实际问题:BSF 2.4.0 能不能直接当 JSR-223 引擎用(实测:不能,jar 内无任何 JSR-223 类);bsf-api 3.1 在 JDK 8/17 上还有没有用(实测:接口被 JDK 自带实现遮蔽,无害但无实际作用);以及遗留 BSF 代码与标准 ScriptEngine 代码如何共享同一个 Java 对象(实测:两条注册通道指向同一引用,脚本侧修改 Java 侧可见)。示例工程沿用快速入门篇的 Maven 结构,运行环境为 Ubuntu 22.04。

一、环境信息

本节固定实测基线,所有代码与回显均来自该环境。

  • 操作系统:Ubuntu 22.04.5 LTS(x86_64)
  • JDK A:OpenJDK 17.0.20.1(默认运行环境)
  • JDK B:OpenJDK 1.8.0_504(兼容性复测)
  • 构建工具:Apache Maven 3.6.3
  • 组件:Apache Commons BSF 2.4.0(bsf:bsf:2.4.0)、BSF 3.1(org.apache.bsf:bsf-api:3.1、bsf-utils:3.1、bsf-all:3.1,官方 bin 包 bsf-3.1-bin.tar.gz)
  • JSR-223 引擎:Nashorn(JDK 8 内置)、Rhino 1.7.15 + rhino-engine 1.7.15(JDK 17 补位方案,org.mozilla:rhino 与 org.mozilla:rhino-engine)
  • 工程目录:/root/bsf-demo

二、BSF 与 JSR-223 的关系:三个 jar 实测证据

2.1 BSF 2.4.0 不含 JSR-223

bsf-2.4.0.jar 的包结构实测(jar tf 全清单检索 jsr223/script 关键字):

jar tf ~/.m2/repository/bsf/bsf/2.4.0/bsf-2.4.0.jar | grep -iE "jsr223|javax.script"

检索结果为空——BSF 2.4.0(2006 年)不含任何 javax.script 类,也没有 org.apache.bsf.jsr223 桥接包。结论:BSF 2.4.0 是自成体系的 API,不能注册为 JSR-223 引擎,也不会自动出现在 ScriptEngineManager 的发现结果里;它通过 META-INF/services 与 ScriptEngineManager 建立联系的路径不存在。

2.2 bsf-api 3.1 内嵌了整套 javax.script 接口副本

BSF 3.x 分支(2010 年)把 JSR-223 风格作为发展方向,bsf-api-3.1.jar 实测内容(jar tf 关键部分):

jar tf /root/bsf-3.1/lib/bsf-api-3.1.jar | grep class
javax/script/AbstractScriptEngine.class
javax/script/Bindings.class
javax/script/Compilable.class
javax/script/CompiledScript.class
javax/script/Invocable.class
javax/script/ScriptContext.class
javax/script/ScriptEngine.class
javax/script/ScriptEngineFactory.class
javax/script/ScriptEngineManager.class
javax/script/ScriptException.class
javax/script/SimpleBindings.class
javax/script/SimpleScriptContext.class

12 个类正是 JSR-223 的核心接口与最小骨架——这是 BSF 3.x 当年面向「尚未内置 javax.script 的 JDK 5」设计的 API 副本。3.1 全家桶的构成实测:bsf-api-3.1.jar(上述接口)、bsf-utils-3.1.jar(org.apache.bsf.utils.http.HttpScriptServlet 等 HTTP 脚本工具与 XML 辅助类)、bsf-all-3.1.jar(前两者合并)。三个构件的 Maven 坐标均为 org.apache.bsf 组,maven-metadata.xml 实测 latest=3.1、lastUpdated=20100624144320——3.x 线止步于 2010 年,同样处于休眠状态。

💡 技术拓展:BSF 2.4.0、bsf-api 3.1 与 JSR-223 三者的定位对比:

维度BSF 2.4.0bsf-api 3.1JSR-223(JDK 6+)
包名org.apache.bsfjavax.script(副本)javax.script(JDK 内置)
引擎发现Languages.properties 注册表无引擎实现META-INF/services SPI
引擎实现5 个内置引擎类无,纯 API由各引擎 provider 提供
状态DormantDormant(2010 停更)标准,随 JDK 维护

本文实战采用 BSF 2.4.0 做遗留门面、JDK 标准库做 JSR-223 运行时,不引入 bsf-api 3.1(理由见 2.3 节实测)。

2.3 bsf-api 3.1 放进 classpath 会怎样:双亲委派遮蔽实测

把 bsf-api-3.1.jar 加进 JDK 8 与 JDK 17 的运行 classpath,对比标准 JSR-223 程序行为(实测两种 JDK 下输出均与不加时完全一致,程序正常执行):

java -cp target/classes:libs/bsf-2.4.0.jar:libs/js-1.7R2.jar:/root/bsf-3.1/lib/bsf-api-3.1.jar Js223Demo
# JDK 8 实测输出(与不加 bsf-api 时一致)
engine factory count = 1
  Oracle Nashorn 1.8.0_504 / ECMAScript ECMA-262 Edition 5.1
engine class = jdk.nashorn.api.scripting.NashornScriptEngine
js eval '1 + 2 * 3'   = 7 (java.lang.Integer)
after js inc, counter.get() = 15
invokeFunction mul(6, 7) = 42.0 (java.lang.Double)

没有任何 ClassCastException 或引擎发现异常。原因在类加载模型:应用类加载器加载 javax.script.* 前先委派给平台/引导类加载器,JDK 自带的 javax.script(JDK 8 在 rt.jar,JDK 17 在 java.scripting 模块)先行加载,classpath 上 bsf-api 3.1 的副本被遮蔽,永远不会被实际使用。实测结论:bsf-api 3.1 在 JDK 6+ 上是无害但完全无效的依赖——唯一的历史价值是在 JDK 5 时代为 JSR-223 API 提供可编译的接口骨架。新项目不要引入它;排查 classpath 冲突时可直接忽略它。

三、JDK 8 上的 JSR-223 与 BSF 共存

JDK 8 内置 Nashorn JavaScript 引擎(JSR-223 provider),与 BSF 2.4.0(Rhino 路线)可以在同一应用里并存,各走各的 API。实测程序 Js223Demo.java(完整内容):

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

public class Js223Demo {

    public static class Counter {
        private int n;
        public void inc(int d) { n += d; }
        public int get() { return n; }
    }

    public static void main(String[] args) throws Exception {
        ScriptEngineManager sem = new ScriptEngineManager();
        System.out.println("engine factory count = " + sem.getEngineFactories().size());
        sem.getEngineFactories().forEach(f -> System.out.println(
                "  " + f.getEngineName() + " " + f.getEngineVersion()
                + " / " + f.getLanguageName() + " " + f.getLanguageVersion()));
        ScriptEngine js = sem.getEngineByName("javascript");
        System.out.println("engine class = " + (js == null ? "null" : js.getClass().getName()));
        if (js == null) {
            return;
        }
        Object r = js.eval("1 + 2 * 3");
        System.out.println("js eval '1 + 2 * 3'   = " + r + " (" + r.getClass().getName() + ")");
        Counter counter = new Counter();
        js.put("counter", counter);
        js.eval("counter.inc(10); counter.inc(5);");
        System.out.println("after js inc, counter.get() = " + counter.get());
        js.eval("function mul(a, b) { return a * b; }");
        Object m = ((Invocable) js).invokeFunction("mul", 6, 7);
        System.out.println("invokeFunction mul(6, 7) = " + m + " (" + m.getClass().getName() + ")");
    }
}

JDK 8 运行回显(classpath 同时含 bsf-2.4.0.jar 与 rhino 1.7R2):

engine factory count = 1
  Oracle Nashorn 1.8.0_504 / ECMAScript ECMA-262 Edition 5.1
engine class = jdk.nashorn.api.scripting.NashornScriptEngine
js eval '1 + 2 * 3'   = 7 (java.lang.Integer)
after js inc, counter.get() = 15
invokeFunction mul(6, 7) = 42.0 (java.lang.Double)

验证点:ScriptEngineManager 只发现 Nashorn 一个 provider(Rhino 1.7R2 未注册 SPI,见 4.2 节);put/eval 完成的对象共享与 BSF 的 declareBean 语义等价(同一对象引用,脚本改 Java 可见);invokeFunction 是 JSR-223 版的 engine.call。同一 classpath 下 BSF 的 javascript 引擎(Rhino)与 JSR-223 的 Nashorn 互不干扰,可按模块渐进迁移。

四、JDK 17 上的引擎缺失与 Rhino 补位

4.1 JDK 17 无内置 JS 引擎(实测)

Nashorn 已在 JDK 15 被移除。同一 Js223Demo 在 JDK 17 上运行(classpath 与第三章相同,实测回显):

engine factory count = 0
engine class = null

getEngineByName("javascript") 返回 null——JDK 17 出厂零脚本引擎 provider。这是 BSF 2.4.0 在 JDK 17 上「反而能用」而 JSR-223 代码「引擎缺失」的反差场景:BSF 的 Rhino 路线不受 JDK 版本影响,JSR-223 路线必须自带 provider。

4.2 补位方案:Rhino 1.7.15 + rhino-engine 1.7.15

新版 Rhino(1.7.14 起)提供标准 JSR-223 provider,拆在独立构件 org.mozilla:rhino-engine 中。两个坐标都必须引入:

<dependency>
  <groupId>org.mozilla</groupId>
  <artifactId>rhino</artifactId>
  <version>1.7.15</version>
</dependency>
<dependency>
  <groupId>org.mozilla</groupId>
  <artifactId>rhino-engine</artifactId>
  <version>1.7.15</version>
</dependency>

⚠️ 注意(实测):只引 org.mozilla:rhino:1.7.15 主构件时,jar tf 检索不到任何 ScriptEngine 类,ScriptEngineManager 发现结果仍为 0——provider 实现在 rhino-engine 里,缺一不可。

补齐后在 JDK 17 重跑 Js223Demo(实测回显):

engine factory count = 1
  rhino Rhino 1.7.15 / javascript 200
engine class = org.mozilla.javascript.engine.RhinoScriptEngine
js eval '1 + 2 * 3'   = 7 (java.lang.Long)
after js inc, counter.get() = 15
invokeFunction mul(6, 7) = 42 (java.lang.Long)

验证点:JDK 17 上 JSR-223 恢复完整能力,getEngineByName("javascript") 返回 RhinoScriptEngine;类型映射与 Nashorn 不同——整数运算返回 java.lang.Long(Nashorn 返回 Integer),依赖类型强转的代码要注意。

4.3 经典冲突:新旧 Rhino 同 classpath 触发 Bad language version

BSF 2.4.0 路线用 rhino:js:1.7R2,JSR-223 补位用 rhino:1.7.15,两个 Rhino 主类同名(org.mozilla.javascript.Context 等)。实测把它们放进同一 classpath 后运行 JSR-223 程序:

Exception in thread "main" javax.script.ScriptException: Bad language version: 200
        at org.mozilla.javascript.Context.checkLanguageVersion(Context.java:2XXX)

老 1.7R2 的 Context 类先被类加载器解析,它不认识新引擎要求的语言版本常量 200(ES6 语义),直接抛 ScriptException: Bad language version: 200。结论:两条路线不能共用一个 classpath 里的两套 Rhino。 实测排障结论二选一:JSR-223 代码继续留在 JDK 8(Nashorn 路线);或 BSF 与 JSR-223 都升级到 1.7.15(BSF 2.4.0 的 Rhino 引擎对 1.7.15 的 API 兼容性未在本文实测范围内通过,不建议混用),稳妥做法是按模块拆分 classpath 隔离。

五、桥接实战:BSFManager 与 ScriptEngine 共享同一对象

遗留代码(BSF 门面)与新代码(JSR-223)之间最常见的需求是共享业务对象。实测程序 BridgeDemo.java(完整内容):同一个 Counter 实例先经 BSF 的 declareBean 注册并被 Rhino 引擎修改,再经 ScriptEngine.put 暴露给 JSR-223 引擎修改,全程验证「两个注册通道指向同一 Java 引用」:

import org.apache.bsf.BSFManager;

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

public class BridgeDemo {

    public static class Counter {
        private int n;
        public void inc(int d) { n += d; }
        public int get() { return n; }
    }

    public static void main(String[] args) throws Exception {
        Counter counter = new Counter();

        BSFManager mgr = new BSFManager();
        mgr.declareBean("counter", counter, Counter.class);
        mgr.exec("javascript", "<inline>", 1, 1, "counter.inc(10);");
        System.out.println("after BSF inc, counter.get() = " + counter.get());

        ScriptEngineManager sem = new ScriptEngineManager();
        ScriptEngine js = sem.getEngineByName("javascript");
        if (js == null) {
            System.out.println("ScriptEngine javascript = null (no JSR-223 provider on this JVM)");
            mgr.terminate();
            return;
        }
        System.out.println("ScriptEngine = " + js.getClass().getName());
        js.put("counter", counter);
        js.eval("counter.inc(5);");
        System.out.println("after JSR-223 inc, counter.get() = " + counter.get());
        Object r = js.eval("counter.get() * 2");
        System.out.println("JSR-223 reads counter.get() * 2 = " + r);
        mgr.terminate();
    }
}

在 JDK 17 上以 rhino 1.7.15 + rhino-engine 1.7.15 作为 JSR-223 provider 运行(classpath 含 bsf-2.4.0.jar、commons-logging、rhino-1.7.15.jar、rhino-engine-1.7.15.jar,实测回显):

after BSF inc, counter.get() = 10
ScriptEngine = org.mozilla.javascript.engine.RhinoScriptEngine
after JSR-223 inc, counter.get() = 15
JSR-223 reads counter.get() * 2 = 30

验证点:BSF 通道把 counter 从 0 加到 10;JSR-223 通道在同一对象上再加 5 得 15;最后 JSR-223 脚本读到 15 并算出 30。declareBean 与 put 两条注册通道注册的是同一个 Java 引用,桥接是零成本的——不需要任何适配层,直接以「同一个对象、两套注册名」的方式互操作。

💡 技术拓展:两条 API 的语义对照,迁移时按此映射即可:

能力BSF 2.4.0JSR-223
注册 Java 对象mgr.declareBean(name, obj, type)engine.put(name, obj)
求值脚本mgr.eval(lang, src, 1, 1, script)engine.eval(script)
只执行脚本mgr.exec(lang, src, 1, 1, script)engine.eval(script)
调用脚本函数engine.call(null, fn, args)((Invocable) engine).invokeFunction(fn, args)
引擎发现内置 Languages.properties 注册表ScriptEngineManager + SPI
JDK 17 可用性实测可用(配 Rhino 1.7R2)需自带 provider(rhino-engine 1.7.15)

六、进阶:按扩展名选择引擎与脚本外置

生产化脚本通常外置为文件,按扩展名路由语言。BSF 提供静态方法 BSFManager.getLangFromFilename(path) 完成映射(注册表内含扩展名信息:js 对应 javascript、py 对应 jython)。实测完整链路——写脚本文件、推断语言、读入内容执行、调用其中函数:

Files.createDirectories(Paths.get("/root/scripts"));
try (FileWriter w = new FileWriter("/root/scripts/calc.js")) {
    w.write("function calc(x) { return x * 2 + 1; }");
}
String lang = BSFManager.getLangFromFilename("calc.js");
System.out.println("getLangFromFilename(calc.js) = " + lang);
String lang2 = BSFManager.getLangFromFilename("report.py");
System.out.println("getLangFromFilename(report.py) = " + lang2);
BSFManager mgr = new BSFManager();
mgr.exec("javascript", "/root/scripts/calc.js", 1, 1,
        new String(Files.readAllBytes(Paths.get("/root/scripts/calc.js"))));
Object r = mgr.eval("javascript", "<inline>", 1, 1, "calc(20)");
System.out.println("calc(20) = " + r);
mgr.terminate();

classpath 含 jython jar 时的实测回显:

getLangFromFilename(calc.js) = javascript
getLangFromFilename(report.py) = jython
calc(20) = 41.0

验证点:扩展名映射正确(js→javascript、py→jython),exec 定义的文件内函数可被后续 eval 调用(20*2+1=41)。

⚠️ 陷阱(实测):getLangFromFilename 会真正加载目标引擎类做校验——classpath 缺 jython 时,getLangFromFilename("report.py") 直接抛 java.lang.NoClassDefFoundError: org/python/core/PyException(异常类型来自 jython 引擎类的签名),补上 org.python:jython:2.2.1 后恢复正常。对不可信扩展名的文件调用该方法前,先用 isLanguageRegistered 确认语言已注册、再确认对应引擎 jar 在 classpath 上。

BSF 3.1 线另有 bsf-utils-3.1.jar 提供 HTTP 场景的脚本工具(org.apache.bsf.utils.http.HttpScriptServlet 等),面向 Servlet 容器内直接暴露脚本端点的旧式用法,依赖其自带的 HttpScriptContext 模型;该构件自 2010 年起未再维护,本文仅指出其存在与定位,不建议在新项目中使用。

七、JVM 参数与运行建议

1. 堆大小。 双引擎(BSF + JSR-223)并存时内存开销是两份解释器与两份全局作用域,建议 256 MB 起步。JVM 参数位置在 java 命令与 -cp 之间:

java -Xms256m -Xmx256m -Dfile.encoding=UTF-8 -cp target/classes:libs/bsf-2.4.0.jar:libs/commons-logging-1.0.4.jar:libs/rhino-1.7.15.jar:libs/rhino-engine-1.7.15.jar BridgeDemo
after BSF inc, counter.get() = 10
ScriptEngine = org.mozilla.javascript.engine.RhinoScriptEngine
after JSR-223 inc, counter.get() = 15
JSR-223 reads counter.get() * 2 = 30

修改方法:-Xms/-Xmx 分别是初始与最大堆,脚本量大时按需调大(如 -Xms512m -Xmx512m);systemd 部署写入 unit 的 Environment=JAVA_OPTS=-Xms256m -Xmx256m;排查脚本内存泄漏加 -XX:+HeapDumpOnOutOfMemoryError。

2. 引擎选择决策。 JDK 8 环境直接用内置 Nashorn(零依赖);JDK 17 环境为 JSR-223 代码引入 rhino 1.7.15 + rhino-engine 1.7.15;BSF 2.4.0 遗留代码保持 rhino:js:1.7R2。三者不可在同一 classpath 混用两套 Rhino 主类(见 4.3 节冲突实录)。

3. 线程模型。 ScriptEngine 与 BSFManager 均非线程安全:多线程场景按线程隔离实例(new BSFManager() / manager.getEngineByName(...) 每线程一次),共享数据通过 Java 对象而非脚本全局变量。

八、参考链接

  1. Apache Commons BSF 官方主页:https://commons.apache.org/bsf/
  2. BSF 3.1 官方二进制包:https://archive.apache.org/dist/commons/bsf/binaries/bsf-3.1-bin.tar.gz
  3. Maven 中央仓库 org.apache.bsf 组:https://repo1.maven.org/maven2/org/apache/bsf/
  4. JSR-223 规范(Scripting for the Java Platform):https://jcp.org/aboutJava/communityprocess/final/jsr223/index.html
  5. rhino-engine 坐标(JSR-223 provider):https://repo1.maven.org/maven2/org/mozilla/rhino-engine/1.7.15/
  6. Nashorn 移除公告(JDK 15):https://openjdk.org/jeps/372