Java 指令碼化程式設計指南

luke,藍騎士發表於2016-12-13

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檔案的服務發現機制發現指令碼引擎。它也可以例項化指令碼引擎來解釋使用特定的指令碼語言編寫的指令碼。使用指令碼程式設計介面的最簡單的方法如下:

  1. 建立一個 ScriptEngineManager  物件
  2. 從 ScriptEngineManager  獲取 ScriptEngine  物件
  3. 使用 ScriptEngine的eval方法執行指令碼

現在,是時候看一些樣本程式碼了。瞭解一些JavaScript有助於閱讀這些例子,但不是強制的。

例項

“Hello,World”

從ScriptEngineManager例項中,我們通過 getEngineByName 方法得到一個JavaScript引擎例項。通過指令碼引擎的eval方法來執行給定的JavaScript程式碼。為簡便起見,本例以及隨後的例子中,我們不對異常進行處理。javax.script API有檢查和執行時異常,你必須妥善處理異常。

import javax.script.*;
public class EvalScript {
    public static void main(String[] args) throws Exception {
        // create a script engine manager
        ScriptEngineManager factory = new ScriptEngineManager();
        // create a JavaScript engine
        ScriptEngine engine = factory.getEngineByName("JavaScript");
        // evaluate JavaScript code from String
        engine.eval("print('Hello, World')");
    }
}

執行一個指令碼檔案

在這個例子中,我們呼叫eval方法來接收java.io.Reader作為輸入源。讀入的指令碼被執行。這種方式能夠成檔案執行指令碼,用相關的輸入流物件讀取URL和資源。

import javax.script.*;
public class EvalFile {
    public static void main(String[] args) throws Exception {
        // create a script engine manager
        ScriptEngineManager factory = new ScriptEngineManager();
        // create JavaScript engine
        ScriptEngine engine = factory.getEngineByName("JavaScript");
        // evaluate JavaScript code from given file - specified by first argument
        engine.eval(new java.io.FileReader(args[0]));
    }
}

假設我們有一個叫”test.js”的檔案,裡面的內容如下:

println("This is hello from test.js");

我們可以使用下面的方式來執行剛剛的指令碼

java EvalFile test.js

指令碼變數

當你的java應用程式嵌入指令碼引擎和指令碼,你可能希望將您的應用程式物件為全域性變數暴露於指令碼中。這個例子演示瞭如何將您的應用程式物件作為全域性變數暴露於指令碼中。我們在應用程式中建立一個 java.io.File物件作為全域性變數,名稱是file。該指令碼可以訪問變數,例如,它可以呼叫它的公共方法。注意訪問java物件、領域和方法的語法依賴於指令碼語言。JavaScript支援最“自然”的類似java的語法。

public class ScriptVars { 
    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        File f = new File("test.txt");
        // expose File object as variable to script
        engine.put("file", f);

        // evaluate a script string. The script accesses "file" 
        // variable and calls method on it
        engine.eval("print(file.getAbsolutePath())");
    }
}

呼叫指令碼函式和方法

有些時候,你可能需要多次呼叫一個特定指令碼函式,例如你的應用程式選單功能可能由指令碼來實現。在選單中的操作事件處理程式中,可能需要呼叫一個特定的指令碼函式。下面的示例演示在Java程式碼呼叫一個特定的指令碼。

import javax.script.*;

public class InvokeScriptFunction {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        // JavaScript code in a String
        String script = "function hello(name) { print('Hello, ' + name); }";
        // evaluate script
        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.
        Invocable inv = (Invocable) engine;

        // invoke the global function named "hello"
        inv.invokeFunction("hello", "Scripting!!" );
    }
}

如果你的指令碼語言是基於物件(如JavaScript)或物件導向的,你可以在指令碼物件上呼叫指令碼方法。

import javax.script.*;

public class InvokeScriptMethod {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        // JavaScript code in a String. This code defines a script object 'obj'
        // with one method called 'hello'. 
        String script = "var obj = new Object(); obj.hello = function(name) { print('Hello, ' + name); }";
        // evaluate script
        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.
        Invocable inv = (Invocable) engine;

        // get script object on which we want to call the method
        Object obj = engine.get("obj");

        // invoke the method named "hello" on the script object "obj"
        inv.invokeMethod(obj, "hello", "Script Method !!" );
    }
}

通過指令碼實現Java介面

有些時候通過指令碼函式或者方法可以很方便的實現java介面,而不是在Java中呼叫。同時,通過介面我們可以避免在很多地方使用javax.script API介面。我們可以得到一個介面實現者物件並將其傳遞給不同的Java api。下面的例子演示了通過指令碼實現 java.lang.Runnable介面。

import javax.script.*;

public class RunnableImpl {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        // JavaScript code in a String
        String script = "function run() { println('run called'); }";

        // evaluate script
        engine.eval(script);

        Invocable inv = (Invocable) engine;

        // get Runnable interface object from engine. This interface methods
        // are implemented by script functions with the matching name.
        Runnable r = inv.getInterface(Runnable.class);

        // start a new thread that runs the script implemented
        // runnable interface
        Thread th = new Thread(r);
        th.start();
    }
}

如果你的指令碼語言是基於物件或者物件導向的,可以通過指令碼物件的指令碼方法來實現Java介面。這避免了不得不呼叫指令碼全域性函式的介面方法。指令碼物件可以儲存介面實現狀態。

import javax.script.*;

public class RunnableImplObject {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        // JavaScript code in a String
        String script = "var obj = new Object(); obj.run = function() { println('run method called'); }";

        // evaluate script
        engine.eval(script);

        // get script object on which we want to implement the interface with
        Object obj = engine.get("obj");

        Invocable inv = (Invocable) engine;

        // get Runnable interface object from engine. This interface methods
        // are implemented by script methods of object 'obj'
        Runnable r = inv.getInterface(obj, Runnable.class);

        // start a new thread that runs the script implemented
        // runnable interface
        Thread th = new Thread(r);
        th.start();
    }
}

指令碼的多作用域

在 script variables 例子中,我們看到怎樣將應用物件暴露為指令碼的全域性變數。它有可能暴露為多個全域性的作用域 。 單作用域是javax.script.Bindings的例項中. 這個藉口派生至java.util.Map<String, Object>。 scope 鍵值對的集合,其中鍵為非空、非空字串。 多scopes 是 javax.script.ScriptContext 介面支援的。支援一個或多個指令碼上下文與相關的域繫結。預設情況下, 每一個指令碼引擎都有一個預設的指令碼上下文。 預設的指令碼上下文有至少一個域叫 ”ENGINE_SCOPE”。不同域的指令碼上下文支援可以通過 getscopes 方法獲取。

import javax.script.*;

public class MultiScopes {
    public static void main(String[] args) throws Exception {
        ScriptEngineManager manager = new ScriptEngineManager();
        ScriptEngine engine = manager.getEngineByName("JavaScript");

        engine.put("x", "hello");
        // print global variable "x"
        engine.eval("println(x);");
        // the above line prints "hello"

        // Now, pass a different script context
        ScriptContext newContext = new SimpleScriptContext();
        Bindings engineScope = newContext.getBindings(ScriptContext.ENGINE_SCOPE);

        // add new variable "x" to the new engineScope        
        engineScope.put("x", "world");

        // execute the same script - but this time pass a different script context
        engine.eval("println(x);", newContext);
        // the above line prints "world"
    }
}

JavaScript 指令碼引擎

Sun的JDK 6中包含了一個基於 Mozilla Rhino JavaScript 指令碼引擎。 這個引擎是基於版本為1.6R2的Mozilla Rhino 。多數 Rhino 實現都被包含在內。少部分元件由於大小和安全原因被排除了:

  1. JavaScript轉位元組碼編譯 (也稱 ”優化器”).。此功能依賴一個類生成庫。 去掉本功能意味著:JavaScript是解釋執行,且不影響指令碼執行,因為優化器是透明的。
  2. 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類或實現多介面並不常見。

  3. E4X (ECMAScript for XML – ECMA Standard 357) 被去掉了. 使用XML JavaScript程式碼會產生一個語法錯誤. 請注意,E4X支援ECMAScript標準是可選的-省略E4X的實現是被支援也是相容 ECMAScript 。
  4. Rhino的命令列工具 (Rhino shell, debugger 等) 沒有被包含在內。但你可以用使用 jrunscript來代替。

JavaScript與Java的通訊

在大多數情況下,訪問Java類、物件和方法很簡單。從JavaScript中訪問屬性和方法與同Java中一樣。這裡,我們突出JavaScript Java訪問的重要方面.。更多的細節請閱讀http://www.mozilla.org/rhino/scriptjava.html。下面是一些JavaScript訪問Java的程式碼片段。本節需要一些JavaScript知識。如果你打算使用JSR-223中非JavaScript指令碼語言,那麼本節可以跳過。

引入Java 包, 類

內建的函式importPackage 和importClass 可以用於引入Java 包和類。

// Import Java packages and classes 
// like import package.*; in Java
importPackage(java.awt);
// like import java.awt.Frame in Java
importClass(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);

全域性變數Packages也可以用於訪問Java包。例如: Packages.java.util.VectorPackages.javax.swing.JFrame. 請注意: ”java” 是 “Packages.java”的快捷引用。還有一些等價的快捷引用字首 : javax, org, edu, com, net, 所以幾乎所有的 JDK 平臺下的類都可以不使用”Packages” 字首而訪問到。

請注意,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");
}

C建立和使用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方法過載細節閱讀

http://www.mozilla.org/js/liveconnect/lc3_method_overloading.html

自定義指令碼引擎

我們不會覆蓋的JSR-223相容指令碼引擎實現細節. 至少, 您需要實現javax.script.ScriptEngine 和javax.script.ScriptEngineFactory 介面。 抽象類javax.script.AbstractScriptEngine 提供了一些ScriptEngine 介面中定義的方法。

在開始實現 JSR-223 引擎之前,您可能需要下載  http://scripting.dev.java.net 工程。這個工程維護了一些流行的開源指令碼語言的 JSR-223 實現。

引用

相關文章