---
title: DIコンテナで実現する簡易プラグイン機構
tags: ["Java", "Spring"]
categories: ["Programming", "Java", "org", "springframework"]
date: 2015-04-13T17:46:04Z
updated: 2015-04-13T17:46:04Z
---
先日のJJUG CCCの[『Embulk』に見るモダンJavaの実践的テクニック
∼並列分散処理システムの実装手法∼](http://www.slideshare.net/HiroshiNakamura/embulk-20150411)を受付からちら見していたとき、
DIコンテナにGuiceを使っており、プラグイン機構にはService Loaderを使っていると聞きました。
断片的に聞いており、
とつぶやいたら、発表者のnahiさんやtokuhiromさんから反応があったので、一応ブログに書いておきます。
自分が思っていたことはそんな難しいものではなく、簡易プラグイン機構はDIで普通にできるよねって話です。
Service Loaderでも最低限のことはできるけど、これはDI使いたくない人向けみたいなものだし、DIコンテナ使っているのならDIコンテナに任せればいいのでは?という思っています。
ここでいう簡易プラグインとは、
* 特定のインタフェースを持っている
* 複数登録できる
* 起動中に動的に変更することはない
* マルチバージョン管理はしない
といったものです。OSGIとか使う必要のないシーンを想定しています。
### 基本編
まずは基本パターンから説明します。
package demo;
public interface MyPlugin {
String action(String input);
}
こんなインタフェースがあったとして、このプラグインを使う製品が
package demo;
import java.util.List;
public class MyProduct {
List plugins;
public void execute(String input) {
System.out.println(plugins);
String result = input;
for (MyPlugin plugin : plugins) {
result = plugin.action(result); // プラグインの結果を次々に適用していく(普通はこんなことしないか・・)
}
System.out.println(result);
}
}
こんな感じだとします。この製品を使うユーザーは自由にプラグインを追加できるケース。
このプラグイン群をいかにして引っ張りあげるかがポイントですが、
確かにJava SE 6で追加された`ServiceLoader`で[実現できます](http://itpro.nikkeibp.co.jp/article/COLUMN/20061215/257003/)。
DIコンテナを使わない条件であれば、`ServiceLoader`で十分だけど、DIコンテナ使っているんならこのくらいDIコンテナでやったほうがいいんじゃない?という率直な感想が今回のツイートの発端となっています。
Spring Frameworkの場合は、DIコンテナに登録されたBeanが複数の同じインタフェースを持っていたら、`List`や `Map`で引っこ抜くことは簡単です。
例えば、次のようなBean定義があるとします(ここではJavaConfigでBean定義する)
package demo;
import aaa.AaaPlugin;
import bbb.BbbPlugin;
import ccc.CccPlugin;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
// XML定義でいうところのに相当する
@Bean
AaaPlugin aaaPlugin() {
return new AaaPlugin();
}
@Bean
BbbPlugin bbbPlugin() {
return new BbbPlugin();
}
@Bean
CccPlugin cccPlugin() {
return new CccPlugin();
}
@Bean
MyProduct myProduct() {
return new MyProduct();
}
}
3つのプラグインは次のような実装です。
package aaa;
import demo.MyPlugin;
public class AaaPlugin implements MyPlugin {
@Override
public String action(String input) {
return input.replace("a", "◯");
}
}
package bbb;
import demo.MyPlugin;
public class BbbPlugin implements MyPlugin {
@Override
public String action(String input) {
return input.replace("b", "●");
}
}
package ccc;
import demo.MyPlugin;
public class CccPlugin implements MyPlugin {
@Override
public String action(String input) {
return input.replace("c", "◎");
}
}
製品コードである`MyProduct`には `List`をそのままインジェクションできます。
package demo;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
public class MyProduct {
@Autowired // ここではオートワイヤリングを使ってインジェクション
List plugins;
public void execute(String input) {
System.out.println(plugins);
String result = input;
for (MyPlugin plugin : plugins) {
result = plugin.action(result);
}
System.out.println(result);
}
}
この`MyProduct`を実行するエントリポイントクラスは次のように書きます。
package demo;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class App {
public static void main(String[] args) {
try (GenericApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class)) {
// DIコンテナから登録されたBeanをルックアップ
MyProduct product = context.getBean(MyProduct.class);
String input = "blackberry";
product.execute(input);
}
}
}
この`main`メソッドを実行すると、以下のように出力されます。
[aaa.AaaPlugin@60438a68, bbb.BbbPlugin@140e5a13, ccc.CccPlugin@3439f68d]
●l◯◎k●erry
`AaaPlugin`、`BbbPlugin`、`CccPlugin`の結果が全て適用されています。(こんな冪等にならなさそうなプラグインシステムは作らないでしょうねw)
ちなみに`Map`でインジェクションして`pirintln`すると、
@Autowired
Map pluginMap;
// ...
System.out.println(pluginMap);
出力結果は次のように、Bean ID(JavaConfigではデフォルトではメソッド名)とインスタンスのマッピングが表示されます。
{aaaPlugin=aaa.AaaPlugin@60438a68, bbbPlugin=bbb.BbbPlugin@140e5a13, cccPlugin=ccc.CccPlugin@3439f68d}
ここまでで、 `ServiceLoader`的なことを実現する方法はわかると思います。
次に検討ポイントとなるのは、プラグインの登録方法です。上記例では製品コードにプラグイン定義を書いたので現実的ではありません。
やりたいのはユーザーが書いたプラグインを含むjarを製品実行時のクラスパスに追加するとそのプラグインが読み込まれるというやり方。
一番簡単なのは、コンポーネントスキャンを使う方法です。
package demo;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
@ComponentScan(basePackages = {"スキャン対象のプラグインパッケージ(トップの階層でOK)"},
includeFilters = @ComponentScan.Filter(value = MyPlugin.class, type = FilterType.ASSIGNABLE_TYPE))
@Configuration
public class AppConfig {
@Bean
MyProduct myProduct() {
return new MyProduct();
}
}
この設定では特定のパッケージ配下の`MyPlugin`インタフェースを実装したクラスを根こそぎDIコンテナに登録できます。スキャン対象が広いと起動が遅くなってしまうので、何かしらプラグインパッケージに規約をさだめるのが良いでしょう。
パッケージルールが嫌な場合は、ユーザーがBean定義とセットでjarを作るという方法があります。
package demo;
import org.springframework.context.annotation.*;
@ImportResource("classpath*:META-INF/plugins.xml")
@Configuration
public class AppConfig {
@Bean
MyProduct myProduct() {
return new MyProduct();
}
}
これはクラスパス配下の任意の`META-INF/plugins.xml`を読み込む設定です。
プラグイン作成側は、プラグイン実装と`META-INF/plugins.xml`に次のような設定を書いてjarを作れば良いです。
設定ファイルパスのルールを決めることでプラグインパッケージは自由に決めることができます。
これは`ServiceLoader`パターンとほとんど同じですね。
これすら嫌な場合、以下のようにプログラマティックにBeanを登録することができるので、自分でプラグイン定義方法を考えて、それを読み込む実装を行えば良いでしょう。
package demo;
import aaa.AaaPlugin;
import bbb.BbbPlugin;
import ccc.CccPlugin;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class AppConfig {
@Bean
InitializingBean pluginRegisterer() {
return new InitializingBean() {
@Autowired
AnnotationConfigApplicationContext context;
@Override
public void afterPropertiesSet() throws Exception {
// プラグインクラス名をなんらかの形で取得する仕組みを作れば良い
context.register(AaaPlugin.class, BbbPlugin.class, CccPlugin.class);
}
};
}
@Bean
MyProduct myProduct() {
return new MyProduct();
}
}
ただ、このやり方はあまり見ない。
余談ですが、上記コードを実行するのに必要な依存関係は
org.springframework
spring-context
4.1.6.RELEASE
org.slf4j
jcl-over-slf4j
1.7.8
org.slf4j
slf4j-simple
1.7.8
だけです。Spring使うと依存関係の定義が大変と思っている人も多いと思いますが、コアな部分に限定すると大したことありません。
ここまで基本的な話です。
### Spring Plugin
このような簡易プラグイン機構をSpringでより簡単に扱うために[Spring Plugin](https://github.com/spring-projects/spring-plugin/)というプロジェクトがあります。
Spring Pluginでは以下のような`Plugin`インタフェースが用意されており、
package org.springframework.plugin.core;
public interface Plugin {
// なんらかのデリミタ(たとえば入力値そのもの)に対して、このプラグインが対応しているかどうかを返す
boolean supports(S delimiter);
}
これを拡張して、自分のプラグインを作ります。前述の例に合わせると、以下のような感じになります。
package demo;
import org.springframework.plugin.core.Plugin;
public interface MyPlugin extends Plugin {
String action(String input);
}
これを実装したプラグインは、先ほどとほぼ同じで次のように実装できます。
package aaa;
import demo.MyPlugin;
public class AaaPlugin implements MyPlugin {
@Override
public boolean supports(String s) {
return s != null;
}
@Override
public String action(String input) {
return input.replace("a", "◯");
}
}
`BbbPlugin`、`CccPlugin`も同様です。
これらのプラグインをさっきと同じように、何らかのやり方でBean定義するのですが、
Spring PluinではDIコンテナに登録されたプラグインを管理するための`org.springframework.plugin.core.PluginRegistry`インタフェースが用意されています。
この`PluginRegistry`を通じて、登録されたプラグインを取得することができます。`MyProduct`のコードは次のようになります。
package product;
import demo.MyPlugin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.plugin.core.PluginRegistry;
import java.util.List;
public class MyProduct {
@Autowired
PluginRegistry pluginRegistry;
public void execute(String input) {
System.out.println(pluginRegistry.getPlugins());
String result = input;
List plugins = pluginRegistry.getPluginsFor(input);
for (MyPlugin plugin : plugins) {
result = plugin.action(result);
}
System.out.println(result);
}
}
`MyPlugin`用の`PluginRegistry`をつくるためのBean定義は以下のように行えます。
package product;
import demo.MyPlugin;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.plugin.core.config.EnablePluginRegistries;
@Configuration
@EnablePluginRegistries(MyPlugin.class)
@ImportResource("classpath*:META-INF/plugins.xml") // コンポーネントスキャンでも可
public class AppConfig {
@Bean
MyProduct product() {
return new MyProduct();
}
}
あとはユーザーに`AaaPlugin`の実装とそれを定義した`META-INF/plugins.xml`を含むjarを作ってもらい、`MyProduct`実行時にクラスパスに追加しておけば読み込まれます。
この製品のエントリポイントは基本編と同じで、
package product;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.support.GenericApplicationContext;
public class App {
public static void main(String[] args) {
try (GenericApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class)) {
MyProduct product = context.getBean(MyProduct.class);
String input = "blackberry";
product.execute(input);
}
}
}
です。これを実行すると、以下のように表示されます。
[aaa.AaaPlugin@58134517, bbb.BbbPlugin@4450d156, ccc.CccPlugin@4461c7e3]
●l◯◎k●erry
Spring Pluginの使い方は
org.springframework.plugin
spring-plugin-core
1.2.0.RELEASE
だけでOKです。
Spring Pluginでは他にもプラグインに[メタデータを加える機構](https://github.com/spring-projects/spring-plugin/#metadata)も用意されており、
プラグインシステムを作るのであればやりそうなことがサポートされています。
ただ、シンプルな実装であり、更新は年に1回くらいしか行われていないように見えます。(Springのバージョンアップくらい)
----
同様のことはたぶん他のDIフレームワークでも実現できると思います。Guiceだと[この辺](https://github.com/google/guice/wiki/Multibindings)ですかね。
DIコンテナにプラグインを管理させると、
* インスタンスのスコープを制御できる(singletonとかprototypeとか)
* インスタンスのライフサイクルイベントにフックできる(`@PostConstruct`とか`@PreDestroy`とか。Springだとprototypeのときは効かないけど・・)
* AOPをかけられる(共通ログとかキャッシュとか)
* 製品側のインフラストラクチャーコードをインジェクションできる
などのメリットがあります。
この辺のメリットが不要な場合は`ServiceLoadaer`でもいいかなーと思います。
本記事で扱ったサンプルコードは[こちら](https://github.com/making/plugin-sample)です。