Wicket+SpringをJavaRebelでリロード可能にしてみた
JavaRebel 1.1M2 の変更点に Springでも動くようになったという記述を発見して、ちょうど良い機会だったので、前から気になっていた Wicket + Spring (wicket-spring-annot) な環境での問題に対処してみました。
Now you can develop Spring with JavaRebel (tested!)
JavaRebel Devel Changelog
この記述だけでは何をすれば良いのかさっぱりですが、BLOGの方にIntegrating JavaRebel with Springというタイムリーな記事が追加されてました。Spring自体にApplicationContextを更新する機能が用意されているので、Beanが見つからない場合に実行すれば良いみたいです。こんな機能があるとは知りませんでした。
((AbstractRefreshableApplicationContext)getWebApplicationContext()).refresh();
WicketとSpringを使った場合の問題として、実行中に @SpringBean なフィールドを追加しても無視されてしまうというのがあります。本来、JavaRebel はフィールドの追加やリフレクションに対応しているので、実行中に追加しても問題はないはずです。
インジェクション関係の処理を追ってみたところ、org.apache.wicket.injection.Injector がクラスのフィールドをリフレクションで取り出して、@SpringBeanの有無を確認するクラスだと分かりました。この Injector クラスは、最初にクラスのフィールドを取り出したときに、フィールドをキャッシュに格納しています。そのため、後からフィールドを追加しても無視されていたようです。
この問題を解決するには、JavaRebelがクラスをリロードした時に、Injector クラスのキャッシュから削除すれば良さそうです。そこで JavaRebel 1.1M1 の時に出てきた JavaRebel SDK を使ってみることにしました。
JavaRebel SDK を Guice に適用した例を見たところ、JavaRebel にリスナークラスを登録しておくと、クラスをリロードした時に呼び出してくれる仕組みになっているようです。
そこで Injector クラスに JavaRebel SDK に用意されている ReloadListener を実装してみました。
//変更前 public class Injector
//変更後 public class Injector implements ReloadListener
クラスがリロードされると、ReloadListener.reloaded() が呼び出されます。Injectorでキャッシュされたフィールドは classToFields という Map に入っていますので、リロードされたクラスに該当するものを削除してやります。
private ConcurrentHashMap/* <Class, Field[]> */classToFields = new ConcurrentHashMap();
public void reloaded(Class klass) { Iterator it = classToFields.keySet().iterator(); while(it.hasNext()) { if (klass.isAssignableFrom((Class)it.next())) { it.remove(); } } }
Listener の実装が終わったら、これを JavaRebel に登録してやる必要があります。登録する場所はどこでも良いのですが、Spring利用時に確実に呼び出されそうな org.apache.wicket.injetion.web.InjectorHolder の setInjector() にしてみました。
// 変更前 public static void setInjector(ConfigurableInjector newInjector) { injector = newInjector; }
ConfigurableInjector は Injector(ReloadListener) を継承しているので、そのまま JavaRebel に登録可能です。ReloaderFactory.getInstance()でReloaderFactoryを取得した後、isReloadEnabled()でJavaRebelが有効かどうかを確認してからListenerとして登録を行います。
// 変更後 public static void setInjector(ConfigurableInjector newInjector) { injector = newInjector; if (ReloaderFactory.getInstance().isReloadEnabled()) { ReloaderFactory.getInstance().addReloadListener(injector); } }
これでひとまず @SpringBean なフィールドを追加しても再起動が必要なくなりました。
ちなみに Injector クラスのコメントに書かれていた WICKET-625 によると、Injector のキャッシュは将来的に削除されるようです。Wicket 1.3.3 を予定しているみたいなので、将来的にこの変更は必要なくなるようです。
@SpringBeanに関連して、SpringのBeanを追加した場合には、ApplicationContext の更新が必要となるという問題もあります。この問題について、BLOGに書かれていた方法で対処してみました。
BLOGに書かれていた方法は、ApplicationContext で Bean が見つからない場合は、ApplicationContext.refresh()してからもう一度試してみる、というものです。Wicket がApplicationContext を利用している場所は複数あるのですが、実行パス的にはorg.apache.wicket.spring.SpringBeanLocator の2箇所で対応すれば良いようです。
一つは SpringBeanLocator.isSingletonBean() です。
// 変更前 public boolean isSingletonBean() { if (singletonCache == null) { singletonCache = Boolean.valueOf(getSpringContext().isSingleton(getBeanName())); } return singletonCache.booleanValue(); }
// 変更後 public boolean isSingletonBean() { if (singletonCache == null) { try { singletonCache = Boolean.valueOf(getSpringContext().isSingleton(getBeanName())); } catch (Exception e) { refreshApplicationContext(); singletonCache = Boolean.valueOf(getSpringContext().isSingleton(getBeanName())); } } return singletonCache.booleanValue(); }
もう一つは SpringBeanLocator.locateProxyTarget() です。
// 変更前 public Object locateProxyTarget() { final ApplicationContext context = getSpringContext(); if (beanName != null && beanName.length() > 0) { try { return lookupSpringBean(context, beanName, getBeanType()); } catch (Exception e) { refreshApplicationContext(); return lookupSpringBean(context, beanName, getBeanType()); } } else { try { return lookupSpringBean(context, getBeanType()); } catch (Exception e) { refreshApplicationContext(); return lookupSpringBean(context, getBeanType()); } } }
// 変更後 public Object locateProxyTarget() { final ApplicationContext context = getSpringContext(); if (beanName != null && beanName.length() > 0) { try { return lookupSpringBean(context, beanName, getBeanType()); } catch (Exception e) { refreshApplicationContext(); return lookupSpringBean(context, beanName, getBeanType()); } } else { try { return lookupSpringBean(context, getBeanType()); } catch (Exception e) { refreshApplicationContext(); return lookupSpringBean(context, getBeanType()); } } }
ちなみに refreshApplicationContext() はこんな感じで単純にrefresh()を呼び出しているだけです。
private void refreshApplicationContext() { ApplicationContext context = getSpringContext(); if (context instanceof AbstractRefreshableApplicationContext) { ((AbstractRefreshableApplicationContext)context).refresh(); } }
以上の変更で、Spring の Bean や @SpringBean なフィールドを追加した場合でもサーバの再起動が必要なくなります。特にページやサービスの追加といった、日常的な部分ではほとんど再起動が必要なくなりますので、結構快適になるのではないかと思います。
今回の変更のpatchと変更を適用したjarを置いておきます。wicket-ioc の方にJavaRebel SDKを含めた状態でビルドしてありますので、jarを置き換えてJavaRebelを有効にするだけで動くと思います。JavaRebelとWicketという恐ろしくマイナー(?)な組み合わせをお使いの方はお試し下さい。