腳本化技術
我喜歡在 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應用已經有721次圍觀