Guiceで簡易的なプラグインシステムを構築するには
Java 界隈には OSGi といういかついダイナミックモジュールシステムがあるのですけども、これは解決しようとしている問題の量が非常に多いのでちょっとしたものを作るために使おうとすると非常に辛い。
そもそも、アプリケーションのブートストラップ部分から全部 OSGi ベースで作りこまないと良い感じに動いてくれません。
そこで、OSGi を前提としたアーキテクチャ設計が必要になるのですけども、ちょっと便利なツール作りたいだけなのに膨大な時間をかけて OSGi を勉強するかと言われると、しませんよね。
という訳で、Java において簡易的なプラグインシステムを Guice で作ってみましょうという話です。
尚、Spring 界隈で実現する方法については@makingさんのエントリをどうぞ。
コードの場所
必要に応じてエントリ内にコードはありますが、実際に動作するものは GitHub 上にあります。
制限事項
ここで作るプラグインシステムでは幾つかの明確な制限事項があります。
- 単一のプロセスで動作する
- 複数のプロセスでプラグインを自律的に共有することはできない
- 単一のクラスローダを前提に動作する
- プラグインシステムとプラグインが同一のクラスローダ上にいるので、クラスパス次第ではクラス定義の乗っ取りが発生する可能性がある、つまりセキュアではない
- プラグインの追加や削除する際にプロセスを再起動しなければならない
- プラグインシステム及びプラグインの自動的なバージョンアップはできない
- プラグインシステムで定義した拡張ポイントにだけプラグインできる
- 追加されたプラグインが更に新しい拡張点を宣言することはできない、つまり、プラグインをプラグインできる仕組みではない
- 追加したプラグインの数に比例してプラグインシステムの起動時間が伸びる
- プラグインの探索を遅延評価することはできない
- GUI アプリケーションを想定しない
- plugin.xml のような自己記述的設定ファイルを持たない
もし、これらの制限事項を乗り越えたいのであれば、OSGi を学習すべきです。
ServiceLoader をちゃんと使う
Java の標準 API にはjava.util.ServiceLoaderというクラスローダをプラグインシステムっぽく使うための便利クラスが定義されています。それを更に便利にした API がjavax.imageio.spi.ServiceRegistryなんですけども、余り知られていないように思います。
最初は ServiceLoader を使ってアプリケーション起動時に機能を拡張する仕組みを作ってみましょう。これを少しづつ改善しながらプラグインシステムを構築していきます。
サンプルコードは、
です。
拡張ポイントの定義
プラグインを表すインターフェースを定義します。
package serviceloader;
public interface Plugin {
String name();
void initialize();
}
拡張ポイントを集約する
ServiceLoader でインスタンス化するコードは以下のようになります。
ここでは、名前でプラグインを検索できるようにしてみました。
package serviceloader;
import java.util.*;
public class PluginRegistry {
Map<String, Plugin> plugins = new HashMap<>();
public PluginRegistry() {
for (Plugin plugin : ServiceLoader.load(Plugin.class)) {
plugin.initialize();
this.plugins.put(plugin.name(), plugin);
}
}
public Optional<Plugin> find(String name) {
return Optional.ofNullable(this.plugins.get(name));
}
}
この ServiceLoader で Plugin をインスタンス化するには、META-INF/services/serviceloader.Plugin
というファイルの中に実装クラスの FQN を記述します。
serviceloader.FirstPlugin
## コメントもかける
serviceloader.SecondPlugin
ここでのポイントは、このプロバイダ構成ファイルには改行区切りで複数のクラス名を記述できる事です。
プラグインの実装コード
プロバイダ構成ファイルには2つのクラスを書きましたが、その片方だけ例示しておきます。
package serviceloader;
public class FirstPlugin implements Plugin {
boolean initialized = false;
@Override
public String name() {
return "first-plugin";
}
@Override
public void initialize() {
this.initialized = true;
}
}