2025-02-15

現在,許多 Java 開發人員都喜歡在 Java 平臺中使用腳本語言,但是使用編譯到 Java 字節碼中的動態語言有時是不可行的。在某些情況中,直接編寫一個 Java 應用程序的腳本 部分 或者在一個腳本中調用特定的 Java 對象是更快捷、更高效的方法。

  這就是 javax.script 產生的原因瞭。Java Scripting API 是從 Java 6 開始引入的,它填補瞭便捷的小腳本語言和健壯的 Java 生態系統之間的鴻溝。通過使用 Java Scripting API,您就可以在您的 Java 代碼中快速整合幾乎所有的腳本語言,這使您能夠在解決一些很小的問題時有更多可選擇的方法。

  1. 使用 jrunscript 執行 JavaScript
  每一個新的 Java 平臺發佈都會帶來新的命令行工具集,它們位於 JDK 的 bin 目錄。Java 6 也一樣,其中 jrunscript 便是 Java 平臺工具集中的一個不小的補充。

  設想一個編寫命令行腳本進行性能監控的簡單問題。這個工具將借用 jmap(見本系列文章 前一篇文章 中的介紹),每 5 秒鐘運行一個 Java 進程,從而瞭解進程的運行狀況。一般情況下,我們會使用命令行 shell 腳本來完成這樣的工作,但是這裡的服務器應用程序部署在一些差別很大的平臺上,包括 Windows? 和 Linux?。系統管理員將會發現編寫能夠同時運行在兩個平臺的 shell 腳本是很痛苦的。通常的做法是編寫一個 Windows 批處理文件和一個 UNIX? shell 腳本,同時保證這兩個文件同步更新。

  但是,任何閱讀過 The Pragmatic Programmer 的人都知道,這嚴重違反瞭 DRY (Dont Repeat Yourself) 原則,而且會產生許多缺陷和問題。我們真正希望的是編寫一種與操作系統無關的腳本,它能夠在所有的平臺上運行。

  當然,Java 語言是平臺無關的,但是這裡並不是需要使用 “系統” 語言的情況。我們需要的是一種腳本語言 — 如,JavaScript。
  清單 1 顯示的是我們所需要的簡單 shell 腳本:
  清單 1. periodic.js
  while (true)
  {
  echo(“Hello, world!”);
  }
  由於經常與 Web 瀏覽器打交道,許多 Java 開發人員已經知道瞭 JavaScript(或 ECMAScript;JavaScript 是由 Netscape 開發的一種 ECMAScript 語言)。問題是,系統管理員要如何運行這個腳本?
  當然,解決方法是 JDK 所帶的 jrunscript 實用程序,如清單 2 所示:
  清單 2. jrunscript
  C:developerWorks5things-scriptingcodejssrc>jrunscript periodic.js
  Hello, world!
  Hello, world!
  Hello, world!
  Hello, world!
  Hello, world!
  Hello, world!
  Hello, world!
  …
  註意,您也可以使用 for 循環按照指定的次數來循環執行這個腳本,然後才退出。基本上,jrunscript 能夠讓您執行 JavaScript 的所有操作。惟一不同的是它的運行環境不是瀏覽器,所以運行中不會有 DOM。因此,最頂層的函數和對象稍微有些不同。

  因為 Java 6 將 Rhino ECMAScript 引擎作為 JDK 的一部分,jrunscript 可以執行任何傳遞給它的 ECMAScript 代碼,不管是一個文件(如此處所示)或是在更加交互式的 REPL(“Read-Evaluate-Print-Loop”)shell 環境。運行 jrunscript 就可以訪問 REPL shell。

  2. 從腳本訪問 Java 對象
  能夠編寫 JavaScript/ECMAScript 代碼是非常好的,但是我們不希望被迫重新編譯我們在 Java 語言中使用的所有代碼 — 這是違背我們初衷的。幸好,所有使用 Java Scripting API 引擎的代碼都完全能夠訪問整個 Java 生態系統,因為本質上一切代碼都還是 Java 字節碼。所以,回到我們之前的問題,我們可以在 Java 平臺上使用傳統的 Runtime.exec() 調用來啟動進程,如清單 3 所示:

  清單 3. Runtime.exec() 啟動 jmap
  var p = java.lang.Runtime.getRuntime().exec(“jmap”, [ “-histo”, arguments[0] ])
  p.waitFor()
  數組 arguments 是指向傳遞到這個函數參數的 ECMAScript 標準內置引用。在最頂層的腳本環境中,則是傳遞給腳本本身的的參數數組(命令行參數)。所以,在清單 3 中,這個腳本預期接收一個參數,該參數包含要映射的 Java 進程的 VMID。
  除此之外,我們可以利用本身為一個 Java 類的 jmap,然後直接調用它的 main() 方法,如清單 4 所示。有瞭這個方法,我們不需要 “傳輸” Process 對象的 in/out/err 流。

  清單 4. JMap.main()
  var args = [ “-histo”, arguments[0] ]
  Packages.sun.tools.jmap.JMap.main(args)
  Packages 語法是一個 Rhino ECMAScript 標識,它指向已經 Rhino 內創建的位於核心 java.* 包之外的 Java 包。

  3. 從 Java 代碼調用腳本
  從腳本調用 Java 對象僅僅完成瞭一半的工作:Java 腳本環境也提供瞭從 Java 代碼調用腳本的功能。這隻需要實例化一個 ScriptEngine 對象,然後加載和評估腳本,如清單 5 所示:

  清單 5. Java 平臺的腳本調用
  import java.io.*;
  import javax.script.*;
  public class App
  {
  public static void main(String[] args)
  {
  try
  {
  ScriptEngine engine =
  new ScriptEngineManager().getEngineByName(“javascript”);
  for (String arg : args)
  {
  FileReader fr = new FileReader(arg);
  engine.eval(fr);
  }
  }
  catch(IOException ioEx)
  {
  ioEx.printStackTrace();
  }
  catch(ScriptException scrEx)
  {
  scrEx.printStackTrace();
  }
  }
  }
  eval() 方法也可以直接操作一個 String,所以這個腳本不一定必須是文件系統的一個文件 — 它可以來自於數據庫、用戶輸入,或者甚至可以基於環境和用戶操作在應用程序中生成。
  4. 將 Java 對象綁定到腳本空間
  僅僅調用一個腳本還不夠:腳本通常會與 Java 環境中創建的對象進行交互。這時,Java 主機環境必須創建一些對象並將它們綁定,這樣腳本就可以很容易找到和使用這些對象。這個過程是 ScriptContext 對象的任務,如清單 6 所示:

  清單 6. 為腳本綁定對象
  import java.io.*;
  import javax.script.*;
  public class App
  {
  public static void main(String[] args)
  {
  try
  {
  ScriptEngine engine =
  new ScriptEngineManager().getEngineByName(“javascript”);
  for (String arg : args)
  {
  Bindings bindings = new SimpleBindings();
  bindings.put(“author”, new Person(“Ted”, “Neward”, 39));
  bindings.put(“title”, “5 Things You Didnt Know”);
  FileReader fr = new FileReader(arg);
  engine.eval(fr, bindings);
  }
  }
  catch(IOException ioEx)
  {
  ioEx.printStackTrace();
  }
  catch(ScriptException scrEx)
  {
  scrEx.printStackTrace();
  }
  }
  }
  訪問所綁定的對象很簡單 — 所綁定對象的名稱是作為全局命名空間引入到腳本的,所以在 Rhino 中使用 Person 很簡單,如清單 7 所示:

  清單 7. 是誰撰寫瞭本文?
  println(“Hello from inside scripting!”)
  println(“author.firstName = ” author.firstName)
  您可以看到,JavaBeans 樣式的屬性被簡化為使用名稱直接訪問,這就好像它們是字段一樣。

  5. 編譯頻繁使用的腳本
  腳本語言的缺點一直存在於性能方面。其中的原因是,大多數情況下腳本語言是 “即時” 解譯的,因而它在執行時會損失一些解析和驗證文本的時間和 CPU 周期。運行在 JVM 的許多腳本語言最終會將接收的代碼轉換為 Java 字節碼,至少在腳本被第一次解析和驗證時進行轉換;在 Java 程序關閉時,這些即時編譯的代碼會消失。將頻繁使用的腳本保持為字節碼形式可以幫助提升可觀的性能。

  我們可以以一種很自然和有意義的方法使用 Java Scripting API。如果返回的 ScriptEngine 實現瞭 Compilable 接口,那麼這個接口所編譯的方法可用於將腳本(以一個 String 或一個 Reader 傳遞過來的)編譯為一個 CompiledScript 實例,然後它可用於在 eval() 方法中使用不同的綁定重復地處理編譯後的代碼,如清單 8 所示:

  清單 8. 編譯解譯後的代碼
  import java.io.*;
  import javax.script.*;
  public class App
  {
  public static void main(String[] args)
  {
  try
  {
  ScriptEngine engine =
  new ScriptEngineManager().getEngineByName(“javascript”);
  for (String arg : args)
  {
  Bindings bindings = new SimpleBindings();
  bindings.put(“author”, new Person(“Ted”, “Neward”, 39));
  bindings.put(“title”, “5 Things You Didnt Know”);
  FileReader fr = new FileReader(arg);
  if (engine instanceof Compilable)
  {
  System.out.println(“Compiling….”);
  Compilable compEngine = (Compilable)engine;
  CompiledScript cs = compEngine.compile(fr);
  cs.eval(bindings);
  }
  else
  engine.eval(fr, bindings);
  }
  }
  catch(IOException ioEx)
  {
  ioEx.printStackTrace();
  }

  catch(ScriptException scrEx)
  {
  scrEx.printStackTrace();
  }
  }
  }
  在大多數情況中,CompiledScript 實例需要存儲在一個長時間存儲中(例如,

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *