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を使用すると、org.seasar.framework.container.hotdeploy.HotdeployUtil#rebuildValueというメソッドが実行され、これにより、セッション中のオブジェクトのクラスが、最新のクラスローダーで再ロードされた上で、シリアライズ、デシリアライズにより、オブジェクトが再生成されるので、クラスローダーの不一致がなくなる。
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