2014年9月23日 星期二

實作 Java 的套件系統(Plugin System):以 jspf 框架為例

在開發比較大一點的系統時,常會有個想法是想要做到可以讓使用者動態下載套件,然後系統就能夠即時把套件套用到系統上
在 Java 上這個概念可以透過 ClassLoader 的技巧來實現。
雖然網路上也有一些文章教導如何自行建立簡單的套件系統,不過現在其實也有一些 Open Source 的專案可以利用了~。

基於不要重複發明輪子的理念,我就開始快速地搜尋了既有的 plugin framework
而查到比較有人在討論的,大概是 OSGi、JPF 和 jspf 吧。
另外也可以看看 [1] 的討論,發問的網友 Marcus 也提到另一個 "Rolling your own" plugin framework。
我自己主要看了 OSGi、JPF 和 jspf 這三個框架,其中 JPF 因為看起來已經很久沒有在維護了,因此直接跳過它
OSGi 從官方網站上,我沒辦法很快看出它是不是開源的專案,所以也就被略過 XD
同時也看了 [1] 中的回應,jspf 的作者也回覆了那個問題,大略是提到說 jspf 主要目的是做輕量級的套件框架(而且不使用 XML)
因此最後就決定從 jspf 入手試試。

jspf 的官方網站 [2] 中,主要可以參考 FAQ 頁面以及 UsageGuide 頁面,大略就可以拼湊出函式庫的基本使用方法。
以下是我這邊建立的簡易範例,使用的 jspf 版本為 1.0.2,在 Java 7 環境下執行。
主要會建立兩個 Java 專案,一個是包含 main() 的主程式(專案名稱為 PluginSystem),另一個則是只有 plugin 的套件程式(專案名稱為 Plugin)。
兩個專案的資料夾結構如下:

[PluginSystem]
  - [src]
    - [plugin]
      - PluginMain.java
    - [test]
      - [plugin]
        - ExecutablePlugin.java

[Plugin]
  - [src]
    - [test]
      - [plugin]
        - ExecutablePlugin.java
        - [impl]
          - ExecutablePluginImpl.java

大略就是建了兩個 package "plugin" 和 "test.plugin",plugin 這個 package 放包含 main() 的主程式,test.plugin 則放套件的介面和實作
其中介面檔 ExecutablePlugin.java 同時放在兩個專案的相同 package,實作檔 ExecutablePluginImpl.java 則只有在 Plugin 這個專案裡。
此外,實作檔的放置位置依照官方文件的建議,放在介面所在的 package 底下的 impl 裡。

接著就是三個 Java 檔的實作內容。

主程式 PluginMain.java
package plugin;

import java.nio.file.Paths;
import java.util.Collection;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import test.plugin.ExecutablePlugin;
import net.xeoh.plugins.base.PluginManager;
import net.xeoh.plugins.base.impl.PluginManagerFactory;
import net.xeoh.plugins.base.util.PluginManagerUtil;

public class PluginMain {
  public static void main(String[] args) {
    Logger log = LogManager.getLogger("Main");
    log.debug("Start.");
    
    while (true) {
      PluginManager pm = PluginManagerFactory.createPluginManager();
      pm.addPluginsFrom(Paths.get("D:", "test", "plugin").toUri());
      
      log.debug("Try to run a plugin...");
      
      PluginManagerUtil pmu = new PluginManagerUtil(pm);
      Collection<ExecutablePlugin> colls = pmu.getPlugins(ExecutablePlugin.class);
      if(colls == null || colls.size() < 1) log.debug("No plugin is found.");
      else {
        for(ExecutablePlugin plugin : colls) {
          plugin.execute();
        }
      }
      try {
        Thread.sleep(2000);
      } catch (InterruptedException e) {}
    }
  }
}

套件的介面 ExecutablePlugin.java
package test.plugin;

import net.xeoh.plugins.base.Plugin;

public interface ExecutablePlugin extends Plugin {
  public void execute ();
}

套件的實作 ExecutablePluginImpl.java
package test.plugin.impl;

import net.xeoh.plugins.base.annotations.PluginImplementation;

import org.apache.log4j.LogManager;
import org.apache.log4j.Logger;

import test.plugin.ExecutablePlugin;

@PluginImplementation
public class ExecutablePluginImpl implements ExecutablePlugin {
  @Override
  public void execute() {
    Logger log = LogManager.getLogger("ExecutablePluginImpl");
    log.debug("Plugin is executed!");
  }
}

執行的測試中,一開始在 D:\test\plugin\ 資料夾中是空的,等到過了 6 秒(即主程式的 while 迴圈跑完三次)後才把 Plugin 專案匯出的 jar 放進去。
執行結果如下:
2014-09-23 18:03:54.449 | DEBUG | Main > Start.
2014-09-23 18:03:54.562 | DEBUG | Main > Try to run a plugin...
2014-09-23 18:03:54.564 | DEBUG | Main > No plugin is found.
2014-09-23 18:03:56.568 | DEBUG | Main > Try to run a plugin...
2014-09-23 18:03:56.568 | DEBUG | Main > No plugin is found.
2014-09-23 18:03:58.570 | DEBUG | Main > Try to run a plugin...
2014-09-23 18:03:58.570 | DEBUG | Main > No plugin is found.
2014-09-23 18:04:00.572 | DEBUG | Main > Try to run a plugin...
2014-09-23 18:04:00.572 | DEBUG | Main > No plugin is found.
2014-09-23 18:04:02.600 | DEBUG | Main > Try to run a plugin...
2014-09-23 18:04:02.600 | DEBUG | ExecutablePluginImpl > Plugin is executed!
2014-09-23 18:04:04.622 | DEBUG | Main > Try to run a plugin...
2014-09-23 18:04:04.622 | DEBUG | ExecutablePluginImpl > Plugin is executed!
可以看到是能夠動態地把中途突然出現的 jar 載入並正確執行!

此外,在嘗試實作的過程中,發現 jar 出現的時間好像跟 PluginManagerFactory.createPluginManager() 的呼叫時間是有關聯的。
如果在主程式中,將 PluginManagerFactory.createPluginManager() 移至 while 迴圈外頭
Plugin 專案的 jar 檔就必須在主程式一開始執行前就要放在套件的指定目錄裡,才能夠被動態偵測到。
這也是為什麼上面的測試程式是把 PluginManagerFactory.createPluginManager() 放在 while 迴圈裡面。

參考資料:
1、Java plugin framework choice
2、jspf: Java Simple Plugin Framwork. 5 minutes and it works. No XML.

沒有留言: