CDIではフィールドを直接参照してはいけない

ちょっとハマったので、メモ
こんなSessionScopedなクラスを作ります。

import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

@Named
@SessionScoped
public class MySession implements Serializable{
    long id;
    
    @PostConstruct
    void init(){
        System.out.println("init my session");
    }
}


そんでもって、こんなサーブレットから使ってみます。

@WebServlet(name = "MyServlet", urlPatterns = {"/MyServlet"})
public class MyServlet extends HttpServlet {
    @Inject
    private MySession sess;
    
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String msg;
        if(sess.id == 0){
            sess.id = 1;
            msg = "first";
        }else{
            msg = "repeat";
        }
 
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println(msg + "<br>");
        out.close();
    }
}


そうすると、まあ、最初の表示では「first」と表示されて、リロードすると「repeat」と表示されるわけです。
で、SessionScopedなので、他のプラウザで開けば「first」と表示されてほしいのですけど、やはり「repeat」と表示されます。
そして、サーバーログには@PostConstructをつけたinitメソッドからの「init my session」という出力があるはずなんですが、なにも表示されません。
@ApplicationScopedにすれば、「init my session」と表示されます。


どうも、CDIではメソッドをごにょごにょしてRequestScopedやSessionScopedを実現しているようで、フィールドを直接アクセスしたらダメみたいです。内部でサブクラスを生成して、それぞれのメソッドをオーバーライドして、そのときにスコープにあったオブジェクトを準備しているのだと思います。@PostConstructはそのときによばれます。


ApplicationScopedの場合は、たぶんコンストラクタをごにょごにょして@PostConstructを呼び出しているのだと思います。


ということで、次のようにセッター・ゲッターを用意してやる必要がありました。

import java.io.Serializable;
import javax.annotation.PostConstruct;
import javax.enterprise.context.SessionScoped;
import javax.inject.Named;

@Named
@SessionScoped
public class MySession implements Serializable{
    long id;
    
    @PostConstruct
    void init(){
        System.out.println("init my session");
    }

    public long getId() {
        return id;
    }
    public void setId(long id) {
        this.id = id;
    }
}


そうして、アクセッサ経由でフィールドアクセスすると、ちゃんとブラウザごとに「first」が表示されるようになって、そのときに「init my session」も出力されます。

@WebServlet(name = "MyServlet", urlPatterns = {"/MyServlet"})
public class MyServlet extends HttpServlet {
    @Inject
    private MySession sess;
    
    public void doGet(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {
        String msg;
        if(sess.getId() == 0){
            sess.setId(1);
            msg = "first";
        }else{
            msg = "repeat";
        }
 
        response.setContentType("text/html;charset=UTF-8");
        PrintWriter out = response.getWriter();
        out.println(msg + "<br>");
        out.close();
    }
}


2011/5/1 追記
トラックバックで指摘があるようにpublicフィールドをもつクラスはCDI管理にすると警告が出るようで、問題が健在化するのはパッケージプライベートのフィールドの場合のみのようです。
また、パッケージプライベートでも、スコープが同じオブジェクトへのインジェクションでは問題にならないようです。
まあ、実プロダクトでフィールドを直接アクセスすることはないでしょうから、問題になるのは、CDIちょっと試してみようってときです。ここで落とし穴ハマると時間もったいない。