先日のJJUG CCCの『Embulk』に見るモダンJavaの実践的テクニック
∼並列分散処理システムの実装手法∼を受付からちら見していたとき、
DIコンテナにGuiceを使っており、プラグイン機構にはService Loaderを使っていると聞きました。
断片的に聞いており、
DIコンテナ使ってるのにService Loader使うの負け(DIコンテナの機能不足)では? #jjug_ccc #ccc_cd4
— Toshiaki Maki (@making) 2015, 4月 11
とつぶやいたら、発表者の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<MyPlugin> 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
で実現できます。
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 id="aaaPlugin" clas="aaa.AaaPlugin" />に相当する
@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<MyPlugin>
をそのままインジェクションできます。
package demo;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.List;
public class MyProduct {
@Autowired // ここではオートワイヤリングを使ってインジェクション
List<MyPlugin> 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<String, MyPlugin> 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を作れば良いです。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean class="aaa.AaaPlugin"/>
</beans>
設定ファイルパスのルールを決めることでプラグインパッケージは自由に決めることができます。
これは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();
}
}
ただ、このやり方はあまり見ない。
余談ですが、上記コードを実行するのに必要な依存関係は
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>4.1.6.RELEASE</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>1.7.8</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.7.8</version>
</dependency>
だけです。Spring使うと依存関係の定義が大変と思っている人も多いと思いますが、コアな部分に限定すると大したことありません。
ここまで基本的な話です。
Spring Plugin
このような簡易プラグイン機構をSpringでより簡単に扱うためにSpring Pluginというプロジェクトがあります。
Spring Pluginでは以下のようなPlugin
インタフェースが用意されており、
package org.springframework.plugin.core;
public interface Plugin<S> {
// なんらかのデリミタ(たとえば入力値そのもの)に対して、このプラグインが対応しているかどうかを返す
boolean supports(S delimiter);
}
これを拡張して、自分のプラグインを作ります。前述の例に合わせると、以下のような感じになります。
package demo;
import org.springframework.plugin.core.Plugin;
public interface MyPlugin extends Plugin<String> {
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<MyPlugin, String> pluginRegistry;
public void execute(String input) {
System.out.println(pluginRegistry.getPlugins());
String result = input;
List<MyPlugin> 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の使い方は
<dependency>
<groupId>org.springframework.plugin</groupId>
<artifactId>spring-plugin-core</artifactId>
<version>1.2.0.RELEASE</version>
</dependency>
だけでOKです。
Spring Pluginでは他にもプラグインにメタデータを加える機構も用意されており、
プラグインシステムを作るのであればやりそうなことがサポートされています。
ただ、シンプルな実装であり、更新は年に1回くらいしか行われていないように見えます。(Springのバージョンアップくらい)
同様のことはたぶん他のDIフレームワークでも実現できると思います。Guiceだとこの辺ですかね。
DIコンテナにプラグインを管理させると、
- インスタンスのスコープを制御できる(singletonとかprototypeとか)
- インスタンスのライフサイクルイベントにフックできる(
@PostConstruct
とか@PreDestroy
とか。Springだとprototypeのときは効かないけど・・) - AOPをかけられる(共通ログとかキャッシュとか)
- 製品側のインフラストラクチャーコードをインジェクションできる
などのメリットがあります。
この辺のメリットが不要な場合はServiceLoadaer
でもいいかなーと思います。
本記事で扱ったサンプルコードはこちらです。