Java平臺的腳本支持

Java平臺為桌面和Web應(yīng)用程序開發(fā)提供了豐富的資源。是,除非有專有軟件解決方案,否則從平臺外部使用其他資源是不切實際的。沒有行業(yè)標(biāo)準(zhǔn)定義或闡明開發(fā)人員如何使用java類文件執(zhí)行其他編程語言。腳本語言還沒有一種標(biāo)準(zhǔn)的,行業(yè)支持的方法來與Java技術(shù)集成。 Java Specification Request(JSR)223 是一項更改,它通過定義標(biāo)準(zhǔn)框架和應(yīng)用程序編程接口(API)來執(zhí)行以下操作,從而幫助開發(fā)人員集成Java技術(shù)和腳本語言:

  1. Access and control Java technology-based objects from a scripting environment
  2. Create web content with scripting languages
  3. Embed scripting environments within Java technology-based applications

本文重點關(guān)注該規(guī)范的第三個目標(biāo),并將展示如何使用Java平臺應(yīng)用程序中的嵌入式腳本環(huán)境。 一個名為ScriptCalc的演示應(yīng)用程序?qū)⑻峁┮粋€有效的示例,說明如何使用JavaScript語言的自定義腳本擴展你的應(yīng)用程序。

為什么要使用腳本語言

大多數(shù)腳本語言是動態(tài)執(zhí)行的。 通常,你可以在不預(yù)先確定變量類型的情況下創(chuàng)建新變量,并且可以重用變量來存儲不同類型的值。 同樣,腳本語言會自動嘗試執(zhí)行許多類型轉(zhuǎn)換,例如,根據(jù)需要將數(shù)字10轉(zhuǎn)換為文本“ 10”。 盡管有些腳本語言是編譯性的,但大多數(shù)語言都是解釋性的。 腳本環(huán)境通常在同一過程中執(zhí)行腳本的編譯和執(zhí)行。 通常,這些環(huán)境在首次執(zhí)行時也會將腳本解析并編譯為中間代碼。

這些高質(zhì)量的腳本語言可幫助你更快地編寫應(yīng)用程序,重復(fù)執(zhí)行命令以及將來自不同技術(shù)的組件集成在一起。 專用腳本語言比通用語言更容易或更快速地執(zhí)行特定任務(wù)。 例如,許多開發(fā)人員認為Perl腳本語言是處理文本和生成報告的好方法。 其他開發(fā)人員將bash or ksh shell 用于命令和作業(yè)控制。 其他腳本語言有助于方便地定義用戶界面或Web內(nèi)容。 開發(fā)人員可以使用Java編程語言和平臺來完成這些任務(wù)中的任何一項,但是腳本語言有時可以更好地完成這項工作。 這個事實并沒有削弱Java平臺的功能和豐富性,而只是承認腳本語言在開發(fā)人員的工具箱中占有重要地位。

將腳本語言與Java平臺相結(jié)合,為開發(fā)人員提供了利用兩種環(huán)境的能力的機會。 出于任何原因,你可以繼續(xù)使用腳本語言,并且可以使用功能強大的Java類庫來擴展這些語言的功能。 如果您是Java語言程序員,則現(xiàn)在可以發(fā)布用戶動態(tài)定制的應(yīng)用程序。 Java平臺和腳本語言之間的協(xié)同作用產(chǎn)生了一個環(huán)境,開發(fā)人員和最終用戶可以在其中協(xié)作創(chuàng)建更有用的動態(tài)應(yīng)用程序。

例如,假設(shè)一個計算器具有一組核心運算。 盡管基本計算器可能只有四個或五個基本操作,但是你可以提供用戶可以自定義的可編程功能鍵。 客戶可以使用他們喜歡的任何腳本語言向計算器添加抵押貸款計算,溫度轉(zhuǎn)換或其他更復(fù)雜的功能。 另一個使用場景文字處理器,它允許客戶提供用于生成各種文件格式的自定義過濾器。 本文其余部分的示例將展示如何使用腳本為客戶提供可定制的Java應(yīng)用程序。

JSR 223 實現(xiàn)

JDK 6和JRE 6庫中包含了用于JavaScript編程語言的Mozilla Rhino引擎。 Java SE 6平臺實現(xiàn)了java.script API,你可以使用符合JSR 223的腳本引擎。 您可以通過訪問Mozilla Rhino web site 網(wǎng)站來了解有關(guān)嵌入式JavaScript技術(shù)引擎的更多信息。

使用腳本 API 的方法

腳本 API 在 Java SE 6平臺中 javax.script 包中. 如下表所示,The API仍然相對較小,由六個接口和六個類組成 .
Table 1: Interfaces and Classes in the Java SE 6 Platform

Interface Class
Bindings AbstractScriptEngine
Compilable CompiledScript
Invocable ScriptEngineManager
ScriptContext SimpleBindings
ScriptEngine SimpleScriptContext
ScriptEngineFactory ScriptException

你的起點應(yīng)該是 ScriptEngineManager 類. ScriptEngineManager 對象可以告訴你你 Java Runtime Environment (JRE) 可以使用那些腳本引擎. 它還可以提供 ScriptEngine 對象,這些對象可以解釋特定腳本語言編寫的攪拌. 使用此API的最簡單方法是執(zhí)行以下操作:

  1. 創(chuàng)建一個 ScriptEngineManager 對象.
  2. 從manager 中獲取一個 ScriptEngine 對象.
  3. 使用 ScriptEngine 對象執(zhí)行 script .

這聽起來很容易, 但是代碼是什么樣的呢? Example 1 代碼執(zhí)行了這三個步驟, 并在控制臺打印 Hello, world!.
Example 1: 通過engine 名稱創(chuàng)建一個 ScriptEngine 對象.

ScriptEngineManager mgr = new ScriptEngineManager();
  ScriptEngine jsEngine = mgr.getEngineByName("JavaScript");
  try {
    jsEngine.eval("print('Hello, world!')");
  } catch (ScriptException ex) {
      ex.printStackTrace();
  }

如果要查詢支持的腳本引擎列表,將值傳遞到腳本環(huán)境或編譯腳本以重復(fù)執(zhí)行, API 僅僅智慧稍微復(fù)雜一點點. 其他 APIs 允許你查詢 ScriptEngineManager 以獲取與特定文件擴展名關(guān)聯(lián)的引擎, 從文件執(zhí)行腳本以及在腳本中調(diào)用特定功能. 本文會介紹其他一些功能.

可用的腳本引擎

ScriptEngineManager 對象為腳本框架提供發(fā)現(xiàn)機制. 可以找到ScriptEngineFactory 類, 該類創(chuàng)建 ScriptEngine 對象. 開發(fā)人員可以使用 JAR Service Provider specification 將腳本引擎添加到JRE. 盡管此規(guī)范不是我們要討論的內(nèi)容,更多信息請參考 JAR File Specification.

Example 1 直接從腳本管理器中獲取了腳本引擎. 但是, 只有當(dāng)你知道引擎名稱時 , 這種訪問 ScriptEngine 對象的方式才有效. 如果需要使用更復(fù)雜的條件來獲取 ScriptEngine 對象, 則可能需要首先獲取 ScriptEngineFactory 對象支持的引擎的整個列表. ScriptEngineFactory 可以為特定的腳本語言創(chuàng)建 ScriptEngine 對象.

Example 2 提供可發(fā)現(xiàn)工廠的列表.
Example 2: 你可以獲取安裝到Java平臺的所有引擎的列表.

ScriptEngineManager mgr = new ScriptEngineManager();
  List<ScriptEngineFactory> factories = mgr.getEngineFactories();

一旦擁有腳本引擎工廠后, 你可以獲取有關(guān)工廠支持的腳本語言的各種詳細信息:

  • 引擎名稱和版本
  • 語言名稱和版本
  • 腳本引擎的別名
  • 腳本語言的 ScriptEngine 對象

Example 3 展示如何獲取這些信息.
Example 3: ScriptEngineFactory 對象提供有關(guān)其提供的引擎的詳細信息.

ScriptEngineManager mgr = new ScriptEngineManager();
  List<ScriptEngineFactory> factories = 
      mgr.getEngineFactories();
  for (ScriptEngineFactory factory: factories) {
    System.out.println("ScriptEngineFactory Info");
    String engName = factory.getEngineName();
    String engVersion = factory.getEngineVersion();
    String langName = factory.getLanguageName();
    String langVersion = factory.getLanguageVersion();
    System.out.printf("\tScript Engine: %s (%s)\n", 
        engName, engVersion);
    List<String> engNames = factory.getNames();
    for(String name: engNames) {
      System.out.printf("\tEngine Alias: %s\n", name);
    }
    System.out.printf("\tLanguage: %s (%s)\n", 
        langName, langVersion);
  }

Example 3 控制臺輸出如下:

ScriptEngineFactory Info
    Script Engine: Java ScriptEngine (2.0.0)
    Engine Alias: Java
    Engine Alias: java
    Engine Alias: bern:java-scriptengine
    Engine Alias: bern-java
    Language: Java (1.8.0_211)
ScriptEngineFactory Info
    Script Engine: Oracle Nashorn (1.8.0_211)
    Engine Alias: nashorn
    Engine Alias: Nashorn
    Engine Alias: js
    Engine Alias: JS
    Engine Alias: JavaScript
    Engine Alias: javascript
    Engine Alias: ECMAScript
    Engine Alias: ecmascript
    Language: ECMAScript (ECMA - 262 Edition 5.1)

請注意,不同版本的JDK或者安裝了不同腳本的引擎,輸出都不太一樣,其中 JavaScript 腳本引擎工廠是java內(nèi)置的引擎。 Rhino是核心JDK 6庫中包含的唯一引擎,Nashorn是JDK8庫中的引擎。 你可以通過將基于JAR文件的服務(wù)提供程序安裝到JRE中來添加其他引擎,如前所述。 本文的代碼示例使用Nashorn引擎。 請注意,腳本引擎工廠提供了許多引擎名稱別名,以幫助您獲取腳本語言的引擎。

創(chuàng)建 ScriptEngine 的方法

一旦獲得了有關(guān)工廠及其提供的引擎的所有信息 , 就可以在運行時決定使用哪個引擎工廠. 如果找到合適的 ScriptEngineFactory, 則創(chuàng)建關(guān)聯(lián)的 ScriptEngine 就比較容易了. 從工廠獲取實際的引擎,如示例4所示, 只需要調(diào)用工廠的 getScriptEngine 方法. 此代碼遍歷所有已知的工廠, 搜索符合語言名稱和版本的工廠. 在此示例中, 條件是硬編碼的. 該代碼獲取支持ECMA - 262 Edition 5.1版的工廠.

Example 4: 獲取滿足你應(yīng)用要求的腳本引擎.

ScriptEngineManager mgr = new ScriptEngineManager();
        List<ScriptEngineFactory> scriptFactories =
                mgr.getEngineFactories();
        ScriptEngine engine = null;
        for (ScriptEngineFactory factory: scriptFactories) {
            String langName = factory.getLanguageName();
            String langVersion = factory.getLanguageVersion();
            if (langName.equals("ECMAScript") &&
                    langVersion.equals("ECMA - 262 Edition 5.1")) {
                engine = factory.getScriptEngine();
                break;
            }
        }

當(dāng)然, 如果確定引擎可用, 則可以直接通過名稱,文件擴展名甚至MIME類型向 ScriptEngineManager 中獲取對象. 一下代碼將獲取 JavaScript programming language engine ,因為 js 是 JavaScript 語言的通用擴展名.

engine = mgr.getEngineByExtension("js");

如何運行腳本

ScriptEngine 對象運行腳本代碼. 引擎的 eval 方法來翻譯腳本, 腳本是從 Stringjava.io.Reader 對象中獲得得字符序列 . Reader 對象也可以從文件獲取其字符. 即使你已經(jīng)部署了應(yīng)用程序,也可以使用此功能來讀取客戶提供得腳本 .

Example 1 使用 eval 方法來計算 String 字符序列:

try {
    jsEngine.eval("print('Hello, world!')");
  } catch (ScriptException ex) {
      ex.printStackTrace();
  }

Nashorn's 的 print 方法實現(xiàn)將其參數(shù)數(shù)據(jù)發(fā)送到控制臺. Hello, world! 消息出現(xiàn)在你的命令控制臺中. 如果在 NetBeansEclipse 等集中開發(fā)環(huán)境中運行此命令 (IDE) , 則輸出將顯示在 IDE 的 調(diào)試或輸出窗口中.

在應(yīng)用程序中使用腳本的最佳原因之一是允許用戶自定義其功能. 允許這種客制化的最簡單的方式是讀取客戶提供的腳本文件. eval 的重載方法可以使用 Reader 參數(shù), 你可以使用該參數(shù)來讀取來自外部文件的腳本.

在應(yīng)用程序的 JAR 文件之外查找資源可能會有問題 . 但是, 如果將腳本放置在classpath 的相對目錄或用戶明確定義的絕對位置中 , 則應(yīng)用程序可以可靠的找到腳本. 如果確定所有用戶自定義的腳本都存在于應(yīng)該程序的JAR目錄下的scripts 子目錄中 , 則應(yīng)確保 JAR 文件的子目錄位于 classpath . 只要你的應(yīng)用程序目錄位于 classpath, 你的應(yīng)用程序就應(yīng)該在 scripts 子目錄中找到客戶定義的腳本 . 你可以使用JAR 中的清單文件(manifest )中 Class-path 語句將 JAR 文件的相對目錄位置放在 classpath 下. JAR文件位置的相對路徑用 . 字符表示. 本位幣的 ScriptCalc demo 應(yīng)用程序使用 manifest.xml 文件,與Example 5中的文件類似.
Example 5: 在classpath 中間 . 有助于應(yīng)用程序查找具有相對于JAR文件的路徑的腳本.

Manifest-Version: 1.0
Ant-Version: Apache Ant 1.6.5
Created-By: 1.6.0-rc-b89 (Sun Microsystems Inc.)
Main-Class: com.sun.demo.calculator.Calculator
Class-Path: .

Example 6 顯示了如何計算客戶提供的文件. 文件名為 /scripts/F1.js, 位于應(yīng)用程序目錄下.
Example 6: eval 方法可以讀取腳本文件.**

ScriptEngineManager engineMgr = new ScriptEngineManager();
        ScriptEngine engine = engineMgr.getEngineByName("ECMAScript");
        InputStream is =
                this.getClass().getResourceAsStream("/scripts/F1.js");
        try {
            Reader reader = new InputStreamReader(is);
            engine.eval(reader);
        } catch (ScriptException ex) {
            ex.printStackTrace();
        }

如何調(diào)用腳本函數(shù)

運行整個腳本很有用, 但是你可能只想調(diào)用特定的腳本函數(shù). 一些腳本引擎實現(xiàn)了 Invocable 接口. 如果引擎實現(xiàn)此接口, 則你可以調(diào)用,或者調(diào)用引擎提供的其他的特定的方法或函數(shù).

腳本引擎不是必須要實現(xiàn) Invocable 接口. 但是, JDK8 中包含的Nashorn JavaScript 技術(shù)實現(xiàn)了這個接口. 如果你的腳本包含一個名叫 sayHello 的函數(shù), 則可以通過將 ScriptEngine 對象強轉(zhuǎn)為 Invocable 對象并調(diào)用其 invokeFunction 方法來重復(fù)調(diào)用它. 另外, 如果你的腳本定了了對象,你可以使用 invokeMethod方法來調(diào)用對象的方法. Example 7 演示了如何使用此接口.
Example 7: 你可以使用 Invocable 接口來調(diào)用特定方法.

ScriptEngineManager engineMgr = new ScriptEngineManager();
        ScriptEngine jsEngine = engineMgr.getEngineByName("js");
        jsEngine.eval("function sayHello() {" +
                "  print('Hello, world!');" +
                "}");
        Invocable invocableEngine = (Invocable) jsEngine;
        invocableEngine.invokeFunction("sayHello");

Example 7 將會在將 Hello, world! 打印到控制臺.

請注意 invokeMethodinvokeFunction 方法可能會引發(fā)多個異常, 因此你需要必火 ScriptException, NoSuchMethodException, 甚至是 NullPointerException 異常.

如何從腳本訪問 Java 對象

JSR 223 的實現(xiàn)提供允許訪問Java 類,方法,屬性的編程語言綁定. 對于該特定腳本環(huán)境的原生對象,訪問機制通常將遵循腳本語言的約定 .

如何將Java 對象放入腳本環(huán)境中?你可以使用Invocable 接口將對象作為參數(shù)傳遞到腳本過程中. 另外, 你也可以在其中 "put" 他們 : Java 編程語言代碼可以通過調(diào)用腳本引擎的 put 方法將Java對象放置到腳本環(huán)境中. 此方法將鍵值對放入一個 javax.script.Bindings 對象中, 該對象由腳本引擎維護. Bindings 對象是可以從引擎內(nèi)部訪問的 key-value 對的映射.

假設(shè)你有一個要處理的腳本名稱列表. Example 8 展示了在Java編程語言中需要處理的列表.
Example 8: Java 編程語言添加名稱到列表.

List<String> namesList = new ArrayList<String>();
  namesList.add("Jill");
  namesList.add("Bob");
  namesList.add("Laureen");
  namesList.add("Ed");

創(chuàng)建名為jsEngineScriptEngine 對象后, 你可以將 namesList Java 對象傳遞到腳本環(huán)境中. put 方法需要一個StringObject 表示的一個 key-value 對. 在 Example 9, 腳本代碼可以使用 namesListKey 引用來訪問 namesList 的Java 對象.
Example 9: 腳本代碼訪問和修改原生的Java 對象.

jsEngine.put("namesListKey", namesList);
        System.out.println("Executing in script environment...");
        try {
            jsEngine.eval("var x;" +
                    "var names = namesListKey.toArray();" +
                    "for(x in names) {" +
                    "  print(names[x] );" +
                    "}" +
                    "namesListKey.add(\"Dana\");");
        } catch (ScriptException ex) {
            ex.printStackTrace();
        }
        System.out.println("Executing in Java environment...");
        namesList.stream().forEach(System.out::println);

namesListKey 鍵值對綁定到腳本引擎作用域后 , 可以將Java對象用作腳本對象 . 使用 namesListKey 變量, 腳本可以訪問 namesList 對象. 在 Example 9 中 , 腳本打印出列表的名稱并添加新的元素 Dana. 從 eval 方法返回后,通過打印 namesList 內(nèi)容 , 該示例顯示腳本已經(jīng)成功訪問和修改了列表.
示例9打印了兩次列表 . 腳本打印該列表并添加了一個名稱 . 在執(zhí)行完腳本之后 , 代碼再次打印該列表, 表明腳本成功的修改了列表:

Executing in script environment...
Jill
Bob
Laureen
Ed
Executing in Java environment...
Jill
Bob
Laureen
Ed
Dana

你也可以使用Invocable 接口將namesList 對象傳遞給腳本代碼. 腳本代碼可以訪問和修改通過 Invocable 接口提供的過程參數(shù),而不是使用 key-value 對綁定機制. Example 10 展示老人如何通過 Invocable 接口使用 Java 對象 . 該代碼將 namesList 的值作為invokeFunction 方法的參數(shù)船體給腳本環(huán)境.
Example 10: 應(yīng)用程序可以使用Invocable接口將值傳遞給腳本.

Invocable invocableEngine = (Invocable)jsEngine;
  try {
    jsEngine.eval("function printNames1(namesList) {" +
                  "  var x;" +
                  "  var names = namesList.toArray();" +
                  "  for(x in names) {" +
                  "    print(names[x]);" +
                  "  }" +
                  "}" +

                  "function addName(namesList, name) {" +
                  "  namesList.add(name);" +
                  "}");
    System.out.println("Executing in script environment...");
    invocableEngine.invokeFunction("printNames1", namesList);
    invocableEngine.invokeFunction("addName", namesList, "Dana");
    System.out.println("Executing in Java environment...");
  } catch (ScriptException ex) {
    ex.printStackTrace();
  } catch (NoSuchMethodException ex) {
    ex.printStackTrace();
  }

你還可以在腳本環(huán)境中創(chuàng)建新的 Java 對象. 你的腳本可以使用原生的Java類 . 你可以從腳本中創(chuàng)建一個Swing消息對話框, 替代將消息打印到控制臺 , 如 Example 11 所示.
Example 11: 腳本可以導(dǎo)入Java平臺程序包.

try {
            jsEngine.eval("" +
                    "var optionPane = " +
                    " javax.swing.JOptionPane.showMessageDialog(null, 'Hello, world!');");
        } catch (ScriptException ex) {
            ex.printStackTrace();
        }
消息框.png

如何訪問腳本對象

eval, invokeMethod, 和 invokeFunction 方法始終返回一個 Object 實例. 對于大多數(shù)腳本引擎, 此對象是你的腳本計算的最終值. 因此,在腳本環(huán)境中訪問對象的最簡單方法是從腳本函數(shù)中返回它們, 或者確保你的腳本計算了你所需的對象.

腳本引擎實現(xiàn)將會某些腳本數(shù)據(jù)類型映射到Java編程語言中對應(yīng)的類型 . 類如 , Nashorn 腳本引擎將數(shù)字 numberstring 類型映射成Java 編程語言的 Double and String 類型. 如果你確定返回類型,你可以強制轉(zhuǎn)化 eval , invokeMethod, 或者 invokeFunction 方法的返回值. 你應(yīng)該查閱腳本引擎文檔以獲取類型映射的詳細信息 . 當(dāng)然 , 你的腳本也可以創(chuàng)建和返回原生Java對象.

總結(jié)

JSR 223規(guī)范定義了Java平臺中的腳本。 Java SE 8平臺實現(xiàn)了此規(guī)范,JDK 6和JRE 6提供了Mozilla Rhino腳本引擎,JDK 8和JRE 8提供了Nashorn腳本引擎 以支持JavaScript技術(shù)。 還可以使用其他腳本引擎,你可以將它們作為常見的JAR擴展添加到您的運行時環(huán)境中。

出于多種原因,你可能需要在應(yīng)用程序中包含腳本支持:

  • 復(fù)雜的配置選項.

  • 用戶自定義功能.

  • 發(fā)布應(yīng)用程序后易于維護.

  • 用戶技術(shù)棧 -- 最終用戶可能熟悉其他腳本語言,而不是Java編程語言.

  • 重用其他編程語言中的代碼模塊.

由于腳本的API相對較小,因此使用Java平臺的腳本很容易. 你可以僅使用 javax.script 包中少數(shù)接口和類將腳本支持快速的添加到應(yīng)用程序中.

更多信息請參考 JSR 223 specification, documentation, and reference implementation

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容