今どきのJava Webフレームワーク(ry)のPlay(Java)版を書いてみた

今どきのJava Webフレームワークってどうなってるの? - きしだのはてなをPlay(Java)でやったらどうなるの?というだけのエントリです。
ちなみに使ったこと無いのでlombok使ってないです。

何も考えてない

なんも難しいこと考えないなら、やっぱScalaテンプレートが楽ですよね*1
なんでもは書けません。

//Application.java
package controllers;

import play.mvc.*;
import views.html.index;

public class Application extends Controller {

    public static Result index() {
        return ok(index.render());
    }

}
<!-- index.scala.html -->
<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>ScalaTemplate Page</title>
    </head>
    <body>
        <h1>Hello World!</h1>

        @add(left: Int, right: Int) = @{
            left + right
        }

        @defining(add(2, 3)){ res =>
            2と3を足すと@res
        }
    </body>
</html>

add関数を定義して、呼び出して表示しています。Scalaテンプレートのコードで。


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

ロジックを分離したい

Scalaテンプレートだけでは、HTMLとScalaテンプレートのコードがまじってしまうし、やはりロジックは分離して、Scalaテンプレートには表示のことだけをやってもらったほうがいいですね。

ということで、こんなJavaクラスにロジックを書くことにします。

package models;

public class CalcLogic {
    public static int add(int left, int right) {
        return left + right;
    }
}

PlayはCDIとか無かった気がするのでstaticメソッドにしました。
ここでは単に足し算してるだけですが、まあもっと長いロジックがあると思ってください。


ついでにこんなクラスも作っておきます。

package models;

import org.joda.time.LocalDate;
import org.joda.time.LocalDateTime;

public class DateLogic {

    public final LocalDateTime now;

    public DateLogic() {
        now = LocalDateTime.now();
    }

    public LocalDate getLastDay(){
        return now.toLocalDate().dayOfMonth().withMaximumValue();
    }
}


これもCDIとか無いのでインスタンスを作る普通のクラスにしました。
getter書くのめんどくさいので、nowフィールド公開しました。Java8セットアップするのめんどかったのでPlayにデフォルトでついてきてるJodaTime使いましたけど確か不変だった気がするので公開して大丈夫ですよね。

そしたら、controllerを修正して、こんなScalaテンプレートを書きます。

package controllers;

import models.DateLogic;
import play.mvc.*;
import views.html.index;

public class Application extends Controller {

    public static Result index() {
        return ok(index.render(new DateLogic()));
    }

}
@(dateLogic: DateLogic)

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>ScalaTemplate Page</title>
    </head>
    <body>
        <h1>Hello World!</h1>
        2と3を足すと@CalcLogic.add(2, 3)<br/>
        今は@dateLogic.now<br/>
        月末は@dateLogic.getLastDay<br/>
    </body>
</html>


ロジックとビューが分離されました。
実行するとこうなります。

DateLogicをアクセスする度newしているので、リロードするたびに時間がかわりますが、結果をCookieに入れておけばセッションで最初にアクセスした時間、Cacheに入れておけばアプリケーションを起動して最初にアクセスした時間が表示されます。
JavaEEみたいにいろいろ豪華な仕組みがあるわけではないですが、クッキーとかキャッシュとか、ライトな感じの言語の経験がある人なら割と一般的なやり方だと思います。*2

パラメータを受け取りたい

さてさて、WebアプリケーションではURLやその中のクエリパラメータ、POSTデータなど、いろいろな入力によって処理することも必要になります。
ということで、ちょっと結果表示用のクラスを用意します。*3

package models;

public class Result {
    private int left;
    private int right;
    private int ans;

    public Result(int left, int right, int ans) {
        this.left = left;
        this.right = right;
        this.ans = ans;
    }
    // getter/setter略
    // IDEが自動で作ってくれるからつらくないもん!
}

冒頭の通りlombok使ってないのでgetter/setterは書きました。というかIDEに生成させました。


そして、リクエストを受け取るためにroutesにルーティングを追加してcontrollerにメソッドを追加します。

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET        /                    controllers.Application.index()
GET        /calc/add            controllers.Application.add(left: Int, right: Int)

# Map static resources from the /public folder to the /assets URL path
GET        /assets/*file        controllers.Assets.at(path="/public", file)
package controllers;

import models.*;
import play.libs.Json;
import play.mvc.*;
import play.mvc.Result;
import views.html.index;

public class Application extends Controller {

    public static Result index() {
        return ok(index.render(new DateLogic()));
    }

    public static Result add(int left, int right){
        int ans = CalcLogic.add(left, right);
        return ok(
            Json.toJson(new models.Result(left, right, ans))
        );
    }
}

routesとかcontrollerは見たまんまです。GETリクエストでパラメータを受けて、計算した結果をレスポンスで返します。
そんではちょっとアクセスしてみましょう。

JSONだ!


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

@XmlRootElement
public class Result {
    private int left;
    private int right;
    private int ans;

    public Result(){}
    
    public Result(int left, int right, int ans) {
        this.left = left;
        this.right = right;
        this.ans = ans;
    }

    public int getLeft() {
        return left;
    }
    public void setLeft(int left) {
        this.left = left;
    }
    public int getRight() {
        return right;
    }
    public void setRight(int right) {
        this.right = right;
    }
    public int getAns() {
        return ans;
    }
    public void setAns(int ans) {
        this.ans = ans;
    }
}
package controllers;

import models.CalcLogic;
import models.DateLogic;
import play.mvc.Controller;
import play.mvc.Result;
import views.html.index;

import javax.xml.bind.JAXB;
import java.io.StringWriter;

public class Application extends Controller {

    public static Result index() {
        return ok(index.render(new DateLogic()));
    }

    public static Result add(int left, int right){
        int ans = CalcLogic.add(left, right);
        StringWriter out = new StringWriter();
        JAXB.marshal(new models.Result(left, right, ans), out);
        return ok(out.toString()).as("application/xml");
    }
}

XMLだ!
やっぱりこの辺は全ていい感じにやってくれる、とはいきませんね。Acceptヘッダに応じて自動的に振り分けるみたいなのも多分出来ません。


これで、人間以外のお客さまに満足していただけるサイトができました!
でもぼくはもう少し見やすいほうがいいな。人間はわがままです。
ということで、こんなScalaテンプレートを作ります。名前はwithrs.scala.htmlとします。

@(res: models.Result)

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>ScalaTemplate Page</title>
    </head>
    <body>
        <h1>Hello World!</h1>
        @{res.getLeft}と@{res.getRight}を足すと@{res.getAns}です
    </body>
</html>

最初の方の例で日付ロジック使った時と特に変わりません。
で、さっきのcontrollerを

package controllers;

import models.CalcLogic;
import models.DateLogic;
import play.mvc.Controller;
import play.mvc.Result;
import views.html.index;
import views.html.withrs;

public class Application extends Controller {

    public static Result index() {
        return ok(index.render(new DateLogic()));
    }

    public static Result add(int left, int right){
        int ans = CalcLogic.add(left, right);
        return ok(withrs.render(new models.Result(left, right, ans)));
    }
}

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

わあ、みやすい!


と、こんな感じで、残念ながらPlayでは基本的なJavaコードをもとにアノテーションでさまざまな入力・出力に対応…というわけにはいかず、適宜振り分ける必要があります。


ついでに、次のようにroutesを書き換えると、URLの分解もできます。

# Routes
# This file defines all application routes (Higher priority routes first)
# ~~~~

# Home page
GET        /                      controllers.Application.index()
GET        /calc/add/:left/:right controllers.Application.add(left: Int, right: Int)

# Map static resources from the /public folder to the /assets URL path
GET        /assets/*file        controllers.Assets.at(path="/public", file)

JAX-RSほど色々面倒みてくれるわけではないですが、入力をうけとって出力を返すというものには十分に使えるように思います。

フォーム入力する

フォーム入力も、同じような感じでできます。


まずは、routesとcontroller作ります。

GET         /calc/post                    controllers.Calc.index()
POST        /calc/post                    controllers.Calc.add()
package controllers;

import models.CalcLogic;
import play.data.Form;
import play.data.validation.Constraints;
import play.mvc.Controller;
import play.mvc.Result;
import views.html.withrs;

public class Calc extends Controller {

    static Form<Input> initForm =  Form.form(Input.class).fill(new Input());

    public static Result index() {

        Input input = initForm.get();

        int ans = CalcLogic.add(input.left, input.right);

        return ok(withrs.render(
            initForm,
            new models.Result(input.left, input.right, ans)
        ));
    }

    public static Result add() {

        Form<Input> bind = initForm.bindFromRequest();

        if(bind.hasErrors()){

            return badRequest(withrs.render(
                bind,
                new models.Result()
            ));

        }else{

            Input input = bind.get();
            int ans = CalcLogic.add(input.left, input.right);

            return ok(withrs.render(
                bind,
                new models.Result(input.left, input.right, ans)
            ));
        }
    }

    public static class Input{
        @Constraints.Required
        public Integer left;
        @Constraints.Required
        public Integer right;
        public Input(){}
    }
}

Form.form(Input.class)でInputクラスへ入力をbindするformが作れます。fillで初期値注入、bindFromRequestでリクエストから自動でバインドします。


テンプレートはこう。

@(input: play.data.Form[controllers.Calc.Input], res: models.Result)

<!DOCTYPE html>
<html>
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
        <title>ScalaTemplate Page</title>
    </head>
    <body>
        <h1>Hello World!</h1>

        <ul>
        @for(entry <- input.errors().entrySet(); ve <- entry.getValue){
            <li>@{entry.getKey} : @{ve.message()}</li>
        }
        </ul>

        <form method="post" action="@routes.Calc.add">
            <input type="text" name="left" value="@{input("left").valueOr("")}" /><input type="text" name="right" value="@{input("right").valueOr("")}" />
            <button type="submit">足す</button>
            答え@res.getAns
        </form>
    </body>
</html>

引数でもらったformに設定されている値(無ければ空文字)をフォームに設定。エラーがあれば表示します。@routesとかなってるのはリバースルーティングです。

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

Inputクラスに@Requiredアノテーションつけてるので空だとエラーになるし、Inputクラスのleftとrightは整数なので、バインドに失敗してもエラーになります。

エラーメッセージはテンプレート中でMessagesクラス使えばメッセージリソースから取得できます。
動的部分以外はただのHTMLですが、Scalaテンプレート以外を使いたい場合もMustacheプラグインとかあります。

設定

とくにありません。今回扱った範囲ぐらいだと特に追加する依存もなく、デフォルト設定のまま出来ます。

まとめ

今回Javaですが、Scala版もまぁほとんど同じです。Controllerとかがもうちょいすっきり書けるくらいだし、それはPlay云々というよりScalaの言語としてのアレコレの話なので割愛。
Play2はフルスタックですが、DIとかオブジェクトの寿命管理とか色々用意してくれているわけでも面倒を見てくれるわけでもないので、結局はORM違うの使ってみたりテンプレートエンジン履き替えてみたりGuice組み込んでみたりとか*4、自分で何とかしなきゃいけない部分は結構あります。
じゃあ結局Javaな文化圏の人たちにはSpringとかJAX-RSとかがいいんじゃないの?という話は一定その通りで、既存の資産があるのにわざわざ乗り換えるものかというとそうではないと思います。*5
ただ、Javaの文化圏に居なかった人たちにとっては割と取っ付き易い感じなのではないかなーと思います。*6

*1: (棒)

*2:クッキーやキャッシュを扱う仕組みはPlayにはデフォルトで用意されています

*3:caseクラス使えたら楽なのnグワーッ

*4:2.1以降はControllerをインスタンス化出来るようになったのでGuiceでサービスクラスの実装を注入、みたいなことが出来るようになりました

*5:そもそもServletじゃないですし

*6:Javaの文化圏に居なかった、あるいは既存システムの移行じゃない新規のJavaプロジェクト、または重厚感溢れない感じで割とライトにJavaを使っているみたいな人が一体どれぐらい居るかと言われたらウッ頭が