8
Help us understand the problem. What are the problem?

More than 5 years have passed since last update.

posted at

updated at

Organization

JSFでバッキングBeanに@PostConstructを付けたメソッドが画面表示時とサブミット時に二度呼ばれて困ったときの話

環境

AP サーバー

GlassFish 3.1.2.2

最初に実装した問題ありのパターン

HelloBean.java
package sample.jsf;

import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.inject.Named;

@Named
@RequestScoped
public class HelloBean {

    private String value;

    @PostConstruct
    public void init() {
        this.log("init");
        this.value = "Hello";
    }

    public void submit() {
        this.log("submit");
    }

    private void log(String tag) {
        System.out.println(tag + " hash:" + this.hashCode());
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}
hello.xhtml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE html
          PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" 
          "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:ui="http://java.sun.com/jsf/facelets"
      xmlns:h="http://java.sun.com/jsf/html">
  <head>
    <title>index</title>
  </head>
  <body>
    <form jsfc="h:form">
      <input type="text" jsfc="h:inputText" value="#{helloBean.value}" />
      <input type="submit" jsfc="h:commandButton" action="#{helloBean.submit}" value="submit" />
    </form>
  </body>
</html>

画面

hello.png

バッキング Bean は @Named を付けることで CDI コンテナ管理にしている。

hello.xhtml を表示すると、テキスト入力項目の値(#{helloBean.value})を参照するため、コンソールに以下のメッセージが出力される。

画面表示時のコンソール出力
情報: init hash:396533266

次に、 submit ボタンをクリックすると次のようにコンソールに出力される。

submit時のコンソール出力
情報: init hash:1209656366
情報: submit hash:1209656366

init() メソッドが、計2回呼ばれてしまっている。

もし init() メソッドで画面に表示するための情報をデータベースから取得していたりすると、 submit() 時も不要なデータベースアクセスが発生してしまう。

スコープを @ConversationScoped にしてみる

前述の問題パターンは、バッキング Bean が @RequestScoped になっていた。
なので、リクエストのたびに @PostConstract が実行されるのは当たり前といえる。

ということで、スコープを @ConversationScoped にしてみた。

@ConversationScoped を使うと、 @RequestScoped より長く @SessionScoped よりも短いスコープを定義できる。このスコープの開始と終了は、コード中にハードに実装する。

HelloBean.java
package sample.jsf;

import java.io.Serializable;

import javax.annotation.PostConstruct;
import javax.enterprise.context.Conversation;
import javax.enterprise.context.ConversationScoped;
import javax.inject.Inject;
import javax.inject.Named;

@Named
@ConversationScoped
public class HelloBean implements Serializable {
    private static final long serialVersionUID = 1L;

    @Inject
    private Conversation conversation;
    private String value;

    @PostConstruct
    public void init() {
        this.log("[@ConversationScoped] init");
        this.value = "Hello";
        this.conversation.begin();
    }

    public void submit() {
        this.log("[@ConversationScoped] submit");
        this.conversation.end();
    }

    private void log(String tag) {
        System.out.println(tag + " hash:" + this.hashCode());
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

init() の最後で begin() して、 submit() の最後で end() を呼ぶようにした。

すると、コンソール出力は以下のようになった。

画面表示時
情報: [@ConversationScoped] init hash:1738863096
submit時
情報: [@ConversationScoped] init hash:1275168439
情報: [@ConversationScoped] submit hash:1275168439

ダメでした。

FacesContext#isPostback() を使って init() メソッド内で条件分岐させる

↓検索したら、同じことで困っている人がいました。

jsf 2 - How to prevent @PostConstruct from being called on postback - Stack Overflow

ここで紹介されていた方法の1つが、 FacesContext#isPostback() を使う方法。

HelloBean.java
package sample.jsf;

import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.faces.context.FacesContext;
import javax.inject.Named;

@Named
@RequestScoped
public class HelloBean {

    private String value;

    @PostConstruct
    public void init() {
        if (FacesContext.getCurrentInstance().isPostback()) {
            this.log("is postback");
        } else {
            this.log("init");
            this.value = "Hello";
        }
    }

    public void submit() {
        this.log("submit");
    }

    private void log(String tag) {
        System.out.println(tag + " hash:" + this.hashCode());
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

init() メソッド内で、 FacesContext.getCurrentInstance().isPostback() の値を取得し、 false のときだけ初期化を実行するようにしている。

画面にアクセスして submit を実行すると、以下のようにコンソールに出力される。

画面表示時
情報: init hash:201077391
submit時
情報: is postback hash:592087746
情報: submit hash:592087746

submit したときは、初期化処理が実行されなくなり、一応問題は解決しました。

そもそも @ViewScoped にすべき?

前述のStackOverflow では、もう1つの解決策が紹介されている。

それは、バッキング Bean を JSF コンテナ管理にして、スコープを @ViewScoped にする、という方法。

HelloBean.java
package sample.jsf;

import java.io.Serializable;

import javax.annotation.PostConstruct;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.ViewScoped;

@ManagedBean
@ViewScoped
public class HelloBean implements Serializable {
    private static final long serialVersionUID = 1L;

    private String value;

    @PostConstruct
    public void init() {
        this.log("[@ViewScoped] init");
        this.value = "Hello";
    }

    public void submit() {
        this.log("[@ViewScoped] submit");
    }

    private void log(String tag) {
        System.out.println(tag + " hash:" + this.hashCode());
    }

    public String getValue() {
        return value;
    }

    public void setValue(String value) {
        this.value = value;
    }
}

実際に動かした結果は以下。

画面表示時
情報: [@ViewScoped] init hash:684562545
submit時
情報: [@ViewScoped] submit hash:684562545

実装がシンプルでいい感じです。

どうしてもバッキング Bean を CDI コンテナ管理にしないといけない、という理由がない限りは(特に思いつかないけど)、こっちの方がよさげです。

参考

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
8
Help us understand the problem. What are the problem?