Servlet内で起動したThreadからSessionスコープのコンポーネントを利用できない。
今日はまったところ。
Seasar2では、Servlet内で起動したThreadからSessionスコープのコンポーネントを利用できません。(Seasar2.3.22で確認。)
原因
HttpSessionの取得先であるHttpServletRequestをThreadLocalで保持しているのが原因。Servletを実行するThreadでは、ServletFilterでHttpServletRequestが設定されますが、Servlet内で起動したThreadでは設定してくれる人がいないため、コンポーネント作成時に以下のようなエラーになります。
org.seasar.framework.exception.EmptyRuntimeException: [ESSR0007]sessionはnullあるいは空であってはいけません at org.seasar.framework.container.deployer.SessionComponentDeployer.deploy(SessionComponentDeployer.java:44) at org.seasar.framework.container.impl.ComponentDefImpl.getComponent(ComponentDefImpl.java:94) at org.seasar.framework.container.impl.S2ContainerImpl.getComponent(S2ContainerImpl.java:128) at s2.SessionScope$3.run(SessionScope.java:74)
サンプル
問題を再現するサンプルコードを書いてみました。(Servletにすると面倒そうなので、サンプルコード内でS2ContainerFilterなどの操作をシミュレーションしています。)
public static void main ( String[] args ) throws InterruptedException { // HttpServletRequest のモック。 HttpServletRequest request = createHttpServletRequest(); SingletonS2ContainerFactory.setConfigPath( "s2/test.dicon" ); SingletonS2ContainerFactory.init(); try { // HttpServletRequestの設定。 // 実環境ではS2ContainerFilterで設定される。 S2Container container = SingletonS2ContainerFactory.getContainer(); container.setRequest( request ); // --- ここから servlet --- // Servlet内でコンテナを使う場合と同じ状態。 // ここでは、Sessionスコープのコンポーネントが取得できる。 System.out.println( "servlet: " + container.getComponent( "kitten" ) ); // Servlet内でThreadを起動。 // Thread 内部では Sessionスコープのデータを取得できない。 Thread t = new Thread() { public void run() { try { S2Container container = SingletonS2ContainerFactory.getContainer(); System.out.println( "Thread: " + container.getComponent( "kitten" ) ); } catch ( Exception e ) { e.printStackTrace(); } } }; t.start(); t.join(); // Threadの終了を待つ。 // --- servlet ここまで --- } finally { // HttpServletRequestの破棄。 // これも実環境ではS2ContainerFilterが行う。 S2Container container = SingletonS2ContainerFactory.getContainer(); container.setRequest( null ); } } /** * HttpServletRequest のモックを生成する。 * @return HttpServletRequestのモック */ static HttpServletRequest createHttpServletRequest () { // HttpSessionのモック。 getAttribute()とsetAttribute()だけ実装。 final HttpSession session = (HttpSession) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class[] {HttpSession.class}, new InvocationHandler() { private final Map map = new HashMap(); public Object invoke ( Object proxy, Method method, Object[] args ) throws Throwable { if ( "getAttribute".equals( method.getName()) ) { return map.get( args[0] ); } else if ( "setAttribute".equals( method.getName()) ) { map.put( args[0], args[1] ); return null; } throw new UnsupportedOperationException(); } } ); // HttpServletRequest のモック。 return (HttpServletRequest) Proxy.newProxyInstance( ClassLoader.getSystemClassLoader(), new Class[] {HttpServletRequest.class}, new InvocationHandler() { public Object invoke ( Object proxy, Method method, Object[] args ) throws Throwable { return session; } } ); }
DICONファイルは次の通りです。
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE components PUBLIC "-//SEASAR//DTD S2Container 2.3//EN" "http://www.seasar.org/dtd/components23.dtd"> <components > <!-- Sessionスコープのコンポーネント --> <component name="kitten" class="java.lang.String" instance="session"> <arg>"mii"</arg> </component> </components>
実行結果です。シミュレーションしたServletの中ではSessionスコープのコンポーネントを使えますが、新規に起動したThread内ではエラーになっています。
servlet: mii org.seasar.framework.exception.EmptyRuntimeException: [ESSR0007]sessionはnullあるいは空であってはいけません at org.seasar.framework.container.deployer.SessionComponentDeployer.deploy(SessionComponentDeployer.java:44) at org.seasar.framework.container.impl.ComponentDefImpl.getComponent(ComponentDefImpl.java:94) at org.seasar.framework.container.impl.S2ContainerImpl.getComponent(S2ContainerImpl.java:128) at s2.SessionScope$1.run(SessionScope.java:48)
対策
Sessionに積んでいたオブジェクトは、「生成コストが大なのでなるべく使い回したい。ただしユーザーごとに1つは必要」というたぐいのオブジェクトであったので、Sessionが使えない場合は再作成するコードにしてとりあえず回避。次のような感じです。
Thread t = new Thread() { public void run() { try { S2Container container = SingletonS2ContainerFactory.getContainer(); if ( container.getRoot().getSession() == null ) { // セッションが使えない場合はprototypeの方で我慢する。 System.out.println( "Thread: " + container.getComponent( "kitten-prototype" ) ); } else { System.out.println( "Thread: " + container.getComponent( "kitten" ) ); } } catch ( Exception e ) { e.printStackTrace(); } } };