きしだのはてな このページをアンテナに追加 RSSフィード

2013-11-08(金) 今どきのJava Webフレームワークってどうなってるの?

[]今どきのJava Webフレームワークってどうなってるの? 12:41 今どきのJava Webフレームワークってどうなってるの?を含むブックマーク

昨日の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コードで。


実行するとこんな感じです。

f:id:nowokay:20131108121958p:image


ロジックを分離したい

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>

ロジックとビューが分離されました。

実行するとこうなります。

f:id:nowokay:20131108121959p:image


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にくるんで返すという処理です。


そんではちょっとアクセスしてみましょう。

f:id:nowokay:20131108122000p:image

JSONだ!


JSONで表示されても、という人もいると思うので、ちょっとResultクラスに@XmlRootElementアノテーションをつけてみます。(import略)

@NoArgsConstructor
@AllArgsConstructor
@Getter @Setter
@XmlRootElement
public class Result {
    int left;
    int right;
    int ans;
}

ん?

f:id:nowokay:20131108122001p:image

ソースをみてみると・・・

<?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をつけてもいいけど、つけないのがいいですね。


アクセスしてみると、こうなります。

f:id:nowokay:20131108122002p:image

わあ、みやすい!


と、こんな感じで、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);
    }

f:id:nowokay:20131108122003p:image


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クラスの要素に結び付けてます。


適当に入力して「足す」ボタンを押すと、結果が表示されます。

f:id:nowokay:20131108122004p:image


requiredをtrueにしているので、入力がなければエラーになるし、Integerに結び付けてるので数値じゃなければエラーになります。

f:id:nowokay:20131108122005p:image


もっと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になっています。


これで次のように動きます。

f:id:nowokay:20131108122006p:image


まちがっても、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.xmlJSFのfaces-config.xmlは、今回ファイルを作成していません。

こんな感じなので、設定まわりもまあ許容範囲かなと思います。


ついでに、pom.xmldependencyだけ書いておきます。

    <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のものでも参考になります。


JAX-RSに関しては、これも前のバージョンのものですが、基本は変わっていないのでひととおり読むといいと思います。

JavaによるRESTfulシステム構築

JavaによるRESTfulシステム構築

kiya2015kiya2015 2013/11/09 00:10 ブクマでも言われているとおり、これを読んで「よしSeasar2からこっちに乗り換えよう」と思う人はいないでしょうね・・・

nowokaynowokay 2013/11/09 01:31 なぜですか?

tkc33tkc33 2013/11/11 21:16 JavaEEやJSFというかJavaの文化圏自体慣れないのでとても助かっています。
データベースまで含めた(JPAとか)今どきのJavaの書き方を知りたいのですが、良い例があるサイトなんかを教えてもらえないでしょうか?己の無知さでJavaEEが嫌になってる感がありまして・・・

tamichantamichan 2013/12/02 18:05 これを見て今後webアプリはJavaで書かなくなると確信した。
数年後には今までJavaで書いてたような案件はDartかCeylonかClojureで書いてるだろうよ