歡迎您光臨本站 註冊首頁

使用JavaScript腳本化Java應用

←手機掃碼閱讀     火星人 @ 2014-03-09 , reply:0

  腳本化技術

  我喜歡在 vim 或者 emacs 編輯環境中進行文檔,代碼以及郵件等的編寫,她們都提供了良好的命令和快捷鍵,但是這些都不足以是的她們被譽為 world-class 編輯器,她們的強大的真正來源,正是腳本技術.使用腳本,您可以將您的 vim 或者 emacs 配置得無所不能,甚至有人通過腳本來 讓 emacs 煮咖啡.

  什麼是腳本化

  腳本化可以使 宿主 程序具有 腳本 所描述的能力,比如流行在 DHTML 頁面中的 JavaScript 技術,JavaScript 可以讓原本是靜態的 HTML 代碼的頁面「活」起來,具有動畫,局部刷新等更高級的功能.應用程序一般是以二進位的形式發布的,用戶很難根據自己的需求對其進行定製,當然,修改配置文件是一種方式,但是不夠靈活.而腳本化則是通過用戶自己設計腳本(程序代碼 ),然後將其 注入 到應用中,是的應用的行為得到改變.

  如何腳本化您的應用

  通常的做法是,將 宿主 程序的一部分組件暴露給腳本,以方便腳本對其定製,這些組件的作用範圍是全局的(可以通過公開介面暴露,也可以將組件實例設置到腳本上下文(context)中),腳本可以在其中添加,修改一些子組件,從而實現定製的目的.本文將通過一個實例來對這個過程以說明,在文章的,我們可以得到一個可以運行的小應用出來,如果您對其有不滿意之處,可以任意的擴展它.

  JDK 6 中,添加了對腳本的支持,並實現了一些常見的腳本語言與 Java 的交互,比如 Python(Jython)、 JavaScript(rhino)等語言,完整的列表請參考 此處.文中使用的腳本語言為 JavaScript,宿主語言為 Java.(JavaScript 在 DHTML 中應用很廣泛,同時,也是我最喜歡的一門編程語言)

  一個小的 todo 管理器

  在文中,我們會先實現一個小型的應用:一個簡單的 todo(待辦事項)管理器,然後開發一個插件(腳本)框架,將使用這個框架對 todo 管理器進行腳本化.

  圖 1. sTodo 主界面

  

  這是一個簡單的 todo 管理器,可以對待辦事項(todo item)進行增刪改查等操作,並且可以將這些事項通過郵件發送給指定郵箱等.這個項目目前託管在 Google,項目名為 sTodo.

  圖 2. sTodo 右鍵菜單

  

  設計和實現

  sTodo 是用純 Java 的 Swing 工具包開發的,其中包含一個嵌入式的資料庫 sqlite,整個應用非常簡單,我們現在考慮為其增加腳本框架,並為其開發兩個腳本,擴展其部分功能.完整的代碼可以從 示例代碼 中獲得.由於 sTodo 為一個開源項目,並且主要由本文開發和維護,可以自由的對其進行修改、擴展,使其成為一個真實可用的應用.

  在開始之前,讀者可以在 sTodo 的項目主頁上下載未經過腳本化的初始版本的源代碼,然後根據文中的步驟自己逐步給 sTodo 加入插件機制.

  編寫腳本框架

  sTodo 中除了主界面之外,還包含其他一些窗口,如用戶配置設置(preference)、新建待辦事項窗口、發送郵件窗口等,這些窗口的實現與腳本化無關,我們主要來看看腳本框架的設計與實現.(如果您恰好對 swing 開發感興趣,可以參考 sTodo 的源碼.)

  設計和實現

  JDK 6 之後,對腳本的支持是對腳本引擎(Script Engine)的抽象,JDK 提供的框架設計得非常好,我們在此只是對其進行一個淺包裝.具體的功能需要代理到 JDK 的實現上.

  下面是插件框架的類圖:

  圖 3. 插件框架類圖

  

  我們現在有了對插件的描述的介面(Plugin),以及對插件管理的介面(PluginManager),並且有了具體的實現類,下面就分別描述一下:

  插件介面:

  定義一個插件所具備的基本功能,包括獲取插件名稱、獲取插件描述、以及將鍵值對插入到插件的上下文、執行插件公開的功能等方法.

  插件管理器介面:

  定義管理所有插件的管理器,包括安裝插件、卸載插件、激活插件、按名稱獲取插件等方法.

  好了,這個簡單的框架基本滿足我們的需求.在實現中,我們可以比較簡單地將 JDK 6 提供的腳本引擎做一個包裝.

  由於插件管理器(PluginManager)的作用範圍是全局的,我們將其實現為一個單例的對象:


  代碼 1. sTodo 插件管理器
  public class TodoPluginManager implements PluginManager {
  private List plist;
  private static TodoPluginManager instance;
  public static TodoPluginManager getInstance() {
  if (instance == null) {
  instance = new TodoPluginManager();
  }
  return instance;
  }
  private TodoPluginManager() {
  plist = new ArrayList(1);
  }
  public void activate(Plugin plugin) {
  }
  public void deactivate(Plugin plugin) {
  }
  public Plugin getPlugin(String name) {
  for (Plugin p : plist) {
  if (p.getName().equals(name)) {
  return p;
  }
  }
  return null;
  }
  public void install(Plugin plugin) {
  plist.add(plugin);
  }
  public List listPlugins() {
  return plist;
  }
  public void removePlugin(String name) {
  for (int i = 0; i < plist.size(); i ) {
  plist.get(i).getName().equals(name);
  plist.remove(i);
  break;
  }
  }
  public void uninstall(Plugin plugin) {
  plist.remove(plugin);
  }
  public int getPluginNumber() {
  return plist.size();
  }
  }

  插件本身比較容易實現,包含一個名為 context 的 Map,以及一些 getter/setter:


 代碼 2. sTodo 插件實現
  public class TodoPlugin implements Plugin {
  private String name;
  private String desc;
  private Map context;
  private ScriptEngine sengine;
  private Invocable engine;
  public TodoPlugin(String file, String name, String desc) {
  this.name = name;
  this.desc = desc;
  context = new HashMap();
  sengine = RuntimeEnv.getScriptEngine();
  engine = RuntimeEnv.getInvocableEngine();
  try {
  sengine.eval(new java.io.FileReader(file));
  } catch (FileNotFoundException e) {
  e.printStackTrace();
  } catch (ScriptException e) {
  e.printStackTrace();
  }
  }
  public TodoPlugin(URL url) {
  }
  public Object execute(String function, Object... objects) {
  Object result = null;
  try {
  result = engine.invokeFunction(function, objects);
  } catch (ScriptException e) {
  e.printStackTrace();
  } catch (NoSuchMethodException e) {
  e.printStackTrace();
  }
  return result;
  }
  public List getAvailiableFunctions() {
  return null;
  }
  public String getDescription() {
  return desc;
  }
  public String getName() {
  return name;
  }
  public void setName(String name) {
  this.name = name;
  }
  public void setDescription(String desc) {
  this.desc = desc;
  }
  /**
  * put value to plug-in context, and then put it into engine context
  */
  public void putValueToContext(String key, Object obj) {
  context.put(key, obj);
  sengine.put(key, obj);
  }
  }

  對執行環境的包裝,主要是對 JDK 提供的 Script Engine 的封裝:


代碼 3. 運行時環境的實現
  public class RuntimeEnv {
  private static ScriptEngineManager manager;
  private static ScriptEngine engine;
  static {
  manager = new ScriptEngineManager();
  engine = manager.getEngineByName("JavaScript");
  }
  public static ScriptEngine getScriptEngine() {
  return engine;
  }
  public static Invocable getInvocableEngine() {
  return (Invocable) engine;
  }
  }

  腳本化 stodo

  好了,基礎框架我們已經有了,如何腳本化具體的應用呢?如前所述,通常的步驟是這樣的:

  公開宿主程序中的組件(Component),可以通過兩種方式:提供 get 方法;將 Component 的實例放進腳本的上下文中,腳本引擎會建立兩者的聯繫.

  在腳本中使用宿主公開的組件,對其進行修改,達到腳本化的目的,比如宿主中公開了 toolbar 組件,我們可以向其上添加一些有用的按鈕,並定製改按鈕的事件處理器.

  公開宿主程序中必要的組件

  ,我們為 sTodo 的入口類 sTodo.java 添加一個方法:


代碼 4. 給 sTodo 添加 initEnv 方法
  public void initEnv(){
  PluginManager pManager = TodoPluginManager.getInstance();
  Plugin menuBar = new TodoPlugin("menubar.js", "menubar", "menubar plguin");
  pManager.install(menuBar);
  List plist = pManager.listPlugins();
  menuBar.putValueToContext("pluginList", plist);
  }

  在 initEnv 中,我們創建一個新的插件,這個插件負責載入 menubar.js 腳本,然後將這個插件安裝在管理器上,我們將一個名為 pluginList 的 List 對象放到這個插件的上下文中.

  然後,我們來到 MainFrame.java 這個類中,在 initUI() 方法中,我們將 menubar 的實例 mBar 公開給腳本:


 代碼 5. 公開 JMenuBar 實例
  Plugin pMenuBar = TodoPluginManager.getInstance().getPlugin("menubar");
  pMenuBar.execute("_customizeMenuBar_", mbar);

  好了,我們來看下一步:

  提供第一個腳本

  我們提供的第一個腳本很簡單,為宿主程序添加一個菜單項,然後通過此菜單的事件處理器,我們讓該腳本彈出一個新的窗口,這個窗口顯示目前被載入到應用中的插件的列表.


  代碼 6. 第一個腳本
  importPackage(java.awt, java.awt.event)
  importPackage(Packages.javax.swing)
  importClass(java.lang.System)
  importClass(java.lang.reflect.Constructor)
  function buildPluginMenu(){
  var menuPlugin = new JMenu();
  menuPlugin.setText("Plugin");
  var menuItemListPlugin = new JMenuItem();
  menuItemListPlugin.setText("list plugins");
  menuItemListPlugin.addActionListener(
  new JavaAdapter(
  ActionListener, {
  actionPerformed : function(event){
  var plFrame = new JFrame("plugins list");
  var epNote = new JEditorPane();
  var s = "";
  for(var i = 0; i
  var pi = pluginList.get(i);
  s = pi.getName() ":" pi.getDescription() "n";
  }
  epNote.setText(s);
  epNote.setEditable(false);
  plFrame.add(epNote, BorderLayout.CENTER);
  plFrame.setSize(200,200);
  plFrame.setLocationRelativeTo(null);
  plFrame.setVisible(true);
  }
  }
  )
  );
  menuPlugin.add(menuItemListPlugin);
  return menuPlugin;
  }
  //this function will be invoked from java code, MainFrame...
  function _customizeMenuBar_(menuBar){
  menuBar.add(buildPluginMenu());
  }

  我們在腳本中創建一個菜單項,名稱為 plugin,這個菜單項中有一個名為 list plugins 的項目,點擊之後會彈出一個對話框,顯示目前被應用到 sTodo 中的插件(腳本):

  圖 4. 點擊 list plugins

  

  圖 5. 顯示目前被應用到 sTodo 中的插件(腳本)

  

  為了保證 list plugins 的功能,我在 initEnv() 方法中加入了另一個插件 style.js.因此我們可以看到,彈出的窗口正確的顯示了目前被載入的插件,這些信息均來自於宿主程序!

  提供第二個腳本

  通常情況下,您可能已經有了一個寫的比較好的應用模塊,而想要在另一個應用中使用這個模塊,比如您有一個 org.free.something 的包,裡邊已經包含了您寫的某個面板,其中包含版權信息聲明等.現在您開發出了另一個應用,如果把兩者集成那就最好了.

  我們開發的第二個插件就是涉及如何引用外部包的問題:

  比如,我們已經有了一個良好的 Help 界面,定義如下:


 代碼 7. 一個已有的 Dialog
  public class HelpDialog extends JDialog{
  private static final long serialVersionUID = -146997705470075999L;
  private JFrame parent;
  public HelpDialog(JFrame parent, String title){
  super(parent, title, true);
  this.parent = parent;
  initComponents();
  }
  private void initComponents(){
  setSize(200, 200);
  add(new JLabel("Here is the help content..."), BorderLayout.NORTH);
  JButton button = new JButton("Click to close help.");
  button.addActionListener(new ActionListener(){
  public void actionPerformed(ActionEvent e) {
  HelpDialog.this.setVisible(false);
  }
  });
  add(button);
  setDefaultCloseOperation(HIDE_ON_CLOSE);
  setLocationRelativeTo(null);
  setResizable(false);
  setVisible(true);
  }
  public static void main(String[] args){
  new HelpDialog(null, "This is help");
  }
  }

  注意,這個類是定義在另一個包中!然後我們在第一個腳本中添加一個 Javascript 方法:


代碼 8. 擴展腳本一
  function buildHelpMenu() {
  var menuHelp = new JMenu();
  menuHelp.setText("Help");
  var menuItemHelp = new JMenuItem();
  menuItemHelp.setText("Help");
  menuItemHelp.addActionListener(
  new JavaAdapter(
  ActionListener, {
  actionPerformed : function(event){
  importPackage(Packages.org.someone.dialog);
  var hDialog = new HelpDialog(null, "This is Help");
  }
  }
  )
  );
  menuHelp.add(menuItemHelp);
  return menuHelp;
  }

  通過腳本引擎,我們導入這個包:


  代碼 9. 導入一個外部 jar 包中的類文件
  importPackage(Packages.org.someone.dialog);

  然後,在不需要修改 Java 代碼的情況下,我們將


 function _customizeMenuBar_(menuBar) {
  menuBar.add(buildPluginMenu());
  }

  改為:


 代碼 10. 修改腳本的入口
  function _customizeMenuBar_(menuBar){
  menuBar.add(buildPluginMenu());
  menuBar.add(buildHelpMenu());
  }

  然後運行 sTodo:

  圖 6. 點擊 Help

  

  圖 7. 運行 Help

  

  結束語

  事實上,幾乎所有的東西都是可以定製的,您的應用只需要提供一個基本而穩健的框架,剩餘的事情全部可以交給腳本來完成,那樣,您可以在不對應用做任何調整的情況下,使其徹底的改頭換面,比如將一個簡單的編輯器定製成一個強大的 IDE,正如 Eclipse 那樣.不過使用腳本更輕量級一些.


[火星人 ] 使用JavaScript腳本化Java應用已經有551次圍觀

http://coctec.com/docs/java/show-post-60488.html