Spark Frameworkの使い方:Getting Start編 はこちら
Spark Web Framework の使い方 : RESTアプリを作成する はこちら
今回はSpark Frameworkを使ったページ(画面)アプリの作成について解説します。
今回使用するライブラリ
- spark-core 2.7.1
- spark-template-thymeleaf 2.7.1
- spark-debug-tools 0.5
spark-template-thymeleafを依存関係に追加します。
<dependency>
<groupId>com.sparkjava</groupId>
<artifactId>spark-template-thymeleaf</artifactId>
<version>2.7.1</version>
</dependency>
Spark Frameworkにおけるページアプリ
RESTアプリを作成するでやったように、Spark FrameworkではHTTP Methodのget・post・put・deleteに対応するマッピングメソッドが用意されています。このうち、ページアプリではget・postを利用します。
get("/", (req, res) -> { ... });
post("/", (req, res) -> { ... });
また、各マッピングメソッドには、レスポンスの内容によって異なるRouteインターフェイスが用意されています。このうち、ページアプリではTemplateViewRouteインターフェイスを利用します。
get(String, TemplateViewRoute, TemplateEngine);
TemplateViewRouteインターフェイスのシグネチャを見てみましょう。
@FunctionalInterface
public interface TemplateViewRoute {
ModelAndView handle(Request request, Response response) throws Exception;
}
TemplateViewRouteインターフェイスはRouteと同様に@FunctionalInterfaceで定義されており、リクエストとレスポンスを引数にとり、ModelAndViewオブジェクトを返却するよう実装します。
ModelAndViewは、テンプレートとなるView名と、テンプレートで使用する変数(Model)を格納するオブジェクトです。
TemplateEngine
マッピングメソッドの第3引数となるTemplateEngineは、実際のテンプレートエンジンを呼び出すためのラッパークラスで、Sparkの付属ライブラリとして以下が提供されています。
- spark-template-closure
- spark-template-freemarker
- spark-template-handlebars
- spark-template-jade
- spark-template-jetbrick
- spark-template-jinjava
- spark-template-jtwig
- spark-template-mustache
- spark-template-mvel
- spark-template-pebble
- spark-template-rocker
- spark-template-thymeleaf
- spark-template-velocity
- spark-template-water
今回は、テンプレートエンジンとしてThymeleaf(spark-template-thymeleaf)を利用することにします。
ページアプリを実装する
前提となる実装
今回はRESTアプリを実装するで作成したTODOリストを管理するRESTアプリを、ページアプリに変換します。
大きな違いとして、前述のTemplateViewRoteを利用するのに加え、RESTアプリとページアプリではURL(パス)の体系が異なります。
- RESTアプリ
findAll : get /todos
findOne : get /todos/:id
create : post /todos
finish : put /todos/:id
remove : delete /todos/:id
- ページアプリ
findAll : get /todos
findOne : -
create : post /todos/create
finish : post /todos/finish
remove : post /todos/remove
実装
それではページアプリを実装していきます。
まずはWebページのテンプレートファイルを作成します。
spark-template-thymeleafのThymeleafTemplateEngineのデフォルトでは、テンプレートファイルはsrc/main/resources/templates/*.htmlというファイル体系になります。
- todos.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link rel="stylesheet" href="/css/styles.css"><!-- CSSファイルを読み込む -->
<title>Todo</title>
</head>
<body>
<h1>Todo List</h1>
<!-- TODOを新規登録するHTMLフォーム -->
<form method="post" action="/todos/create">
<input name="title">
<button>create</button>
</form>
<hr>
<!-- 登録されたTODOを表示するテーブル -->
<table th:if="${not #lists.isEmpty(todos)}">
<thead>
<tr>
<th>title</th>
<th>created</th>
<th></th>
<th></th>
</tr>
</thead>
<tbody>
<tr th:each="todo : ${todos}" th:object="${todo}">
<td th:text="*{title}"></td>
<td th:text="*{#temporals.formatISO(created)}"></td> <!-- (1) -->
<td th:switch="*{finished}">
<!-- 登録されたTODOを完了するHTMLフォーム -->
<span th:case="true">finished</span>
<form th:case="*" method="post" action="/todos/finish">
<button name="id" th:value="*{id}">finish</button>
</form>
</td>
<td>
<!-- 登録されたTODOを削除するHTMLフォーム -->
<form method="post" action="/todos/remove">
<button name="id" th:value="*{id}">remove</button>
</form>
</td>
</tr>
</tbody>
</table>
</body>
</html>
| no. | description |
|---|---|
| (1) |
#temporals#formatISOメソッドでJava 8 Date and Time APIの日付オブジェクトをフォーマットしています。spark-template-thymeleafではデフォルトでthymeleaf-extras-java8timeをサポートしており、#temporalsを利用することができます。 |
ここでは
action属性の指定方法としてリンクURL式(@{})を利用していませんが、spark-template-thymeleafではリンクURL式をサポートしていないためです。Thymeleafの機能をすべてサポートしているわけではないので注意する必要がありそうです。
次にコントローラクラスを作成します。
前回と同様、ハンドラの処理が複雑化することを考慮して、ハンドラをApplication.javaから分離してコントローラクラスを作成します。
- TodoController.java
public class TodoController {
private static final TodoRepository repository = new TodoRepository();
// (1)
public static final TemplateViewRoute findAll = (req, res) -> {
Map<String, Object> model = new HashMap<>(); // (2)
model.put("todos", repository.findAll()); // (3)
return new ModelAndView(model, "todos"); // (4)
};
// (5)
public static final Route create = (req, res) -> {
repository.create(req.queryParams("title"); // (6)
res.redirect("/todos"); // (7)
return null; // (8)
};
public static final Route finish = (req, res) -> {
repository.finish(req.queryParams("id"));
res.redirect("/todos");
return null;
};
public static final Route remove = (req, res) -> {
repository.remove(req.queryParams("id"));
res.redirect("/todos");
return null;
};
}
| no. | description |
|---|---|
| (1) | 画面を返却するハンドラはTemplateViewRouteインターフェイスを実装します。@FunctionalInterfaceなので分かりづらいかもしれませんが、匿名クラスのオブジェクトを生成してクラスフィールドに格納しています。 |
| (2) | 戻り値のModelAndViewオブジェクトに指定するMap<String, Object>オブジェクト(Model)を生成します。 |
| (3) | Modelに変数を追加します。ここでは"todos"という名前で、リポジトリから取得したTODOリストをセットしています。 |
| (4) | 戻り値として、前述のModelとView名(テンプレートファイルの名前)をセットしたModelAndViewオブジェクトを返却します。テンプレートファイルの解決とHTMLの生成は前述のとおりTemplateEngineが実施します。 |
| (5) | create以降のパスでは、処理を行った後findAllのパスにリダイレクトします。リダイレクトするパスでは画面を返却しないため、TemplateViewRouteではなくRouteインターフェイスを実装します。 |
| (6) |
Request#queryParamsメソッドでHTMLフォームで入力されたtitleを取得できます。 |
| (7) |
Response#redirectメソッドでリダイレクトすることができます。なお、 Responseを取れない状況ならSpark.redirectメソッドを利用してリダイレクトすることも可能です。 |
| (8) | リダイレクトしてもメソッドは終了しないので、nullを返却します。ちなみにTemplateViewRouteを実装した場合は、無駄にModelAndViewオブジェクトを生成して返却する必要があります。 |
リダイレクト後のロジックをスキップする手段として、リクエストの処理を中断する
Spark.haltメソッドがありますが、Spark.afterメソッドで適用したフィルタもスキップしてしまうので、利用には注意が必要そうです。
これで必要なピースは揃ったので、あとはリクエストマッピングの設定を行うだけです。
- Application.java
public class Application {
private static final TemplateEngine templateEngine = new ThymeleafTemplateEngine();
public static void main(String[] args) {
// configure application.
staticFiles.location("/public"); // (1)
staticFiles.expireTime(600L);
enableDebugScreen();
// configure routes.
path("/todos", () -> { // (2)
// (3)
get("", TodoController.findAll, templateEngine);
// (4)
post("/create", TodoController.create);
post("/finish", TodoController.finish);
post("/remove", TodoController.remove);
});
}
}
| no. | description |
|---|---|
| (1) | 静的リソースをホストするため、staticFilesメソッドが用意されています。ここでは、src/main/resources/publicディレクトリのCSSファイルを公開し、ブラウザキャッシュを10分間保持させています。 |
| (2) |
pathメソッドは、パスの階層化・グルーピングに使用します。例えば、2番目のpostメソッドにはパス/createを指定しているので、/todos/createというパスへのリクエストがマッピングされることになります。 |
| (3) | 画面を返却するメソッドとパスに対するリクエストを処理するハンドラに先ほど作成したコントローラクラスを指定し、テンプレートを処理するTemplateEngineにThymeleafTemplateEngineを指定しています。 |
| (4) | リダイレクトするパスでは画面を返却する必要がないため、TemplateEngineは指定しません。 |
実行
アプリを起動して、ブラウザからhttp://localhost:4567/todosにアクセスすると、TODOリストの画面が返却され、ページアプリとして機能していることが分かると思います。
まとめ
TemplateViewRouteとテンプレートエンジンを利用して、ページアプリをシンプルに実装できることが分かりました。
REST APIに比べると、HTMLフォームのパラメータからモデルに変換する機構がないため、ここをどう実装するかがポイントになりそうですね。