Scripting Java

这篇翻译不完整。请帮忙从英语翻译这篇文章

这篇文章描述了如何在rhino中使用java。使用脚本调用Java有很多用途,它使得我们可以利用Java中现有的库,来帮助我们构建强大的脚本。我们可以通过编写脚本,来对Java程序进行测试。可以通过脚本来进行探索式编程,辅助Java的开发,所谓探索式编程,就是通过快速地编程调用库或API来探索这些库或API可以做什么,显而易见,脚本语言很适合探索式编程。

这里注意,ECMA标准并没有包含和Java(或者其他任何对象系统)交互的标准。本文所描述的所有内容,应该被认为是一个扩展。

 

访问 Java Packages 和 Classes

Java的每段代码都是类的一部分,每一个JAVA类都是包的一部分。在Javascript中,脚本不属于任何package。我们可以访问Java包中的类么?

Rhino定义了一个顶层的变量Packages。Packages的所有属性都是Java中顶层的包,比如java和com。比如我们可以访问java包:

js> Packages.java
[JavaPackage java]

还有一种更方便的方式,Rhino定义了一个顶层的变量java,等价于Packages.java。所以上面的例子可以更简介地写成:

js> java
[JavaPackage java]

我们可以通过访问包的下层,来直接访问java类:

js> java.io.File
[JavaClass java.io.File]

如果你的脚本需要访问很多的Java类,每次都附带完整的包名会使得编程很麻烦。Rhino提供了一个顶层的方法importPackage,它的功能和Java的import一样。比如,我们可以导入java.io包中的所有类,然后直接通过类名File来访问java.io.File:

js> importPackage(java.io)
js> File
[JavaClass java.io.File]

这里importPackage(java.io)使得java.io包中的所有类(例如File)可以在顶层被访问。这和Java中的java.io.*;等价。

要注意Java会暗中导入java.lang.*,但是Rhino不会。因为JavaScript的顶层对象Boolean、Math、Number、Object和String和java.lang包中同名的类并不相同。因为这种冲突,建议不要用importPackage来导入java.lang包。

有一点要注意的,就是Rhino对于指定包名或类名时是如何处理错误的。如果java.Myclass是可访问的,Rhino会试图加载名为java.MyClass的类,如果加载失败,它会假设java.MyClass是一个包名,不会报错:

js> java.MyClass
[JavaPackage java.MyClass]

只有在你试图将这个对象当作类使用时,才会报错。

额外的包和类

额外的包和类也可以在Rhino中使用。确认你的.jar或.class文件在你的classpath里,你就可以在你的JavaScript应用中导入它们。这些包基本不会在java包中,所以你在使用时,需要在包前加上前缀"Packages"。 比如你想导入 org.mozilla.javascript 包,你应该像下面这样去使用importPackage():

$ java org.mozilla.javascript.tools.shell.Main
js> importPackage(Packages.org.mozilla.javascript);
js> Context.currentContext;
org.mozilla.javascript.Context@bb6ab6

偶尔,我们也会见到在一些例子中使用包的完整名称,而没有使用importPackage()。这也是可以的,只是会让你多打一些字。如果使用完整的名称,上面的例子就会变成下面这样:

$ java org.mozilla.javascript.tools.shell.Main
js> jsPackage = Packages.org.mozilla.javascript;
[JavaPackage org.mozilla.javascript]
js> jsPackage.Context.currentContext;
org.mozilla.javascript.Context@bb6ab6

同样,你可以通过importClass()来导入一个类,上面的例子也可以像这样写:

$ java org.mozilla.javascript.tools.shell.Main
js> importClass(Packages.org.mozilla.javascript.Context);
js>  Context.currentContext;
org.mozilla.javascript.Context@bb6ab6

和Java一起工作

现在我们可以访问Java类,下一步就是要创建一个对象。方法就和在Java中一样, 用new来创建对象:

js> new java.util.Date()
Thu Jan 24 16:18:17 EST 2002

如果我们将创建的对象存放在JavaScript变量中,我们可以调用它的方法:

js> f = new java.io.File("test.txt")
test.txt
js> f.exists()
true
js> f.getName()
test.txt

静态方法和属性可以直接通过类对象来访问:

js> java.lang.Math.PI
3.141592653589793
js> java.lang.Math.cos(0)
1

不像Java,在JavaScript里,方法就是一个对象。它可以被评估,也可以被调用。如果我们去查看这个方法,我们可以看到这个方法所有重载的形式:

js> f.listFiles
function listFiles() {/*
java.io.File[] listFiles()
java.io.File[] listFiles(java.io.FilenameFilter)
java.io.File[] listFiles(java.io.FileFilter)
*/}

输出告诉我们,File类有listFiles方法的三种重载:一种不包含参数的,另一种包含一个FilenameFilter类型的参数,第三个包含一个FileFilter类型的参数。所有的方法都返回一个File对象数组。可以观察到Java方法的参数和返回类型在探索式编程中是非常有用的,尤其是在对一个方法的参数和返回对象不确定的时候。

另一个有助于探索式编程的特性,是可以看到对象中定义的所有方法和属性。用JavaScript的for..in , 我们可以打印这些值:

js> for (i in f) { print(i) }
exists
parentFile
mkdir
toString
wait
[44 others]

注意这里不仅列出了File类中的所有方法,也列出了从基类java.lang.Object中继承的方法,例如wait。这使得我们可以更好地处理那些有复杂继承关系的对象,因为我们可以看到对象中所有可用的方法。

Rhino可以通过属性名来方便地访问JavaBean的属性。一个JavaBean的属性foo被方法getFoo和setFoo定义,另外,一个也叫foo的boolean类型的属性,可以被isFoo来定义。比如, 下面的代码实际上调用了File对象的getName和isDirectory方法。

js> f.name
test.txt
js> f.directory
false

Calling Overloaded Methods

The process of choosing a method to call based upon the types of the arguments is called overload resolution. In Java, overload resolution is performed at compile time, while in Rhino it occurs at runtime. This difference is inevitable given JavaScript's use of dynamic typing as was discussed in Chapter 2: since the type of a variable is not known until runtime, only then can overload resolution occur.

As an example, consider the following Java class that defines a number of overloaded methods and calls them.

public class Overload {

    public String f(Object o) { return "f(Object)"; }
    public String f(String s) { return "f(String)"; }
    public String f(int i)    { return "f(int)"; }

    public String g(String s, int i) { return "g(String,int)"; }
    public String g(int i, String s) { return "g(int,String)"; }

    public static void main(String[] args) {
        Overload o = new Overload();
        Object[] a = new Object[] { new Integer(3), "hi", Overload.class };
        for (int i = 0; i != a.length; ++i)
            System.out.println(o.f(a[i]));
    }
}

When we compile and execute the program, it produces the output

f(Object)
f(Object)
f(Object)

However, if we write a similar script

var o = new Packages.Overload();
var a = [ 3, "hi", Packages.Overload ];
for (var i = 0; i != a.length; ++i)
    print(o.f(a[i]));

and execute it, we get the output

f(int)
f(String)
f(Object)

Because Rhino selects an overloaded method at runtime, it calls the more specific type that matches the argument. Meanwhile Java selects the overloaded method purely on the type of the argument at compile time.

Although this has the benefit of selecting a method that may be a better match for each call, it does have an impact on performance since more work is done at each call. In practice this performance cost hasn't been noticeable in real applications.

Because overload resolution occurs at runtime, it can fail at runtime. For example, if we call Overload's method g with two integers we get an error because neither form of the method is closer to the argument types than the other:

js> o.g(3,4)
js:"<stdin>", line 2: The choice of Java method Overload.g
matching JavaScript argument types (number,number) is ambiguous;
candidate methods are: 
class java.lang.String g(java.lang.String,int)
class java.lang.String g(int,java.lang.String)

A more precise definition of overloading semantics can be found at http://www.mozilla.org/js/liveconnect/lc3_method_overloading.html.

Implementing Java Interfaces

Now that we can access Java classes, create Java objects, and access fields, methods, and properties of those objects, we have a great deal of power at our fingertips. However, there are a few instances where that is not enough: many APIs in Java work by providing interfaces that clients must implement. One example of this is the Thread class: its constructor takes a Runnable that contains a single method run that will be called when the new thread is started.

To address this need, Rhino provides the ability to create new Java objects that implement interfaces. First we must define a JavaScript object with function properties whose names match the methods required by the Java interface. To implement a Runnable, we need only define a single method run with no parameters. If you remember from Chapter 3, it is possible to define a JavaScript object with the {propertyName: value} notation. We can use that syntax here in combination with a function expression to define a JavaScript object with a run method:

js> obj = { run: function () { print("\nrunning"); } }
[object Object]
js> obj.run()

running

Now we can create an object implementing the Runnable interface by constructing a Runnable:

js> r = new java.lang.Runnable(obj);
[object JavaObject]

In Java it is not possible to use the new operator on an interface because there is no implementation available. Here Rhino gets the implementation from the JavaScript object obj. Now that we have an object implementing Runnable, we can create a Thread and run it. The function we defined for run will be called on a new thread.

js> t = new java.lang.Thread(r)
Thread[Thread-2,5,main]
js> t.start()
js>

running

The final js prompt and the output from the new thread may appear in either order, depending on thread scheduling.

Behind the scenes, Rhino generates the bytecode for a new Java class that implements Runnable and forwards all calls to its run method over to an associated JavaScript object. The object that implements this class is called a Java adapter. Because the forwarding to JavaScript occurs at runtime, it is possible to delay defining the methods implementing an interface until they are called. While omitting a required method is bad practice for programming in the large, it's useful for small scripts and for exploratory programming.

The JavaAdapter Constructor

In the previous section we created Java adapters using the new operator with Java interfaces. This approach has its limitations: it's not possible to implement multiple interfaces, nor can we extend non-abstract classes. For these reasons there is a JavaAdapter constructor.

The syntax of the JavaAdapter constructor is:

new JavaAdapter(javaIntfOrClass, [javaIntf, ..., javaIntf,] javascriptObject)

Here javaIntfOrClass is an interface to implement or a class to extend and javaIntf are aditional interfaces to implement. The javascriptObject is the JavaScript object containing the methods that will be called from the Java adapter.

In practice there's little need to call the JavaAdapter constructor directly. Most of the time the previous syntaxes using the new operator will be sufficient.

JavaScript Functions as Java Interfaces

Often we need to implement an interface with only one method, like in the previous Runnable example or when providing various event listener implementations. To facilitate this Rhino allows to pass JavaScript function when such interface is expected. The function is called as the implementation of interface method.

Here is the simplified Runnable example:

js> t = java.lang.Thread(function () { print("\nrunning"); });
Thread[Thread-0,5,main]
js> t.start()
js> 
running

Rhino also allows to use JavaScript function as implementation of Java interface with more then method if all the methods has the same signature. When calling the function, Rhino passes method's name as the additional argument. Function can use it to distinguish on behalf of which method it was called:

js> var frame = new Packages.javax.swing.JFrame();
js> frame.addWindowListener(function(event, methodName) {
	if (methodName == "windowClosing") {     
            print("Calling System.exit()..."); java.lang.System.exit(0);
	}
    });
js> frame.setSize(100, 100);
js> frame.visible = true;
true
js> Calling System.exit()...

Creating Java Arrays

Rhino provides no special syntax for creating Java arrays. You must use the java.lang.reflect.Array class for this purpose. To create an array of five Java strings you would make the following call:

js> a = java.lang.reflect.Array.newInstance(java.lang.String, 5);
[Ljava.lang.String;@7ffe01

To create an array of primitive types, we must use the special TYPE field defined in the associated object class in the java.lang package. For example, to create an array of bytes, we must use the special field java.lang.Byte.TYPE:

js> a = java.lang.reflect.Array.newInstance(java.lang.Character.TYPE, 2);
[C@7a84e4

The resulting value can then be used anywhere a Java array of that type is allowed.

js> a[0] = 104
104
js> a[1] = 105
105
js> new java.lang.String(a)
hi

Java Strings and JavaScript Strings

It's important to keep in mind that Java strings and JavaScript strings are not the same. Java strings are instances of the type java.lang.String and have all the methods defined by that class. JavaScript strings have methods defined by String.prototype. The most common stumbling block is length, which is a method of Java strings and a dynamic property of JavaScript strings:

js> javaString = new java.lang.String("Java")
Java
js> jsString = "JavaScript"
JavaScript
js> javaString.length()
4
js> jsString.length
10

Rhino provides some help in reducing the differences between the two types. First, you can pass a JavaScript string to a Java method that requires a Java string and Rhino will perform the conversion. We actually saw this feature in action on the call to the java.lang.String constructor in the preceding example.

Rhino also makes the JavaScript methods available to Java strings if the java.lang.String class doesn't already define them. For example:

js> javaString.match(/a.*/)
ava

JavaImporter Constructor

JavaImporter is a new global constructor that allows to omit explicit package names when scripting Java:

var SwingGui = JavaImporter(Packages.javax.swing,
                            Packages.javax.swing.event,
                            Packages.javax.swing.border,
                            java.awt.event,
                            java.awt.Point,
                            java.awt.Rectangle,
                            java.awt.Dimension);
...

with (SwingGui) {
    var mybutton = new JButton(test);
    var mypoint = new Point(10, 10);
    var myframe = new JFrame();
...
}

Previously such functionality was available only to embeddings that used org.mozilla.javascript.ImporterTopLevel class as the top level scope. The class provides additional importPackage() and importClass() global functions for scripts but their extensive usage has tendency to pollute the global name space with names of Java classes and prevents loaded classes from garbage collection.

See Bugzilla 245882 for details.

Java Exceptions

Exceptions thrown by Java methods can be caught by JavaScript code using try...catch statement. Rhino wraps Java exceptions into error objects with the following properties:

  • javaException: the original exception thrown by the Java method
  • rhinoException: the exception wrapped by the Rhino runtime

The instanceof operator can be used to query the type of an exception:

try { 
    java.lang.Class.forName("NonExistingClass"); 
} catch (e) {
    if (e.javaException instanceof java.lang.ClassNotFoundException) {
       print("Class not found");
    }
}

Rhino also supports an extension to the try...catch statement that allows to define conditional catching of exceptions:

function classForName(name) {
    try {
        return java.lang.Class.forName(name);
    } catch (e if e.javaException instanceof java.lang.ClassNotFoundException) {
        print("Class " + name + " not found");
    } catch (e if e.javaException instanceof java.lang.NullPointerException) {
        print("Class name is null");
    }
}

classForName("NonExistingClass");
classForName(null);

文档标签和贡献者

 此页面的贡献者: hujun
 最后编辑者: hujun,