hakobera's blog

技術メモ。たまに雑談

SpringでHotdeploy その1

Slim3がなかなか出てこないけど、案件は迫ってきているので、仕方なく自分で実装することしました。
備忘録代わりに、何日かやってみた経過を記したいと思う。対象は Spring 2.5.2 以上です。

SpringでHotdeployしようとする場合、Classオブジェクトやインスタンスをキャッシュさせないことが目標になります。
では、Springでキャッシュしているのはどこかというと、とりあえず次の2箇所。

  • ClassLoader
  • BeanFactory

というわけで、第1回目はClassLoaderを何とかする方法から。

ClassLoaderでキャッシュさせないようにする方法

キャッシュしないようなClassLoaderを作って、そいつをBeanFactoryに渡すようにします。
org.springframework.core.OverridingClassLoaderがそのまま使えそうですが、残念ながらフィルターのパターンが excluded なので、適しません。というわけで、OverridingClassLoaderを参考に実装してみます。

ReloadableClassLoaderでは、処理対象のクラスに対しては、クラスのLoad処理を親に委譲せず、CachedIntrospectionResultsのキャッシュを破棄するdisposeメソッドを追加しています。また、org.springframework.core.SmartClassLoaderを実装してCglib2AopProxyでキャッシュが利用されるのを防いでいます。

これをServletFilterで、スレッドのContextClassLoaderとして設定するようにすれば良いわけです。これ、基本はSeaser2のHotdeployClassLoaderと同じです。

実際のソースはこんな感じ。

■ ReloadableClassLoader

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

import org.springframework.beans.CachedIntrospectionResults;
import org.springframework.core.SmartClassLoader;
import org.springframework.util.Assert;
import org.springframework.util.FileCopyUtils;

public class ReloadableClassLoader extends ClassLoader implements SmartClassLoader {
	
	private static final String CLASS_FILE_SUFFIX = ".class";
	
	private final Set<String> includedPackages = new HashSet<String>();

	private final Set<String> includedClasses = new HashSet<String>();

	private final Object inclusionMonitor = new Object();

	
	public ReloadableClassLoader(ClassLoader parent) {
		super(parent);
	}
	
	public void dispose() {
		CachedIntrospectionResults.clearClassLoader(this);
	}
	
	public void includePackage(String packageName) {
		Assert.notNull(packageName, "Package name must not be null");
		synchronized (this.inclusionMonitor) {
			this.includedPackages.add(packageName);
		}
	}

	public void includeClass(String className) {
		Assert.notNull(className, "Class name must not be null");
		synchronized (this.inclusionMonitor) {
			this.includedClasses.add(className);
		}
	}

	protected boolean isIncluded(String className) {
		synchronized (this.inclusionMonitor) {
			if (this.includedClasses.contains(className)) {
				return true;
			}
			for (Iterator it = this.includedPackages.iterator(); it.hasNext();) {
				String packageName = (String) it.next();
				if (className.startsWith(packageName)) {
					return true;
				}
			}
		}
		return false;
	}

	protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException {
		Class result = null;
		if (isIncluded(name)) {
			result = loadClassForOverriding(name);
		}
		if (result != null) {
			if (resolve) {
				resolveClass(result);
			}
			return result;
		} else {
			return super.loadClass(name, resolve);
		}
	}

	protected Class loadClassForOverriding(String name) throws ClassNotFoundException {
		Class klass = findLoadedClass(name);
		if (klass == null) {
			byte[] bytes = loadBytesForClass(name);
			if (bytes != null) {
				klass = defineClass(name, bytes, 0, bytes.length);
			}
		}
		return klass;
	}

	protected byte[] loadBytesForClass(String name) throws ClassNotFoundException {
		InputStream is = openStreamForClass(name);
		if (is == null) {
			return null;
		}
		try {
			// Load the raw bytes.
			return FileCopyUtils.copyToByteArray(is);
		}
		catch (IOException ex) {
			throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
		}
	}

	protected InputStream openStreamForClass(String name) {
		String internalName = name.replace('.', '/') + CLASS_FILE_SUFFIX;
		return getParent().getResourceAsStream(internalName);
	}
		
	public boolean isClassReloadable(Class clazz) {
		return isIncluded(clazz.getName());
	}
	
}

■ ReloadableContextFilter

import java.io.IOException;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;


import org.springframework.web.filter.OncePerRequestFilter;

public class ReloadableContextFilter extends OncePerRequestFilter {

	@Override
	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
		if (ReloadableUtil.isReloadable()) {
			ClassLoader original = Thread.currentThread().getContextClassLoader();
			ReloadableClassLoader reloadableClassLoader = new ReloadableClassLoader(original);
			Thread.currentThread().setContextClassLoader(reloadableClassLoader);
		
			try {
				ReloadableUtil.reload();
				filterChain.doFilter(request, response);
			} finally {
				reloadableClassLoader.dispose();
				Thread.currentThread().setContextClassLoader(original);
			}
		} else {
			filterChain.doFilter(request, response);
		}
	}
	
}

なお、ReloadableUtil.isReloadable()はSystem.getProperty()とかに保持してあるフラグをみて、Hotdeployが必要かどうか判定するメソッドです。
具体的な実装は、システムプロパティにしようか、S2みたいにファイルの有無でやろうか、どちらかで悩み中。

次は、とりあえずBeanFactory関連を何とかする方法を書きます。