1. 程式人生 > >Java中調用JavaScript方法

Java中調用JavaScript方法

兩個 tint work object c ash 沒有 .net nashorn alc

我們都知道腳本語言非常靈活,在處理某些問題的時候 Java 實現用十幾行來寫,用 js 可能不到十行就寫完,並且非常簡潔,那麽有沒有一種優雅的方式將 Java 與腳本語言結合呢,在 Java SE6(代號 Mustang)中,這將成為現實。

Mustang 的腳本引擎

JSR 233 為 Java 設計了一套腳本語言 API。這一套 API 提供了在 Java 程序中調用各種腳本語言引擎的接口

任何實現了這一接口的腳本語言引擎都可以在 Java 程序中被調用。在 Mustang 的發行版本中包括了一個基於 Mozilla Rhino 的 JavaScript 腳本引擎,也就是說在 JavaSE6+ 的版本默認可以直接調用執行 JavaScript。

Mozilla Rhino

Rhino 是一個純 Java 的開源的 JavaScript 實現。他的名字來源於 O‘Reilly 關於 JavaScript 的書的封面,是的,就是那本犀牛書.....

Rhino 項目可以追朔到 1997 年,當時 Netscape 計劃開發一個純 Java 實現的 Navigator,為此需要一個 Java 實現的 JavaScript —— Javagator。

它也就是 Rhino 的前身。起初 Rhino 將 JavaScript 編譯成 Java 的二進制代碼執行,這樣它會有最好的性能。後來由於編譯執行的方式存在垃圾收集的問題並且編譯和裝載過程的開銷過大,不能滿足一些項目的需求,Rhino 提供了解釋執行的方式。

隨著 Rhino 開放源代碼,越來越多的用戶在自己的產品中使用了 Rhino,同時也有越來越多的開發者參與了 Rhino 的開發並做出了很大的貢獻。如今 Rhino 將被包含在 Java SE 中發行,更多的 Java 開發者將從中獲益。

Rhino 提供了如下功能

  • 對 JavaScript 1.5+ 的完全支持
  • 直接在 Java 中使用 JavaScript 的功能
  • 一個 JavaScript shell 用於運行 JavaScript 腳本
  • 一個 JavaScript 的編譯器,用於將 JavaScript 編譯成 Java 二進制文件

支持的腳本語言

在 dev.java.net 可以找到官方的腳本引擎的實現項目。這一項目基於BSD License ,表示這些腳本引擎的使用將十分自由。

目前該項目已對包括 Groovy, JavaScript, Python, Ruby, PHP 在內的二十多種腳本語言提供了支持。這一支持列表還將不斷擴大。

在Java中的基本使用

在 Mustang 中對腳本引擎的檢索使用了工廠模式。首先需要實例化一個工廠 : ScriptEngineManager

ScriptEngineManager factory = new ScriptEngineManager();

ScriptEngineManager 將在 Thread Context ClassLoader 的 Classpath 中根據 jar 文件的 META-INF 來查找可用的腳本引擎。它提供了 3 種方法來檢索腳本引擎:

// create engine by name
ScriptEngine engine = factory.getEngineByName ("JavaScript");
// create engine by name
ScriptEngine engine = factory.getEngineByExtension ("js");
// create engine by name
ScriptEngine engine = factory.getEngineByMimeType ("application/javascript");

下面的代碼將會打印出當前的 JDK 所支持的所有腳本引擎:

ScriptEngineManager factory = new ScriptEngineManager();
for (ScriptEngineFactory available : factory.getEngineFactories()) {
  System.out.println(available.getEngineName());
  // 打印腳本具體名稱信息
  System.out.println(available.getNames());
}

即可看到 [nashorn, Nashorn, js, JS, JavaScript, javascript, ECMAScript, ecmascript] 等輸出,說明可執行 Js 腳本

執行Js腳本

啥也別說了,都知道要幹嘛,來打印句 HelloWorld 再說:

public class RunJavaScript {
  public static void main(String[] args){
    ScriptEngineManager factory = new ScriptEngineManager();
    ScriptEngine engine = factory.getEngineByName ("JavaScript");
    engine.eval("print(‘Hello World‘)");
  }
}

如果你的 Js 有語法錯誤,就會拋出 javax.script.ScriptException 異常

如果我們要解釋一些更復雜的腳本語言,或者想在運行時改變該腳本該如何做呢?腳本引擎支持一個重載的 eval 方法,它可以從一個 Reader 讀入所需的腳本,或者得到 Js 文件的絕對路徑,直接用自帶的 load 函數讀取:

ScriptEngineManager factory = new ScriptEngineManager();
ScriptEngine engine = factory.getEngineByName ("JavaScript");
engine.eval(new Reader("HelloWorld.js"));


File file = new File(Main.class.getClassLoader().getResource("test.js").getFile());
engine.eval(new FileReader(file));

String scriptPath = Main.class.getClassLoader().getResource("test.js").getPath();
engine.eval("load(‘" + scriptPath + "‘)");

這裏註意下 FileReader 中相對路徑到底是相對誰的(JVM 啟動的位置),可使用 ClassLoader 讀取 src 下的文件

Java 程序將動態的去讀取腳本文件並解釋執行,這意味著,在程序運行中你可以隨意修改 Js 文件裏的代碼,會得到實時修改結果。

對於這一簡單的 Hello World 腳本來說,IO 操作將比直接執行腳本損失 20% 左右的性能(SE6 時,個人測試),但他帶來的靈活性 --- 在運行時動態改變代碼的能力,在某些場合是十分激動人心的。

最後來看看完整的測試代碼:

package com.bfchengnuo.javascript;

import javax.script.*;
import java.io.FileNotFoundException;

/**
 * Created by 冰封承諾Andy on 2017/12/30.
 */
public class Main {
  public static void main(String[] args) throws ScriptException, NoSuchMethodException, FileNotFoundException {
    ScriptEngineManager manager = new ScriptEngineManager();

    System.out.println("當前 JDK 支持的腳本語言引擎:");
    for (ScriptEngineFactory available : manager.getEngineFactories()) {
      System.out.println(available.getEngineName());
      System.out.println(available.getNames());
    }

    ScriptEngine engine = manager.getEngineByName("JavaScript");
    if (!(engine instanceof Invocable)) {
      System.out.println("Invoking methods is not supported.");
      return;
    }
    engine.eval("print(‘Hello World‘)");

    Invocable inv = (Invocable) engine;

    // File file = new File(Main.class.getClassLoader().getResource("test.js").getFile());
    // engine.eval(new FileReader(file));
    String scriptPath = Main.class.getClassLoader().getResource("test.js").getPath();
    engine.eval("load(‘" + scriptPath + "‘)");

    // 獲取對象
    Object calculator = engine.get("calculator");

    int x = 3;
    int y = 4;
    Object addResult = inv.invokeMethod(calculator, "add", x, y);
    Object subResult = inv.invokeMethod(calculator, "subtract", x, y);
    Object mulResult = inv.invokeMethod(calculator, "multiply", x, y);
    Object divResult = inv.invokeMethod(calculator, "divide", x, y);

    System.out.println(addResult);
    System.out.println(subResult);
    System.out.println(mulResult);
    System.out.println(divResult);
  }
}

對應的 Js 文件:

var calculator = {};

calculator.add = function (n1, n2) { return n1 + n2};
calculator.subtract = function (n1, n2) {return n1 - n2};
calculator.multiply = function (n1, n2) {return n1 * n2};
calculator.divide = function (n1, n2) {return n1 / n2};

腳本語言與 Java 的通信

ScriptEngine 的 put 方法用於將一個 Java 對象映射成一個腳本語言的變量。現在有一個 Java Class,它只有一個方法:

public class HelloWorld {
  String s = "Hello World";
  public void sayHello(){
    System.out.println(s);
  }
}

接下來就是讓 Js 使用這個類!還是看代碼最實在:

public class TestPut {
  public static void main(String[] args) throws ScriptException {
    ScriptEngineManager factory = new ScriptEngineManager();
    ScriptEngine engine = factory.getEngineByName("JavaScript");
    HelloWorld hello = new HelloWorld();
    engine.put("script_hello", hello);
    engine.eval("script_hello.sayHello()");
  }
}

首先我們實例化一個 HelloWorld,然後用 put 方法將這個實例映射為腳本語言的變量 script_hello。那麽我們就可以在 eval() 函數中像 Java 程序中同樣的方式來調用這個實例的方法。

使用 invokeFunction 來執行 Js 函數:

public class TestInv {
  public static void main(String[] args) throws Exception {
    ScriptEngineManager factory = new ScriptEngineManager();
    ScriptEngine engine = factory.getEngineByName("JavaScript");
    String script = "function say(first,second) { print(first +‘ ‘+ second); }";
    engine.eval(script);
    Invocable inv = (Invocable) engine;
    inv.invokeFunction("say", "Hello", "Tony");
  }
}

這裏使用了 ScriptEngine 的兩個可選接口之一 : Invocable,Invocable 表示當前的 engine 可以作為函數被調用。

這裏我們將 engine 強制轉換為 Invocable 類型,使用 invokeFunction 方法將參數傳遞給腳本引擎。invokeFunction 這個方法使用了可變參數的定義方式,可以一次傳遞多個參數,並且將腳本語言的返回值作為它的返回值。


Invocable 接口還有一個方法用於從一個 engine 中得到一個 Java Interface 的實例,它接受一個 Java 的 Interface 類型作為參數,返回這個 Interface 的一個實例。

也就是說你可以完全用腳本語言來寫一個 Java Interface 的所有實現!然後直接調用使用返回的實現類就可以了!

engine.eval(script);
Invocable inv = (Invocable) engine;
MaxMin maxMin = inv.getInterface(MaxMin.class);

是不是感覺很爽?

其他

其他的還可以對 Js 進行編譯、在 Js 中調用 Java 的代碼,但感覺用的不多,最常用的還是 Java 調用 Js 的方法

參考

https://www.ibm.com/developerworks/cn/java/j-lo-mustang-script/index.html

https://www.w3cschool.cn/java/scripting-in-java-call-javascript-function.html

Java中調用JavaScript方法