Apache Commons BSF 2.4.0 快速入门与集成示例
Apache Commons BSF 2.4.0 快速入门,含 Maven 工程搭建、JavaScript/BeanShell/Jython 多引擎集成、对象双向共享、jar 直跑、引擎依赖速查与 JDK 17/8 双实测结论。
Apache Commons BSF 2.4.0 快速入门与集成示例
目标:以 Apache Commons BSF 2.4.0 为例,从零完成工程搭建与最小集成,跑通 JavaScript(Rhino)、BeanShell、Jython 三个脚本引擎的表达式求值、函数回调与 Java 对象双向共享,并给出 JDK 17 与 JDK 8 的双实测兼容性结论;本文全部代码与回显均实测通过。
Apache Commons BSF(Bean Scripting Framework)的核心价值:给 Java 应用一个统一的脚本执行门面——不管底层是 JavaScript、Python 还是 BeanShell,调用方都面对同一套 BSFManager/BSFEngine 接口。它同时解决了三件事:按语言名统一求值入口(eval/exec)、把 Java 对象暴露给脚本(declareBean)、把脚本函数暴露给 Java(engine.call)。BSF 目前为 Dormant(休眠)状态,最新稳定版即本文实测的 2.4.0(2006-10-06 构建),坐标 bsf:bsf:2.4.0。本文在一台 Ubuntu 22.04 服务器(OpenJDK 17 + Maven 3.6.3)上完成全部验证,环境与安装步骤见专栏安装篇。
一、环境信息
本节固定实测基线,所有代码与回显均来自该环境。
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- JDK:OpenJDK 17.0.20.1(另在 OpenJDK 1.8.0_504 上复测,见第七章)
- 构建工具:Apache Maven 3.6.3
- 组件:Apache Commons BSF 2.4.0(
bsf:bsf:2.4.0,传递依赖commons-logging:1.0.4) - 脚本引擎:Rhino 1.7R2(JavaScript)、BeanShell 2.0b4、Jython 2.2.1
- 工程目录:
/root/bsf-demo - 版本自证:
BSFManager.getVersion()实测返回240.20061006
安装前置(JDK/Maven/依赖引入的完整步骤)见专栏《Ubuntu 22.04 安装 Apache Commons BSF 指南》,本文从「依赖已在 pom 中声明」直接开始。
二、十分钟最小可用
本节用一个可运行的工程把 BSF 的核心闭环跑起来:pom.xml + 一个 BsfVerify.java + 一条编译命令 + 一条运行命令。
2.1 工程与依赖
工程采用标准 Maven 布局(目录 /root/bsf-demo),pom.xml 关键内容如下(完整文件见安装篇 4.4 节):
<dependencies>
<dependency>
<groupId>bsf</groupId>
<artifactId>bsf</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>rhino</groupId>
<artifactId>js</artifactId>
<version>1.7R2</version>
</dependency>
</dependencies>
依赖关系实测(mvn -B dependency:tree 回显):
[INFO] +- bsf:bsf:jar:2.4.0:compile
[INFO] | \- commons-logging:commons-logging:jar:1.0.4:compile
[INFO] \- rhino:js:jar:1.7R2:compile
要点:BSF 本体自带引擎注册表(语言名 → 引擎类),但引擎实现类不在 bsf.jar 里,运行期必须额外提供目标语言的引擎 jar。JavaScript 对应 rhino:js:1.7R2,其他语言的对应关系见第六章速查表。
2.2 最小验证代码
src/main/java/BsfVerify.java(完整内容):
import org.apache.bsf.BSFEngine;
import org.apache.bsf.BSFManager;
public class BsfVerify {
public static void main(String[] args) throws Exception {
System.out.println("BSF version = " + BSFManager.getVersion());
BSFManager mgr = new BSFManager();
Object r1 = mgr.eval("javascript", "<inline>", 1, 1, "1 + 2 * 3");
System.out.println("eval '1 + 2 * 3' = " + r1 + " (" + r1.getClass().getName() + ")");
Object r2 = mgr.eval("javascript", "<inline>", 1, 1, "'Apache BSF'.toUpperCase()");
System.out.println("eval toUpperCase = " + r2);
mgr.declareBean("name", "Ubuntu 22.04", String.class);
Object r3 = mgr.eval("javascript", "<inline>", 1, 1, "'BSF on ' + name");
System.out.println("JS reads name = " + r3);
mgr.exec("javascript", "<inline>", 1, 1, "function add(a, b) { return a + b; }");
BSFEngine engine = mgr.loadScriptingEngine("javascript");
Object r4 = engine.call(null, "add", new Object[]{7, 35});
System.out.println("engine.call add = " + r4);
mgr.terminate();
System.out.println("BsfVerify OK");
}
}
逐行拆解四个核心动作:
new BSFManager():创建脚本管理器,一个实例持有一组已加载的引擎与一个对象注册表,线程内复用、用完terminate()释放。mgr.eval("javascript", "<inline>", 1, 1, script):求值脚本并取返回值。第 1 参语言名,第 2 参来源名(仅用于错误信息,可写<inline>),第 3/4 参行列号(从 1 起),第 5 参是脚本内容(String或Reader)。mgr.declareBean("name", value, type):把 Java 对象注册为脚本全局变量,脚本内直接引用变量名name。engine.call(null, "add", args):调用脚本中已定义的函数,第 1 参传null表示在全局对象上调用。注意函数调用走BSFEngine.call而不是BSFManager.apply,原因见第四章。
2.3 编译与运行
cd /root/bsf-demo && mvn -q compile && echo "EXIT=$?"
EXIT=0
R=~/.m2/repository
java -Dfile.encoding=UTF-8 -cp target/classes:$R/bsf/bsf/2.4.0/bsf-2.4.0.jar:$R/commons-logging/commons-logging/1.0.4/commons-logging-1.0.4.jar:$R/rhino/js/1.7R2/js-1.7R2.jar BsfVerify
BSF version = 240.20061006
eval '1 + 2 * 3' = 7 (java.lang.Integer)
eval toUpperCase = APACHE BSF
JS reads name = BSF on Ubuntu 22.04
engine.call add = 42.0
BsfVerify OK
验证点:四项功能全部符合预期——求值返回 Integer、字符串方法可用、Java 对象注入后脚本可读、脚本函数回调返回 Double 42.0。至此 BSF 集成最小闭环完成。
💡 技术拓展:除了 BSF 的
BSFManager门面,Java 生态还有两条脚本集成路线——JSR-223(javax.script,JDK 6 起内置)与各引擎自带的直接 API(如直接new Context()用 Rhino)。三者对比:
| 路线 | 入口 | 语言覆盖 | 状态 |
|---|---|---|---|
| BSF(本文) | BSFManager | 注册表内 23 个语言名,2.4.0 内置 5 个引擎实现 | Dormant,经典遗留系统常用 |
| JSR-223 | ScriptEngineManager | 依 provider 而定(Nashorn/Rhino/Jython 等) | JDK 标准,新项目首选 |
| 引擎直接 API | Rhino Context 等 | 单一语言 | 灵活但与引擎绑定 |
本文所用为 BSF 路线,与 JSR-223 的桥接互操作见专栏第 5 篇。
三、第一个多语言程序
本节把三个引擎(JavaScript、BeanShell、Jython)放进同一个程序,验证「同一套 API、多语言运行」的 BSF 核心卖点,同时完成 Java 对象与三种脚本语言的双向操作。
3.1 补齐引擎依赖
在 2.1 节依赖基础上追加 BeanShell 与 Jython:
<dependency>
<groupId>org.beanshell</groupId>
<artifactId>bsh</artifactId>
<version>2.0b4</version>
</dependency>
<dependency>
<groupId>org.python</groupId>
<artifactId>jython</artifactId>
<version>2.2.1</version>
</dependency>
版本说明:org.beanshell:bsh:2.0b4 自带 BSF 引擎适配类 bsh.util.BeanShellBSFEngine(与 BSF 内置注册表对接);org.python:jython:2.2.1 是 Jython 2.2.1 的中央仓库坐标(注意不是 jython:jython,后者只到 2.2-beta1),实测在 JDK 8 与 JDK 17 上均可运行。
3.2 多语言验证代码
src/main/java/BsfMulti.java(完整内容):
import org.apache.bsf.BSFManager;
import java.util.Arrays;
import java.util.Vector;
public class BsfMulti {
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 {
BSFManager mgr = new BSFManager();
Object r1 = mgr.eval("javascript", "<inline>", 1, 1, "1 + 2 * 3");
System.out.println("javascript eval = " + r1 + " (" + r1.getClass().getName() + ")");
Object r2 = mgr.eval("beanshell", "<inline>", 1, 1, "1 + 2 * 3");
System.out.println("beanshell eval = " + r2 + " (" + r2.getClass().getName() + ")");
Object r3 = mgr.eval("jython", "<inline>", 1, 1, "sum(range(1, 101))");
System.out.println("jython eval = " + r3 + " (" + r3.getClass().getName() + ")");
Counter counter = new Counter();
mgr.declareBean("counter", counter, Counter.class);
mgr.exec("javascript", "<inline>", 1, 1, "counter.inc(10); counter.inc(32);");
System.out.println("JS inc 10+32 = " + counter.get());
Counter counter2 = new Counter();
mgr.declareBean("counter", counter2, Counter.class);
mgr.exec("beanshell", "<inline>", 1, 1, "counter.inc(40); counter.inc(2);");
System.out.println("bsh inc 40+2 = " + counter2.get());
Counter counter3 = new Counter();
mgr.declareBean("counter", counter3, Counter.class);
mgr.exec("jython", "<inline>", 1, 1, "counter.inc(50)\ncounter.inc(2)");
System.out.println("jython inc 50+2 = " + counter3.get());
mgr.exec("javascript", "<inline>", 1, 1, "function add(a, b) { return a + b; }");
Object r4 = mgr.loadScriptingEngine("javascript").call(null, "add", new Object[]{3, 4});
System.out.println("JS add(3, 4) = " + r4 + " (" + r4.getClass().getName() + ")");
mgr.terminate();
}
}
代码要点:三个引擎用同一 BSFManager 调度,语言差异只在第 1 个参数;declareBean 注入的 Java 对象在三种语言里都能直接调用方法(JavaScript 用分号、BeanShell 是 Java 语法、Jython 用换行缩进);Jython 的多行脚本用 \n 传给 exec。
3.3 运行与验证
cd /root/bsf-demo && mvn -q compile
R=~/.m2/repository
java -Dfile.encoding=UTF-8 -Dpython.cachedir=/tmp/jython-cache -cp target/classes:$R/bsf/bsf/2.4.0/bsf-2.4.0.jar:$R/commons-logging/commons-logging/1.0.4/commons-logging-1.0.4.jar:$R/rhino/js/1.7R2/js-1.7R2.jar:$R/org/beanshell/bsh/2.0b4/bsh-2.0b4.jar:$R/org/python/jython/2.2.1/jython-2.2.1.jar BsfMulti
javascript eval = 7 (java.lang.Integer)
beanshell eval = 7 (java.lang.Integer)
jython eval = 5050 (org.python.core.PyInteger)
JS inc 10+32 = 42
bsh inc 40+2 = 42
jython inc 50+2 = 52
JS add(3, 4) = 7.0 (java.lang.Double)
结果解读:Rhino 与 BeanShell 的算术求值返回 Integer,Jython 返回自己的 PyInteger(BSF 统一了入口、保留了各引擎的原生返回类型);Java 对象在三种脚本里均可直接调用 inc() 修改内部状态,Java 侧 get() 读到的就是脚本改过的值,双向共享成立。-Dpython.cachedir=/tmp/jython-cache 是给 Jython 指定包缓存目录的 JVM 参数,缺省时会在 jar 同目录建缓存,打印 can't create package cache dir 警告(不影响正确性,见 FAQ)。
四、集成常见坑(实测实录)
本节收录集成过程中真实踩到的四个坑,全部附实测现象与修复方式。
坑 1:eval 第 5 参传 null 直接 NPE。 BSF 2.4.0 各引擎在求值前直接对第 5 参调用 toString(),传 null 抛 java.lang.NullPointerException(实测栈指向 JavaScriptEngine.eval 第 125 行)。脚本内容必须作为第 5 参传入;第 2 参 source 只是来源名,不要误把脚本放第 2 参。
坑 2:BSFManager.apply 对函数调用「只执行函数体、忽略参数」。 实测 mgr.apply("javascript", "<inline>", 1, 1, "function add(a, b) { return a + b; }", args, names) 返回 org.mozilla.javascript.Undefined——BSF 2.4.0 的 BSFEngineImpl.apply 默认实现只把函数体当脚本求值,参数与函数名不参与调用。Rhino 与 BeanShell 引擎均未覆写 apply。正确姿势:mgr.loadScriptingEngine("javascript") 取引擎对象后调 engine.call(null, "add", new Object[]{7, 35}),实测返回 42.0。
坑 3:BeanShell 的 apply 触发 ArrayStoreException。 即使给 bsh.util.BeanShellBSFEngine 传正确参数,mgr.apply 也在引擎内部 Vector.copyInto(String[]) 处抛 ArrayStoreException(bsh 2.0b4 引擎代码的真实缺陷,实测复现)。绕过方式同坑 2:改用 engine.call,实测 call(null, "add", new Object[]{3, 4}) 返回 Integer 7。
坑 4:JDK 17 编译的类在 JDK 8 上运行报版本错误。 实测把 JDK 17 编译产物放到 JDK 8 运行:
Error: UnsupportedClassVersionError: BsfVerify has been compiled by a more recent version of the Java Runtime (class file version 61.0), this version of the Java Runtime only recognizes class file versions up to 52.0
61.0 是 JDK 17、52.0 是 JDK 8。修复:给目标 JDK 8 编译时把 Maven 属性改为 <maven.compiler.release>8</maven.compiler.release>,或直接在 JDK 8 下重编。bsf.jar 自身是 major=46(Java 1.2)字节码,不参与此问题。
五、无构建环境:官方 bin 包集成
没有 Maven 的服务器上,用官方 bsf-bin-2.4.0.tar.gz 加中央仓库补齐 jar 也能完整集成,编译运行两条命令搞定(下载与 sha256 校验步骤见专栏安装篇,此处从解压后开始)。
mkdir -p /root/jar-run && cd /root/jar-run
cp /root/bsf-2.4.0/lib/bsf.jar .
cp /root/bsf-demo/src/main/java/BsfVerify.java .
curl -sO --max-time 60 https://repo1.maven.org/maven2/rhino/js/1.7R2/js-1.7R2.jar
curl -sO --max-time 60 https://repo1.maven.org/maven2/commons-logging/commons-logging/1.0.4/commons-logging-1.0.4.jar
mkdir -p out
javac -encoding UTF-8 -cp bsf.jar -d out BsfVerify.java
java -Dfile.encoding=UTF-8 -cp out:bsf.jar:js-1.7R2.jar:commons-logging-1.0.4.jar BsfVerify
BSF version = 240.20061006
eval '1 + 2 * 3' = 7 (java.lang.Integer)
eval toUpperCase = APACHE BSF
JS reads name = BSF on Ubuntu 22.04
engine.call add = 42.0
BsfVerify OK
验证点:与 Maven 方案输出完全一致。jar 直跑场景 classpath 的完整清单就是「bsf.jar + 语言引擎 jar + commons-logging-1.0.4.jar + 编译输出目录」,多一个不多、少一个报 NoClassDefFoundError。
六、脚本引擎依赖速查表
BSF 2.4.0 内置注册表(jar 内 org/apache/bsf/Languages.properties)共登记 23 个语言名,实际可用的引擎取决于 classpath 上有没有对应实现。实测可跑通的四个引擎与依赖坐标如下:
| 语言名 | 引擎类(bsf.jar 内) | 引擎实现依赖(Maven 坐标) | 实测结论 |
|---|---|---|---|
javascript | org.apache.bsf.engines.javascript.JavaScriptEngine | rhino:js:1.7R2 | 表达式/函数/对象共享全通,推荐 |
beanshell | bsh.util.BeanShellBSFEngine(由 bsh jar 提供) | org.beanshell:bsh:2.0b4 | eval/exec/call 全通;apply 有缺陷 |
jython | org.apache.bsf.engines.jython.JythonEngine | org.python:jython:2.2.1 | eval/exec/对象共享全通;建议设 python.cachedir |
xslt | org.apache.bsf.engines.xslt.XSLTEngine | xalan:xalan:2.7.2 | 输入输出可通;参数传递在 2.7.2 下失效(见第 4 篇) |
注册表里其余语言名(如 jacl、netrexx、groovy、ruby 等)仅登记了历史引擎类名,对应实现依赖(Jacl、NetRexx、Groovy 1.x、JRuby 0.9 等)均已停止维护,未在本文实测范围内,生产上不建议引入。
💡 技术拓展:同为「Java 调脚本」需求,引擎选择的关键差异:
| 引擎 | 语言 | 成熟度 | 与 BSF 2.4.0 的适配 |
|---|---|---|---|
| Rhino 1.7R2 | JavaScript | 高(浏览器级引擎) | 内置引擎类直接适配,实测首选 |
| BeanShell 2.0b4 | Java 语法脚本 | 中(轻量解释器) | 引擎类在 bsh jar 内,apply 有缺陷 |
| Jython 2.2.1 | Python | 中(Python 2.2 语法) | 引擎类适配完整,需配 cachedir |
本文集成示例以 JavaScript(Rhino)为主线,因其在依赖体积、稳定性与功能完整度上表现最好。
七、JDK 兼容性实测结论
BSF 2.4.0 是 2006 年的产物,能否在新 JDK 上运行是选型的第一疑问。本节给出一台机器上的双 JDK 实测。
实测 1:字节码版本。 bsf-2.4.0.jar 主版本号 major=46,即 Java 1.2 字节码,属于所有现代 JDK 都兼容的最低档。
实测 2:OpenJDK 17.0.20.1。 第二章、第三章全部示例在该 JDK 上编译运行通过(回显如各节所示),包括 Rhino 1.7R2、BeanShell 2.0b4、Jython 2.2.1 三个引擎的求值、函数调用与对象共享。
实测 3:OpenJDK 1.8.0_504。 同一套源码用 JDK 8 重新编译(javac -cp "libs/*" -d classes8)后运行第三章的 BsfMulti,实测输出与 JDK 17 完全一致:
javascript eval = 7 (java.lang.Integer)
beanshell eval = 7 (java.lang.Integer)
jython eval = 5050 (org.python.core.PyInteger)
JS inc 10+32 = 42
bsh inc 40+2 = 42
jython inc 50+2 = 52
JS add(3, 4) = 7.0 (java.lang.Double)
结论: BSF 2.4.0 + Rhino 1.7R2/BeanShell 2.0b4/Jython 2.2.1 组合在 JDK 8 与 JDK 17 上均可正常工作;跨 JDK 迁移时唯一要管住的是「用哪个 JDK 编译你的业务代码」(见坑 4)。JDK 8 上另有内置的 Nashorn 引擎可走 JSR-223 路线(见专栏第 5 篇的实测对比)。
八、参考链接
- Apache Commons BSF 官方主页:https://commons.apache.org/bsf/
- BSF 2.4.0 官方二进制包:https://archive.apache.org/dist/commons/bsf/binaries/bsf-bin-2.4.0.tar.gz
- Maven 中央仓库 bsf:bsf:2.4.0:https://repo1.maven.org/maven2/bsf/bsf/2.4.0/
- Rhino 1.7R2 坐标:https://repo1.maven.org/maven2/rhino/js/1.7R2/
- BeanShell 坐标:https://repo1.maven.org/maven2/org/beanshell/bsh/2.0b4/
- Jython 2.2.1 坐标:https://repo1.maven.org/maven2/org/python/jython/2.2.1/