Java 脚本化编程指南
Java 脚本化编程指南
Java脚本化API为谁准备?
脚本语言的一些有用的特性是:
方便:大多数脚本语言都是动态类型的。您通常可以创建新的变量,而不声明变量类型,并且您可以重用变量来存储不同类型的对象。此外,脚本语言往往会自动执行许多类型的转换,例如, 必要时 将数字10转换为“10”。
开发快速原型:您可以避免编辑编译运行周期,只使用“编辑运行”!
应用扩展/定制:你可以“具体化”的部分应用程序,例如一些配置脚本,业务逻辑/规则和财务应用中的数学表达式 。为应用添加命令行模式,用于调试、运行时配置/部署时间。现在大多数应用程序都有一个基于Web的GUI配置工具。但是系统管理员/部署人员常常喜欢命令行工具。一个“标准”的脚本语言可以用来实现这个目的,而不是发明特设的脚本语言。
Java 脚本 API 是一种独立于框架的脚本语言,使用来自于Java代码的脚本引擎 。通过java脚本API,可以使用Java语言编写定制/可扩展的应用程序并将自定义脚本语言选择留给最终用户 。Java 应用程序开发者不需要在开发过程中选择扩展语言。如果你使用JSR-223 API来编写应用,那么你的用户可以使用任何JSR-223兼容的脚本语言。
脚本包
Java
脚本功能是在javax.script
包中。这是一个比较小的,简单的API
。脚本的出发点是 ScriptEngineManager
类。一个ScriptEngineManager
对象可以通过jar
文件的服务发现机制发现脚本引擎。它也可以实例化脚本引擎来解释使用特定的脚本语言编写的脚本。使用脚本编程接口的最简单的方法如下:
创建一个ScriptEngineManager
对象
从ScriptEngineManager
获取 ScriptEngine
对象
使用ScriptEngine
的eval
方法执行脚本
现在,是时候看一些样本代码了。了解一些JavaScript有助于阅读这些例子,但不是强制的。
实例
Hello,World
从ScriptEngineManager实例中,我们通过 getEngineByName 方法得到一个JavaScript引擎实例。通过脚本引擎的eval方法来执行给定的JavaScript代码。为简便起见,本例以及随后的例子中,我们不对异常进行处理。javax.script API有检查和运行时异常,你必须妥善处理异常。
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class Case1 {
public static void main(String[] args) throws ScriptException {
//获取脚本引擎管理器
ScriptEngineManager scriptEngineManager = new ScriptEngineManager();
//根据引擎名获取JavaScript引擎
ScriptEngine engine = scriptEngineManager.getEngineByName("JavaScript");
//执行JavaScript方法
engine.eval("print('hello world')");
}
}
执行一个脚本文件
在这个例子中,我们调用eval
方法来接收java.io.Reader
作为输入源。读入的脚本被执行。这种方式能够成文件执行脚本,用相关的输入流对象读取URL
和资源
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.nio.CharBuffer;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class ExecuteOneJsFile {
public static void main(String[] args) throws FileNotFoundException, ScriptException {
// 引擎manager
ScriptEngineManager factory = new ScriptEngineManager();
// 创建一个 JavaScript engine
ScriptEngine engine = factory.getEngineByName("JavaScript");
// 下面是打印文件的内容读东西
String jsText = null;
CharBuffer cbuf = null;
String jsFilePath = "d:\\test.js";
File file = new File(jsFilePath);
FileReader fReader = null;
try {
fReader = new FileReader(file);
// fReader.mark((int)file.length());不支持重置文件到开始
cbuf = CharBuffer.allocate((int) file.length());
fReader.read(cbuf);
jsText = new String(cbuf.array());
System.out.println(jsText);
// 执行D盘下面的test.js文件
// fReader.reset();//重置fRead的指针为文件的开始部分,这里需要配合mark函数的使用(不支持)
engine.eval( new FileReader(file));
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
脚本变量
当你的java
应用程序嵌入脚本引擎和脚本,你可能希望将您的应用程序对象为全局变量暴露于脚本中。这个例子演示了如何将您的应用程序对象作为全局变量暴露于脚本中。我们在应用程序中创建一个 java.io.File
对象作为全局变量,名称是file
。该脚本可以访问变量,例如,它可以调用它的公共方法。注意访问java
对象、领域和方法的语法依赖于脚本语言。JavaScript
支持最“自然”的类似java
的语法。
package com.neil.java;
import java.io.File;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class VariantWithJavaxScript {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
//创建一个Java的对象
File file = new File("readme.txt");
//将file放入JavaScript中
//key4file是key,可以使用该key来访问该变量
engine.put("key4file", file);
//执行使用java的api来调用file的getAbsolutePath方法
System.out.println(file.getAbsolutePath());
//在js语句中调用file的getAbsolutePath方法;
engine.eval("print(key4file.getAbsolutePath())");
}
}
调用脚本函数和方法
有些时候,你可能需要多次调用一个特定脚本函数,例如你的应用程序菜单功能可能由脚本来实现。在菜单中的操作事件处理程序中,可能需要调用一个特定的脚本函数。下面的示例演示在Java
代码调用一个特定的脚本。
package com.neil.java;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class FunctionWithJavaxScript {
public static void main(String[] args) throws ScriptException, NoSuchMethodException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
// 编写一个JavaScript函数
String script = "function hello(name) { print('Hello, ' + name); }";
// 执行上面的语句,就会在JavaScript运行环境上创建一个hello(name)的函数
engine.eval(script);
// javax.script.Invocable is an optional interface.
// Check whether your script engine implements or not!
// Note that the JavaScript engine implements Invocable interface.
//判断JavaScript引擎是不是可调用的接口,来执行hello函数
Invocable inv = (Invocable) engine;
// 调用全局函数hello,并为其传入一个参数""
inv.invokeFunction("hello", "neil,牛中超");
}
}
如果你的脚本语言是基于对象(如JavaScript
)或面向对象的,你可以在脚本对象上调用脚本方法。
package com.neil.java;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class FunctionOfObjectWithJavaxScript {
public static void main(String[] args) throws ScriptException, NoSuchMethodException {
// 获取manager和engine
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
/*
* 创建一个js语句 js的语句首先创建一个object对象,该对象的成员hello指向的是一个名为hello的函数
* hello的函数是print
*/
String js = "var object = new Object(); object.hello = function(name) {print( name );}";
// 使用eval指向上面的js语句,将object刷到JavaScript运行环境上
engine.eval(js);
// 判断当前的engine是否可调用
Invocable invocable = (Invocable) engine;
// 在Java上创建一个Object对象,来获取object
// get方法和上面的put方法很类似,用于java和javascript进行对象的互相访问
Object obj = engine.get("object");
//调用object的hello方法
invocable.invokeMethod(obj, "hello", "Neil,牛中超");
}
}
通过脚本实现Java
接口
有些时候通过脚本函数或者方法可以很方便的实现java
接口,而不是在Java
中调用。同时,通过接口我们可以避免在很多地方使用javax.script API
接口。我们可以得到一个接口实现者对象并将其传递给不同的Java api
。下面的例子演示了通过脚本实现java.lang.Runnable
接口。
package com.neil.java;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class CallJavaInterface {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
//在js里面创建一个run方法,作为下面的Java中的Runnable对象的run方法
String js = "function run(){print('hello, Neil,牛中超')}";
//执行该js,将run方法刷到JavaScript运行环境中
engine.eval(js);
//获取JavaScript引擎的可调用接口
Invocable invocable = (Invocable) engine;
//从engine根据传入的类类型(本例中传入的是Runnable类型)来获取一个Runnable接口
//且javascript的invocable.getInterface方法会自动从js引擎中
//根据相同的函数名来实例化Runnable接口的方法
//也就是,getInterface会自动把上面创建的run方法用来实例化runnable的run方法
Runnable runnable = invocable.getInterface(Runnable.class);
//在java中创建一个线程,并启动
Thread thread = new Thread(runnable);
thread.start();
}
}
如果你的脚本语言是基于对象或者面向对象的,可以通过脚本对象的脚本方法来实现Java
接口。这避免了不得不调用脚本全局函数的接口方法。脚本对象可以存储接口实现状态。
package com.neil.java;
import javax.script.Invocable;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class CallJavaInterfaceWithJsObjMethod {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
//创建一个对象,并给该对象实现一个run方法,run方法不能带参数
String js = "var yourName = new Object(); yourName.run = function(){ print('Hello, Neil,牛中超')}";
//执行上面的js语句,将yourName刷到JavaScript运行环境上
engine.eval(js);
//获取engine的可调用接口
Invocable invocable = (Invocable) engine;
//获取JavaScript运行环境上的yourName对象
Object object = engine.get("yourName");
//用object中的方法来实现Runnable需要实现的的同名方法(也就是run方法)
Runnable runnable = invocable.getInterface(object,Runnable.class);
//创建线程,并启动
Thread thread = new Thread(runnable);
thread.start();
}
}
脚本的多作用域
在script variables
例子中,我们看到怎样将应用对象暴露为脚本的全局变量。它有可能暴露为多个全局的作用域 。 单作用域是javax.script.Bindings
的实例中. 这个借口派生至java.util.Map<String, Object>
。 scope
键值对的集合,其中键为非空、非空字符串。 多scopes
是 javax.script.ScriptContext
接口支持的。支持一个或多个脚本上下文与相关的域绑定。默认情况下, 每一个脚本引擎都有一个默认的脚本上下文。 默认的脚本上下文有至少一个域叫 ENGINE_SCOPE
。不同域的脚本上下文支持可以通过getscopes
方法获取。
package com.neil.java;
import javax.script.Bindings;
import javax.script.ScriptContext;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
import javax.script.SimpleScriptContext;
public class MultiScopes {
public static void main(String[] args) throws ScriptException {
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
//向JavaScript引擎默认的上下文范围中写入变量
engine.put("name", "Neil");
engine.eval("print(name)");
//创建一个新的上下文
ScriptContext context = new SimpleScriptContext();
//获取一个用来给新创建的上下文制定的ENGINE_SCOPE
Bindings engineScope = context.getBindings(ScriptContext.ENGINE_SCOPE);
//在新的上下文的ENGINE_SCOPE中设置一个name变量
engineScope.put("name", "牛中超");
//从新的上下文中获取name
engine.eval("print(name)",context);
}
}
JavaScript 脚本引擎
Sun
的JDK 6
中包含了一个基于Mozilla Rhino JavaScript
脚本引擎。 这个引擎是基于版本为1.6R2
的Mozilla Rhino
。多数Rhino
实现都被包含在内。少部分组件由于大小和安全原因被排除了:
JavaScript
转字节码编译 (也称优化器
)。此功能依赖一个类生成库。 去掉本功能意味着:JavaScript
是解释执行,且不影响脚本执行,因为优化器是透明的。Rhino
的JavaAdapter
也被去掉了。JavaAdapter
是一个JavaScript
可扩展Java
类和JavaScript
可实现Java
接口功能。此功能也是需要类生成库的。我们把Rhino
的JavaAdapter
替换
为Sun
实现的JavaAdapter
。在Sun
的实现中,仅仅实现了JavaScript
对象可实现Java
单接口功能。例如,下面的代码会正确执行。var v = new java.lang.Runnable() { run: function() { print('hello'); } } v.run();
在大多数情况下,
JavaAdapter
是采用匿名类语法来实现单接口。 使用JavaAdapter
来扩展Java
类或实现多接口并不常见。E4X (ECMAScript for XML - ECMA Standard 357)
被去掉了. 使用XML JavaScript
代码会产生一个语法错误. 请注意,E4X
支持ECMAScript
标准是可选的-省略E4X的实现是被支持也是兼容ECMAScript
。Rhino
的命令行工具(Rhino shell, debugger 等)
没有被包含在内。但你可以用使用jrunscript
来代替。
JavaScript与Java的通信
在大多数情况下,访问Java类
、对象
和方法
很简单。从JavaScript
中访问属性和方法与同Java
中一样。这里,我们突出JavaScript Java
访问的重要方面。更多的细节请阅读。下面是一些JavaScript
访问Java
的代码片段。本节需要一些JavaScript
知识。如果你打算使用JSR-223
中非JavaScript
脚本语言,那么本节可以跳过。
引入Java 包, 类
内置的函数importPackage
和importClass
可以用于引入Java 包
和类
。
全局变量Packages
也可以用于访问Java
包。例如: Packages.java.util.Vector
, Packages.javax.swing.JFrame
. 请注意: java
是 Packages.java
的快捷引用。还有一些等价的快捷引用前缀 :javax
, org
, edu
, com
, net
, 所以几乎所有的JDK
平台下的类都可以不使用Packages
前缀而访问到
package com.neil.java;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import javax.script.ScriptEngine;
import javax.script.ScriptEngineManager;
import javax.script.ScriptException;
public class ImportPackage {
public static void main(String[] args) throws FileNotFoundException, ScriptException {
String filePath = "d:\\import.js";
File file = new File(filePath);
FileReader fileReader = new FileReader(file);
if(fileReader!=null)
{
ScriptEngineManager manager = new ScriptEngineManager();
ScriptEngine engine = manager.getEngineByName("JavaScript");
//执行
engine.eval(fileReader);
}
}
}
import.js
的文件内容如下
// Import Java packages and classes
// like import package.*; in Java
Packages.java.awt;
// like import java.awt.Frame in Java
Packages.java.awt.Frame;
// Create Java Objects by "new ClassName"
var frame = new java.awt.Frame("hello");
// Call Java public methods from script
frame.setVisible(true);
// Access "JavaBean" properties like "fields"
print(frame.title)
需要注意的是上面创建的一个
JFrame
没有添加关闭事件,需要在任务管理器中关闭掉其所以来的 java
线程。
请注意,java.lang
不是默认引入的 (与Java
不同),因为会与JavaScript's
内置的 Object
, Boolean
, Math
等冲突。
importPackage
和importClass
函数”污染” 了JavaScript中的全局变量。为了避免这种情况,你可以使用JavaImporter。
// create JavaImporter with specific packages and classes to import
var SwingGui = new JavaImporter(javax.swing,
javax.swing.event,
javax.swing.border,
java.awt.event);
with (SwingGui) {
// within this 'with' statement, we can access Swing and AWT
// classes by unqualified (simple) names.
var mybutton = new JButton("test");
var myframe = new JFrame("test");
}
创建和使用Java的数组
在 JavaScript
中,创建一个对象时与Java
中一样,而创建Java
数组时需要显式的使用Java
反射。但一旦创建好后,访问其中的元素或获取大小就和Java
中一样。 另外,也可以使用脚本数组用在Java
方法中期望的Java
数组(因为可以自动转换)。所以在大多数情况下我们不需要显式地创建Java
数组。
// create Java String array of 5 elements
var a = java.lang.reflect.Array.newInstance(java.lang.String, 5);
// Accessing elements and length access is by usual Java syntax
a[0] = "scripting is great!";
print(a.length);
实现Java 接口
在JavaScript
中,可以使用Java
匿名类语法形式实现Java
中接口:
var r = new java.lang.Runnable() {
run: function() {
print("running...\n");
}
};
// "r" can be passed to Java methods that expect java.lang.Runnable
var th = new java.lang.Thread(r);
th.start();
当接口中只有一个需要实现的方法时,你可以自己传入脚本的函数(因为可以自动转换)。
function func() {
print("I am func!");
}
// pass script function for java.lang.Runnable argument
var th = new java.lang.Thread(func);
th.start();
重载
Java
方法是使用参数类型重载的。在Java
中,重载发生在编译阶段 (执行javac
)。当脚本中调用Java
方法时,脚本的翻译器或编译器需要选择适当的方法。对于JavaScript
引擎,您不需要做任何特别的——正确的Java
方法重载变体是根据参数类型选择的。 但有时,您可能希望(或有)显式地选择一个特定的过载变体。
var out = java.lang.System.out;
// select a particular println function
out["println(java.lang.Object)"]("hello");
更多JavaScript
的Java
方法重载细节阅读
自定义脚本引擎
我们不会覆盖的JSR-223
兼容脚本引擎实现细节. 至少, 您需要实现javax.script.ScriptEngine
和javax.script.ScriptEngineFactory
接口。 抽象类javax.script.AbstractScriptEngine
提供了一些ScriptEngine
接口中定义的方法。
在开始实现JSR-223
引擎之前,您可能需要下载http://scripting.dev.java.net
工程。这个工程维护了一些流行的开源脚本语言的 JSR-223
实现。
引用
1. JSR-223 Scripting for the Java Platform
2. JavaScript Developer Connection
3. Java Method Overloading and LiveConnect 3
4. Rhino:JavaScript for Java
5. Scripting Java (from JavaScript)
6. scripting.dev.java.net project