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 SDKGuice に適用した例を見たところ、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という恐ろしくマイナー(?)な組み合わせをお使いの方はお試し下さい。