Apache Commons BSF 2.4.0 常用 API 与脚本引擎详解
Apache Commons BSF 2.4.0 常用 API 与脚本引擎详解,逐个实测 BSFManager/BSFEngine 核心方法、23 个内置语言注册、Rhino/BeanShell/Jython/XSLT 四引擎行为与真实陷阱。
Apache Commons BSF 2.4.0 常用 API 与脚本引擎详解
目标:系统拆解 Apache Commons BSF 2.4.0 的 API 面板——
BSFManager的 23 个内置语言注册与核心门面方法、BSFEngine的引擎级调用、对象注册表全生命周期,并逐个实测 JavaScript(Rhino 1.7R2)、BeanShell 2.0b4、Jython 2.2.1、XSLT(Xalan 2.7.2)四个引擎的行为差异与真实陷阱;本文全部代码与回显均实测通过。
BSF 2.4.0 的 API 面只有两个主角:BSFManager(门面,管引擎生命周期、脚本执行、对象注册)与 BSFEngine(单语言引擎句柄,管函数调用等引擎级操作)。理解它的关键在三点:一是内置注册表决定了「语言名 → 引擎类」的映射与懒加载行为;二是 eval/exec 的参数语义(脚本内容在第 5 参);三是函数调用必须走 BSFEngine.call 而非门面的 apply。本文示例基于 Maven 工程(依赖引入与工程搭建见专栏快速入门篇),运行环境为 Ubuntu 22.04 + OpenJDK 17.0.20.1,与 JDK 8 的兼容性结论见快速入门篇第七章。
一、环境信息
本节固定实测基线,所有代码与回显均来自该环境。
- 操作系统:Ubuntu 22.04.5 LTS(x86_64)
- JDK:OpenJDK 17.0.20.1
- 构建工具:Apache Maven 3.6.3
- 组件:Apache Commons BSF 2.4.0(
bsf:bsf:2.4.0+ 传递依赖commons-logging:1.0.4) - 引擎实现:Rhino 1.7R2、BeanShell 2.0b4(
org.beanshell:bsh)、Jython 2.2.1(org.python:jython)、Xalan 2.7.2(xalan:xalan) - 版本自证:
BSFManager.getVersion()实测返回240.20061006;bsf-2.4.0.jar文件大小 113369 字节、字节码主版本 major=46
二、API 总览与内置语言注册表
2.1 BSFManager 的方法面板
BSFManager 是全部操作的入口。用 javap 实测其公开方法(回显):
javap -cp ~/.m2/repository/bsf/bsf/2.4.0/bsf-2.4.0.jar org.apache.bsf.BSFManager
public class org.apache.bsf.BSFManager {
public static final java.lang.String VERSION;
public org.apache.bsf.BSFManager();
public static java.lang.String getVersion();
public static java.lang.String getLangFromFilename(java.lang.String) throws org.apache.bsf.BSFException;
public static boolean isLanguageRegistered(java.lang.String);
public static void registerScriptingEngine(java.lang.String, java.lang.String, java.lang.String[]);
public org.apache.bsf.BSFEngine loadScriptingEngine(java.lang.String) throws org.apache.bsf.BSFException;
public java.lang.Object eval(java.lang.String, java.lang.String, int, int, java.lang.Object) throws org.apache.bsf.BSFException;
public void exec(java.lang.String, java.lang.String, int, int, java.lang.Object) throws org.apache.bsf.BSFException;
public void iexec(java.lang.String, java.lang.String, int, int, java.lang.Object) throws org.apache.bsf.BSFException;
public java.lang.Object apply(java.lang.String, java.lang.String, int, int, java.lang.Object, java.util.Vector, java.util.Vector) throws org.apache.bsf.BSFException;
public java.lang.Object compileExpr(java.lang.String, java.lang.String, int, int, java.lang.Object) throws org.apache.bsf.BSFException;
public java.lang.Object compileScript(java.lang.String, java.lang.String, int, int, java.lang.Object) throws org.apache.bsf.BSFException;
public java.lang.Object compileApply(java.lang.String, java.lang.String, int, int, java.lang.Object, java.util.Vector, java.util.Vector) throws org.apache.bsf.BSFException;
public void declareBean(java.lang.String, java.lang.Object, java.lang.Class) throws org.apache.bsf.BSFException;
public void undeclareBean(java.lang.String) throws org.apache.bsf.BSFException;
public java.lang.Object lookupBean(java.lang.String);
public void registerBean(java.lang.Object);
public java.lang.Object getObjectRegistry();
public void setObjectRegistry(org.apache.bsf.util.ObjectRegistry);
public java.lang.String getClassPath();
public void setClassPath(java.lang.String);
public java.lang.ClassLoader getClassLoader();
public void setClassLoader(java.lang.ClassLoader);
public java.lang.String getTempDir();
public void setTempDir(java.lang.String);
public void terminate();
}
方法分四组:静态注册组(getLangFromFilename、isLanguageRegistered、registerScriptingEngine)、脚本执行组(eval/exec/iexec/apply 及 compile 系列)、对象注册组(declareBean/undeclareBean/lookupBean/registerBean)、环境配置组(classPath/classLoader/tempDir/registry)。日常高频使用的只有执行组的三个(eval、exec、loadScriptingEngine)与对象组的三个(declareBean、lookupBean、undeclareBean),其余按需取用。
2.2 内置注册表:23 个语言名
注册表固化在 jar 内资源文件 org/apache/bsf/Languages.properties 中,格式为「语言名 = 引擎类名, 扩展名」。实测逐语言探活(isLanguageRegistered 查询已注册名,回显):
String[] langs = {"javascript", "jacl", "netrexx", "java", "javaclass",
"bml", "vbscript", "jscript", "perlscript", "perl", "jpython", "jython",
"lotusscript", "xslt", "pnuts", "beanbasic", "beanshell", "ruby",
"judoscript", "groovy", "objectscript", "prolog", "rexx"};
Arrays.asList(langs).forEach(l -> System.out.println(
"isLanguageRegistered(" + l + ") = " + BSFManager.isLanguageRegistered(l)));
isLanguageRegistered(javascript) = true
isLanguageRegistered(jacl) = true
isLanguageRegistered(netrexx) = true
isLanguageRegistered(java) = true
isLanguageRegistered(javaclass) = true
isLanguageRegistered(bml) = true
isLanguageRegistered(vbscript) = true
isLanguageRegistered(jscript) = true
isLanguageRegistered(perlscript) = true
isLanguageRegistered(perl) = true
isLanguageRegistered(jpython) = true
isLanguageRegistered(jython) = true
isLanguageRegistered(lotusscript) = true
isLanguageRegistered(xslt) = true
isLanguageRegistered(pnuts) = true
isLanguageRegistered(beanbasic) = true
isLanguageRegistered(beanshell) = true
isLanguageRegistered(ruby) = true
isLanguageRegistered(judoscript) = true
isLanguageRegistered(groovy) = true
isLanguageRegistered(objectscript) = true
isLanguageRegistered(prolog) = true
isLanguageRegistered(rexx) = true
23 个语言名全部返回 true——但注册不等于可用:注册表只映射类名,引擎类按需加载,classpath 上缺对应实现就会在真正执行时报 NoClassDefFoundError。实测确认:javascript 对应的 JavaScriptEngine 需要 rhino:js:1.7R2;xslt 需要独立安装 xalan:xalan:2.7.2(JDK 内置的 javax.xml.transform 不提供 org.apache.xpath 包);jython 需要 org.python:jython:2.2.1;beanshell 的引擎类 bsh.util.BeanShellBSFEngine 在 org.beanshell:bsh:2.0b4 里。按扩展名推断语言名的静态方法 getLangFromFilename 有同样的加载行为,见 3.5 节。
三、BSFManager 核心 API 详解
3.1 eval:求值并取回返回值
签名 eval(String lang, String source, int lineNo, int columnNo, Object script)。第 1 参语言名;第 2 参来源名(进错误信息与调试,内联脚本可写 <inline>);第 3/4 参是脚本在来源中的行列号(从 1 起,用于异常定位);第 5 参是脚本内容本体,String 或 Reader。实测:
BSFManager mgr = new BSFManager();
Object r1 = mgr.eval("javascript", "<inline>", 1, 1, "1 + 2 * 3");
System.out.println("r1 = " + r1 + " (" + r1.getClass().getName() + ")");
Object r2 = mgr.eval("javascript", "<inline>", 1, 1, "'Apache BSF'.toUpperCase()");
System.out.println("r2 = " + r2);
mgr.terminate();
r1 = 7 (java.lang.Integer)
r2 = APACHE BSF
⚠️ 陷阱(实测):第 5 参传
null直接抛java.lang.NullPointerException(栈指向JavaScriptEngine.eval第 125 行的oscript.toString())。脚本内容必须落在第 5 参;把脚本误放第 2 参是新手最常见的错误。
3.2 exec 与 iexec:只执行、不取值
exec 与 eval 参数完全一致,区别是不取返回值,适合执行定义类脚本(函数定义、副作用)。iexec 是 isolated exec:为脚本建立独立作用域执行,不污染引擎全局对象。实测 exec 定义函数后由 eval 复用:
BSFManager mgr = new BSFManager();
mgr.exec("javascript", "<inline>", 1, 1, "function add(a, b) { return a + b; }");
Object r = mgr.eval("javascript", "<inline>", 1, 1, "add(1, 2) * 10");
System.out.println("r = " + r);
mgr.terminate();
r = 30
验证点:exec 定义的全局函数在同一 BSFManager 后续 eval 中可见,说明同一语言名复用同一引擎实例(BSFManager 内部缓存已加载引擎)。
3.3 declareBean / lookupBean / undeclareBean:对象注册表全生命周期
declareBean(name, bean, type) 把 Java 对象注册为脚本全局变量;lookupBean(name) 从注册表取回;undeclareBean(name) 注销。实测全生命周期:
public static class Counter {
private int n;
public void inc(int d) { n += d; }
public int get() { return n; }
}
BSFManager mgr = new BSFManager();
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("after JS inc, counter.get() = " + counter.get());
Object r = mgr.eval("javascript", "<inline>", 1, 1, "'n=' + counter.get()");
System.out.println("JS reads counter = " + r);
Object looked = mgr.lookupBean("counter");
System.out.println("lookupBean('counter') = " + looked
+ " (" + looked.getClass().getName() + ")");
mgr.undeclareBean("counter");
System.out.println("lookupBean after undeclare = " + mgr.lookupBean("counter"));
mgr.terminate();
after JS inc, counter.get() = 42
JS reads counter = n=42
lookupBean('counter') = BsfDemo$Counter@73d4cc9e (BsfDemo$Counter)
lookupBean after undeclare = null
验证点:脚本对 counter.inc() 的调用直接改写 Java 侧对象(10 + 32 = 42),共享是「同一对象引用」而非拷贝;lookupBean 取回的就是原对象;undeclareBean 后脚本侧变量与注册表条目同时失效,再 lookupBean 返回 null。registerBean(bean) 是免名注册的便捷方法(按对象默认名注册),与 declareBean 共享同一个 ObjectRegistry。
3.4 apply 的真实语义(重要陷阱)
门面 apply(lang, source, lineNo, columnNo, funcBody, args, argNames) 从签名看是「调用函数」,实测却不是:BSFEngineImpl.apply 的默认实现只把 funcBody 当脚本求值,args/argNames 完全不参与。实测给 Rhino 引擎传完整参数:
Object r = mgr.apply("javascript", "<inline>", 1, 1,
"function add(a, b) { return a + b; }",
new Vector<Object>(Arrays.asList(7, 35)),
new Vector<String>(Arrays.asList("a", "b")));
System.out.println("r = " + r + " (" + r.getClass().getName() + ")");
r = org.mozilla.javascript.Undefined (org.mozilla.javascript.Undefined)
返回 Undefined——函数体作为表达式求值的结果是函数对象本身(Rhino 里表现为 Undefined),参数没有传进去。Rhino 与 BeanShell 引擎均未覆写 apply(BeanShell 更是直接抛 ArrayStoreException,见 4.2 节)。结论:函数调用一律走 BSFEngine.call,见 3.5 节。
3.5 loadScriptingEngine 与 engine.call:函数调用正解
loadScriptingEngine(lang) 返回该语言的 BSFEngine 句柄(同一 BSFManager 内重复调用返回缓存的同一实例);BSFEngine.call(Object recv, String method, Object[] args) 在脚本全局对象上按名调用函数。实测:
BSFManager mgr = new BSFManager();
mgr.exec("javascript", "<inline>", 1, 1, "function add(a, b) { return a + b; }");
org.apache.bsf.BSFEngine engine = mgr.loadScriptingEngine("javascript");
Object r = engine.call(null, "add", new Object[]{7, 35});
System.out.println("r = " + r + " (" + r.getClass().getName() + ")");
mgr.terminate();
r = 42.0 (java.lang.Double)
验证点:engine.call(null, "add", ...) 正确返回 42.0(Rhino 把数值统一映射为 Double)。第 1 参 recv 传 null 表示全局对象;若函数是某个脚本对象的成员,传该对象。另外静态方法 getLangFromFilename 按扩展名推断语言名,实测 calc.js 返回 javascript;但它会对目标引擎类做加载校验——classpath 缺 jython 时对 report.py 直接抛 NoClassDefFoundError: org/python/core/PyException,补上 jython jar 后正常返回 jython(两现象均实测)。
3.6 terminate:资源释放
terminate() 结束管理器、释放全部引擎资源。BSF 无连接池等系统资源,terminate 主要让脚本引擎有机会清理(Rhino 的 Context.exit() 等)。长生命周期应用建议「每线程一个 BSFManager、用完 terminate」;短生命周期程序可省略。
四、脚本引擎逐一实测
4.1 JavaScript(Rhino 1.7R2):最完整的适配
Rhino 引擎是 BSF 内置引擎中适配最完整的:表达式求值、函数定义与调用、Java 对象互操作全部可用(回显见 3.1/3.2/3.5 节)。类型映射规律:整数运算返回 java.lang.Integer,函数返回值与浮点运算返回 java.lang.Double,字符串返回 java.lang.String。依赖坐标 rhino:js:1.7R2;更高版本 Rhino(1.7.14 起)拆分了 org.mozilla:rhino-engine 构件并改变了兼容面,与 BSF 2.4.0 搭配仍以 1.7R2 实测最稳。
4.2 BeanShell 2.0b4:能用,但 apply 有缺陷
BeanShell 是 Java 语法的解释型脚本,引擎适配类 bsh.util.BeanShellBSFEngine 位于 bsh jar 内。实测 eval/exec/对象共享/engine.call 全通:
BSFManager mgr = new BSFManager();
Object r1 = mgr.eval("beanshell", "<inline>", 1, 1, "1 + 2 * 3");
System.out.println("r1 = " + r1);
Counter counter = new Counter();
mgr.declareBean("counter", counter, Counter.class);
mgr.exec("beanshell", "<inline>", 1, 1, "counter.inc(40); counter.inc(2);");
System.out.println("counter.get() = " + counter.get());
mgr.exec("beanshell", "<inline>", 1, 1, "int add(int a, int b) { return a + b; }");
Object r2 = mgr.loadScriptingEngine("beanshell").call(null, "add", new Object[]{3, 4});
System.out.println("call add(3, 4) = " + r2 + " (" + r2.getClass().getName() + ")");
mgr.terminate();
r1 = 7
counter.get() = 42
call add(3, 4) = 7 (java.lang.Integer)
⚠️ 陷阱(实测):
mgr.apply("beanshell", ...)在引擎内部Vector.copyInto(String[])处抛java.lang.ArrayStoreException——bsh 2.0b4 引擎把实参向量拷进字符串数组的代码缺陷,无法从调用方参数规避。绕过方式:一律改用loadScriptingEngine(...).call(...)。
4.3 Jython 2.2.1:需配包缓存目录
Jython 引擎适配完整,求值返回 Jython 原生类型(org.python.core.PyInteger),declareBean 注入的 Java 对象可直接调用(回显见快速入门篇第三章,实测 sum(range(1, 101)) 返回 5050、counter.inc(50) 后 Java 侧读数 52)。
⚠️ 陷阱(实测):不加参数运行时 Jython 打印
*sys-package-mgr*: can't create package cache dir, '/.../cachedir'警告——它想在 jar 同目录创建包缓存而不可写。修复:JVM 参数-Dpython.cachedir=/tmp/jython-cache(目录存在且可写),实测警告消失,且首次启动后的重复启动明显加速(包扫描结果被缓存)。
4.4 XSLT(Xalan 2.7.2):输入可通,参数传递失效
XSLT 引擎的用法与脚本语言不同:declareBean("xslt:src", xmlString, String.class) 提供转换输入(XML 字符串),脚本位传样式表源码,返回值是包装 DOM 节点的 XSLTResultNode(经 getNode() 取回 org.w3c.dom.Node)。实测完整链路:
BSFManager mgr = new BSFManager();
mgr.declareBean("xslt:src", String.valueOf(
"<?xml version='1.0'?><order><item name='ssd' qty='2' price='199.0'/>"
+ "<item name='hdd' qty='3' price='59.5'/></order>"), String.class);
String xsl = "<?xml version='1.0'?>"
+ "<xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'>"
+ "<xsl:template match='/order'><summary>"
+ "<xsl:for-each select='item'>"
+ "<line name='{@name}' amount='{@qty * @price}'/>"
+ "</xsl:for-each></summary></xsl:template></xsl:stylesheet>";
Object result = mgr.eval("xslt", "<inline>", 1, 1, xsl);
System.out.println("result type = " + result.getClass().getName());
Transformer t = TransformerFactory.newInstance().newTransformer();
t.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter sw = new StringWriter();
t.transform(new DOMSource(((org.apache.bsf.engines.xslt.XSLTResultNode) result).getNode()),
new StreamResult(sw));
System.out.println(sw.toString().trim());
mgr.terminate();
result type = org.apache.bsf.engines.xslt.XSLTResultNode
<?xml version="1.0" encoding="UTF-8"?>
<summary>
<line name="ssd" amount="398.0"/>
<line name="hdd" amount="178.5"/>
</summary>
验证点:XML 字符串输入(xslt:src 约定名)生效,XSLT 1.0 求值正确(2199.0=398.0,359.5=178.5),返回类型为 XSLTResultNode。
⚠️ 陷阱一(实测):classpath 缺 Xalan 时报
java.lang.NoClassDefFoundError: org/apache/xpath/objects/XObject——XSLTEngine硬依赖org.apache.xpath,JDK 内置的 javax.xml.transform 实现不含该包。修复:加依赖xalan:xalan:2.7.2(自动带入serializer:2.7.2、xml-apis:1.3.04)。
⚠️ 陷阱二(实测):
declareBean向样式表传参在 Xalan 2.7.2 下失效——XSLTEngine.declareBean用new XObject(bean)包装参数,而 Xalan 2.7.2 的该构造器创建 CLASS_UNKNOWN 类型对象,样式表中引用$rate时报警告Can not convert #UNKNOWN (java.lang.String) to a number且参数取值为空(换成Double同样失败,实测复现)。结论:BSF 2.4.0 + Xalan 2.7.2 组合下 XSLT 只可通「输入 + 求值 + 输出」主链路,额外参数请写入 XML 输入或样式表默认值,不要经declareBean传递。
五、生产配置要点
1. JVM 堆。 脚本引擎内存开销主要是解释器与作用域对象,常规 256 MB 起步足够。JVM 参数位置在 java 命令与 -cp 之间:
java -Xms256m -Xmx256m -Dfile.encoding=UTF-8 -cp out:bsf.jar:js-1.7R2.jar:commons-logging-1.0.4.jar BsfVerify
BsfVerify OK
修改方法:-Xms/-Xmx 分别是初始与最大堆,按脚本规模调大(如 -Xms512m -Xmx512m);systemd 部署写进 unit 的 Environment=JAVA_OPTS=-Xms256m -Xmx256m;大量脚本常驻时加 -XX:+HeapDumpOnOutOfMemoryError。
2. 引擎实例与线程。 BSFManager 非线程安全:每线程独立创建并在用完时 terminate();同一 BSFManager 内同语言复用单引擎实例,脚本全局状态(如 Rhino 全局函数)在同实例内共享。
3. classpath 清单。 运行期最小集:bsf-2.4.0.jar + commons-logging-1.0.4.jar + 目标引擎实现 jar(Rhino/BeanShell/Jython/Xalan 按需)。commons-logging 是 BSF 的传递依赖,用于内部日志,缺失时报 NoClassDefFoundError: org/apache/commons/logging/LogFactory。
4. 类加载。 BSFManager 默认用 BSFClassLoader 加载引擎类,setClassLoader() 可接管(如隔离部署的应用传入应用类加载器);setClassPath() 可为引擎附加查找路径。无特殊需求保持默认即可。
六、参考链接
- Apache Commons BSF 官方主页:https://commons.apache.org/bsf/
- BSF 2.4.0 二进制与源码包:https://archive.apache.org/dist/commons/bsf/binaries/ 与 https://archive.apache.org/dist/commons/bsf/source/
- Maven 中央仓库 bsf:bsf:2.4.0:https://repo1.maven.org/maven2/bsf/bsf/2.4.0/
- Xalan 2.7.2 坐标:https://repo1.maven.org/maven2/xalan/xalan/2.7.2/
- Rhino 1.7R2 坐标:https://repo1.maven.org/maven2/rhino/js/1.7R2/
- Jython 2.2.1 坐标:https://repo1.maven.org/maven2/org/python/jython/2.2.1/