HOT deployでのClassCastException対策その2
現時点(Seasar2.4.34まで)のHOT deployでは、HttpSessionを直接使用して、リクエストをまたいでHttpSessionに格納されたオブジェクトを取得してキャストすると、ClassCastExceptionが発生する。
例.XxxAction#indexでセッションにXxxDtoを格納して、XxxAction#index2でセッションから取得する。
public class XxxAction { @Resource protected XxxDto xxxDto; @Execute(validator = false) public String index() { HttpSession session = RequestUtil.getRequest().getSession(); session.setAttribute("xxxDto", xxxDto); System.out.println(xxxDto.getClass().getClassLoader()); // ※1 return "xxx.jsp"; } @Execute(validator = false) public String index2() { HttpSession session = RequestUtil.getRequest().getSession(); System.out.println(XxxDto.class.getClassLoader()); // ※2 System.out.println(session.getAttribute("xxxDto").getClass().getClassLoader()); // ※3 XxxDto dto = (XxxDto) session.getAttribute("xxxDto"); // ここでClassCastExceptionが発生 return "xxx.jsp"; } }
原因
原因は、やっぱりクラスローダーが異なるから。
上記を実行すると、次のように標準出力される。(上から順に※1、2、3に対応)
org.seasar.framework.container.hotdeploy.HotdeployClassLoader@10bc9e9 org.seasar.framework.container.hotdeploy.HotdeployClassLoader@b630d1 org.seasar.framework.container.hotdeploy.HotdeployClassLoader@10bc9e9
※2と3のクラスローダーが異なっていることが原因。
HotdeployClassLoaderは、org.seasar.framework.container.hotdeploy.HotdeployFilterにより、
リクエストの度に、生成と破棄が行われる。※1と2はリクエストが異なるので、当然HotdeployClassLoaderも別のものになるが、※3は1でセッションに格納したXxxDtoを取得しただけなので、クラスローダーも1と同じHotdeployClassLoaderになる。
対策
回避方法は以下の2通り。
1.HttpSessionを直接操作せず、Map sessionScopeを使用する。
Seasar2にはsessionScopeというHttpSessionの中身がマッピングされたMap
sessionScopeの取得には、フィールドを用意してDIしてもらうか、次のようにS2Containerから直接取得すればよい。
Map<String, Object> sessionScope = SingletonS2Container.getComponent("sessionScope");
例.
public class XxxAction { @Resource protected XxxDto xxxDto; @Resource protected Map<String, Object> sessionScope; @Execute(validator = false) public String index() { sessionScope.put("xxxDto", xxxDto); return "xxx.jsp"; } @Execute(validator = false) public String index2() { XxxDto dto = (XxxDto) sessionScope.get("xxxDto"); return "xxx.jsp"; } }
2.セッションに格納するオブジェクトに@Component(instance = InstanceType.SESSION)アノテーションをつける。
@Component(instance = InstanceType.SESSION)アノテーションがつけられたオブジェクトは、Seasar2が自動的にセッションスコープで管理してくれる。
例.
@Component(instance = InstanceType.SESSION) public class XxxDto implements Serializable { private static final long serialVersionUID = 1L; String xxx; } public class XxxAction { @Resource protected XxxDto xxxDto; @Execute(validator = false) public String index() { xxxDto.xxx = "hoge"; return "xxx.jsp"; } @Execute(validator = false) public String index2() { String xxx = xxxDto.xxx; return "xxx.jsp"; } }
番外だけど、対策1で出てきたHotdeployUtil#rebuildValueを直接実行することでも回避できる模様。ただし、正式にドキュメントに記載されている方法でもなく、あえてこの方法を選択する利点はないと思う。
例.
public class XxxAction { @Resource protected XxxDto xxxDto; @Execute(validator = false) public String index() { HttpSession session = RequestUtil.getRequest().getSession(); session.setAttribute("xxxDto", xxxDto); return "xxx.jsp"; } @Execute(validator = false) public String index2() { HttpSession session = RequestUtil.getRequest().getSession(); XxxDto dto = (XxxDto) HotdeployUtil.rebuildValue(session.getAttribute("xxxDto")); return "xxx.jsp"; } }
なお、別にSeasar2に限った話ではなく、本来そうすべきことだが、セッションに格納するオブジェクトは、必ず、java.io.Serializableを実装する必要がある。Serializableを実装していないオブジェクトを、リクエストをまたいでセッションにputしてgetすると、対策1のようにsessionScopeを使用していてもClassCastExceptionが発生する。
おそらく、org.seasar.framework.container.hotdeploy.HotdeployUtil#rebuildValueから実行される、org.seasar.framework.container.hotdeploy.RebuilderImpl#rebuildで、ObjectOutputStream#writeObjectを実行する際に、NotSerializableExceptionが発生するためだと思うが、NotSerializableExceptionはcatchされて、再生成されていない元のオブジェクトがそのまま返されるため、クラスローダーの不一致が発生する。
NotSerializableExceptionが飛んでくるわけではないので、sessionScopeを使っているのにClassCastExceptionが発生する場合は、焦らずにSerializableなオブジェクトになっているか確認するといい。
補足
これを書くのに色々調べていたら、こんなのを見つけた。どうやら、次のバージョン(Seasar 2.4.35)からは、HttpSessionから直接getAttributeしてもClassCastExceptionは発生しなくなる模様。これでまた一つ嵌りどころがなくなる。
[S2Container] HOT deploy で Http セッションから取得したオブジェクトを新しいクラスローダでロードする処理を,sessionScope の Map ではなく HttpSession で行うようにしました.
https://www.seasar.org/issues/browse/CONTAINER-351