いつもJenkinsオジサマの画像関連Twitterでお世話になっているIntelliJ IDEAマスターこと@masanobuimaiさんが何とJSF2.2を触られていて、色々もやもやされていました。
「あー自分も最初の頃そういう疑問を多々思っていた」
と思い出しつつ
「あれっ、今もモヤっとしてるじゃん! (゚д゚) ガタッ」
と机に足をぶつけながら読んでました。
人間慣れとは恐ろしいもので、いつの間にか細かいことを気にしなくなってて、初心忘れるべからずだなぁ…と改めて思いつつ、何となくつぶやいたら無言のプレッシャーが(^^;
ということで、GitHubにある今井さんのコード
https://github.com/masanobuimai/JSFSample/tree/01-managed-beans
を拝見しながら、自分なりに考えたもの・仕事で使ってるようなもので書いてみました。
https://github.com/kikutaro/JSFSample-01-managed-beans
2014/6/24 追記
@megascusさんが「あるべき姿」を書かれていました(>_<)
自分が書いた以下コードは、いまいさんのブログにあったようにCDIを使うと~という流れで書いたため、少し微妙な感じです。
私も色々意識せずシンプルに普通に書くとmegascusさんのようになるかなと思います!
画面挙動
僕は今井さんのようにリッチではなく、Java EEが使える有償のIntelliJは持っていないので(´・ω・`) NetBeansを利用して、まずはいまいさんの元コードを動かしてみました。
起動するとメニューが出て
一番上のPlainを選択すると以下画面となります。数値は乱数のようです。
計算ボタンを押下すると、足し算をして結果を下に表示します。
計算時には、左辺が偶数であればその旨をメッセージとして表示し、右辺が奇数であればその旨をメッセージとして表示する仕様のようです。
画像だけみると、メッセージの感じが伝わりにくいですが、1つ前に入ってた値に対するメッセージとなってます。
管理ビーン
用語…確かにいつもフワフワしてます。英語では主に
- Backing Bean
- Managed Bean
ですが、日本語だと人によってマチマチで
- バッキングビーン
- バックビーン
- 管理ビーン
- 管理対象Bean
とか…英語・カタカナも混じったり(^^;さらに面倒なのはJSFとCDIの両者に存在するため
などの接頭辞が(^^;
自分のブログも統一できていないのですが、今日は以降、管理ビーン(CDIの)で合わせておきます。
Getter/Setter
管理ビーンでGetter/Setterの嵐になるのが嫌、というのは誰もが皆同じですね(^^;
Lombokは無しで、とのことですが、私は初めてJava EE 6で開発したPrjでLombokを使わずにやっていましたが、IDEで自動生成できるとはいえGetter/Setterが何ともうっとうしくて、それ以降のプロジェクトではLombokを利用しています。
ということで私が書いた版ではLombok使いました。以下はCalcクラスの例です。
package com.jsfsample.calc;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter @Setter
@AllArgsConstructor
public class Calc {
private long left;
private long right;
private long answer;
}
CDI(Contexts and Dependency Injection)
この辺は色々モヤモヤしますよね(^^;
個人的には今回のサンプルレベルであれば、あまりInjectを利用することもなく、必要に応じてnewしてもいいのかなぁと思います。
でもそれだと上記モヤモヤの回答にならないので、CDIを積極的に使う姿勢でやってみます。
ListのオブジェクトをInjectできる?
Listやプリミティブ型などは直接Injectできないため、いまいさんが書かれているようにProducerを用意する必要があり、定義してみました。
package com.jsfsample.calc;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import javax.enterprise.inject.Produces;
public class CalcProducer {
@Produces
public List<Calc> getCalcList(){
return new LinkedList<>();
}
}
ちなみに元のコードをみるとコンストラクタでnewしてますが、多分自分もそうします(^^;Producerをあえて使うと上記になる感じです。
Injectできるのは管理ビーンだけ?
元文章を読んでいると、InjectできるクラスはCDIで@RequestScopedのようなアノテーションがつけられたCDI管理ビーンだけ…のような認識をされているのかなと(もし違ってたらすみません(>_<;;)
この辺、私も昔そう思っていたのですが、Inject対象はCDI管理ビーンでなくてもでき、普通のPOJOなんかもInjectできます。
ただ、そうするためにはbeans.xmlを利用して以下bean-discover-modeの定義が必要です。
xml version="1.0" encoding="UTF-8"
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlnsxsi="http://www.w3.org/2001/XMLSchema-instance"
xsischemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="all">
</beans>
ただし、NetBeansでbeans.xmlを追加した場合、デフォルトはannotatedとなっており、コメントをみると強く推奨しています。
xml version="1.0" encoding="UTF-8"
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlnsxsi="http://www.w3.org/2001/XMLSchema-instance"
xsischemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
bean-discovery-mode="annotated">
</beans>
この辺の理由は@n_agetsuさんが詳しく細かくまとめられているので、そちらをどうぞ(^^;
Java EE環境におけるCDIのデフォルト化
というか、CDIやるなら同じく@n_agetsuさんの以下が大変参考になります。
少し脱線しましたが、戻ります。
最初にLombokうんぬんの部分で定義した私のCalcはLombok関連のアノテーションのみで、単なるPOJOですが、上記beans.xmlでall指定していれば
@Inject
private Calc calc;
でInjectされます。
ただし今回の元コードでCalcをnewしてるのはresetメソッドで、newした際に乱数を当て込んでいます。
この処理は自分のCalcクラスだと、コンストラクタに実装してもいい気がするのですが、今回は先ほど定義したProducer用のクラスを使って以下のようにしました。
package com.jsfsample.calc;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import javax.enterprise.inject.Produces;
public class CalcProducer {
@Produces
public List<Calc> getCalcList(){
return new LinkedList<>();
}
@Produces
public static Calc getCalc(){
Random rand = new Random(System.currentTimeMillis());
return new Calc(
Math.abs(rand.nextInt() % 10) + 1,
Math.abs(rand.nextInt() % 10) + 1,
0
);
}
}
ここも大げさな書き方してますが、元々のコードのresetメソッドでやっているように普通にnewすると思います(^^;あえてProducer使ったら、という例かなと。
staticメソッドにしている理由は後ほど。
Producerでスコープを指定
上記にあるRequestScopedへViewScopedのInjectは私も自信がないです。
が、一応動きました(^^;いいのかなぁ…。
ちなみにやった方法としては、先ほどProducerでListをInjectできるようにした部分でスコープを指定しただけです。
package com.jsfsample.calc;
import java.util.LinkedList;
import java.util.List;
import java.util.Random;
import javax.enterprise.inject.Produces;
import javax.faces.view.ViewScoped;
public class CalcProducer {
@Produces @ViewScoped
public List<Calc> getCalcList(){
return new LinkedList<>();
}
}
元ネタは以下サイトの「7. Producer method scopes」です。
Java EE CDI Producer methods tutorial
管理ビーンであるCalcViewクラスはRequestScopedになっています。
@Named
@RequestScoped
@Getter @Setter
public class CalcView extends AbstractBean{
@Inject
private Calc calc;
@Inject
private List<Calc> results;
この書き方はあまりやったことないので、実際にはCalcViewをViewScopedにしておくかなぁ。
抽象クラスに画面共通処理を定義
1画面1管理ビーンを基本にしていますが、実際のプロジェクトでは管理ビーンの雛型として抽象クラスを設けています。
さきのコードでネタばれしてますが、今回の定義だと以下のような抽象クラスです。
package com.jsfsample.calc.plain;
import javax.faces.application.FacesMessage;
import javax.faces.context.FacesContext;
public abstract class AbstractBean {
protected void message(String clientId, String message){
FacesContext.getCurrentInstance().addMessage(
clientId,
new FacesMessage(message)
);
}
}
で、管理ビーンのCalcViewクラスはAbstractBeanを継承しています。
package com.jsfsample.calc.plain;
import com.jsfsample.calc.Calc;
import com.jsfsample.calc.CalcProducer;
import java.util.List;
import javax.annotation.PostConstruct;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.inject.Named;
import lombok.Getter;
import lombok.Setter;
@Named
@RequestScoped
@Getter @Setter
public class CalcView extends AbstractBean{
@Inject
private Calc calc;
@Inject
private List<Calc> results;
@PostConstruct
public void init(){
}
public void calc(){
long l = calc.getLeft();
long r = calc.getRight();
long a = l + r;
calc.setAnswer(a);
if (l % 2 == 0) message("left", "左辺 " + l + " は偶数です");
if (r % 2 != 0) message("right", "右辺 " + r + " は奇数です");
results.add(0, calc);
calc = CalcProducer.getCalc();
}
}
messageメソッドの部分で使っています。
メッセージ処理は元コードではFaceUtilsクラスを定義して、flashを利用されていますが、今回は上記のようにしました。
あまりよくないのはわかっているのですが、メッセージを動的に生成したり変えたりしたい、というのはよくあって、わりと上記のように管理ビーンから制御してしまうことが今のプロジェクトでは多いです。モデルがビューを気にするんじゃないよ、と言われるのはわかるのですが…。
……
…
管理ビーン用の抽象クラスのメリットは今回の例だと1画面しかないのでわかりにくいですが、普段のプロジェクトだと、画面IDや画面遷移処理を定義させています。
また、各画面で必ず実装してね、というものは抽象メソッドとしておいて、継承先の管理ビーンで書く形をとっています。
元コードにあるresetメソッドがCalcViewクラスにないですが、Calcインスタンスの生成をProducer使ったため、ここではそれを普通のメソッドとして呼び出してます(いいのか(^^;)
なのでstaticメソッドにしていました。ここは微妙ですね。
Facelet
最後にビューですが元コードでHTMLのtableタグで書かれていた部分をFaceletっぽく変えました。
<h:form prependId="false">
<h:panelGrid columns="3">
<h:inputText id="left" value="#{calcView.calc.left}" />
<h:outputText value="+" />
<h:inputText id="right" value="#{calcView.calc.right}" />
<h:message for="left" />
<h:outputText value=" " />
<h:message for="right" />
</h:panelGrid>
</h:form>
あとレンダリングされた際にformのIDがつかないようにprependId属性をfalseにしています。
formのid属性をしていない場合、適当なidがふられて、form内のコンポーネントのidがわかりにくくなります。
prependId属性を指定いない場合(あるいはtrueの場合)
prependId属性をfalseにした場合
j_idt5というのが付くか付かないかですね。
動作確認
戻るボタンを忘れてしまいましたが(^^;
動作としては同じになりました。
まとめ
言葉が刺さりますね(^^;JSFに限った話ではなく、Java EE全般な気もします。
Java EEを利用してる方々はだいぶ増えてる感じがするので、うちではこうやってるよ、的な情報がもっと出てくるといいなぁとm(_ _)m
明日(というか今日)朝イチで顧客打合せなんだけど…(^^;ちょっと夜遊びをしすぎました。