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

#環境
##AP サーバー
GlassFish 3.1.2.2

#最初に実装した問題ありのパターン
```java: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;
    }
}
```

```xml: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](https://qiita-image-store.s3.amazonaws.com/0/28302/4d658444-ec79-3e5e-069c-2f9b9431e014.png)

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

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

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

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

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

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

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

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

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

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

```java: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()` を呼ぶようにした。

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

```text:画面表示時
情報: [@ConversationScoped] init hash:1738863096
```

```text: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](http://stackoverflow.com/questions/13252450/how-to-prevent-postconstruct-from-being-called-on-postback)

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

```java: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 を実行すると、以下のようにコンソールに出力される。


```text:画面表示時
情報: init hash:201077391
```

```text:submit時
情報: is postback hash:592087746
情報: submit hash:592087746
```

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

#そもそも @ViewScoped にすべき？

[前述のStackOverflow](http://stackoverflow.com/questions/13252450/how-to-prevent-postconstruct-from-being-called-on-postback) では、もう１つの解決策が紹介されている。

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

```java: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;
    }
}
```

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

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

```text:submit時
情報: [@ViewScoped] submit hash:684562545
```

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

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

#参考
- [1-3) JSFとConversationスコープ - javar4java](https://sites.google.com/site/javar4java/contexts-and-dependency-injection-cdi/1-3-jsftoconversationsukopu)
- [jsf 2 - How to prevent @PostConstruct from being called on postback - Stack Overflow](http://stackoverflow.com/questions/13252450/how-to-prevent-postconstruct-from-being-called-on-postback)