在服务器端运行JavaScript文件(一)
简介:
把JavaScript脚本与服务器上Java代码相结合,从而获得在服务器端和客户端都能够自由使用的JavaScript脚本。另外,经过这一系列的被展现技术,无论是基于Ajax还是非Ajax的客户端,都将允许你维护一个单一的代码,因为大多数的服务器端的代码依然是用Java来编写的,同时你还会发现公开给JavaScript的Java EE(Java企业版)的功能特征。在这一系列的技术中,将学习一下内容
1.怎样在服务器端运行JavaScript脚本;
2.用Ajax远程调用JavaScript的功能;
3.与JSP一起使用Java Scripting API。
典型的Ajax应用,在客户端使用JavaScript脚本语言,并且和服务器端的编程语言不同,例如在服务器端使用Java语言。这样,对于某些程序开发者就必须实现两次,在Web浏览器端使用JavaScript语言,而在服务器端使用另外一种语言。通过使用把JavaScript和服务器端的Java代码相结合,获取Javax.script API对脚本语言的完全支持,这样就可以避免代码的双重发行。另外,JDK6(Java SE Development Kit 6)中已经包含了Mazilla’s Rhino JavaScript引擎,如果使用JDK6的话,就不需要重新安装了。
在这一系列的文章的开始,通过一个简单的脚本运行器,来讲解在Java EE应用程序中怎样执行JavaScript文件。这个脚本会访问在JSP页面中被叫做”implicit objects”的对象,例如application对象、session对象,request对象以及response对象。这个例子大部分都是由可重用的代码组成,这样在你的服务器端的应用程序中就可以很容易的使用这些代码作为起始代码。
使用Javax.script API
这一节概要介绍Javax.script API。将学习一下内容:
1.怎样执行访问Java对象的脚本;
2.怎样用Java代码调用JavaScript的功能;
3.为编译后的脚本实现缓存机制。
执行脚本
javax.script API很简单,通过使用下列方法,创建一个ScriptEngineManager的实例就可以开始了,这个实例可以获得一个ScriptEngine对象(代码1)。
1.getEngineByName();
2.getEngineByExtension();
3.getEngineByMineType()。
代码1:获取一个ScriptEngine实例。
- import javax.script.*;
- ...
- ScriptEngineManager manager = new ScriptEngineManager();
- ScriptEngine engine = manager.getEngineByName("JavaScript");
- ...
- engine.eval(...);
还可以使用getEngineFactouies()方法获得一个有效的脚本引擎的列表。当前只有一个JavaScript引擎被绑定在JDK6中,但是ScriptEngineManager实现了一种发现机制,能够主动查找发现支持Java平台的JSR-223脚本规范的第三方引擎,但是必须把第三方脚本引擎的JAR文件放到Java的CLASSPATH路径中。
获得javax.script.ScriptEngine实例之后,就可以调用eval()方法来执行脚本。还可以把Java对象作为脚本变量,通过Bindings实例传递给eva()方法。代码2包含了ScriptDemo.java的列子中,把demoVar和strBuf两个变量传递给要执行的DemoScript.js脚本,然后取得变量中的被编辑的值。
代码2:ScriptDemo.js
- package jsee.demo;
- import javax.script.*;
- import java.io.*;
- public class ScriptDemo {
- public static void main(String args[]) throws Exception {
- // Get the JavaScript engine
- ScriptEngineManager manager = new ScriptEngineManager();
- ScriptEngine engine = manager.getEngineByName("JavaScript");
- // Set JavaScript variables
- Bindings vars = new SimpleBindings();
- vars.put("demoVar", "value set in ScriptDemo.java");
- vars.put("strBuf", new StringBuffer("string buffer"));
- // Run DemoScript.js
- Reader scriptReader = new InputStreamReader(
- ScriptDemo.class.getResourceAsStream("DemoScript.js"));
- try {
- engine.eval(scriptReader, vars);
- } finally {
- scriptReader.close();
- }
- // Get JavaScript variables
- Object demoVar = vars.get("demoVar");
- System.out.println("[Java] demoVar: " + demoVar);
- System.out.println(" Java object: " + demoVar.getClass().getName());
- System.out.println();
- Object strBuf = vars.get("strBuf");
- System.out.println("[Java] strBuf: " + strBuf);
- System.out.println(" Java object: " + strBuf.getClass().getName());
- System.out.println();
- Object newVar = vars.get("newVar");
- System.out.println("[Java] newVar: " + newVar);
- System.out.println(" Java object: " + newVar.getClass().getName());
- System.out.println();
- }
- }
DempScript.js文件中(代码3)包含了一个叫做printType()的函数,使用这个函数输出每个脚本变量的类型。上例中还调用了strBuf对象的append()方法,编辑了demoVar对象的值,并且设定了一个新的叫做newVar的脚本变量。
如果传递给PrintType()方法的对象有getClass()方法,那么就一定是可以用obj.getClass().name方法取得类名的Java对象,下面的JavaScript表达式调用了对象的java.lang.Class实例的getName()方法;如果对象没有getClass()方法,printType()方法就调用所有的JavaScript对象都有的toSource()方法。
代码3:DemoScript.js- println("Start script /r/n");
- // Output the type of an object
- function printType(obj) {
- if (obj.getClass)
- println(" Java object: " + obj.getClass().name);
- else
- println(" JS object: " + obj.toSource());
- println("");
- }
- // Print variable
- println("[JS] demoVar: " + demoVar);
- printType(demoVar);
- // Call method of Java object
- strBuf.append(" used in DemoScript.js");
- println("[JS] strBuf: " + strBuf);
- printType(strBuf);
- // Modify variable
- demoVar = "value set in DemoScript.js";
- println("[JS] demoVar: " + demoVar);
- printType(demoVar);
- // Set a new variable
- var newVar = { x: 1, y: { u: 2, v: 3 } }
- println("[JS] newVar: " + newVar);
- printType(newVar);
- println("End script /r/n");
下面列出了ScriptDemo.java例子的输出结果,首先注意的是demoVar变量是作为JavaScript的String类型被报告的,而strBuf变量依然是java.lang.StringBuffer类型。对于原始变量和Java string 类型的变量类型是作为JavaScript的本地对象来报告的,而对于其他的任意Java对象(包括数组对象)的类型,都会被报告为其自身的类型。
Start script
[JS] demoVar: value set in ScriptDemo.java
JS object: (new String("value set in ScriptDemo.java"))
[JS] strBuf: string buffer used in DemoScript.js
Java object: java.lang.StringBuffer
[JS] demoVar: value set in DemoScript.js
JS object: (new String("value set in DemoScript.js"))
[JS] newVar: [object Object]
JS object: ({x:1, y:{u:2, v:3}})
End script
[Java] demoVar: value set in DemoScript.js
Java object: java.lang.String
[Java] strBuf: string buffer used in DemoScript.js
Java object: java.lang.StringBuffer
[Java] newVar: [object Object]
Java object: sun.org.mozilla.javascript.internal.NativeObject
脚本运行之后,引擎会带出所有的变量(包括脚本中新的变量),并进行与JavaScript相反的类型转换,把JavaScript的原始类型和strings类型转换成Java对象,其他的JavaScript对象会使用引擎内部的特殊的API,把它们封装到Java对象中,例如:sun.org.mozilla.javascript.internal.NativeObject.
调用功能函数
在前面的例子中,我们已经看到在JavaScript中调用Java的方法是可能的。现在我们开始学习在Java代码中怎样调用JavaScript的功能函数。首先必须执行包含我们想要调用的函数的脚本,然后把ScriptEngine实例对象转化成javax.script.Invocable类型,这类型中提供了invokeFunction()和invokeMethod()方法。如果脚本中实现了所有的Java接口中的方法,那么还可以使用getInterface()方法来获得用脚本语言中编写的Java对象的方法。
InvDemo.java例子(代码5)中执行了一个叫做InvScript.js的脚本,脚本中包含了demoFunction()方法的实现。把ScriptEngine实例转换成Invocable类型的对象后,把函数的名字和参数传递给引擎的invokeFunction()方法,返回值是被demoFunction()方法返回的。
代码5:
- package jsee.demo;
- import javax.script.*;
- import java.io.*;
- public class InvDemo {
- public static void main(String args[]) throws Exception {
- // Get the JavaScript engine
- ScriptEngineManager manager = new ScriptEngineManager();
- ScriptEngine engine = manager.getEngineByName("JavaScript");
- // Run InvScript.js
- Reader scriptReader = new InputStreamReader(
- InvDemo.class.getResourceAsStream("InvScript.js"));
- try {
- engine.eval(scriptReader);
- } finally {
- scriptReader.close();
- }
- // Invoke a JavaScript function
- if (engine instanceof Invocable) {
- Invocable invEngine = (Invocable) engine;
- Object result = invEngine.invokeFunction("demoFunction", 1, 2.3);
- System.out.println("[Java] result: " + result);
- System.out.println(" Java object: "
- + result.getClass().getName());
- System.out.println();
- } else
- System.out.println("NOT Invocable");
- }
- }
InvScript.js文件(代码6)中包含了demoFunction()函数和在前面例子的脚本文件中相同的printType()函数。
代码6:
- println("Start script /r/n");
- function printType(obj) {
- if (obj.getClass)
- println(" Java object: " + obj.getClass().name);
- else
- println(" JS object: " + obj.toSource());
- println("");
- }
- function demoFunction(a, b) {
- println("[JS] a: " + a);
- printType(a);
- println("[JS] b: " + b);
- printType(b);
- var c = a + b;
- println("[JS] c: " + c);
- printType(c);
- return c;
- }
- println("End script /r/n");
如果看一下InvDemo.java的输出结果,就会发现,数字参数被转换成了JavaScript对象,demoFunction()方法的返回值是作为Java对象来获得的。这些转换仅限于原始数据类型和strings类型,任何其他的被传递的对象的类型,在JVM和JavaScript引擎之间都不会被改变,反之亦然。
输出结果:
Start script
End script
[JS] a: 1
JS object: (new Number(1))
[JS] b: 2.3
JS object: (new Number(2.3))
[JS] c: 3.3
JS object: (new Number(3.3))
[Java] result: 3.3
Java object: java.lang.Double
注意:javax.script.Invocable是一个可选接口,有些脚本引擎可能没有实现。JDK6的JavaScript引擎支持这个接口。
编译脚本
脚本每次被执行时都需要从新解释,这样会浪费CPU资源。如果相同脚本要多次运行,就可以使用另一个叫做javax.script.Compilable的可选接口来编译脚本,这样就可以明显的降低解释脚本的次数。这个可选接口在JDK6的脚本引擎中被支持。
CachedScript类(代码8)接收一个脚本文件,并且只有在脚本源代码被再次编辑时才从新编译。getCompiledScript()方法调用了脚本引擎的compile()方法,这方法返回了执行脚本的javax.script.CompiledScript对象的eval()方法。
代码8:
- package jsee.cache;
- import javax.script.*;
- import java.io.*;
- import java.util.*;
- public class CachedScript {
- private Compilable scriptEngine;
- private File scriptFile;
- private CompiledScript compiledScript;
- private Date compiledDate;
- public CachedScript(Compilable scriptEngine, File scriptFile) {
- this.scriptEngine = scriptEngine;
- this.scriptFile = scriptFile;
- }
- public CompiledScript getCompiledScript()
- throws ScriptException, IOException {
- Date scriptDate = new Date(scriptFile.lastModified());
- if (compiledDate == null || scriptDate.after(compiledDate)) {
- Reader reader = new FileReader(scriptFile);
- try {
- compiledScript = scriptEngine.compile(reader);
- compiledDate = scriptDate;
- } finally {
- reader.close();
- }
- }
- return compiledScript;
- }
- }
ScriptCache类中(代码9)还使用Java.util.LinkedHashMap对象为被编译的脚本实现了一个脚本仓库。这个map的初始容量被设定为缓存脚本的最大数,并且装载系数是1,这两个参数保证了cacheMap不必重新进行哈希计算。
默认条件下,LinkedHashMap类为其内部实体采用插入顺,因此比需把LinkedHashMap()构造器的第三个参数设定为true,这样map中的实体对象就可以用访问顺来代替默认顺。
- package jsee.cache;
- import javax.script.*;
- import java.io.*;
- import java.util.*;
- public abstract class ScriptCache {
- public static final String ENGINE_NAME = "JavaScript";
- private Compilable scriptEngine;
- private LinkedHashMap<String, CachedScript> cacheMap;
- public ScriptCache(final int maxCachedScripts) {
- ScriptEngineManager manager = new ScriptEngineManager();
- scriptEngine = (Compilable) manager.getEngineByName(ENGINE_NAME);
- cacheMap = new LinkedHashMap<String, CachedScript>(
- maxCachedScripts, 1, true) {
- protected boolean removeEldestEntry(Map.Entry eldest) {
- return size() > maxCachedScripts;
- }
- };
- }
- public abstract File getScriptFile(String key);
- public synchronized CompiledScript getScript(String key)
- throws ScriptException, IOException {
- CachedScript script = cacheMap.get(key);
- if (script == null) {
- script = new CachedScript(scriptEngine, getScriptFile(key));
- cacheMap.put(key, script);
- }
- return script.getCompiledScript();
- }
- public ScriptEngine getEngine() {
- return (ScriptEngine) scriptEngine;
- }
- }