昨日のSeasar2のエントリについたコメントなどで、「とはいえ代わりに何つかうの?」みたいな話が出てたので、とりあえずJava EEのWebフレームワークについて簡単にまとめてみます。
Java SE 8+Java EE 7+lombokで書いていますが、基本的なところはJava SE 7+Java EE 6でも大丈夫です。
なので、今どきとは書いてますが、基本的には2009年12月のJava EE 6ということで、実はすでに4年近くたってます。
何も考えてない
なんも難しいこと考えないなら、やっぱJSPが楽ですよね。
なんでも書けちゃう。
<%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <h1>Hello JSP!</h1> <%! int add(int left, int right){ return left + right; } %> <% int res = add(2, 3); out.println("2と3を足すと" + res); %> </body> </html>
addメソッドを定義して、呼び出して表示しています。Javaコードで。
ロジックを分離したい
JSPだけでは、HTMLとJavaコードがまじってしまうし、やはりロジックは分離して、JSPには表示のことだけをやってもらったほうがいいですね。
ということで、こんなJavaクラスにロジックを書くことにします。
import javax.enterprise.context.ApplicationScoped; import javax.inject.Named; @Named @ApplicationScoped public class CalcLogic { public int add(int left, int right){ return left + right; } }
@NamedアノテーションをつけてCDI管理して、オブジェクトの寿命を @ApplicationScopedとしてアプリケーション起動中ずっと有効にしています。ステートレスなロジックはアプリケーションスコープにしてオブジェクトを使いまわすことが多いですよね。
ここでは単に足し算してるだけですが、まあもっと長いロジックがあると思ってください。
ついでにこんなクラスも作っておきます。
import java.time.LocalDate; import java.time.LocalDateTime; import java.time.temporal.TemporalAdjusters; import javax.annotation.PostConstruct; import javax.enterprise.context.RequestScoped; import javax.inject.Named; import lombok.Getter; @Named @RequestScoped public class DateLogic { @Getter private LocalDateTime now; @PostConstruct public void init(){ now = LocalDateTime.now(); } public LocalDate getLastDay(){ return now.toLocalDate() .with(TemporalAdjusters.lastDayOfMonth()); } }
これもCDIに登録しますけど、今回は@RequestScopedにしてリクエストのたびにオブジェクトが生成されるようにしています。あと、コンストラクタではなくて @PostConstructアノテーションをつけたinitメソッドで初期化を行うようにしてます。
getter書くのめんどくさいので、nowフィールドにはlombokの @Getterアノテーションつけてます。
あとはgetLastDayとして、月の最終日を返しています。
Java 8のDate Time APIですね。ただ、残念ながら、GlassFish4は現状でJava 8のlambda構文には対応していません。
そしたら、こんなJSPを書きます。
<%@page contentType="text/html" pageEncoding="UTF-8"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <h1>Hello World!</h1> 2と3を足すと${calcLogic.add(2, 3)}<br/> 今は${dateLogic.now}<br/> 月末は${dateLogic.lastDay}<br/> </body> </html>
ロジックとビューが分離されました。
実行するとこうなります。
DateLogicのスコープを @RequestScopedにしているので、リロードするたびに時間がかわりますが、これを @SessionScopedにすればセッションで最初にアクセスした時間、 @ApplicationScopedにすればアプリケーションを起動して最初にアクセスした時間が表示されます。
CDIを使うと、オブジェクトの寿命管理が楽になるので便利です。
パラメータを受け取りたい
さてさて、WebアプリケーションではURLやその中のクエリパラメータ、POSTデータなど、いろいろな入力によって処理することも必要になります。
ということで、ちょっと結果表示用のクラスを用意します。
import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @NoArgsConstructor @AllArgsConstructor @Getter @Setter public class Result { int left; int right; int ans; }
アノテーションたくさんですが、基本的にはint型のフィールドが3つあるだけです。ここにlombokでセッターゲッター、デフォルトコンストラクタ、フィールドを初期化するためのコンストラクタを追加してます。
そして、リクエストを受け取るためのクラスを用意します。
import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.inject.Named; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.QueryParam; @Named @ApplicationScoped @Path("/calc") public class CalcService { @Inject CalcLogic logic; @Path("add") @GET public Result add( @QueryParam("left") int p1, @QueryParam("right") int p2) { int ans = logic.add(p1, p2); return new Result( p1, p2, ans); } }
@NamedでCDIに登録して、@ApplicationScopedとしています。
@Pathアノテーションで、このクラスは/calcというパスを処理するように指定しています。
で、logicフィールドに@Injectアノテーションつけて、さっきの計算ロジックをもってきます。
あとは、実際にリクエストを処理するaddメソッドの用意。
ここでは、@Pathアノテーションでaddというパスを処理するように指定して、@GETアノテーションでHTTPのGETメソッドを処理するようにしています。
あと、addメソッドの引数に @QueryParamアノテーションをつけて、この引数にURL中のleft=xxxやright=xxxの値が入るようにしています。
JAX-RSの仕様をしらなくても、ここを見るだけでどのようなリクエストを処理するかなんとなく把握できると思います。
処理的には、受け取ったパラメータをlogicに渡して、Resultにくるんで返すという処理です。
そんではちょっとアクセスしてみましょう。
JSONだ!
JSONで表示されても、という人もいると思うので、ちょっとResultクラスに@XmlRootElementアノテーションをつけてみます。(import略)
@NoArgsConstructor @AllArgsConstructor @Getter @Setter @XmlRootElement public class Result { int left; int right; int ans; }
<?xml version="1.0" encoding="UTF-8"?> <result> <ans>8</ans> <left>3</left> <right>5</right> </result>
XMLだ!
NetBeansのブラウザはXMLをいい感じには表示してくれませんでした。
実際には@Producesアノテーションでmimeタイプを指定しておいたほうがいいでしょう。複数のmimeタイプを指定すると、ブラウザからのAcceptヘッダにしたがって適切な出力をしてくれます。
@Path("add") @GET @Produces({MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML}) public Result add( @QueryParam("left") int p1, @QueryParam("right") int p2) { int ans = logic.add(p1, p2); return new Result( p1, p2, ans); }
これで、人間以外のお客さまに満足していただけるサイトができました!
でもぼくはもう少し見やすいほうがいいな。人間はわがままです。
ということで、こんなJSPを作ります。名前はwithrs.jspとします。
<%@page contentType="text/html" pageEncoding="MS932"%> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=MS932"> <title>JSP Page</title> </head> <body> <h1>Hello JAX-RS</h1> ${it.left}と${it.right}を足すと${it.ans}です </body> </html>
itというオブジェクトを使ってるところがミソです。
あと、エンコーディングをMS932にしてるのは、現状ではエンコーディングをちゃんと指定できなくてシステムデフォルトのエンコーディングで表示されてしまうからです。Macなどではutf-8のほうがいいかもしれません。いまのところはGlassFishの起動オプションでエンコーディングを指定しておくほうがいいです。
で、まあさっきのaddメソッドでmimeタイプをHTMLにして、@Templateアノテーションを追加しておきます。
@Path("add") @GET @Produces(MediaType.TEXT_HTML) @Template(name="/withrs") public Result add( @QueryParam("left") int p1, @QueryParam("right") int p2) { int ans = logic.add(p1, p2); return new Result( p1, p2, ans); }
テンプレート名には拡張子jspをつけてもいいけど、つけないのがいいですね。
と、こんな感じで、JAX-RSを使うと基本的なJavaコードをもとにアノテーションでさまざまな入力・出力に対応できます。
ただ、残念ながら、この@Templateに関してはjerseyの独自拡張ということになっていて、JAX-RS標準ではありません。次版で標準化されることを期待しています。
ついでに、次のような@Pathアノテーションの指定と@PathParamアノテーションを使うと、URLの分解もできます。
@Path("add/{left}/{right}") @GET @Produces(MediaType.TEXT_HTML) @Template(name="/withrs") public Result addPath( @PathParam("left") int p1, @PathParam("right") int p2) { return add(p1, p2); }
JAX-RSはかなり強力かつ柔軟なので、入力をうけとって出力を返すというものには十分に使えるように思います。
もともとHTML出力は標準としては考えられてなかった感じがあって、まだ少し実装がこなれる必要がると思いますけど。
フォーム入力する
フォーム入力も、JAX-RSでPOSTを受け取るようにすればいいんですけど、ちょっとめんどうです。
ということでJSF。
まずは、画面を管理するクラスをつくります。
@Named @SessionScoped @Setter @Getter public class CalcController implements Serializable{ Integer left; Integer right; Integer ans; @Inject CalcLogic logic; public void add(){ ans = logic.add(left, right); } }
@SessionScopedにしてCDIに登録しています。もうすこし狭いスコープでもいいけど、今回はこれで。
あと、フィールドは未入力状態に対応できるよう、intではなくInteger型にしています。クラスに@Setter/@Getterアノテーションがついているので、すべてのフィールドにアクセッサが自動生成されます。
あとは、CalcLogicをもってくるようにして、addメソッドで計算してます。
で、こんなJSPつくります。
<%@page contentType="text/html" pageEncoding="UTF-8"%> <%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %> <%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %> <!DOCTYPE html> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>JSP Page</title> </head> <body> <h1>Hello JSF!</h1> <f:view> <h:form> <h:inputText label="左辺" value="#{calcController.left}" required="true"/> と <h:inputText label="右辺" value="#{calcController.right}" required="true"/> <h:commandButton value="足す" action="#{calcController.add()}"/> 答え<h:outputText value="#{calcController.ans}"/> </h:form> <h:messages/> </f:view> </body> </html>
inputTextコンポーネントで入力、outputTextコンポーネントで出力、commandButtonでボタンを置いて、それぞれCalcControllerクラスの要素に結び付けてます。
requiredをtrueにしているので、入力がなければエラーになるし、Integerに結び付けてるので数値じゃなければエラーになります。
もっとHTMLっぽく書きたいという人は、Faceletsを使うのがいいと思います。
<?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:h="http://xmlns.jcp.org/jsf/html" xmlns:jsf="http://xmlns.jcp.org/jsf"> <head> <title>Facelet Title</title> </head> <body jsf:id="body"> <h1>Hello from Facelets</h1> <form jsf:id="form"> <input type="text" label="左辺" jsf:value="#{calcController.left}" required="true"/> と <input type="text" label="右辺" jsf:value="#{calcController.right}" required="true"/> <input type="submit" value="足す" jsf:action="#{calcController.add()}"/> 答え #{calcController.ans} </form> <div jsfc="h:messages"/> </body> </html>
jsf:idとかjsf:valueとかjsfcとか、JSF独自の属性が入るものの普通っぽいHTMLになっています。
まちがっても、JSFでWeb全部まかなおうとしてはダメですが、管理系や業務アプリ、登録フォームなどフォーム主体の画面にはとてもよいです。
Ajax対応もされていて、JavaScriptを記述せずにかなりの処理ができるので、たいしたことないAjaxを持った画面が大量にあるという場合には、かなりいい気がします。
2013/11/9追記
Ajaxへの対応も書いておきます。
JSFのフォームをAjax対応するには、基本はf:ajaxタグを追加するだけです。
<?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:h="http://xmlns.jcp.org/jsf/html" xmlns:f="http://xmlns.jcp.org/jsf/core" xmlns:jsf="http://xmlns.jcp.org/jsf"> <head jsf:id="head"> <title>Facelet Title</title> </head> <body jsf:id="body"> <h1>Hello from Facelets</h1> <form jsf:id="form"> <input jsf:id="left" type="text" label="左辺" jsf:value="#{calcController.left}" required="true"/> と <input jsf:id="right" type="text" label="右辺" jsf:value="#{calcController.right}" required="true"/> <input jsf:id="btn" type="submit" value="足す" jsf:action="#{calcController.add()}"> <span jsfc="f:ajax" render="ans msg" execute="left right"/> </input> 答え <span jsfc="h:outputText" id="ans" value="#{calcController.ans}"/> <div id="msg" jsfc="h:messages"/> </form> </body> </html>
変更点としては、Ajax呼び出しで送信するデータをもったコンポーネントや受信して更新するコンポーネントを指定する必要があるため、inputタグやdivタグにidをつけています。また、裸のEL式ではid指定できないため、「答え」のところもh:outputTextコンポーネントにしています。
また、JavaScriptが生成されるheadも指定できる必要があるため、headにもidをつけています。
これは、実際につくるときにはajaxじゃないときにも対応しておいたほうがいいです。もちろん、formごと指定すれば個別のidを指定できる必要はありません。
その上で、次のようなf:ajaxタグを追加して、executeで送信するコンポーネントのID、renderで表示更新するコンポーネントのIDを指定しています。
<span jsfc="f:ajax" render="ans msg" execute="left right"/>
基本的にJavaScriptをさわらずAjaxを基本にした画面が構築できるので、Java技術者しかいない、JavaScriptが使える人がいたとしても、ソースをJavaScriptとJavaに対応するのが大変というときにはおすすめです。
ただ、FaceletsでのHTML親和性の高い記述というのは、デザイナとの協業があるってとき以外は面倒なだけなので、おとなしくh:outputTextタグとか書いておいたほうがいいですね。
追記ここまで。
設定
設定はだいたいNetBeansが勝手にやってくれるのであまり気にしないのですが、とりあえず設定ファイルものせておきます。
web.xmlには、JSF関連の設定が必要です。
<?xml version="1.0" encoding="UTF-8"?> <web-app version="3.1" xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"> <context-param> <param-name>javax.faces.PROJECT_STAGE</param-name> <param-value>Development</param-value> </context-param> <servlet> <servlet-name>Faces Servlet</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>Faces Servlet</servlet-name> <url-pattern>*.xhtml</url-pattern> <url-pattern>/faces/*</url-pattern> </servlet-mapping> <session-config> <session-timeout> 30 </session-timeout> </session-config> <welcome-file-list> <welcome-file>faces/index.xhtml</welcome-file> </welcome-file-list> </web-app>
JAX-RSでは、設定クラスが必要になります。ここでルートパスなどを決めます。また、サービスクラスはここで登録しておく必要があります。NetBeansを使う上では、勝手に管理してくれるので気にする必要はあまりないですが。
@Templateを使うときにJspMvcFeatureの登録は自力で行う必要があります。
import java.util.Set; import javax.ws.rs.core.Application; import org.glassfish.jersey.server.mvc.jsp.JspMvcFeature; @javax.ws.rs.ApplicationPath("ws") public class ApplicationConfig extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> resources = new java.util.HashSet<>(); addRestResourceClasses(resources); resources.add(JspMvcFeature.class); return resources; } private void addRestResourceClasses(Set<Class<?>> resources) { resources.add(sample.recentweb.CalcService.class); } }
CDIのbeans.xmlやJSFのfaces-config.xmlは、今回ファイルを作成していません。
こんな感じなので、設定まわりもまあ許容範囲かなと思います。
ついでに、pom.xmlのdependencyだけ書いておきます。
<dependencies> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.12.2</version> </dependency> <dependency> <groupId>org.glassfish.jersey.ext</groupId> <artifactId>jersey-mvc-jsp</artifactId> <version>2.0</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax</groupId> <artifactId>javaee-web-api</artifactId> <version>7.0</version> <scope>provided</scope> </dependency> </dependencies>
まとめ
リクエストを受け取ってレスポンスを返す、という場合には、レスポンスがHTMLであれXMLであれ、JAX-RSが便利です。
また、フォーム形式での入力にはJSFがいいです。Ajax対応もしているし、PrimeFacesなどコンポーネントも充実しています。
テンプレートに関しては、JSFの場合はFaceletsがいい感じになってきましたが、JAX-RSからのテンプレートには使えません。JAX-RS用テンプレートとしてはThymeleafなどJava EE標準ではないテンプレートを使うことが多いと思います。このとき、Faceletsとレイアウトを共有するといったことが難しく、結構こまります。
ということで、それを解決しようとするとJSFに対応したJavaEE外のテンプレートを使うことになるんではないでしょうか。JSFは、管理画面や購入シーケンスなど、ある程度は通常Webと分離したところで使うことになるので、テンプレートを共有しないという解決方法もありますが。
今回、全般において、Java EE独自の特別なオブジェクトは使っていません。基本的にアノテーションでの対応です。
なので、ユニットテストを書くときにWebフレームワークを気にする必要があまりありません。JSFまわりでは独自オブジェクトを結構つかうことになっていまいますが。
で、なんにせよ、オブジェクトの管理はCDIがだいぶ育ってきました。
DIコンテナは変数に勝手にインジェクションしてくれることに注目されがちですが、一番の目的はオブジェクトのライフサイクル管理です。CDIはとても便利です。@Transactional みたいなアノテーションも導入されてトランザクション管理もできるようになったので、通常の範囲だとEJBがいらない子になりました。
今すぐ使えるかといわれると、実行環境の問題などもあって、まだJava EE 6の範囲でしか使えないものもありますが、それでも結構使い物になってきていると思います。
書籍
Java EE 7対応の書籍はまだ日本語では出ていませんが、Java EE 6のものでも参考になります。
Beginning Java EE 6~GlassFish 3で始めるエンタープライズJava (Programmer's SELECTION)
- 作者: Antonio Goncalves,日本オラクル株式会社,株式会社プロシステムエルオーシー
- 出版社/メーカー: 翔泳社
- 発売日: 2012/03/09
- メディア: 大型本
- 購入: 5人 クリック: 147回
- この商品を含むブログ (29件) を見る
JAX-RSに関しては、これも前のバージョンのものですが、基本は変わっていないのでひととおり読むといいと思います。
- 作者: Bill Burke,arton,菅野良二
- 出版社/メーカー: オライリージャパン
- 発売日: 2010/08/23
- メディア: 大型本
- 購入: 28人 クリック: 804回
- この商品を含むブログ (41件) を見る