インジェクションの方法
wicket-guiceエクステンション
一般的にはwicket-guiceエクステンションのGuiceComponentInjectorを使うことになっているが、これは非常に問題が多い。まず、「オープンソース徹底活用 WicketによるWebアプリケーション開発」の380ページの例はおそらく間違い(ただし、この本は買って損の無い本です。とても助かってます)。
public class GuiceTestPage extends WebPage { @Inject Service service; public GuiceTestPage() { String message = service.doSomething(); add(new Label("messageLabel", message)); } }
とあるが、
そもそもGuiceではフィールドインジェクションされたオブジェクトをコンストラクタ内で使用することはできない。というより、通常のGuiceの動作では(もしあれば)コンストラクタ注入を行い、コンストラクタが実行された後でフィールドインジェクションが行われるため、仮にGuiceのInjectorを使用してGuiceTestPageのインスタンスを生成したとしても上記の動作は成立しない。
それに加えて、Guiceエクステンションの前提は、GuiceTestPageを単純にnewすること。どういうことかというと、例えばsetResponsePage(GuiceTestPage.class)とすると、Wicket内でGuiceTestPageのインスタンス作成が行われるわけだが、その際の生成方法としては、単純なnewに相当するものが使用される。したがって、コンストラクタの実行時にはGuiceは全く無関係である。そして、GuiceTestPageのコンストラクタ内で、ページ以下のコンポーネント(ラベルやフィールド等)がすべて生成された後で初めてGuiceComponentInjectorによって、フィールドインジェクションやメソッドインジェクションが行われるのである。
したがって、二重の意味で上記の例は間違っている。
つまり、GuiceComponentInjectorを使用しようが、元々のGuiceのInjectorを使用しようが、例えば「コンストラクタ内でデータベースを参照し、その内容によってラベル内のメッセージを変更する」(データベース参照をInjectするのであれば)と言ったことはできない。
上記のようなフィールドインジェクションの有効な利用方法としては、以下のようなケースである。
public class GuiceTestPage extends WebPage { @Inject Service service; public GuiceTestPage() { add(new Form<Void>("form") { @Override protected void onSubmit() { service.doSomething(); } }); } }
この場合はレンダリング時にServiceが利用されることはない。フォームがサブミットされて初めてdoSomethingが呼び出される。
解決法
この問題の解決はいくつかあると思われる。一つは、グローバルな領域にInjectorを保持しておき、それを参照することである。
public class InjectorHolder { public static Injector injector; } ... public class GuiceTestPage extends WebPage { public GuiceTestPage() { Service service = InjectorHolder.injector.getInstance(Service.class); String message = service.doSomething(); add(new Label("messageLabel", message)); } }
もう一つは、あらかじめSessionにInjectorを格納しておき、ページコンストラクタでそれを取り出す方法がある。Injectorのインスタンスは特にSessionごとに変更されるわけではないので、これは冗長であろう(コンポーネントからはApplicationにもアクセスできるので、Applicationに保持しておくのもいいかもしれない)。
public class GuiceTestPage extends WebPage { public GuiceTestPage() { MySession session = (MySession)getSession(); Injector injector = session.getInjector(); Service service = injector.getInstance(Service.class); String message = service.doSomething(); add(new Label("messageLabel", message)); } }
さらに一つは、コンストラクタ自体を正常な形にしてGuiceにGuiceTestPageのインスタンスを作成させることである。
public class GuiceTestPage extends WebPage { @Inject public GuiceTestPage(Service service) { String message = service.doSomething(); add(new Label("messageLabel", message)); } }
以下ではこの解決法をさぐってみる。
ページインスタンスをGuiceに作成させる方法(不可)
Wicketの内部をさぐってみると、setResponsePage()にページクラスが渡された場合、そのページのインスタンスを作成するのは、IPageFactoryというオブジェクトであることがわかる。 これをApplicationのinit()時に独自のものに置き換える方法が一つ考えられる。MyPageFactoryにてGuiceのInjectorを使い、ページインスタンスを生成するのである。
protected void init() { getSessionSettings().setPageFactory(new MyPageFactory()); } ... public class MyPageFactory implements IPageFactory { }
しかしこれではうまくいかない。なぜなら、
- 引数無しのコンストラクタの場合はよいが、パラメータ付のコンストラクタの取り扱いが面倒。
- ページのコンストラクタ内でnewしているパネルやフィールド等には適用されない。
結論
WicketにてGuiceを使う場合、GuiceComponentInjectorでは全く力不足である。かと言って他の方法でも満足のいく注入は難しい。それより、DIを捨ててInjectorをサービスロケータとして用いることである。Injectorインスタンスはアプリの実行中変更されることの無いただ一つのインスタンスなのであるから、これをシングルトンとして参照すればよい。ただし、以下のようにInjectorをラップした方がよいだろう。
public class ServiceLocator { private static Injector injector; public static void setInjector(Injector injector) { ServiceLocator.injector = injector; } public static <T> T getInstance(Class<T>type) { return injector.getInstance(type); } }
現在のところ、この解決方法しか思いつかない。
追加
DIでなくサービスロケータを使う理由は他にもある。 Wicketでは、ページ及びその中のコンポーネントはすべて直列化されるため、DIを使って注入されたオブジェクトもその際にすべて直列化される(されなければならない)のである。したがって、
- これらのオブジェクトはSerializableでなければならない。
- これらのオブジェクトが大きいとWicketの直列化領域を圧迫する。
- これらのオブジェクト内で、もしDBアクセス等を行っている場合(というより、間違いなく行うだろうが)、直列化復帰後に再度DBに接続しなければならない。
もちろん、これらの「注入されるオブジェクト」の中の一部のフィールドをtransientにするなどし、必要な場合は再度DBに接続する仕組みを提供すれば問題無いが、Wicketで無理やりDIを使いたいがために面倒な話になりかねないと思われる。
もともとDIに対応していない既存のフレームワークを利用しようとする場合、DIをそこに組み込むのは困難で面倒なことになりかねない。このような場合にもサービスロケータは有効である。Injectorをサービスロケータとして使うも参照のこと。
参考
WicketとSpringの連携法の文書を翻訳してくださってる方がいます。
ただ、Springの場合でもオプション1がベストかと思われるのですが。。。