I code it

Code and Life

第十四章 Java应用中的JavaScript

虽然JavaScript在设计之初是为了网页的动态化,但是随着这个语言被人们普遍接受之后,它就被多种其他编程语言所实现,比如C语言的SpiderMonkey,Java语言的Rhino,以及Google的浏览器Chrome中使用的V8引擎(该引擎的执行速度是现在所有的引擎中最快的)。将JavaScript嵌入C/C++/Java应用有很多的好处,比如更好的实现用户配置,提供应用程序的扩展性,控制变化快速的需求等等。

14.1 脚本化基础

一般而言,脚本语言有这样的好处:
  • 弱类型,动态数据类型,便于变量的复用
  • 开发速度较编译型语言为快,因为大部分脚本语言为解释执行
  • 应用程序的高可配置性
开发应用程序的语言成为宿主环境,而嵌入到宿主环境的即为脚本语言,比如在浏览器Firefox中,嵌入其内的JavaScript可以定制UI,这为Firefox提供了大量可用的插件机制。而著名的编辑环境vim/Emacs更是提供了难以计数的插件,这些插件可以自由的引用宿主环境中的一些对象,比如UI组件,UI组件的内部模型等等。从而使得软件的定制性更高,比如几乎每一个Emacs用户的快捷键,窗口数目,窗口的背景色都是不同的。

我们在本书中,当然是以JavaScript语言为本,我们在本节来讨论如何使用JavaScript来脚本化Java应用,首先需要了解的是执行JavaScript代码的基础:JavaScript引擎。

14.1.1脚本化框架

Rhino JavaScript引擎本来由Mozilla开发,后来在JDK6的时候加入了JDK,因此如果你使用的JDK版本为6或更高,则无需任何配置,即可使用这个脚本化框架。事实上,JDK6中带的这个脚本化框架是与脚本无关的一个框架,用户可以使用已经存在的脚本引擎,甚至实现自己的脚本引擎。目前,已经可以使用的脚本包括python, Groovy等,当然也有JavaScript。

使用脚本化框架,可以在Java代码中执行JavaScript脚本中的函数,可以引用JavaScript变量,而JavaScript可以充分利用Java中的大量可用的工具包,创建Java对象,调用Java中对象的方法,使用Java代码中共有的属性等等。通过脚本化框架,我们可以使用Java开发出宿主环境的结构,然后使用JavaScript来定制用户界面布局,流程控制等,从而实现脚本化

14.2 使用Rhino引擎

在本章中,假设读者的计算机中都有JDK6或以上的版本,如果没有,可以更新安装或者参考其他资料。在JDK6的JAVA_HOME\bin下,有一个名为jrunscript的脚本,运行的时候会进入一个命令环境,可以运行JavaScript片段,加载外部JS文件等,我们以这个工具为例来说明如何让Java代码与JS交互的一些基本概念。

14.2.1 直接对脚本求值

脚本化的Hello,world版本:
 1: import javax.script.*;
 2:
 3: public class HelloScript {
 4:
 5:     public static void main(String args[]){
 6:         ScriptEngineManager manager = new ScriptEngineManager();
 7:         ScriptEngine engine = manager.getEngineByName("javascript");
 8:         try {
 9:             engine.eval("print('Hello, world');");
 10:         } catch (ScriptException e) {
 11:             // TODO Auto-generated catch block
 12:             e.printStackTrace();
 13:         }
 14:     }
 15: }
 

首先,创建ScriptEngineManager对象,然后根据引擎名称”javascript”来获取一个JavaScript引擎的实例,然后调用引擎实例的eval方法。应该注意的是,print函数是rhino引擎内建的全局函数,在浏览器上的JS环境中是无法执行的。

eval方法可以接受一个字符串,然后直接求值,也可以接受一个Reader对象,将一个脚本文件完整求值。另外,eval还接受第二个参数 ,以指定运行时的上下文对象,如果不指定,则按照全局上下文来求值。

14.2.2 传递Java对象

前面提到,脚本化技术允许我们在Java中访问JavaScript变量,调用JavaScript函数,以及反过来,从JavaScript中访问Java对象的属性,方法。

我们先创建一个Person的简单类,其中包含name和age属性,以及getter/setter,然后通过脚本来访问这个对象。

 1: package com.rc.scripting;
 2:
 3: public class Person{
 4:     String name;
 5:     int age;
 6:
 7:     public Person(String name, int age){
 8:         this.name = name;
 9:         this.age = age;
 10:     }
 11:
 12:     public String getName() {
 13:         return name;
 14:     }
 15:
 16:     public void setName(String name) {
 17:         this.name = name;
 18:     }
 19:
 20:     public int getAge() {
 21:         return age;
 22:     }
 23:
 24:     public void setAge(int age) {
 25:         this.age = age;
 26:     }
 27: }
有了这个Person类之后,我们在来建立一个脚本文件,命名为script.js:
 1: print(jack);
 2: print('\n');
 3: print(jack.getName());
 

然后,我们将HelloScript类的main做一些简单修改,创建新的Person对象jack,并将其put到引擎中,然后去eval刚才建立的脚本文件script.js:

 1: public static void main(String args[]){
 2:     ScriptEngineManager manager = new ScriptEngineManager();
 3:     ScriptEngine engine = manager.getEngineByName("javascript");
 4:
 5:     Person jack = new Person("jack", 28);
 6:
 7:     engine.put("jack", jack);
 8:
 9:     try {
 10:         engine.eval(new java.io.FileReader("scripts/script.js"));
 11:     } catch (ScriptException e) {
 12:         // TODO Auto-generated catch block
 13:         e.printStackTrace();
 14:     } catch (FileNotFoundException e) {
 15:         // TODO Auto-generated catch block
 16:         e.printStackTrace();
 17:     }
 18: }
可以得到如下结果:
 1: com.rc.scripting.Person@18ac738
 2: jack

14.2.3 调用脚本内的函数

再来看看在Java中调用JavaScript函数,首先在脚本script.js中定义函数sayHello:
 1: function sayHello(name){
 2:     print("hello, "+name+"!");
 3: }
然后在Java代码中,将引擎转换为可调用(Invocable)引擎,应该注意的是,Invocable接口是一个可选的接口,如果要编写自己的脚本引擎,可以不实现此接口。rhino引擎实现了该接口,因此我们可以很方便的做一个转换,然后调用invoceFunction方法来调用脚本中的函数:
 1: public static void main(String args[]){
 2:     ScriptEngineManager manager = new ScriptEngineManager();
 3:     ScriptEngine engine = manager.getEngineByName("javascript");
 4:
 5:     try {
 6:         engine.eval(new java.io.FileReader("scripts/script.js"));
 7:     } catch (ScriptException e) {
 8:         e.printStackTrace();
 9:     } catch (FileNotFoundException e) {
 10:         e.printStackTrace();
 11:     }
 12:
 13:     Invocable invEngine = (Invocable)engine;
 14:
 15:     try {
 16:         invEngine.invokeFunction("sayHello", "jack");
 17:     } catch (ScriptException e) {
 18:         e.printStackTrace();
 19:     } catch (NoSuchMethodException e) {
 20:         e.printStackTrace();
 21:     }
 22: }
 

运行结果为:

 1: hello, jack!
 

 

由于JavaScript是”面向对象”的语言,因此我们可以调用一个对象的方法,我们将全局函数sayHello包装在对象object上,作为object的一个属性:

 1: var object = new Object();
 2:
 3: object.sayHello = function(name){
 4:     print("hello, "+name+"!");
 5: }
然后在Java代码中通过invokeMethod方法来调用,首先从脚本引擎中取道这个对象的实例,通过enginge.get(name)方法来实现:
 1: public static void main(String args[]){
 2:     ScriptEngineManager manager = new ScriptEngineManager();
 3:     ScriptEngine engine = manager.getEngineByName("javascript");
 4:
 5:     try {
 6:         engine.eval(new java.io.FileReader("scripts/script.js"));
 7:     } catch (ScriptException e) {
 8:         e.printStackTrace();
 9:     } catch (FileNotFoundException e) {
 10:         e.printStackTrace();
 11:     }
 12:
 13:     Invocable invEngine = (Invocable)engine;
 14:
 15:     Object obj = engine.get("object");
 16:     try {
 17:         invEngine.invokeMethod(obj, "sayHello", "jack again");
 18:     } catch (ScriptException e) {
 19:         e.printStackTrace();
 20:     } catch (NoSuchMethodException e) {
 21:         e.printStackTrace();
 22:     }
 23: }

14.2.4 在脚本中使用Java资源

javax.script提供的最值得一提的功能就是可以在脚本中使用所有的java资源,由于Java语言已经发展的极为成熟,有大量可用的工具包可供使用,而很多情况下,限于Java语言本身的的功能限制,我们可能需要具有其他特征如动态性,弱类型,嵌套函数等脚本特性,将两者结合起来,一方面使得编程工作更为有趣,更为高效快速,又不必担心代码的质量或运行效率。

在JDK6中的脚本化框架中,我们可以在脚本中导入Java包,Java类,实现Java接口(在下一小节中详细讨论),这给开发人员带来极大的便利,javax.script提供了两个内置函数importPackage和importClass分别用以导入Java包和类。

比如下列语句:

 1: importPackage(java.awt, java.awt.event)
 2: importPackage(Packages.javax.swing)
 3: importPackage(java.io)
 4: importClass(java.lang.System)
我们来看一个小例子,通过导入javax.swing工具包,绘制一个小窗口:
 1: importPackage(java.awt, java.awt.event);
 2: importPackage(Packages.javax.swing);
 3:
 4: (function(title){
 5:     var frame = new JFrame(title);
 6:     frame.setSize(300, 150);
 7:
 8:     var label = new JLabel("I'am a label");
 9:     frame.add(label);
 10:     frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 11:
 12:     frame.setVisible(true);
 13: })("Script frame");
得到下图这个小窗体:

clip_image002

图 JavaScript通过swing绘制小窗体

14.2.5 实现Java接口

通过接口的方式,可以更好的分离宿主环境与脚本之间的依赖,Java暴露给脚本以接口,脚本实现接口,然后在Java中可以在完全不知道脚本是如何实现的前提下进行调用,我们来看一个简单的示例:
 1: var task = {
 2:     run : function(){
 3:         print("start execute...");
 4:     }
 5: }
脚本实现了java.lang.Runnable接口,Runnable接口仅有一个方法:run。然后在Java代码中,我们从引擎对象engine中获得此对象task, 然后创建一个新的线程以使用Runnable接口的实现:
 1: public static void main(String args[]){
 2:     ScriptEngineManager manager = new ScriptEngineManager();
 3:     ScriptEngine engine = manager.getEngineByName("javascript");
 4:
 5:     try {
 6:         engine.eval(new java.io.FileReader("scripts/script.js"));
 7:     } catch (ScriptException e) {
 8:         e.printStackTrace();
 9:     } catch (FileNotFoundException e) {
 10:         e.printStackTrace();
 11:     }
 12:
 13:     Invocable invEngine = (Invocable)engine;
 14:
 15:     Object obj = engine.get("task");
 16:
 17:     Runnable r = invEngine.getInterface(obj, Runnable.class);
 18:
 19:     Thread thread = new Thread(r);
 20:     thread.start();
 21: }
 

通过引擎的getInterface(object, class)来获得此接口的实现,由于JavaScript的弱类型,Java端只能通过判断对象是否与接口的定义匹配来确定其是否实现了指定的接口,因此getInterface的第二个参数是一个“类”类型。

另一个相关的例子是,在JavaScript代码中实现Java接口,所有代码均属于JavaScript代码:

 1: var task = new java.lang.Runnable(){
 2:     run : function(){
 3:         print("start execute...");
 4:     }
 5: };
 6:
 7: var thread = new java.lang.Thread(task);
 8: thread.start();
与上例相同,会得到下面的执行结果:
 1: start execute...
 

14.3 实例:sTodo

14.3.1 sTodo简介

sTodo是一个简单的待办事项管理工具,使用Java的Swing工具包作为UI展现,而内部使用一个嵌入式的数据库SQLite作为数据源,并使用了JavaScript来进行界面风格的定制,功能的扩展等。

运行时的界面如下图:

clip_image002[5]

图sTodo运行界面

事实上,上图中的菜单栏中的plugin和help都是通过脚本添加的,sTodo原始的界面如下:

clip_image004

图 sTodo原始界面

sTodo中有一个对javax.script的浅包装,作为sTodo中的插件机制。插件机制我们在下一小节详细讨论。sTodo的其他模块,比如数据源模块,邮件发送模块等与本章的主题无关,这里不做讨论,sTodo托管在google code上,是一个完全开源的小项目,有兴趣的读者可以自行研究。

14.3.2 sTodo的插件机制

sTodo的插件机制是对javax.script做的一个浅包装。插件在物理上是一个脚本文件,sTodo的Java端代码公开一些对象供JavaScript访问,比如菜单栏,这样JavaScript代码则可以创建新的菜单,提供新的功能给Java。同样的,在JavaScript端,公开一个入口,供Java端的代码调用。

clip_image006

图 sTodo插件机制结构图

插件具有状态,并被安装在插件管理器中,可以被激活。sTodo的生命周期中,有一个单例的插件管理器,可以在一个模块内安装插件,并在另外一个模块中使用,为了保证访问插件的时刻必须在安装之后,我们可以在软件初始化的时候将依赖关系调整好,然后将异步的响应动作放到UI组件的监听器上。

我们来详细查看一下sTodo的工作机制:

在sTodo的初始化中:

 1: public void initEnv(){
 2:     PluginManager pManager = TodoPluginManager.getInstance();
 3:
 4:     Plugin system =
 5:         new TodoPlugin("scripts/system.js", "system", "system initialize");
 6:     pManager.install(system);
 7: }
我们会安装一个system.js的插件,这个插件的功能是初始化脚本环境,加载其他插件(也就是脚本)。然后在main方法中:
 1: public static void main(String[] args){
 2:     STodo sTodo = new STodo(new MainFrame("My todo list"));
 3:     sTodo.initEnv();
 4:
 5:     Plugin system = TodoPluginManager.getInstance().getPlugin("system");
 6:     system.putValueToContext("Application", sTodo);
 7:     system.putValueToContext("DataModel", new DataModel());
 8:     system.putValueToContext("Util", new Util());
 9:
 10:     system.execute("main", new Object());
 11: }
将一些公用的组件暴露给脚本环境,如Application,DataModel,Util等,以便脚本环境可以直接访问Java端的swing组件及数据模型。最后,执行system.execute。也就是执行main函数,这个main函数定义在插件system中。

最后,还有一个方法用以启动sTodo的Java端:

 1: public void launch(){
 2:     SwingUtilities.invokeLater(new Runnable(){
 3:         public void run() {
 4:             mainFrame.initUI();
 5:         }
 6:     });
 7: }
这个方法通过JavaScript端(system插件对应的脚本)来完成。流程是这样:Java端执行main方法,初始化环境,加载脚本,然后想底层的引擎中放入公有对象(Application等),然后调用JavaScript端的main函数,JavaScript端开始初始化其自身的环境,与共有对象交互,最后反过来调用Java端的launch启动UI。

14.3.3 sTodo中的脚本

脚本入口main方法:
 1: function main(){
 2:     var app = Application;
 3:     var ui = app.getUI();
 4:
 5:     //set look and feel to windows
 6:     ui.setLookAndFeel("windows");
 7:
 8:     //load some new scripts
 9:     app.activePlugin("scripts/json.js");
 10:     app.activePlugin("scripts/date.js");
 11:     app.activePlugin("scripts/util.js");
 12:     app.activePlugin("scripts/menubar.js");
 13:     app.activePlugin("scripts/misc.js");
 14:
 15:     app.launch();
 16:     //loadTodosFromFile("todos.txt");
 17: }
这里的main函数首先获取Java端暴露的Application对象,然后设置L&F为Windows平台,然后依次加载一些脚本,最后调用Application上的方法launch启动Java端。

另外一个暴露给Java端的函数是初始化菜单栏的函数:

 1: //this function will be invoked from java code, MainFrame...
 2: function _customizeMenuBar_(menuBar){
 3:     menuBar.add(buildPluginMenu());
 4:     menuBar.add(buildHelpMenu());
 5: }
menubar参数通过Java传递过来,然后依次调用buildPluginMenu和buildHelpMenu函数,创建新的菜单项,注册事件监听器,我们这里进讨论buildPluginMenu函数:
 1: function buildPluginMenu(){
 2:     var menuPlugin = new JMenu();
 3:     menuPlugin.setText("Plugin");
 4:     menuPlugin.setIcon(new ImageIcon("imgs/plugin.png"));
 5:
 6:     var menuItemListPlugin = new JMenuItem();
 7:     menuItemListPlugin.setText("list plugins");
 8:     menuItemListPlugin.addActionListener(
 9:     new JavaAdapter(
 10:         ActionListener, {
 11:             actionPerformed : function(event){
 12:                 var plFrame = new JFrame("plugins list");
 13:                 var epNote = new JEditorPane();
 14:                 var s = "";
 15:                 pluginList = Application.getPluginList();
 16:                 for(var i = 0; i<pluginList.size();i++){
 17:                     var pi = pluginList.get(i);
 18:                     s += pi.getName()+":"+pi.getDescription()+"\n";
 19:                 }
 20:                 epNote.setText(s);
 21:                 epNote.setEditable(false);
 22:                 plFrame.add(epNote, BorderLayout.CENTER);
 23:                 plFrame.setSize(200,200);
 24:                 plFrame.setLocationRelativeTo(null);
 25:                 plFrame.setVisible(true);
 26:             }
 27:         }
 28:     ));
 29:
 30:     menuPlugin.add(menuItemListPlugin);
 31:
 32:     return menuPlugin;
 33: }
 

buildPluginMenu函数创建一个新的JMenu对象,并为其添加一个JMenuItem,当点击该菜单项时,创建一个新的JFrame,列出当前软件已经安装的插件(通过调用Application.getPluginList()获取),运行效果如下图:

clip_image008

图 列出当前已经安装的插件列表

当然,sTodo中还有很多地方使用了插件,感兴趣的朋友可以自行参考,比如通过脚本来建立新的待办事项,发送待办事项到指定邮箱,日期格式的分析等。

14.4 实例:可编程计算器phoc

所谓可编程计算机是指,phoc本身可以通过程序来进行扩展,当然这里的程序是指JavaScript脚本,利用第三方的优秀开源工具jmathtool的绘制功能,phoc可以绘制出2d及3d的函数图像。phoc事实上是建立在javax.script基础上的一个包装,计算功能,函数定义功能,变量功能都是直接使用脚本引擎提供的能力。

14.4.1 phoc简介

我们先来看看phoc的界面:

clip_image002[7]

图 phoc主界面

和其他的计算器界面并无二致,但是读者可能已经注意到,有两个一般不会出现在计算器上的按钮,Func和Var,正如其名字显示的那样,这两个按钮分别为:函数定义和变量定义,函数定义可以定义自己的函数,如:

clip_image004[5]

图 函数定义面板

定义之后的函数可以直接在输入框中使用。由于使用了JavaScript语言作为解释器,因此这个计算器甚至具有计算对象,字符串,布尔值等的功能。

下面是用phoc绘制的2d的sin函数在-PI到PI之间的图形:

clip_image006

图 phoc 2d绘图示例

下图为phoc绘制的3d图形:

clip_image008[5]

图 phoc 3d绘图示例

14.4.2 phoc中的脚本

phoc中包含两个脚本文件,分别为math.js和plot.js。math.js中对JavaScript的内建Math函数做了一个重定义和包装,plot.js则对jmathtool工具包做了Java-JavaScript的转换,使得在phoc中使用JavaScript函数来绘制2d和3d的图形。
 1: var PI = Math.PI;
 2: var E = Math.E;
 3:
 4: function sqrt(x){
 5:     return Math.sqrt(x);
 6: }
 7:
 8: function sin(x){
 9:     return Math.sin(x);
 10: }
 11:
 12: function cos(x){
 13:     return Math.cos(x);
 14: }
 15:
 16: function sum(){
 17:     var result = 0;
 18:     for(var i = 0, len = arguments.length; i < len; i++){
 19:         var current = arguments[i];
 20:         if(isNaN(current)){
 21:             throw new Error("not a number exception");
 22:         }else{
 23:             result += current;
 24:         }
 25:     }
 26:
 27:     return result;
 28: }
 29:
math.js的部分代码,目的仅为在phoc的函数定义器及输入框中减少输入量。这个脚本无需展开讨论,下面看一下plot.js:
 1: importPackage(Packages.javax.swing)
 2: importPackage(java.awt)
 3: importClass(org.math.array.DoubleArray)
 4: importClass(org.math.plot.Plot2DPanel)
 5: importClass(org.math.plot.Plot3DPanel)
 6:
 7: /**
 8:  * a little helper for plot
 9:  */
 10: function increment(start, step, stop){
 11:     var x = [];
 12:     for(start; start+step < stop; start += step){
 13:         x.push(start);
 14:     }
 15: }
首先导入”org.math.array.DoubleArray”及”org.math.plot.*”,jmathtool的绘图接口包装在Plot2DPanel和Plot2DPanel中。
 1: /**
 2:  * i.e.
 3:  * plot(function(x){return Math.sin(x);}, {
 4:  * start : -3.0,
 5:  * step : 0.1,
 6:  * stop : 3.0
 7:  * });
 8:  */
 9: function plot2d(func, range){
 10:     var x = DoubleArray.increment(range.start, range.step, range.stop);
 11:     var y = new Array(x.length);
 12:
 13:     for(var i = 0, len = x.length; i < len; i++){
 14:         y[i] = func(x[i]);
 15:     }
 16:
 17:     var plot = new Plot2DPanel();
 18:     plot.addLegend("SOUTH");
 19:
 20:     var name = func.toString();
 21:     plot.addLinePlot(name, Color.GREEN, x, y);
 22:
 23:     var frame = new JFrame("plot function : "+name);
 24:     frame.setSize(600, 600);
 25:     frame.setContentPane(plot);
 26:     frame.setVisible(true);
 27: }
 28:
 29: /**
 30:  * plot function 3d
 31:  */
 32: function plot3d(func, xrange, yrange){
 33:     var x = DoubleArray.increment(xrange.start, xrange.step, xrange.stop);
 34:     var y = DoubleArray.increment(yrange.start, yrange.step, yrange.stop);
 35:
 36:     var name = func.toString();
 37:
 38:     var z = (function(x, y){
 39:         var dims = new Array(y.length, x.length);
 40:         var r = java.lang.reflect.Array.newInstance(java.lang.Double.TYPE, dims);
 41:         for(var i = 0, len = x.length; i < len; i++){
 42:             for(var j = 0, len2 = y.length; j < len2; j++){
 43:                     r[j][i] = func(x[i], y[j]);
 44:             }
 45:         }
 46:         return r;
 47:     })(x, y);
 48:
 49:     var plot = new Plot3DPanel("SOUTH");
 50:
 51:     plot.addGridPlot(name, x, y, z);
 52:
 53:     var frame = new JFrame("plot function : "+name);
 54:     frame.setSize(600, 600);
 55:     frame.setContentPane(plot);
 56:     frame.setVisible(true);
 57: }
 

使用jmathtool的Plot*DPanel产生新的Panel,然后创建新的JFrame对象,并将Panel添加在JFrame上管理。

Comments