
PHPなどのシングルスレッドのWebアプリケーションを長く開発していると、何も考えずにクラス変数を使ってしまう...。
Tomcatは、シングルスレッドの設定を行わない限り、マルチスレッドで動作する(あらかじめいくつか複数のスレッドを起動しておき、リクエスト毎にどれかを使うような感じ)のでクラス変数を使うと全てのスレッドからアクセスが共有されてしまいます。
これは、たとえば現在のHTTPセッション(セッション間データを扱う「セッション変数」ではなく、1セッション内という意味)のみで統一的に扱いたいデータをクラス変数に入れておくと、HTTPセッションが終了してもそのデータが保持されたままになってしまうということです。
クラス変数はプロセス内で共有される変数なので、Tomcat起動中は同じプロセスですから当然といえば当然なのですが...、例えばデータベースコネクションなんかは上記のような形で扱いたいところです。
ThreadLocal クラス
複数のスレッドで共有したくないデータを扱う方法としてThreadLocal(スレッド固有)クラスを使う方法があります。
このクラスは、スレッドのIDのようなものをキーとしたMapにデータを保存することによってスレッド毎にデータを分離させることができます。
使い方は、
class HogeClass
{
private static ThreadLocal<HogeClass> holder = new ThreadLocal<HogeClass>()
{
@Override
public HogeClass initalValue()
{
return new HogeClass();
}
};
// この変数をクラス変数のように扱いたい
private Connection conn = null;
// Singletonとして扱いたい場合はコンストラクタをprivateに
private HogeClass() { }
// 外部からインスタンス化すらさせたくない場合はgetInstance()をprivateに
private static HogeClass getInstance()
{
return holder.get();
}
// 以下、アクセッサ
public static getConnection()
{
return getInstance().conn;
}
public static void setConnection(Connection conn)
{
getInstance().conn = conn;
}
}
という感じです。initialValue()メソッドは、デフォルトではnullを返しますが、サブクラスで定義することによってここで初期化した値をThreadLocal.get()で返すようにすることができます。
このように実装することで、外からはstaticメンバのアクセッサを使っているかのように扱うことができます。
新たな(?)問題
これで、スレッド単位でのデータを扱うことはできるようになりました。・・・が、これだけではいくつか問題があります。
- Tomcatではスレッドを使い回すので再利用したスレッドにThreadLocalデータが残ってしまう
- スレッド自体は終了しないため、セッション終了時にデータが解放されない
これらは、先に説明したTomcatのスレッド管理の仕組みに起因するものです。
解決するためには、セッション終了時にデータの解放処理を行えばよさそうです。これを実現するために、単純なサーブレットフィルタを作ります。
web.xml(一部)
<filter>
<filter-name>ResourceFilter</filter-name>
<filter-class>common.filters.ResourceFilter</filter-class>
</filter>
<!-- filter-mapping の一番最初に入れる -->
<filter-mapping>
<filter-name>ResourceFilter</filter-name>
<url-pattern>*.do</url-pattern>
</filter-mapping>
ResourceFilterクラス
package common.filters;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import common.thread.ThreadContext;
public class ResourceFilter implements Filter
{
@Override
public void destroy()
{
}
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException
{
// アクションの実行
chain.doFilter(req, res);
// ThreadLocal データの削除
ThreadContext.destroy();
}
@Override
public void init(FilterConfig arg0) throws ServletException
{
}
}
ここで、ThreadContextというクラスが出てきていますが、これはデータの解放を容易にするためにスレッドローカルを一元的に管理するクラスです。各クラスでThreadLocalをprivate staticとして持つのではなく、ThreadContextに委譲させる考え方です。
ThreadContext クラス
ThreadContextクラスでは、各クラスをキーとしたMapをThreadLocalとして保存するようにします。これによって、クラス毎のデータを保存しやすくなります。
ThreadContext
package common.thread;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import common.Destroyable;
public class ThreadContext
{
private static ThreadLocal<Map<Class<?>, Destroyable>> threadTable = new ThreadLocal<Map<Class<?>, Destroyable>>()
{
@Override
public Map<Class<?>, Destroyable> initialValue()
{
return new HashMap<Class<?>, Destroyable>();
}
};
/**
* スレッドローカルなデータを削除する
*/
public static void destroy()
{
Map<Class<?>, Destroyable> map = threadTable.get();
Iterator<Destroyable> it = map.values().iterator();
while(it.hasNext())
{
Destroyable obj = it.next();
obj.destroy();
}
threadTable.remove();
}
public static <T extends Destroyable> void put(Class<T> key, T value)
{
threadTable.get().put(key, value);
}
@SuppressWarnings("unchecked")
public static <T extends Destroyable> T get(Class<T> key)
{
return (T)threadTable.get().get(key);
}
}
Destroyable
package common;
public interface Destroyable
{
public void destroy();
}
自動的にデータの破棄を行うため、ThreadContextで扱うクラスはDestroyableインターフェイスを実装している必要があります(空の実装でも構いません)。
注意点
ResourceFilterでdestroy()を実行していますが、FilterChain内で処理が中断されてしまうと(例外などで)、destroy()に到達しませんので、try~catch~finally内で実行するなどした方が良いかもしれません。もっと良い方法があれば、誰か教えてください・・・。

「TomcatでHTTPセッション単位のデータを扱う方法」への1件のフィードバック