11.2. チュートリアル(Todoアプリケーション Thymeleaf編)


11.2.1. はじめに

11.2.1.1. このチュートリアルで学ぶこと

  • TERASOLUNA Server Framework for Java (5.x)による基本的なアプリケーションの開発方法

  • MavenおよびSTS(Eclipse)プロジェクトの構築方法

  • TERASOLUNA Server Framework for Java (5.x)の アプリケーションのレイヤ化 に従った開発方法


11.2.1.2. 対象読者

  • SpringのDIやAOPに関する基礎的な知識がある

  • Servlet/テンプレートエンジン(JSPなど)を使用してWebアプリケーションを開発したことがある

  • SQLに関する知識がある


11.2.1.3. 検証環境

このチュートリアルは以下の環境で動作確認している。他の環境で実施する際は本書をベースに適宜読み替えて設定していくこと。

種別

名前

OS

Windows 10

JVM

Java 17

IDE

Spring Tool Suite 4.17.1.RELEASE (以降「STS」と呼ぶ。設定方法はSTS4の設定手順を参照されたい。)

Build Tool

Apache Maven 3.8.6 (以降「Maven」と呼ぶ)

Application Server

Apache Tomcat 10.1.15

Web Browser

Google Chrome 117


11.2.2. 作成するアプリケーションの説明

本チュートリアルでは、ViewとしてThymeleafを使用して開発するメリットを体感できるよう、最初にHTMLで画面デザインのみ実装したモックアップ(以降、プロトタイプと呼ぶ)を作成し、そこにアプリケーションの機能を追加していく。
本ガイドラインでは、HTMLで作成したプロトタイプにThymeleafの属性を付与してテンプレート化したものを「テンプレートHTML」と呼ぶ。

11.2.2.1. アプリケーションの概要

TODOを管理するアプリケーションを作成する。TODOの一覧表示、TODOの登録、TODOの完了、TODOの削除を行える。

../_images/image0011.png

11.2.2.2. アプリケーションの業務要件

アプリケーションの業務要件は、以下の通りとする。

ルールID

説明

B01

未完了のTODOは5件までしか登録できない

B02

完了済みのTODOは完了できない

Note

本要件は学習のためのもので、現実的なTODO管理アプリケーションとしては適切ではない。


11.2.2.3. アプリケーションの処理仕様

アプリケーションの処理仕様と画面遷移は、以下の通りとする。

../_images/image0021.png

項番

プロセス名

HTTPメソッド

URL

備考

1

Show all TODO

-

/todo/list

2

Create TODO

POST

/todo/create

作成処理終了後、Show all TODOへリダイレクト

3

Finish TODO

POST

/todo/finish

完了処理終了後、Show all TODOへリダイレクト

4

Delete TODO

POST

/todo/delete

削除処理終了後、Show all TODOへリダイレクト


11.2.2.3.1. Show all TODO

  • TODOを全件表示する

  • 未完了のTODOに対しては「Finish」と「Delete」用のボタンが付く

  • 完了のTODOは打ち消し線で装飾する

  • TODOの件名のみ表示する


11.2.2.3.2. Create TODO

  • フォームから送信されたTODOを保存する

  • TODOの件名は1文字以上30文字以下であること

  • アプリケーションの業務要件のB01を満たさない場合はエラーコードE001でビジネス例外をスローする

  • 処理が成功した場合は、遷移先の画面で「Created successfully!」を表示する


11.2.2.3.3. Finish TODO

  • フォームから送信されたtodoIdに対応するTODOを完了済みにする

  • 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする

  • アプリケーションの業務要件のB02を満たさない場合はエラーコードE002でビジネス例外をスローする

  • 処理が成功した場合は、遷移先の画面で「Finished successfully!」を表示する


11.2.2.3.4. Delete TODO

  • フォームから送信されたtodoIdに対応するTODOを削除する

  • 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする

  • 処理が成功した場合は、遷移先の画面で「Deleted successfully!」を表示する


11.2.2.4. エラーメッセージ一覧

エラーメッセージとして、以下の3つを定義する。

エラーコード

メッセージ

置換パラメータ

E001

[E001] The count of un-finished Todo must not be over {0}.

{0}… max unfinished count

E002

[E002] The requested Todo is already finished. (id={0})

{0}… todoId

E404

[E404] The requested Todo is not found. (id={0})

{0}… todoId


11.2.3. 環境構築

本チュートリアルでは、インフラストラクチャ層のRepositoryImplの実装として、

  • データベースを使用せずjava.util.Mapを使ったインメモリ実装のRepositoryImpl

  • MyBatis3を使用してデータベースにアクセスするRepositoryImpl

  • Spring Data JPAを使用してデータベースにアクセスするRepositoryImpl

の3種類を用意している。用途に応じていずれかを選択する。

チュートリアルの進行上、まずはインメモリ実装を試し、その後MyBatis3またはSpring Data JPAを選ぶのが円滑である。


11.2.3.1. プロジェクトの作成

まず、mvn archetype:generateを利用して、実装するインフラストラクチャ層向けのブランクプロジェクトを作成する。 ここでは、Windowsのコマンドプロンプトを使用してブランクプロジェクトを作成する手順となっている。

Note

インターネット接続するために、プロキシサーバーを介する必要がある場合、以下の作業を行うため、STSのProxy設定と、MavenのProxy設定が必要である。

Tip

Bash上でmvn archetype:generateを実行する場合は、以下のように”^“を”\“に置き換えて実行すればよい。

mvn archetype:generate -B\
 -DarchetypeGroupId=org.terasoluna.gfw.blank\
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-thymeleaf-archetype\
 -DarchetypeVersion=5.9.0.RELEASE\
 -DgroupId=com.example.todo\
 -DartifactId=todo\
 -Dversion=1.0.0-SNAPSHOT

11.2.3.1.1. O/R Mapperに依存しないブランクプロジェクトの作成

データベースを使用せずjava.util.Mapを使ったインメモリ実装のRepositoryImpl用のプロジェクトを作成する場合は、以下のコマンドを実行してO/R Mapperに依存しないブランクプロジェクトを作成する。本チュートリアルを順序通り読み進める場合は、まずはこの方法でプロジェクトを作成すること

mvn archetype:generate -B^
 -DarchetypeGroupId=org.terasoluna.gfw.blank^
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-thymeleaf-archetype^
 -DarchetypeVersion=5.9.0.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT

11.2.3.1.2. MyBatis3用のブランクプロジェクトの作成

MyBatis3を使用してデータベースにアクセスするRepositoryImpl用のプロジェクトを作成する場合は、 以下のコマンドを実行してMyBatis3用のブランクプロジェクトを作成する。このプロジェクト作成方法はMyBatis3を使用したインフラストラクチャ層の作成で使用する。

mvn archetype:generate -B^
 -DarchetypeGroupId=org.terasoluna.gfw.blank^
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-thymeleaf-mybatis3-archetype^
 -DarchetypeVersion=5.9.0.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT

11.2.3.1.3. JPA用のブランクプロジェクトの作成

Spring Data JPAを使用してデータベースへアクセスするRepositoryImpl用のプロジェクトを作成する場合は、以下のコマンドを実行してJPA用のブランクプロジェクトを作成する。このプロジェクト作成方法はSpring Data JPAを使用したインフラストラクチャ層の作成で使用する。

mvn archetype:generate -B^
 -DarchetypeGroupId=org.terasoluna.gfw.blank^
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-thymeleaf-jpa-archetype^
 -DarchetypeVersion=5.9.0.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT

11.2.3.2. プロジェクトのインポート

作成したブランクプロジェクトをSTSへインポートする。

STSのメニューから、[File] -> [Import] -> [Maven] -> [Existing Maven Projects] -> [Next]を選択し、archetypeで作成したプロジェクトを選択する。

New MVC Project Import

Root DirectoryにC:\work\todoを設定し、Projectsにtodoのpom.xmlが選択された状態で、 [Finish] を押下する。

New MVC Project Import

インポートが完了すると、Package Explorerに次のようなプロジェクトが表示される。

workspace

Note

インポート後にビルドエラーが発生する場合は、プロジェクト名を右クリックし、「Maven」->「Update Project…」をクリックし、「OK」ボタンをクリックすることでエラーが解消されるケースがある。

../_images/update-project1.png

Note

ビルド時にJSPファイルで以下のエラーが発生する場合がある。

The superclass "javax.servlet.http.HttpServlet", determined from the Dynamic Web Module facet version (2.5), was not found on the Java Build Path

この場合は、プロジェクト名を右クリックし、「Properties」を選択しプロパティ画面を開く。

「Project Facets」の「Dynamic Web Module」を5.0へ変更することで解決する。

../_images/change-DynamicWebModule1.png

Tip

パッケージの表示形式は、デフォルトは「Flat」だが、「Hierarchical」にしたほうが見通しがよい。

Package Explorerの「View Menu」 (右端の下矢印)をクリックし、「Package Presentation」->「Hierarchical」を選択する。

../_images/presentation-hierarchical1.png

Package PresentationをHierarchicalにすると、以下の様な表示になる。

../_images/presentation-hierarchical-view1.png

Warning

O/R Mapperを使用するブランクプロジェクトの場合、H2 Databaseがdependencyとして定義されているが、この設定は簡易的なアプリケーションを簡単に作成するためのものであり、実際のアプリケーション開発で使用されることは想定していない。

以下の定義は、実際のアプリケーション開発を行う際は削除すること。

<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Note

上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。

上記の依存ライブラリはterasoluna-gfw-parentが依存しているSpring Bootで管理されている。


11.2.3.3. プロジェクトの構成

本チュートリアルで作成するプロジェクトの構成を以下に示す。

Note

前節の「プロジェクト構成」ではマルチプロジェクトにすることを推奨していたが、本チュートリアルでは、学習容易性を重視しているためシングルプロジェクト構成にしている。

ただし、実プロジェクトで適用する場合は、マルチプロジェクト構成を強く推奨する。

マルチプロジェクトの作成方法は、「Webアプリケーション向け開発プロジェクトの作成」を参照されたい。


[O/R Mapperに依存しないブランクプロジェクト、JPA用のブランクプロジェクト用を作成した場合の構成]

src
  └main
      ├java
      │  └com
      │    └example
      │      └todo
      │        ├ app ... (1)
      │        │   ├common
      │        │   │  └error
      │        │   ├todo
      │        │   └welcome
      │        ├domain ... (2)
      │        │  ├model ... (3)
      │        │  ├repository ... (4)
      │        │  │   └todo
      │        │  └service ... (5)
      │        │      └todo
      │        └ config ... (6)
      │            ├app
      │            └web
      ├resources
      │  └META-INF
      │      └spring ... (6)
      └webapp
          ├resources
          │  └app
          │    └css ... (7)
          └WEB-INF
              └views ... (8)

項番

説明

(1)

アプリケーション層のクラスを格納するパッケージ。

本チュートリアルでは、Todo管理業務用のクラスを格納するためのパッケージを作成する。

(2)

ドメイン層のクラスを格納するパッケージ。

(3)

Domain Objectを格納するパッケージ。

(4)

Repositoryを格納するパッケージ。

本チュートリアルでは、Todoオブジェクト(Domain Object)用のRepositoryを格納するためのパッケージを作成する

(5)

Serviceを格納するパッケージ。

本チュートリアルでは、Todo管理業務用のServiceを格納するためのパッケージを作成する。

(6)

Spring関連の設定ファイルを格納するディレクトリ。

(7)

cssファイルを格納するディレクトリ。

(8)

ThymeleafのテンプレートHTMLを格納するディレクトリ。


[MyBatis3用のブランクプロジェクトを作成した場合の構成]

src
  └main
      ├java
      │  └com
      │    └example
      │      └todo
      │        ├ app
      │        │   ├common
      │        │   │  └error
      │        │   ├todo
      │        │   └welcome
      │        ├domain
      │        │  ├model
      │        │  ├repository
      │        │  │   └todo
      │        │  └service
      │        │      └todo
      │        └ config
      │            ├app
      │            │ └mybatis ... (9)
      │            └web
      ├resources
      │  ├META-INF
      │  │  └spring
      │  └com
      │    └example
      │      └todo
      │        └domain
      │            └repository ... (10)
      │                 └todo
      └webapp
          ├resources
          │  └app
          │    └css
          └WEB-INF
              └views

項番

説明

(9)

MyBatis関連の設定ファイルを格納するディレクトリ。

(10)

SQLを記述するMyBatisのMapperファイルを格納するディレクトリ。

本チュートリアルでは、Todoオブジェクト用のRepositoryのMapperファイルを格納するためのディレクトリを作成する。


11.2.3.4. 設定ファイルの確認

チュートリアルを進める上で必要となる設定の多くは、作成したブランクプロジェクトに既に設定済みの状態である。

チュートリアルを実施するだけであれば、これらの設定の理解は必須ではないが、アプリケーションを動かすためにどのような設定が必要なのかを理解しておくことを推奨する。

アプリケーションを動かすために必要な設定(設定ファイル)の解説については、「設定ファイルの解説」を参照されたい。

Note

まず、手を動かしてTodoアプリケーションを作成したい場合は、設定ファイルの確認は読み飛ばしてもよいが、Todoアプリケーションを作成した後に一読して頂きたい。


11.2.3.5. プロジェクトの動作確認

Todoアプリケーションの開発を始める前に、プロジェクトの動作確認を行う。

ブランクプロジェクトでは、トップページを表示するためのControllerとテンプレートHTMLの実装が用意されているため、トップページを表示する事で動作確認を行う事ができる。

ブランクプロジェクトから提供されているController(src/main/java/com/example/todo/app/welcome/HelloController.java)は、以下のような実装となっている。

package com.example.todo.app.welcome;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;

/**
 * Handles requests for the application home page.
 */
// (1)
@Controller
public class HelloController {

    // (2)
    private static final Logger logger = LoggerFactory
            .getLogger(HelloController.class);

    /**
     * Simply selects the home view to render by returning its name.
     */
    // (3)
    @GetMapping(value = "/")
    public String home(Locale locale, Model model) {
        // (4)
        logger.info("Welcome home! The client locale is {}.", locale);

        Date date = new Date();
        DateFormat dateFormat = DateFormat.getDateTimeInstance(DateFormat.LONG,
                DateFormat.LONG, locale);

        String formattedDate = dateFormat.format(date);

        // (5)
        model.addAttribute("serverTime", formattedDate);

        // (6)
        return "welcome/home";
    }

}

項番

説明

(1)
Controllerとしてcomponent-scanの対象とするため、クラスレベルに@Controllerアノテーションが付与している。
(2)
(4)でログ出力するためのロガーを生成している。
ロガーの実装はlogbackのものであるが、APIはSLF4Jのorg.slf4j.Loggerを使用している。
(3)
@GetMappingアノテーションを使用して、”/“ (ルート)へのアクセスに対するメソッドとしてマッピングを行っている。
(4)
メソッドが呼ばれたことを通知するためのログをinfoレベルで出力している。
(5)
画面に表示するための日付文字列を、serverTimeという属性名でModelに設定している。
(6)
view名としてwelcome/homeを返す。ViewResolverの設定によりテンプレートHTMLとしてWEB-INF/views/welcome/home.htmlを利用して生成したHTMLが返される。

ブランクプロジェクトから提供されているテンプレートHTML(src/main/webapp/WEB-INF/views/welcome/home.html)は、以下のような実装となっている。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet"
    href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <div id="wrapper">
        <h1 id="title">Hello world!</h1>
        <!-- (7) -->
        <p th:text="|The time on the server is ${serverTime}.|">The time on the server is 2018/01/01 00:00:00 JST.</p>
    </div>
</body>
</html>

項番

説明

(7)
ControllerでModelに設定したserverTimeを表示する。
th:text属性は、記述した要素のコンテンツを属性値で上書きする。
th:text属性に、変数式${}で変数名を指定することで、ControllerでModelに登録した変数を参照できる。
ユーザの入力値を表示する場合は、th:text属性を用いて、必ずXSS対策を行うこと。

プロジェクトを右クリックして「Run As」->「Run on Server」を選択する。

../_images/image0311.jpg

APサーバー(Tomcat v10.1 Server at localhost)を選択し、「Next」をクリックする。

../_images/image0321.jpg

todoが「Configured」に含まれていることを確認して「Finish」をクリックしてサーバーを起動する。

../_images/image0331.jpg

起動すると以下のようなログが出力される。”/“というパスに対してcom.example.todo.app.welcome.HelloControllerのhomeメソッドがマッピングされていることが分かる。

date:2022-12-01 11:41:47      thread:main     X-Track:        level:INFO      logger:o.springframework.web.servlet.DispatcherServlet  message:Initializing Servlet 'appServlet'
date:2022-12-01 11:41:47      thread:main     X-Track:        level:TRACE     logger:o.s.w.s.m.m.a.RequestMappingHandlerMapping       message:
    c.e.t.a.w.HelloController:
    {GET [/]}: home(Locale,Model)
date:2022-12-01 11:41:47      thread:main     X-Track:        level:DEBUG     logger:o.s.w.s.m.m.a.RequestMappingHandlerMapping       message:1 mappings in 'requestMappingHandlerMapping'
date:2022-12-01 11:41:48      thread:main     X-Track:        level:INFO      logger:o.springframework.web.servlet.DispatcherServlet  message:Completed initialization in 915 ms

ブラウザでhttp://localhost:8080/todoにアクセスすると、以下のように表示される。

../_images/image0341.png

コンソールを見ると、

  • 共通ライブラリから提供しているTraceLoggingInterceptorのTRACEログ

  • Controllerで実装したINFOログ

が出力されていることがわかる。

date:2022-12-01 11:48:22      thread:http-nio-8080-exec-3     X-Track:d41df6b34f7d4002b7d1cf415e5ada95        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[START CONTROLLER] HelloController.home(Locale,Model)
date:2022-12-01 11:48:22      thread:http-nio-8080-exec-3     X-Track:d41df6b34f7d4002b7d1cf415e5ada95        level:INFO      logger:com.example.todo.app.welcome.HelloController     message:Welcome home! The client locale is ja.
date:2022-12-01 11:48:22      thread:http-nio-8080-exec-3     X-Track:d41df6b34f7d4002b7d1cf415e5ada95        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[END CONTROLLER  ] HelloController.home(Locale,Model)-> view=welcome/home, model={serverTime=2022年12月1日 11:48:22 JST}
date:2022-12-01 11:48:22      thread:http-nio-8080-exec-3     X-Track:d41df6b34f7d4002b7d1cf415e5ada95        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[HANDLING TIME   ] HelloController.home(Locale,Model)-> 38,729,900 ns

Note

TraceLoggingInterceptorはControllerの開始、終了でログを出力する。終了時にはViewModelの情報および処理時間が出力される。


11.2.4. Todoアプリケーションのプロトタイプ作成

HTMLでTodoアプリケーションのプロトタイプを作成する。

本チュートリアルでは、ここで作成したプロトタイプにThymeleafの属性を付与して、Todoアプリケーションの画面を実装していく。


11.2.4.1. プロトタイプ作成

アプリケーションの概要で示した画面をプロトタイプとして作成する。

../_images/image0011.png

Note

実際のアプリケーション開発で作成するプロトタイプ

実際のアプリケーション開発では、ユースケースごとに画面の状態が確認できるプロトタイプ(本チュートリアルの例では、「TODOを作成した状態」や「TODOを完了した状態」など)を作成するのが一般的だと思われるが、今回はThymeleafを使用したアプリケーションの作成を学ぶチュートリアルで、プロトタイプの正しい作り方を解説することは主眼ではないため、省略する。

また、プロトタイプをブランクプロジェクトベースで作成するかは開発プロジェクトの判断に任せるが、本チュートリアルでは、プロトタイプからアプリケーションを開発する工程を理解しやすいように、ブランクプロジェクトベースでプロトタイプを作成している。

Package Explorer上で右クリック -> New -> File を選択し、「Create New File」ダイアログを表示し、

項番

項目

入力値

1

Enter or select the parent folder

todo/src/main/webapp/WEB-INF/views/todo

2

File name

list.html

を入力して「Finish」する。

作成したファイルは以下のディレクトリに格納される。

../_images/create-list-jsp1.png

アプリケーションの概要で示した画面をHTMLとして表示するために必要なプロトタイプの実装を行う。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Todo List</title>
<style type="text/css">
.strike {
    text-decoration: line-through;
}

.inline {
    display: inline-block;
}
</style>
</head>
<body>
    <h1>Todo List</h1>
    <div id="todoForm">
        <!-- (1) -->
        <form action="/todo/create" method="post">
            <input type="text">
            <button>Create Todo</button>
        </form>
    </div>
    <hr />
    <div id="todoList">
        <ul>
            <li>
                <!-- (2) -->
                <span>Send a e-mail</span>
                <form action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span>Have a lunch</span>
                <form action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span class="strike">Read a book</span><!-- (3) -->
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
        </ul>
    </div>
</body>
</html>

項番

説明

(1)
新規作成処理用のformを表示する。
action属性には新規作成処理を実行するためのパス(/todo/create)を指定する。
新規作成処理は更新系の処理なので、method属性にはPOSTメソッドを指定する。
(2)
未完了のTODOに対しては「Finish」と「Delete」用のボタンを表示する。
action属性には更新処理、削除処理を実行するためのパス(/todo/finishor /todo/delete)を指定する。
更新処理、削除処理は更新系の処理なので、method属性にはPOSTメソッドを指定する。
なお、「Finish」と「Delete」用のボタンをインラインブロック要素(display: inline-block;)としてTODOの横に表示させている。
(3)
完了しているTODOには、打ち消し線(text-decoration: line-through;)を装飾する。
完了しているTODOに対しては「Delete」用のボタンのみを表示する。

11.2.4.2. 画面の静的表示の確認

作成したプロトタイプのデザインをWebブラウザで確認すると、以下のように表示される。(以降、プロトタイプやテンプレートHTMLをブラウザで直接開く事を静的表示と呼ぶ。)

../_images/image0011.png

11.2.4.3. CSSファイルの使用

上記例ではスタイルシートをHTMLファイルの中で直接定義していたが、実際のアプリケーションを開発する場合は、CSSファイルに定義するのが一般的である。

ここでは、スタイルシートをCSSファイルに定義する方法について説明する。

ブランクプロジェクトから提供しているCSSファイル(src/main/webapp/resources/app/css/styles.css)にスタイルシートの定義を追加する。 なお、ここでは、以降で使用するスタイルシートも含めて、CSSファイルに定義している。

/* ... */

.strike {
    text-decoration: line-through;
}

.inline {
    display: inline-block;
}

.alert {
    border: 1px solid;
    margin-bottom: 5px;
}

.alert-error {
    background-color: #c60f13;
    border-color: #970b0e;
    color: white;
}

.alert-success {
    background-color: #5da423;
    border-color: #457a1a;
    color: white;
}

.text-error {
    color: #c60f13;
}

.alert ul {
    margin: 15px 0px 15px 0px;
}

#todoList li {
    margin-top: 5px;
}

プロトタイプからCSSファイルを読み込む。

<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Todo List</title>
<!-- (1) -->
<link rel="stylesheet" href="../../../resources/app/css/styles.css">
</head>
<body>
    <h1>Todo List</h1>
    <div id="todoForm">
        <form action="/todo/create" method="post">
            <input type="text">
            <button>Create Todo</button>
        </form>
    </div>
    <hr />
    <div id="todoList">
        <ul>
            <li>
                <span>Send a e-mail</span>
                <form action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span>Have a lunch</span>
                <form action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span class="strike">Read a book</span>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
        </ul>
    </div>
</body>
</html>

項番

説明

(1)
HTMLからスタイルシートの定義を削除し、代わりにスタイルシートを定義したCSSファイルを読み込む。

CSSファイルを適用すると、以下のようなレイアウトになる。

../_images/list-screen-css1.png

11.2.5. Todoアプリケーションの作成

プロトタイプからTodoアプリケーションを作成する。作成する順は、以下の通りである。
  • ドメイン層(+ インフラストラクチャ層)

    • Domain Object作成

    • Repository作成

    • RepositoryImpl作成

    • Service作成

  • アプリケーション層

    • Controller作成

    • Form作成

    • View作成


RepositoryImplの作成は、選択したインフラストラクチャ層の種類に応じて実装方法が異なる。

ここでは、データベースを使用せずjava.util.Mapを使ったインメモリ実装のRepositoryImplを作成する方法について説明を行う。データベースを使用する場合は、「データベースアクセスを伴うインフラストラクチャ層の作成」に記載されている内容で読み替えて、Todoアプリケーションを作成して頂きたい。


11.2.5.1. ドメイン層の作成

11.2.5.1.1. Domain Objectの作成

Domainオブジェクトを作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.domain.model

2

Name

Todo

3

Interfaces

java.io.Serializable

を入力して「Finish」する。

../_images/image0571.png

作成したクラスは以下のディレクトリに格納される。

../_images/image0581.png

作成したクラスに以下のプロパティを追加する。

  • ID → todoId

  • タイトル → todoTitle

  • 完了フラグ → finished

  • 作成日 → createdAt

package com.example.todo.domain.model;

import java.io.Serializable;
import java.util.Date;

public class Todo implements Serializable {

    private static final long serialVersionUID = 1L;

    private String todoId;

    private String todoTitle;

    private boolean finished;

    private Date createdAt;

    public String getTodoId() {
        return todoId;
    }

    public void setTodoId(String todoId) {
        this.todoId = todoId;
    }

    public String getTodoTitle() {
        return todoTitle;
    }

    public void setTodoTitle(String todoTitle) {
        this.todoTitle = todoTitle;
    }

    public boolean isFinished() {
        return finished;
    }

    public void setFinished(boolean finished) {
        this.finished = finished;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}

Tip

Getter/SetterメソッドはSTSの機能を使って自動生成することができる。フィールドを定義した後、エディタ上で右クリックし、「Source」->「Generate Getters and Setters…」を選択する。

../_images/image0591.png

serialVersionUID以外を選択して「Generate」

../_images/image0601.png

11.2.5.1.2. Repositoryの作成

TodoRepositoryインタフェースを作成する。
データベースを使用する場合は、「データベースアクセスを伴うインフラストラクチャ層の作成」に記載されている内容で読み替えて、Repositoryを作成する。

Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.domain.repository.todo

2

Name

TodoRepository

を入力して「Finish」する。

作成したインタフェースは以下のディレクトリに格納される。

../_images/image0611.png

作成したインタフェースに、今回のアプリケーションで必要となる以下のCRUD操作を行うメソッドを定義する。

  • TODOの1件取得 → findById

  • TODOの全件取得 → findAll

  • TODOの1件作成 → create

  • TODOの1件更新 → update

  • TODOの1件削除 → delete

  • 完了済みTODO件数の取得 → countByFinished

package com.example.todo.domain.repository.todo;

import java.util.Collection;
import java.util.Optional;

import com.example.todo.domain.model.Todo;

public interface TodoRepository {
    Optional<Todo> findById(String todoId);

    Collection<Todo> findAll();

    void create(Todo todo);

    boolean update(Todo todo);

    void delete(Todo todo);

    long countByFinished(boolean finished);
}

Note

java.util.Optional型はJava 8から導入されたnullを安全に扱うための仕組みであり、コーディングミスによるNullPointerException等の防止に寄与する。

Repositoryの1件取得(findById)の戻り値をOptional型にすべきか否かは、以下のようにO/R Mapperにより異なるため、採用するO/R Mapperの仕様を確認されたい。

  • O/R Mapperに依存しない場合とMyBatis3を利用する場合は、Optional型にしても良い。

  • Spring Data JPAを利用する場合は、Repositoryのメソッドシグネチャが決まっており必ずOptional型となる。

本チュートリアルではO/R Mapperによらず戻り値にOptional型に統一しているが、これはRepositoryを上記3種類のO/R Mapperで実装し、それらを呼び出すServiceの実装を統一するためである。

Note

ここでは、TodoRepositoryの汎用性を上げるため、「完了済み件数を取得する」メソッド(long countFinished())ではなく、「完了状態がxxである件数を取得する」メソッド(long countByFinished(boolean))として定義している。

long countByFinished(boolean)の引数としてtrueを渡すと「完了済みの件数」、falseを渡すと「未完了の件数」が取得できる仕様としている。


11.2.5.1.3. RepositoryImplの作成(インフラストラクチャ層)

ここでは、説明を単純化するため、java.util.Mapを使ったインメモリ実装のRepositoryImplを作成する。
データベースを使用する場合は、「データベースアクセスを伴うインフラストラクチャ層の作成」に記載されている内容で読み替えて、RepositoryImplを作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.domain.repository.todo

2

Name

TodoRepositoryImpl

3

Interfaces

com.example.todo.domain.repository.todo.TodoRepository

を入力して「Finish」する。

作成したクラスは以下のディレクトリに格納される。

../_images/image0621.png

作成したクラスにCRUD操作を実装する。

Note

RepositoryImplには、業務ロジックは含めず、Domainオブジェクトの保存先への出し入れ(CRUD操作)に終始することが実装ポイントである。

package com.example.todo.domain.repository.todo;

import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;

import org.springframework.stereotype.Repository;

import com.example.todo.domain.model.Todo;

@Repository // (1)
public class TodoRepositoryImpl implements TodoRepository {
    private static final Map<String, Todo> TODO_MAP = new ConcurrentHashMap<String, Todo>();

    @Override
    public Optional<Todo> findById(String todoId) {
        return Optional.ofNullable(TODO_MAP.get(todoId));
    }

    @Override
    public Collection<Todo> findAll() {
        return TODO_MAP.values();
    }

    @Override
    public void create(Todo todo) {
        TODO_MAP.put(todo.getTodoId(), todo);
    }

    @Override
    public boolean update(Todo todo) {
        TODO_MAP.put(todo.getTodoId(), todo);
        return true;
    }

    @Override
    public void delete(Todo todo) {
        TODO_MAP.remove(todo.getTodoId());
    }

    @Override
    public long countByFinished(boolean finished) {
        long count = 0;
        for (Todo todo : TODO_MAP.values()) {
            if (finished == todo.isFinished()) {
                count++;
            }
        }
        return count;
    }
}

項番

説明

(1)
Repositoryとしてcomponent-scan対象とするため、クラスレベルに@Repositoryアノテーションをつける。

Note

本チュートリアルでは、インフラストラクチャ層に属するクラス(RepositoryImpl)をドメイン層のパッケージ(com.example.todo.domain)に格納しているが、完全に層別にパッケージを分けるのであれば、インフラストラクチャ層のクラスは、com.example.todo.infra以下に作成した方が良い。

ただし、通常のプロジェクトでは、インフラストラクチャ層が変更されることを前提としていない(そのような前提で進めるプロジェクトは、少ない)。そこで、作業効率向上のために、ドメイン層のRepositoryインタフェースと同じ階層に、RepositoryImplを作成しても良い。


11.2.5.1.4. Serviceの作成

まず、TodoServiceインタフェースを作成する。

Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.domain.service.todo

2

Name

TodoService

を入力して「Finish」する。

作成したインタフェースは以下のディレクトリに格納される。

../_images/image0631.png

作成したインタフェースに以下の業務処理を行うメソッドを定義する。

  • Todoの全件取得 → findAll

  • Todoの新規作成 → create

  • Todoの完了 → finish

  • Todoの削除 → delete

package com.example.todo.domain.service.todo;

import java.util.Collection;

import com.example.todo.domain.model.Todo;

public interface TodoService {
    Collection<Todo> findAll();

    Todo create(Todo todo);

    Todo finish(String todoId);

    void delete(String todoId);
}

次に、TodoServiceインタフェースに定義したメソッドを実装するTodoServiceImplクラスを作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.domain.service.todo

2

Name

TodoServiceImpl

3

Interfaces

com.example.todo.domain.service.todo.TodoService

を入力して「Finish」する。

作成したクラスは以下のディレクトリに格納される。

../_images/image0641.png
package com.example.todo.domain.service.todo;

import java.util.Collection;
import java.util.Date;
import java.util.UUID;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;

import com.example.todo.domain.model.Todo;
import com.example.todo.domain.repository.todo.TodoRepository;

import jakarta.inject.Inject;

@Service// (1)
@Transactional // (2)
public class TodoServiceImpl implements TodoService {

    private static final long MAX_UNFINISHED_COUNT = 5;

    @Inject// (3)
    TodoRepository todoRepository;

    @Override
    @Transactional(readOnly = true) // (4)
    public Collection<Todo> findAll() {
        return todoRepository.findAll();
    }

    @Override
    public Todo create(Todo todo) {
        long unfinishedCount = todoRepository.countByFinished(false);
        if (unfinishedCount >= MAX_UNFINISHED_COUNT) {
            // (5)
            ResultMessages messages = ResultMessages.error();
            messages.add(ResultMessage.fromText(
                    "[E001] The count of un-finished Todo must not be over "
                            + MAX_UNFINISHED_COUNT + "."));
            // (6)
            throw new BusinessException(messages);
        }

        // (7)
        String todoId = UUID.randomUUID().toString();
        Date createdAt = new Date();

        todo.setTodoId(todoId);
        todo.setCreatedAt(createdAt);
        todo.setFinished(false);

        todoRepository.create(todo);
        /* REMOVE THIS LINE IF YOU USE JPA
            todoRepository.save(todo); // (8)
           REMOVE THIS LINE IF YOU USE JPA */

        return todo;
    }

    @Override
    public Todo finish(String todoId) {
        Todo todo = findOne(todoId);
        if (todo.isFinished()) {
            ResultMessages messages = ResultMessages.error();
            messages.add(ResultMessage.fromText(
                    "[E002] The requested Todo is already finished. (id="
                            + todoId + ")"));
            throw new BusinessException(messages);
        }
        todo.setFinished(true);
        todoRepository.update(todo);
        /* REMOVE THIS LINE IF YOU USE JPA
            todoRepository.save(todo); // (9)
           REMOVE THIS LINE IF YOU USE JPA */
        return todo;
    }

    @Override
    public void delete(String todoId) {
        Todo todo = findOne(todoId);
        todoRepository.delete(todo);
    }

    // (10)
    private Todo findOne(String todoId) {
        // (11)
        return todoRepository.findById(todoId).orElseThrow(() -> {
            ResultMessages messages = ResultMessages.error();
            messages.add(ResultMessage.fromText(
                    "[E404] The requested Todo is not found. (id=" + todoId
                            + ")"));
            return new ResourceNotFoundException(messages);
        });
    }
}

項番

説明

(1)
Serviceとしてcomponent-scanの対象とするため、クラスレベルに@Serviceアノテーションをつける。
(2)
クラスレベルに、@Transactionalアノテーションをつけることで、公開メソッドをすべてトランザクション管理する。
アノテーションを付与することで、メソッド開始時にトランザクションを開始、メソッド正常終了時にトランザクションのコミットが行われる。
また、途中で非検査例外が発生した場合は、トランザクションがロールバックされる。

データベースを使用しない場合は、@Transactionalアノテーションは不要である。
(3)
@Injectアノテーションで、TodoRepositoryの実装をインジェクションする。
(4)
参照のみ行う処理に関しては、readOnly=trueをつける。
O/R Mapperによっては、この設定により、参照時のトランザクション制御の最適化が行われる(JPAを使用する場合、効果はない)。

データベースを使用しない場合は、@Transactionalアノテーションは不要である。
(5)
結果メッセージを格納するクラスとして、共通ライブラリで用意されているorg.terasoluna.gfw.common.message.ResultMessageを用いる。
今回は、エラーメッセージを例外に追加する際に、ResultMessages.error()でメッセージ種別を指定して、ResultMessageを追加している。
(6)
業務エラーが発生した場合、共通ライブラリで用意されているorg.terasoluna.gfw.common.exception.BusinessExceptionをスローする。
(7)
一意性のある値を生成するために、UUIDを使用している。データベースのシーケンスを用いてもよい。
(8)
Spring Data JPAを使用してデータベースにアクセスする場合は、createメソッドではなく、saveメソッドを呼び出す。
(9)
Spring Data JPAを使用してデータベースにアクセスする場合は、updateメソッドではなく、saveメソッドを呼び出す。
(10)
1件取得は、finishメソッドでもdeleteメソッドでも使用するため、メソッドとして用意しておく(interfaceに公開しても良い)。
(11)
対象のデータが存在しない場合、共通ライブラリで用意されているorg.terasoluna.gfw.common.exception.ResourceNotFoundExceptionをスローする。

Note

本節では、説明を単純化するため、エラーメッセージをハードコードしているが、メンテナンスの観点で本来は好ましくない。通常、メッセージは、プロパティファイルに外部化することが推奨される。

プロパティファイルに外部化する方法は、プロパティ管理を参照されたい。


11.2.5.2. アプリケーション層の作成

ドメイン層の実装が完了したので、次はドメイン層を利用して、アプリケーション層の作成に取り掛かる。
画面(テンプレートHTML)には、プロトタイプとして作成したHTMLファイルを使用する。

11.2.5.2.1. Controllerの作成

まずは、Todo管理業務にかかわる画面遷移を、制御するControllerを作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.app.todo

2

Name

TodoController

を入力して「Finish」する。

Note

上位パッケージがドメイン層と異なるので注意すること。

作成したクラスは以下のディレクトリに格納される。

../_images/image0651.png
package com.example.todo.app.todo;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller // (1)
@RequestMapping("todo") // (2)
public class TodoController {

}

項番

説明

(1)
Controllerとしてcomponent-scanの対象とするため、クラスレベルに、@Controllerアノテーションをつける。
(2)
TodoControllerが扱う画面遷移のパスを、すべて<contextPath>/todo配下にするため、クラスレベルに@RequestMapping(“todo”)を設定する。

11.2.5.2.2. Show all TODOの実装

本チュートリアルで作成する画面では、

  • 新規作成フォームの表示

  • TODOの全件表示

を行う。

はじめに、TODOの全件表示を行うための処理を実装する。


11.2.5.2.2.1. Formの作成

Formクラス(JavaBean)を作成する。

Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.app.todo

2

Name

TodoForm

3

Interfaces

java.io.Serializable

を入力して「Finish」する。

作成したクラスは以下のディレクトリに格納される。

../_images/image0661.png

作成したクラスに以下のプロパティを追加する。

  • タイトル → todoTitle

package com.example.todo.app.todo;

import java.io.Serializable;

public class TodoForm implements Serializable {
    private static final long serialVersionUID = 1L;

    private String todoTitle;

    public String getTodoTitle() {
        return todoTitle;
    }

    public void setTodoTitle(String todoTitle) {
        this.todoTitle = todoTitle;
    }

}

11.2.5.2.2.2. Controllerの実装

一覧画面表示処理をTodoControllerに追加する。

package com.example.todo.app.todo;

import java.util.Collection;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.todo.domain.model.Todo;
import com.example.todo.domain.service.todo.TodoService;

import jakarta.inject.Inject;

@Controller
@RequestMapping("todo")
public class TodoController {

    @Inject // (1)
    TodoService todoService;

    @ModelAttribute // (2)
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }

    @GetMapping("list") // (3)
    public String list(Model model) {
        Collection<Todo> todos = todoService.findAll();
        model.addAttribute("todos", todos); // (4)
        return "todo/list"; // (5)
    }
}

項番

説明

(1)
TodoServiceを、DIコンテナによってインジェクションさせるために、@Injectアノテーションをつける。

DIコンテナの管理するTodoService型のインスタンス(TodoServiceImplのインスタンス)がインジェクションされる。
(2)
Formを初期化する。

@ModelAttributeアノテーションをつけることで、このメソッドの返り値のformオブジェクトが、todoFormという名前でModelに追加される。
これは、TodoControllerの各処理で、model.addAttribute("todoForm", form)を実装するのと同義である。
(3)
/todo/listというパスにGETメソッドを使用してリクエストされた際に、一覧画面表示処理用のメソッド(listメソッド)が実行されるように@GetMappingアノテーションを設定する。

クラスレベルに@RequestMapping(“todo”)が設定されているため、ここでは@GetMapping("list")のみで良い。
(4)
ModelにTodoのリストを追加して、Viewに渡す。
(5)
View名としてtodo/listを返すと、spring-mvc.xmlまたはSpringMvcConfig.javaに定義したViewResolverの設定によりテンプレートHTMLとしてWEB-INF/views/todo/list.htmlを利用して生成したHTMLが返される。

Note

@GetMapping@PostMappingは、対応するHTTPメソッドにマッピングする。

詳細は、リクエストとハンドラメソッドのマッピング方法を参照されたい。


11.2.5.2.2.3. テンプレートHTMLの実装

Todoアプリケーションのプロトタイプ作成で作成したプロトタイプにThymeleafの属性を付与してテンプレートHTMLを実装し、Controllerから渡されたModelを表示する。

TODOの一覧表示エリアを表示するために必要なテンプレートHTMLの実装を行う。

<!DOCTYPE html>
<!-- (1) -->
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Todo List</title>
<!-- (2) -->
<link rel="stylesheet"
    href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <h1>Todo List</h1>
    <div id="todoForm">
        <form action="/todo/create" method="post">
            <input type="text">
            <button>Create Todo</button>
        </form>
    </div>
    <hr />
    <div id="todoList">
        <!-- (3) -->
        <ul th:remove="all-but-first">
            <!-- (4) -->
            <li th:each="todo : ${todos}">
                <!-- (5)(6) -->
                <span th:class="${todo.finished} ? 'strike'" th:text="${todo.todoTitle}">Send a e-mail</span>
                <!-- (7) -->
                <form th:if="${!todo.finished}" action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span>Have a lunch</span>
                <form action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span class="strike">Read a book</span>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
        </ul>
    </div>
</body>
</html>

項番

説明

(1)
Thymeleaf独自の属性を使用するため、<html>タグにThymeleafのネームスペースを付与する。
(2)
<link>タグにth:href属性を付与する。
th:href属性値には、リンクURL式 @{}を用いている。
リンクURL式に”/“(スラッシュ)から始まるパスを指定することで、コンテキストルートからの相対パスが出力される。
(3)
最初の子要素をThymeleafのテンプレートとして利用し、2番目以降の子要素は静的表示時のみに表示するために、Thymeleafのth:remove属性を使用する。
th:remove属性にall-but-firstを指定することで、Thymeleafでの処理時には、指定したタグにおける最初の子要素以外の要素が削除される。
(4)
th:each属性の右項にはControllerでModelに追加したコレクションtodosを指定し、左項にはコレクションの要素オブジェクトを格納する変数名todoを指定している。
これにより、th:each属性を付与した配下の要素がtodosの要素数分繰り返し出力される。
(5)
th:class属性を使用することで、動的にclass属性を設定できる。
th:text属性と同様に、変数式を利用してModelに登録した変数やth:each属性で定義した変数を参照できる。
ここではEL式を利用して、th:each属性で取り出したTodo型オブジェクトtodofinishedプロパティを参照して打ち消し線(text-decoration: line-through;)を装飾するかどうかを判断する。
(6)
th:text属性を使用することで、記述した要素のコンテンツを属性値で上書きする。
文字列値を出力する際は、XSS対策のため、必ずth:text属性を使用してHTMLエスケープを行うこと。
XSS対策についての詳細は、Output Escapingを参照されたい。
(7)
th:if属性は条件に応じて、要素を出力するかどうか制御するための属性であり、todofinishedプロパティを参照して「Finish」ボタンの生成を判断する。

Note

Thymeleafのth:object属性を用いると、オブジェクト名を省略してプロパティを指定することが出来る。

list.htmlの<li>タグの部分は、th:object属性を用いることで以下のように記述量を減らすことが出来る。

  • list.html

    <!-- (1) -->
    <li th:each="todo : ${todos}" th:object="${todo}">
        <!-- (2) -->
        <span th:class="*{finished} ? 'strike'" th:text="*{todoTitle}">Send a e-mail</span>
        <form th:if="*{!finished}" action="/todo/finish" method="post" class="inline">
            <button>Finish</button>
        </form>
        <form action="/todo/delete" method="post" class="inline">
            <button>Delete</button>
        </form>
    </li>
    

    項番

    説明

    (1)
    th:object属性にオブジェクトを変数式${}で指定する。
    (2)
    オブジェクトのプロパティを選択変数式*{}で指定する。これは、変数式を用いてth:class="${todo.finished} ? 'strike'"th:text="${todo.todoTitle}"と指定するのと同じ結果になる。

STSで「todo」プロジェクトを右クリックし、「Run As」→「Run on Server」でWebアプリケーションを起動する。
ブラウザでhttp://localhost:8080/todo/todo/listにアクセスすると、以下のような画面が表示される。
../_images/image0671.png

なお、表示されている「Create Todo」ボタンについては、「Create TODO」の実装が終了していないため、表示はされるが機能しない。


Note

上記で表示されている画面には、TODOが1件も登録されていないため、TODOの一覧は出力されない。

以下のように、ドメイン層の作成で作成したTodoRepositoryImplを一時的に修正し初期データを登録することで、TODOの一覧が出力されることを確認できる。

なお、次節「Create TODOの実装」で実際にTODOを登録できるようになるため、一覧の出力が確認できたら削除して構わない。

  • TodoRepositoryImpl.java

    package com.example.todo.domain.repository.todo;
    
    import java.util.Collection;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    import org.springframework.stereotype.Repository;
    
    import com.example.todo.domain.model.Todo;
    
    @Repository
    public class TodoRepositoryImpl implements TodoRepository {
        private static final Map<String, Todo> TODO_MAP = new ConcurrentHashMap<String, Todo>();
    
        static {
            Todo todo1 = new Todo();
            todo1.setTodoId("1");
            todo1.setTodoTitle("Send a e-mail");
            Todo todo2 = new Todo();
            todo2.setTodoId("2");
            todo2.setTodoTitle("Have a lunch");
            Todo todo3 = new Todo();
            todo3.setTodoId("3");
            todo3.setTodoTitle("Read a book");
            todo3.setFinished(true);
            TODO_MAP.put(todo1.getTodoId(), todo1);
            TODO_MAP.put(todo2.getTodoId(), todo2);
            TODO_MAP.put(todo3.getTodoId(), todo3);
        }
    
        // omitted
    

以下のように画面に出力される。

../_images/show-all-todo-note1.png

11.2.5.2.3. Create TODOの実装

次に、一覧表示画面から「Create TODO」ボタンを押した後の、新規作成処理を実装する。


11.2.5.2.3.1. マッパーインタフェースの作成

Beanマッピングのマッパーインタフェースを作成する。

Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.app.todo

2

Name

TodoMapper

を入力して「Finish」する。

作成したクラスは以下のディレクトリに格納される。

../_images/create-bean-mapper1.png

作成したクラスに以下の@Mapperアノテーションを付与したBeanマッピングメソッドを追加する。

  • Todo map(TodoForm form)

    • @Mappingアノテーションによるマッピング除外項目定義

      • createdAt

      • finished

package com.example.todo.app.todo;

import org.mapstruct.Mapper;
import org.mapstruct.Mapping;

import com.example.todo.domain.model.Todo;

@Mapper
public interface TodoMapper {

    @Mapping(target = "createdAt", ignore = true)
    @Mapping(target = "finished", ignore = true)
    Todo map(TodoForm form);

}

Note

マッパーインタフェース追加後、以下のようなビルドエラーが発生する場合がある。

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.todo.app.todo.TodoMapper'

この場合は、プロジェクト名を右クリックし、「Run As」->「Maven build」をクリックする。 Goalsに「compile」を指定し「Run」をクリックする。

../_images/mvnBuild2.png

ビルドが成功した後、プロジェクト名を右クリックし、「Run As」->「Maven install」をクリックする。


11.2.5.2.3.2. Controllerの修正

新規作成処理をTodoControllerに追加する。

package com.example.todo.app.todo;

import java.util.Collection;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;

import com.example.todo.domain.model.Todo;
import com.example.todo.domain.service.todo.TodoService;

import jakarta.inject.Inject;
import jakarta.validation.Valid;

@Controller
@RequestMapping("todo")
public class TodoController {

    @Inject
    TodoService todoService;

    // (1)
    @Inject
    TodoMapper beanMapper;

    @ModelAttribute
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }

    @GetMapping("list")
    public String list(Model model) {
        Collection<Todo> todos = todoService.findAll();
        model.addAttribute("todos", todos);
        return "todo/list";
    }

    @PostMapping("create") // (2)
    public String create(@Valid TodoForm todoForm, BindingResult bindingResult, // (3)
            Model model, RedirectAttributes attributes) { // (4)

        // (5)
        if (bindingResult.hasErrors()) {
            return list(model);
        }

        // (6)
        Todo todo = beanMapper.map(todoForm);

        try {
            todoService.create(todo);
        } catch (BusinessException e) {
            // (7)
            model.addAttribute(e.getResultMessages());
            return list(model);
        }

        // (8)
        attributes.addFlashAttribute(ResultMessages.success().add(ResultMessage
                .fromText("Created successfully!")));
        return "redirect:/todo/list";
    }

}

項番

説明

(1)
FormオブジェクトをDomainObjectに変換するために、TodoMapperインタフェースをインジェクションする。
(2)
/todo/createというパスにPOSTメソッドを使用してリクエストされた際に、新規作成処理用のメソッド(createメソッド)が実行されるように@PostMappingアノテーションを設定する。
(3)
フォームの入力チェックを行うため、Formの引数に@Validアノテーションをつける。入力チェック結果は、その直後の引数BindingResultに格納される。
(4)
正常に作成が完了した後にリダイレクトし、一覧画面を表示する。
リダイレクト先への情報を格納するために、引数にRedirectAttributesを加える。
(5)
入力エラーがあった場合、一覧画面に戻る。
Todo全件取得を再度行う必要があるので、listメソッドを再実行する。
(6)
Mapstructを用いて、TodoFormオブジェクトからTodoオブジェクトを作成する。
(7)
業務処理を実行して、BusinessExceptionが発生した場合、結果メッセージをModelに追加して、一覧画面に戻る。
(8)
正常に作成が完了したので、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。
リダイレクトすることにより、ブラウザを再読み込みして、再び新規登録処理がPOSTされることがなくなる。(詳しくは、「PRG(Post-Redirect-Get)パターンについて」を参照されたい)
なお、今回は成功メッセージであるため、ResultMessages.success()を使用している。

11.2.5.2.3.3. Formの修正

入力チェックのルールを定義するため、Formオブジェクトにアノテーションを追加する。

package com.example.todo.app.todo;

import java.io.Serializable;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class TodoForm implements Serializable {
    private static final long serialVersionUID = 1L;

    @NotNull // (1)
    @Size(min = 1, max = 30) // (2)
    private String todoTitle;

    public String getTodoTitle() {
        return todoTitle;
    }

    public void setTodoTitle(String todoTitle) {
        this.todoTitle = todoTitle;
    }
}

項番

説明

(1)
@NotNullアノテーションを使用して必須チェックを有効化する。
(2)
@Sizeアノテーションを使用して文字数チェックを有効化する。

11.2.5.2.3.4. テンプレートHTMLの修正

TODOを新規作成するため、テンプレートHTMLに以下の実装を追加する。

  • TODOの入力フォームにThymeleafの属性を付与する

  • 入力チェックエラーを表示するエリアを追加する

  • 結果メッセージを表示するエリアを追加する

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Todo List</title>
<link rel="stylesheet"
    href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <h1>Todo List</h1>
    <div id="todoForm">
        <!-- (1) -->
        <div th:if="${resultMessages} != null" class="alert alert-success" th:class="|alert alert-${resultMessages.type}|">
            <ul>
                <li th:each="message : ${resultMessages}" th:text="${message.text}">Created successfully!</li>
            </ul>
        </div>
        <!-- (2) -->
        <form action="/todo/create" th:action="@{/todo/create}" method="post">
            <!-- (3) -->
            <input type="text" th:field="${todoForm.todoTitle}" />
            <!-- (4) -->
            <span id="todoTitle.errors" th:errors="${todoForm.todoTitle}" class="text-error">size must be between 1 and 30</span>
            <button>Create Todo</button>
        </form>
    </div>
    <hr />
    <div id="todoList">
        <ul th:remove="all-but-first">
            <li th:each="todo : ${todos}">
                <span th:class="${todo.finished} ? 'strike'" th:text="${todo.todoTitle}">Send a e-mail</span>
                <form th:if="${!todo.finished}" action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span>Have a lunch</span>
                <form action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span class="strike">Read a book</span>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
        </ul>
    </div>
</body>
</html>

項番

説明

(1)
新規作成処理の結果メッセージを表示する。
th:if属性を使用し、ServiceやControllerでresultMessagesオブジェクトがModelに登録されている場合のみ、結果メッセージを表示している。
また、th:class属性を使用することで、ResultMessagesに設定されたメッセージタイプ(例:info,error)に応じたclass属性を設定している。

Note

一般的にThymeleafを利用して画面を実装する場合、HTMLファイルを直接ブラウザで表示することを考慮し、Thymeleafのテンプレートとしては不要だがHTML表示時に必要となる属性や文字列(コード例におけるclass="alert alert-success"Created successfully!)を記述する。

(2)
新規作成処理用のformを実装する。
th:action属性には、リンクURL式 @{}を用いて新規作成処理を実行するためのパス(/todo/create)を指定する。
(3)
<input>タグでフォームのプロパティをバインドする。
th:field属性値を<input>タグに適用すると、id属性、name属性、value属性が付加される。
(4)
th:errors属性を付与することで、指定したプロパティに対する入力エラーがあった場合に表示される。th:errors属性の値は、<input>タグのth:field属性と合わせる。

フォームに適切な値を入力してsubmitすると、以下のように、成功メッセージが表示される。

../_images/image0681.png
../_images/image0691.png

なお、TODOの横に表示されている「Finish」、「Delete」ボタンについては、「Finish TODO」、「Delete TODO」の実装が終了していないため、表示はされるが機能しない。

未完了のTODOが5件登録済みの場合は、業務エラーとなり、エラーメッセージが表示される。

../_images/image0701.png

入力フォームを、空文字にしてsubmitすると、以下のように、エラーメッセージが表示される。

../_images/image0711.png

11.2.5.2.4. Finish TODOの実装

「Finish」ボタンにTODOを完了させるための処理を追加する。


11.2.5.2.4.1. Formの修正

完了処理用のFormについても、TodoFormを使用する。

TodoFormtodoIdプロパティを追加する必要があるが、単純に追加してしまうと、新規作成処理でもtodoIdプロパティのチェックが実行されてしまう。
一つのFormクラスを使用して複数のformから送信されるリクエストパラメータをバインドする場合は、groups属性を使用して、入力チェックルールをグループ化する。

Formクラスに以下のプロパティを追加する。

  • ID → todoId

package com.example.todo.app.todo;

import java.io.Serializable;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class TodoForm implements Serializable {
    // (1)
    public static interface TodoCreate {
    };

    public static interface TodoFinish {
    };

    private static final long serialVersionUID = 1L;

    // (2)
    @NotNull(groups = { TodoFinish.class })
    private String todoId;

    // (3)
    @NotNull(groups = { TodoCreate.class })
    @Size(min = 1, max = 30, groups = { TodoCreate.class })
    private String todoTitle;

    public String getTodoId() {
        return todoId;
    }

    public void setTodoId(String todoId) {
        this.todoId = todoId;
    }

    public String getTodoTitle() {
        return todoTitle;
    }

    public void setTodoTitle(String todoTitle) {
        this.todoTitle = todoTitle;
    }

}

項番

説明

(1)
入力チェックルールをグループ化するためのインタフェースを作成する。
入力チェックルールのグループ化については、入力チェックを参照されたい。

ここでは、新規作成処理用のインタフェースとしてTodoCreateを、完了処理用のインタフェースとしてTodoFinishを作成している。
(2)
todoIdは完了処理で使用するプロパティである。
そのため、@NotNullアノテーションのgroups属性には、完了処理用の入力チェックルールである事を示すTodoFinishインタフェースを指定する。
(3)
todoTitleは新規作成処理で使用するプロパティである。
そのため、@NotNullアノテーションと@Sizeアノテーションのgroups属性には、新規作成処理用の入力チェックルールである事を示すTodoCreateインタフェースを指定する。

11.2.5.2.4.2. Controllerの修正

完了処理をTodoControllerに追加する。

グループ化した入力チェックルールを適用するためには、@Valid アノテーションの代わりに、@Validated アノテーションを使用することに注意する。

package com.example.todo.app.todo;

import java.util.Collection;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;

import com.example.todo.app.todo.TodoForm.TodoCreate;
import com.example.todo.app.todo.TodoForm.TodoFinish;
import com.example.todo.domain.model.Todo;
import com.example.todo.domain.service.todo.TodoService;

import jakarta.inject.Inject;
import jakarta.validation.groups.Default;

@Controller
@RequestMapping("todo")
public class TodoController {

    @Inject
    TodoService todoService;

    @Inject
    TodoMapper beanMapper;

    @ModelAttribute
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }

    @GetMapping("list")
    public String list(Model model) {
        Collection<Todo> todos = todoService.findAll();
        model.addAttribute("todos", todos);
        return "todo/list";
    }

    @PostMapping("create")
    public String create(@Validated({ Default.class, // (1)
            TodoCreate.class }) TodoForm todoForm, BindingResult bindingResult,
            Model model, RedirectAttributes attributes) {

        if (bindingResult.hasErrors()) {
            return list(model);
        }

        Todo todo = beanMapper.map(todoForm);

        try {
            todoService.create(todo);
        } catch (BusinessException e) {
            model.addAttribute(e.getResultMessages());
            return list(model);
        }

        attributes.addFlashAttribute(ResultMessages.success().add(ResultMessage
                .fromText("Created successfully!")));
        return "redirect:/todo/list";
    }

    @PostMapping("finish") // (2)
    public String finish(@Validated({ Default.class, // (3)
            TodoFinish.class }) TodoForm form, BindingResult bindingResult,
            Model model, RedirectAttributes attributes) {
        // (4)
        if (bindingResult.hasErrors()) {
            return list(model);
        }

        try {
            todoService.finish(form.getTodoId());
        } catch (BusinessException e) {
            // (5)
            model.addAttribute(e.getResultMessages());
            return list(model);
        }

        // (6)
        attributes.addFlashAttribute(ResultMessages.success().add( ResultMessage
                .fromText("Finished successfully!")));
        return "redirect:/todo/list";
    }
}

項番

説明

(1)
グループ化した入力チェックルールを適用するために、@Validアノテーションを@Validatedアノテーションに変更する。
value属性には、適用する入力チェックルールのグループ(グループインタフェース)を指定する。
Default.classは、グループ化されていない入力チェックルールを適用するために用意されているグループインタフェースである。
(2)
/todo/finishというパスにPOSTメソッドを使用してリクエストされた際に、完了処理用のメソッド(finishメソッド)が実行されるように@PostMappingアノテーションを設定する。
(3)
適用する入力チェックのグループとして、完了処理用のグループインタフェース(TodoFinishインタフェース)を指定する。
(4)
入力エラーがあった場合、一覧画面に戻る。
(5)
業務処理を実行して、BusinessExceptionが発生した場合は、結果メッセージをModelに追加して、一覧画面に戻る。
(6)
正常に作成が完了した場合は、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。

Note

新規作成処理用と完了処理用を別々のFormクラスとして作成しても良い。別々のFormクラスにした場合、入力チェックルールをグループ化する必要がないため、入力チェックルールの定義はシンプルになる。

ただし、処理毎にFormクラスを作成した場合、

  • クラス数が増える

  • プロパティが重複するため入力チェックルールを一元管理できない

ため、仕様変更が発生した場合に修正コストが高くなる可能性があるという点に注意してほしい。

また、@ModelAttributeメソッドを使用して複数のFormを初期化した場合、毎回すべてのFormが初期化されるため、不要なインスタンスが生成されることになる。


11.2.5.2.4.3. テンプレートHTMLの修正

完了処理用のformを実装する。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Todo List</title>
<link rel="stylesheet"
    href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <h1>Todo List</h1>
    <div id="todoForm">
        <div th:if="${resultMessages} != null" class="alert alert-success" th:class="|alert alert-${resultMessages.type}|">
            <ul>
                <li th:each="message : ${resultMessages}" th:text="${message.text}">Created successfully!</li>
            </ul>
        </div>
        <form action="/todo/create" th:action="@{/todo/create}" method="post">
            <input type="text" th:field="${todoForm.todoTitle}" />
            <span id="todoTitle.errors" th:errors="${todoForm.todoTitle}" class="text-error">size must be between 1 and 30</span>
            <button>Create Todo</button>
        </form>
    </div>
    <hr />
    <div id="todoList">
        <ul th:remove="all-but-first">
            <li th:each="todo : ${todos}">
                <span th:class="${todo.finished} ? 'strike'" th:text="${todo.todoTitle}">Send a e-mail</span>
                <!-- (1) -->
                <form th:if="${!todo.finished}" action="/todo/finish" th:action="@{/todo/finish}"
                    method="post" class="inline">
                    <!-- (2) -->
                    <input type="hidden" name="todoId" th:value="${todo.todoId}" />
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span>Have a lunch</span>
                <form action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span class="strike">Read a book</span>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
        </ul>
    </div>
</body>
</html>

項番

説明

(1)
th:if属性を使用し、TODOが未完了の場合は、TODOを完了させるためのリクエストを送信するformを表示する。
th:action属性にはリンクURL式 @{}を用いて完了処理を実行するためのパス(/todo/finish)を指定する。
(2)
リクエストパラメータとしてtodoIdを送信する。
th:value属性を使用して、todoオブジェクトのtodoIdプロパティを値に設定している。

TODOを新規作成した後に、「Finish」ボタン押下すると、以下のように打ち消し線が入り、完了したことがわかる。

../_images/image0751.png
../_images/image0761.png

11.2.5.2.5. Delete TODOの実装

「Delete」ボタンにTODOを削除するための処理を追加する。


11.2.5.2.5.1. Formの修正

削除処理用のFormについても、TodoFormを使用する。

package com.example.todo.app.todo;

import java.io.Serializable;

import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

public class TodoForm implements Serializable {
    public static interface TodoCreate {
    };

    public static interface TodoFinish {
    };

    // (1)
    public static interface TodoDelete {
    }

    private static final long serialVersionUID = 1L;

    // (2)
    @NotNull(groups = { TodoFinish.class, TodoDelete.class })
    private String todoId;

    @NotNull(groups = { TodoCreate.class })
    @Size(min = 1, max = 30, groups = { TodoCreate.class })
    private String todoTitle;

    public String getTodoId() {
        return todoId;
    }

    public void setTodoId(String todoId) {
        this.todoId = todoId;
    }

    public String getTodoTitle() {
        return todoTitle;
    }

    public void setTodoTitle(String todoTitle) {
        this.todoTitle = todoTitle;
    }

}

項番

説明

(1)
削除処理用の入力チェックルールをグループ化するためのインタフェースとしてTodoDeleteを作成する。
(2)
削除処理ではtodoIdプロパティを使用する。
そのため、todoId@NotNullアノテーションのgroups属性には、削除処理用の入力チェックルールである事を示すTodoDeleteインタフェースを指定する。

11.2.5.2.5.2. Controllerの修正

削除処理をTodoControllerに追加する。完了処理とほぼ同じである。

package com.example.todo.app.todo;

import java.util.Collection;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;

import com.example.todo.app.todo.TodoForm.TodoCreate;
import com.example.todo.app.todo.TodoForm.TodoDelete;
import com.example.todo.app.todo.TodoForm.TodoFinish;
import com.example.todo.domain.model.Todo;
import com.example.todo.domain.service.todo.TodoService;

import jakarta.inject.Inject;
import jakarta.validation.groups.Default;

@Controller
@RequestMapping("todo")
public class TodoController {

    @Inject
    TodoService todoService;

    @Inject
    TodoMapper beanMapper;

    @ModelAttribute
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }

    @GetMapping("list")
    public String list(Model model) {
        Collection<Todo> todos = todoService.findAll();
        model.addAttribute("todos", todos);
        return "todo/list";
    }

    @PostMapping("create")
    public String create(@Validated({ Default.class,
            TodoCreate.class }) TodoForm todoForm, BindingResult bindingResult,
            Model model, RedirectAttributes attributes) {

        if (bindingResult.hasErrors()) {
            return list(model);
        }

        Todo todo = beanMapper.map(todoForm);

        try {
            todoService.create(todo);
        } catch (BusinessException e) {
            model.addAttribute(e.getResultMessages());
            return list(model);
        }

        attributes.addFlashAttribute(ResultMessages.success().add(ResultMessage
                .fromText("Created successfully!")));
        return "redirect:/todo/list";
    }

    @PostMapping("finish")
    public String finish(@Validated({ Default.class,
            TodoFinish.class }) TodoForm form, BindingResult bindingResult,
            Model model, RedirectAttributes attributes) {
        if (bindingResult.hasErrors()) {
            return list(model);
        }

        try {
            todoService.finish(form.getTodoId());
        } catch (BusinessException e) {
            model.addAttribute(e.getResultMessages());
            return list(model);
        }

        attributes.addFlashAttribute(ResultMessages.success().add(ResultMessage
                .fromText("Finished successfully!")));
        return "redirect:/todo/list";
    }

    @PostMapping("delete") // (1)
    public String delete(@Validated({ Default.class,
            TodoDelete.class }) TodoForm form, BindingResult bindingResult,
            Model model, RedirectAttributes attributes) {

        if (bindingResult.hasErrors()) {
            return list(model);
        }

        try {
            todoService.delete(form.getTodoId());
        } catch (BusinessException e) {
            model.addAttribute(e.getResultMessages());
            return list(model);
        }

        attributes.addFlashAttribute(ResultMessages.success().add(ResultMessage
                .fromText("Deleted successfully!")));
        return "redirect:/todo/list";
    }

}

項番

説明

(1)

/todo/deleteというパスにPOSTメソッドを使用してリクエストされた際に、削除処理用のメソッド(deleteメソッド)が実行されるように@PostMappingアノテーションを設定する。


11.2.5.2.5.3. テンプレートHTMLの修正

削除処理用のformを実装する。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Todo List</title>
<link rel="stylesheet"
    href="../../../resources/app/css/styles.css" th:href="@{/resources/app/css/styles.css}">
</head>
<body>
    <h1>Todo List</h1>
    <div id="todoForm">
        <div th:if="${resultMessages} != null" class="alert alert-success" th:class="|alert alert-${resultMessages.type}|">
            <ul>
                <li th:each="message : ${resultMessages}" th:text="${message.text}">Created successfully!</li>
            </ul>
        </div>
        <form action="/todo/create" th:action="@{/todo/create}" method="post">
            <input type="text" th:field="${todoForm.todoTitle}" />
            <span id="todoTitle.errors" th:errors="${todoForm.todoTitle}" class="text-error">size must be between 1 and 30</span>
            <button>Create Todo</button>
        </form>
    </div>
    <hr />
    <div id="todoList">
        <ul th:remove="all-but-first">
            <li th:each="todo : ${todos}">
                <span th:class="${todo.finished} ? 'strike'" th:text="${todo.todoTitle}">Send a e-mail</span>
                <form th:if="${!todo.finished}" action="/todo/finish" th:action="@{/todo/finish}"
                    method="post" class="inline">
                    <input type="hidden" name="todoId" th:value="${todo.todoId}" />
                    <button>Finish</button>
                </form>
                <!-- (1) -->
                <form action="/todo/delete" th:action="@{/todo/delete}"
                    method="post" class="inline">
                    <!-- (2) -->
                    <input type="hidden" name="todoId" th:value="${todo.todoId}" />
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span>Have a lunch</span>
                <form action="/todo/finish" method="post" class="inline">
                    <button>Finish</button>
                </form>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
            <li>
                <span class="strike">Read a book</span>
                <form action="/todo/delete" method="post" class="inline">
                    <button>Delete</button>
                </form>
            </li>
        </ul>
    </div>
</body>
</html>

項番

説明

(1)
th:action属性にはリンクURL式 @{}を用いて削除処理を実行するためのパス(/todo/delete)を指定する。
(2)
type="hidden"属性を使用して、リクエストパラメータとしてtodoIdを送信する。

未完了状態のTODOの「Delete」ボタンを押下すると、以下のようにTODOが削除される。

../_images/image0771.png
../_images/image0781.png

11.2.6. データベースアクセスを伴うインフラストラクチャ層の作成

ここでは、Domainオブジェクトをデータベースに永続化するためのインフラストラクチャ層の実装方法について説明する。

本チュートリアルでは、以下の2つのO/R Mapperを使用したインフラストラクチャ層の実装方法について説明する。

  • MyBatis3

  • Spring Data JPA


11.2.6.1. O/R Mapperに依存したブランクプロジェクトの作成

ここでは、O/R Mapperに依存したブランクプロジェクトの作成を行う。

まず、使用するO/R Mapperに応じてプロジェクトを作成し直す。

次に、データベースアクセスを伴うインフラストラクチャ層の作成までで作成したsrcフォルダ以下のうち、TodoRepositoryImplクラス以外のファイルを新規作成したプロジェクトにコピーする

ただし、コピーするファイルは新規作成したファイル・変更を加えたファイルに限り、修正を加えていないファイルはコピーしないこと


11.2.6.2. データベースのセットアップ

ここでは、データベースのセットアップを行う。

本チュートリアルでは、データベースのセットアップの手間を省くため、H2 Databaseを使用する。


11.2.6.2.1. todo-infra.propertiesの修正

APサーバ起動時にH2 Database上にテーブルが作成されるようにするために、src/main/resources/META-INF/spring/todo-infra.propertiesの設定を変更する。

database=H2
# (1)
database.url=jdbc:h2:mem:todo;DB_CLOSE_DELAY=-1;INIT=create table if not exists todo(todo_id varchar(36) primary key, todo_title varchar(30), finished boolean, created_at timestamp)
database.username=sa
database.password=
database.driverClassName=org.h2.Driver
# connection pool
cp.maxActive=96
cp.maxIdle=16
cp.minIdle=0
cp.maxWait=60000

項番

説明

(1)
接続URLのINITパラメータに、テーブルを作成するDDL文を指定する。

Note

INITパラメータに設定しているDDL文をフォーマットすると、以下の様なSQLとなる。

create table if not exists todo (
    todo_id varchar(36) primary key,
    todo_title varchar(30),
    finished boolean,
    created_at timestamp
)

11.2.6.3. MyBatis3を使用したインフラストラクチャ層の作成

ここでは、MyBatis3を使用してインフラストラクチャ層のRepositoryImplを作成する方法について説明する。

Spring Data JPAを使用する場合は、本節を読み飛ばして、Spring Data JPAを使用したインフラストラクチャ層の作成に進んでよい。

11.2.6.3.1. TodoRepositoryの作成

TodoRepositoryは、O/R Mapperを使用しない場合と同じ方法で作成する。
作成方法は、「Repositoryの作成」を参照されたい。

11.2.6.3.2. TodoRepositoryImplの作成

MyBatis3を使用する場合、RepositoryImplはRepositoryインタフェース(Mapperインタフェース)から自動生成される。
そのため、TodoRepositoryImplの作成は不要である。作成した場合は削除すること。

11.2.6.3.3. Mapperファイルの作成

TodoRepositoryインタフェースのメソッドが呼び出された際に実行するSQLを定義するためのMapperファイルを作成する。

Package Explorer上で右クリック -> New -> File を選択し、「Create New File」ダイアログを表示し、

項番

項目

入力値

1

Enter or select the parent folder

todo/src/main/resources/com/example/todo/domain/repository/todo

2

File name

TodoRepository.xml

を入力して「Finish」する。

作成したファイルは以下のディレクトリに格納される。

../_images/create-mapper-for-mybatis31.png

TodoRepositoryインタフェースに定義したメソッドが呼び出された際に実行するSQLを記述する。

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<!-- (1) -->
<mapper namespace="com.example.todo.domain.repository.todo.TodoRepository">

    <!-- (2) -->
    <resultMap id="todoResultMap" type="Todo">
        <id property="todoId" column="todo_id" />
        <result property="todoTitle" column="todo_title" />
        <result property="finished" column="finished" />
        <result property="createdAt" column="created_at" />
    </resultMap>

    <!-- (3) -->
    <select id="findById" parameterType="String" resultMap="todoResultMap">
    <![CDATA[
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at
        FROM
            todo
        WHERE
            todo_id = #{todoId}
    ]]>
    </select>

    <!-- (4) -->
    <select id="findAll" resultMap="todoResultMap">
    <![CDATA[
        SELECT
            todo_id,
            todo_title,
            finished,
            created_at
        FROM
            todo
    ]]>
    </select>

    <!-- (5) -->
    <insert id="create" parameterType="Todo">
    <![CDATA[
        INSERT INTO todo
        (
            todo_id,
            todo_title,
            finished,
            created_at
        )
        VALUES
        (
            #{todoId},
            #{todoTitle},
            #{finished},
            #{createdAt}
        )
    ]]>
    </insert>

    <!-- (6) -->
    <update id="update" parameterType="Todo">
    <![CDATA[
        UPDATE todo
        SET
            todo_title = #{todoTitle},
            finished = #{finished},
            created_at = #{createdAt}
        WHERE
            todo_id = #{todoId}
    ]]>
    </update>

    <!-- (7) -->
    <delete id="delete" parameterType="Todo">
    <![CDATA[
        DELETE FROM
            todo
        WHERE
            todo_id = #{todoId}
    ]]>
    </delete>

    <!-- (8) -->
    <select id="countByFinished" parameterType="Boolean"
        resultType="Long">
    <![CDATA[
        SELECT
            COUNT(*)
        FROM
            todo
        WHERE
            finished = #{finished}
    ]]>
    </select>

</mapper>

項番

説明

(1)
mapper要素のnamespace属性に、Repositoryインタフェースの完全修飾クラス名(FQCN)を指定する。
(2)
<resultMap>要素に、検索結果(ResultSet)とJavaBeanのマッピング定義を行う。
マッピングファイルの詳細はデータベースアクセス(MyBatis3編)を参照されたい。
(3)
todoId(PK)が一致するレコードを1件取得するSQLを実装する。
<select>要素のresultMap属性には、適用するマッピング定義のIDを指定する。
(4)
全レコードを取得するSQLを実装している。
<select>要素のresultMap属性に、適用するマッピング定義のIDを指定する。
アプリケーションの要件には記載がないが、最新のTODOが先頭に表示されるようにレコードを並び替えている。
(5)
引数に指定されたTodoオブジェクトを挿入するSQLを実装する。
<insert>要素のparameterType属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。
(6)
引数に指定されたTodoオブジェクトを更新するSQLを実装する。
<update>要素のparameterType属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。
(7)
引数に指定されたTodoオブジェクトを削除するSQLを実装する。
<delete>要素のparameterType属性に、パラメータのクラス名(FQCN又はエイリアス名)を指定する。
(8)
引数に指定されたfinishedに一致するTodoの件数を取得するSQLを実装する。

以上で、MyBatis3を使用したインフラストラクチャ層の作成が完了したので、Service及びアプリケーション層の作成を行う。

Service及びアプリケーション層を作成後にAPサーバーを起動し、Todoの表示を行うと、以下のようなSQLログやトランザクションログが出力される。

date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[START CONTROLLER] TodoController.list(Model)
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Creating new transaction with name [com.example.todo.domain.service.todo.TodoServiceImpl.findAll]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Acquired Connection [13059960, URL=jdbc:h2:mem:todo-mybatis3, H2 JDBC Driver] for JDBC transaction
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:DEBUG     logger:c.e.t.d.repository.todo.TodoRepository.findAll   message:==>  Preparing: SELECT todo_id, todo_title, finished, created_at FROM todo
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:DEBUG     logger:c.e.t.d.repository.todo.TodoRepository.findAll   message:==> Parameters:
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:DEBUG     logger:c.e.t.d.repository.todo.TodoRepository.findAll   message:<==      Total: 0
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Initiating transaction commit
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Committing JDBC transaction on Connection [13059960, URL=jdbc:h2:mem:todo-mybatis3, H2 JDBC Driver]
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Releasing JDBC Connection [13059960, URL=jdbc:h2:mem:todo-mybatis3, H2 JDBC Driver] after transaction
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[END CONTROLLER  ] TodoController.list(Model)-> view=todo/list, model={todoForm=com.example.todo.app.todo.TodoForm@4e5a28b2, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
date:2022-12-01 16:49:29      thread:http-nio-8080-exec-2     X-Track:d3ef4363deef452d8823a00db0fbc71a        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[HANDLING TIME   ] TodoController.list(Model)-> 205,791,900 ns

11.2.6.4. Spring Data JPAを使用したインフラストラクチャ層の作成

ここでは、Spring Data JPAを使用してインフラストラクチャ層のRepositoryImplを作成する方法について説明する。


11.2.6.4.1. Entityの修正

TodoクラスとデータベースのTODOテーブルをマッピングするために、JPAのアノテーションを設定する。

package com.example.todo.domain.model;

import java.io.Serializable;
import java.util.Date;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Table;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;

// (1)
@Entity
@Table(name = "todo")
public class Todo implements Serializable {
    private static final long serialVersionUID = 1L;

    // (2)
    @Id
    @Column(name = "todo_id")
    private String todoId;

    @Column(name = "todo_title")
    private String todoTitle;

    @Column(name = "finished")
    private boolean finished;

    // (3)
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "created_at")
    private Date createdAt;

    public String getTodoId() {
        return todoId;
    }

    public void setTodoId(String todoId) {
        this.todoId = todoId;
    }

    public String getTodoTitle() {
        return todoTitle;
    }

    public void setTodoTitle(String todoTitle) {
        this.todoTitle = todoTitle;
    }

    public boolean isFinished() {
        return finished;
    }

    public void setFinished(boolean finished) {
        this.finished = finished;
    }

    public Date getCreatedAt() {
        return createdAt;
    }

    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}

項番

説明

(1)
JPAのエンティティであることを示す@Entityアノテーションを付け、対応するテーブル名を@Tableアノテーションで設定する。
(2)
主キーとなるカラムに対応するフィールドに、@Idアノテーションをつける。
(3)
java.util.Date型は、java.sql.Date, java.sql.Time, java.sql.Timestampのインスタンスを格納できるため、明示的にどの型のインスタンスを設定するか指定する必要がある。
createdAtプロパティには、Timestampを指定する。

11.2.6.4.2. TodoRepositoryの作成

Spring Data JPAのRepository機能を使用してTodoRepositoryの作成を行う。

Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、

項番

項目

入力値

1

Package

com.example.todo.domain.repository.todo

2

Name

TodoRepository

3

Extended interfaces

org.springframework.data.jpa.repository.JpaRepository<T, ID>

を入力して「Finish」する。

package com.example.todo.domain.repository.todo;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;

import com.example.todo.domain.model.Todo;

// (1)
public interface TodoRepository extends JpaRepository<Todo, String> {

    @Query("SELECT COUNT(t) FROM Todo t WHERE t.finished = :finished") // (2)
    long countByFinished(@Param("finished") boolean finished); // (3)

}

項番

説明

(1)
JpaRepositoryのGenericsのパラメータを指定する。
左から順に、Entityのクラス(Todo)、主キーのクラス(String)を指定する。
基本的なCRUD操作(findById, findAll, save, deleteByIdなど)は、JpaRepositoryインタフェースに定義済みであるため、TodoRepositoryにはcountByFinishedメソッドのみ定義すればよい。
(2)
countByFinishedメソッドを呼び出した際に実行するJPQLを、@Queryアノテーションで指定する。
(3)
(2)で指定したJPQL内のバインド変数に対応するメソッド引数に、@Paramアノテーションを指定する。
ここでは、JPQL中の”:finished”に値を埋め込むために、メソッド引数のfinished@Param(“finished”)を付けている。

11.2.6.4.3. TodoRepositoryImplの作成

Spring Data JPAを使用する場合、RepositoryImplはRepositoryインタフェースから自動生成される。そのため、TodoRepositoryImplの作成は不要である。作成した場合は削除すること。

Note

当ページをはじめから順に実施した場合、TodoRepositoryImplの削除が必要になるが、削除したタイミングでTodoServiceImplがコンパイルエラーとなる。

JPA用のメソッドに切り替える必要があるため、ソースコード内のコメントを参考にcreateメソッドとupdateメソッドをsaveメソッドへ変更する必要がある。



以上で、Spring Data JPAを使用したインフラストラクチャ層の作成が完了したので、Service及びアプリケーション層の作成を行う。

Service及びアプリケーション層を作成後にAPサーバーを起動し、Todoの表示を行うと、以下のようなSQLログや、トランザクションログが出力される。

date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[START CONTROLLER] TodoController.list(Model)
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:DEBUG     logger:o.h.engine.transaction.internal.TransactionImpl  message:On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:DEBUG     logger:o.h.engine.transaction.internal.TransactionImpl  message:begin
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:DEBUG     logger:org.hibernate.SQL                                message:/* <criteria> */ select t1_0.todo_id,t1_0.created_at,t1_0.finished,t1_0.todo_title from todo t1_0
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:DEBUG     logger:o.h.engine.transaction.internal.TransactionImpl  message:committing
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[END CONTROLLER  ] TodoController.list(Model)-> view=todo/list, model={todoForm=com.example.todo.app.todo.TodoForm@5fcdd91a, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[HANDLING TIME   ] TodoController.list(Model)-> 247,911,500 ns

11.2.7. おわりに

このチュートリアルでは、以下の内容を学習した。

  • TERASOLUNA Server Framework for Java (5.x)による基本的なアプリケーションの開発方法

  • MavenおよびSTS(Eclipse)プロジェクトの構築方法

  • TERASOLUNA Server Framework for Java (5.x)のアプリケーションのレイヤ化に従った開発方法

    • POJO(+ Spring)を使用したドメイン層の実装

    • POJO(+ Spring MVC)とThymeleafを使用したアプリケーション層の実装

    • MyBatis3を使用したインフラストラクチャ層の実装

    • Spring Data JPAを使用したインフラストラクチャ層の実装

    • O/R Mapperを使用しないインフラストラクチャ層の実装

本チュートリアルで作成したTODO管理アプリケーションには、以下の改善点がある。アプリケーションの修正を学習課題として、ガイドライン中の該当する説明を参照されたい。


11.2.8. Appendix

11.2.8.1. 設定ファイルの解説

アプリケーションを動かすためにどのような設定が必要なのかを理解するために、設定ファイルの解説を行う。
ここでは、チュートリアルで作成するTodoアプリケーションで使用しない設定については、解説を割愛している箇所がある。

11.2.8.1.1. web.xml

web.xmlには、WebアプリケーションとしてTodoアプリをデプロイするための設定を行う。

作成したブランクプロジェクトのsrc/main/webapp/WEB-INF/web.xmlは、以下のような設定となっている。

<?xml version="1.0" encoding="UTF-8"?>
<!-- (1) -->
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd"
    version="6.0">

    <context-param>
        <param-name>logbackDisableServletContainerInitializer</param-name>
        <param-value>true</param-value>
    </context-param>

    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <listener>
        <listener-class>ch.qos.logback.classic.servlet.LogbackServletContextListener</listener-class>
    </listener>

    <!-- (2) -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- Root ApplicationContext -->
        <param-value>
            com.example.todo.config.app.ApplicationContextConfig
            com.example.todo.config.web.SpringSecurityConfig
        </param-value>
    </context-param>

    <listener>
        <listener-class>org.terasoluna.gfw.web.logging.HttpSessionEventLoggingListener</listener-class>
    </listener>

    <!-- (3) -->
    <filter>
        <filter-name>MDCClearFilter</filter-name>
        <filter-class>org.terasoluna.gfw.web.logging.mdc.MDCClearFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>MDCClearFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>exceptionLoggingFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>exceptionLoggingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>XTrackMDCPutFilter</filter-name>
        <filter-class>org.terasoluna.gfw.web.logging.mdc.XTrackMDCPutFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>XTrackMDCPutFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>

    <!-- (4) -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- ApplicationContext for Spring MVC -->
            <param-value>com.example.todo.config.web.SpringMvcConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <!-- (5) -->
    <error-page>
        <error-code>500</error-code>
        <location>/common/error/systemError</location>
    </error-page>

    <error-page>
        <error-code>404</error-code>
        <location>/common/error/resourceNotFoundError</location>
    </error-page>

    <error-page>
        <exception-type>java.lang.Exception</exception-type>
        <location>/WEB-INF/views/common/error/unhandledSystemError.html</location>
    </error-page>

    <!-- (6) -->
    <session-config>
        <!-- 30min -->
        <session-timeout>30</session-timeout>
        <cookie-config>
            <http-only>true</http-only>
            <!-- <secure>true</secure> -->
        </cookie-config>
        <tracking-mode>COOKIE</tracking-mode>
    </session-config>

</web-app>

項番

説明

(1)
Servlet6.0を使用するための宣言。
(2)
サーブレットコンテキストリスナーの定義。

ブランクプロジェクトでは、

  • アプリケーション全体で使用されるApplicationContextを作成するためのContextLoaderListener

  • HttpSessionに対する操作をログ出力するための HttpSessionEventLoggingListener

が設定済みである。

(3)
サーブレットフィルタの定義。

ブランクプロジェクトでは、

  • 共通ライブラリから提供しているサーブレットフィルタ

  • Spring Frameworkから提供されている文字エンコーディングを指定するためのCharacterEncodingFilter

  • Spring Securityから提供されている認証・認可用のサーブレットフィルタ

が設定済みである。

(4)
Spring MVCのエントリポイントとなるDispatcherServletの定義。

DispatcherServletの中で使用するApplicationContextを、(2)で作成したApplicationContextの子として作成する。
(2)で作成したApplicationContextを親にすることで、(2)で読み込まれたコンポーネントも使用することができる。
(5)
エラーページの定義。

ブランクプロジェクトでは、

  • サーブレットコンテナにHTTPステータスコードとして、404又は500が応答

  • サーブレットコンテナに例外が通知

された際の遷移先が定義済みである。

(6)
セッション管理の定義。

ブランクプロジェクトでは、

  • セッションタイムアウトとして、30分

が定義済みである。



11.2.8.1.2. Bean定義ファイル

作成したブランクプロジェクトには、以下のBean定義ファイルとプロパティファイルが作成される。

  • src/main/java/com/example/todo/config/app/ApplicationContextConfig.java

  • src/main/java/com/example/todo/config/app/TodoCodeListConfig.java

  • src/main/java/com/example/todo/config/app/TodoDomainConfig.java

  • src/main/java/com/example/todo/config/app/TodoInfraConfig.java

  • src/main/resources/META-INF/spring/todo-infra.properties

  • src/main/java/com/example/todo/config/app/TodoEnvConfig.java

  • src/main/java/com/example/todo/config/web/SpringMvcConfig.java

  • src/main/java/com/example/todo/config/web/SpringSecurityConfig.java

Note

O/R Mapperに依存しないブランクプロジェクトを作成した場合は、todo-infra.propertiesTodoEnvConfig.javaは作成されない。

Note

本ガイドラインでは、Bean定義ファイルを役割(層)ごとにファイルを分割することを推奨している。

これは、どこに何が定義されているか想像しやすく、メンテナンス性が向上するからである。今回のチュートリアルのような小さなアプリケーションでは効果はないが、アプリケーションの規模が大きくなるにつれ、効果が大きくなる。


11.2.8.1.2.1. applicationContext

ApplicationContextConfig.javaには、Todoアプリ全体に関わる設定を行う。

作成したブランクプロジェクトのsrc/main/java/com/example/todo/config/app/ApplicationContextConfig.java は、以下のような設定となっている。
なお、チュートリアルで使用しないコンポーネントについての説明は割愛する。
package com.example.todo.config.app;

import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.terasoluna.gfw.common.exception.ExceptionCodeResolver;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.common.exception.SimpleMappingExceptionCodeResolver;
import org.terasoluna.gfw.web.exception.ExceptionLoggingFilter;

/**
 * Application context.
 */
@Configuration
@EnableAspectJAutoProxy
@Import({ TodoDomainConfig.class }) // (1)
public class ApplicationContextConfig {

    /**
     * Configure {@link PasswordEncoder} bean.
     * @return Bean of configured {@link DelegatingPasswordEncoder}
     */
    @Bean("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
        idToPasswordEncoder.put("pbkdf2", pbkdf2PasswordEncoder());
        idToPasswordEncoder.put("bcrypt", bCryptPasswordEncoder());
        /* When using commented out PasswordEncoders, you need to add bcprov-jdk18on.jar to the dependency.
        idToPasswordEncoder.put("argon2", argon2PasswordEncoder());
        idToPasswordEncoder.put("scrypt", sCryptPasswordEncoder());
        */
        return new DelegatingPasswordEncoder("pbkdf2", idToPasswordEncoder);
    }

    /**
     * Configure {@link Pbkdf2PasswordEncoder} bean.
     * @return Bean of configured {@link Pbkdf2PasswordEncoder}
     */
    @Bean
    public Pbkdf2PasswordEncoder pbkdf2PasswordEncoder() {
        return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    }

    /**
     * Configure {@link BCryptPasswordEncoder} bean.
     * @return Bean of configured {@link BCryptPasswordEncoder}
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /* When using commented out PasswordEncoders, you need to add bcprov-jdk18on.jar to the dependency.
    @Bean
    public Argon2PasswordEncoder argon2PasswordEncoder() {
        return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    }
    @Bean
    public SCryptPasswordEncoder sCryptPasswordEncoder() {
        return SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
    }
    */

    /**
     * Configure {@link PropertySourcesPlaceholderConfigurer} bean.
     * @param properties Property files to be read
     * @return Bean of configured {@link PropertySourcesPlaceholderConfigurer}
     */
    // (2)
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
            @Value("classpath*:/META-INF/spring/*.properties") Resource... properties) {
        PropertySourcesPlaceholderConfigurer bean = new PropertySourcesPlaceholderConfigurer();
        bean.setLocations(properties);
        return bean;
    }

    /**
     * Configure {@link MessageSource} bean.
     * @return Bean of configured {@link ResourceBundleMessageSource}
     */
    @Bean("messageSource")
    public MessageSource messageSource() {
        ResourceBundleMessageSource bean = new ResourceBundleMessageSource();
        bean.setBasenames("i18n/application-messages");
        return bean;
    }

    /**
     * Configure {@link ExceptionCodeResolver} bean.
     * @return Bean of configured {@link SimpleMappingExceptionCodeResolver}
     */
    @Bean("exceptionCodeResolver")
    public ExceptionCodeResolver exceptionCodeResolver() {
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("ResourceNotFoundException", "e.xx.fw.5001");
        map.put("InvalidTransactionTokenException", "e.xx.fw.7001");
        map.put("BusinessException", "e.xx.fw.8001");
        map.put(".DataAccessException", "e.xx.fw.9002");
        SimpleMappingExceptionCodeResolver bean = new SimpleMappingExceptionCodeResolver();
        bean.setExceptionMappings(map);
        bean.setDefaultExceptionCode("e.xx.fw.9001");
        return bean;
    }

    /**
     * Configure {@link ExceptionLogger} bean.
     * @return Bean of configured {@link ExceptionLogger}
     */
    @Bean("exceptionLogger")
    public ExceptionLogger exceptionLogger() {
        ExceptionLogger bean = new ExceptionLogger();
        bean.setExceptionCodeResolver(exceptionCodeResolver());
        return bean;
    }

    /**
     * Configure {@link ExceptionLoggingFilter} bean.
     * @return Bean of configured {@link ExceptionLoggingFilter}
     */
    @Bean("exceptionLoggingFilter")
    public ExceptionLoggingFilter exceptionLoggingFilter() {
        ExceptionLoggingFilter bean = new ExceptionLoggingFilter();
        bean.setExceptionLogger(exceptionLogger());
        return bean;
    }
}

項番

説明

(1)
ドメイン層に関するBean定義ファイルをimportする。
(2)
プロパティファイルの読み込み設定を行う。
src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。
この設定により、プロパティファイルの値をBean定義ファイル内で${propertyName}形式で埋め込んだり、Javaクラスに@Value("${propertyName}")でインジェクションすることができる。

11.2.8.1.2.2. todo-domain

TodoDomainConfig.javaには、Todoアプリのドメイン層に関わる設定を行う。

作成したブランクプロジェクトのsrc/main/java/com/example/todo/config/app/TodoDomainConfig.javaは、以下のような設定となっている。
なお、チュートリアルで使用しないコンポーネントについての説明は割愛する。
package com.example.todo.config.app;

import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.common.exception.ResultMessagesLoggingInterceptor;

/**
 * Bean definitions for domain layer.
 */
@Configuration
@ComponentScan(basePackages = { "com.example.todo.domain" }) // (1)
@Import({ TodoInfraConfig.class,
    TodoCodeListConfig.class }) // (2)
public class TodoDomainConfig {

    /**
     * Configure messages logging AOP.
     * @param exceptionLogger Bean defined by ApplicationContext#exceptionLogger
     * @see com.example.todo.config.app.ApplicationContext#exceptionLogger()
     * @return Bean of configured {@link ResultMessagesLoggingInterceptor}
     */
    @Bean("resultMessagesLoggingInterceptor")
    public ResultMessagesLoggingInterceptor resultMessagesLoggingInterceptor(
            ExceptionLogger exceptionLogger) {
        ResultMessagesLoggingInterceptor bean = new ResultMessagesLoggingInterceptor();
        bean.setExceptionLogger(exceptionLogger);
        return bean;
    }

    /**
     * Configure messages logging AOP advisor.
     * @param resultMessagesLoggingInterceptor Bean defined by #resultMessagesLoggingInterceptor
     * @see #resultMessagesLoggingInterceptor(ExceptionLogger)
     * @return Advisor configured for PointCut
     */
    @Bean
    public Advisor resultMessagesLoggingInterceptorAdvisor(
            ResultMessagesLoggingInterceptor resultMessagesLoggingInterceptor) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(
                "@within(org.springframework.stereotype.Service)");
        return new DefaultPointcutAdvisor(pointcut, resultMessagesLoggingInterceptor);
    }
}

項番

説明

(1)
ドメイン層のクラスを管理するcom.example.todo.domainパッケージ配下をcomponent-scan対象とする。
これにより、com.example.todo.domainパッケージ配下のクラスに@Repository,@Serviceなどのアノテーションを付けることで、Spring Framerowkが管理するBeanとして登録される。
登録されたクラス(Bean)は、ControllerやServiceクラスにDIする事で、利用する事が出来る。
(2)
インフラストラクチャ層に関するBean定義ファイルをimportする。

Note

O/R Mapperに依存するブランクプロジェクトを作成した場合は、@Transactionalアノテーションによるトランザクション管理を有効にするために、@EnableTransactionManagementアノテーションが設定されている。

@EnableTransactionManagement

11.2.8.1.2.3. todo-infra

TodoInfraConfig.javaには、Todoアプリのインフラストラクチャ層に関わる設定を行う。

作成したブランクプロジェクトのsrc/main/java/com/example/todo/config/app/TodoInfraConfig.javaは、以下のような設定となっている。

TodoInfraConfig.javaは、インフラストラクチャ層によって設定が大きく異なるため、ブランクプロジェクト毎に説明を行う。作成したブランクプロジェクト以外の説明は読み飛ばしてもよい。


11.2.8.1.2.3.1. O/R Mapperに依存しないブランクプロジェクトを作成した場合のtodo-infra

O/R Mapperに依存しないブランクプロジェクトを作成した場合、以下のように空定義のファイルが作成される。

package com.example.todo.config.app;

import org.springframework.context.annotation.Configuration;

/**
 * Bean definitions for infrastructure layer.
 */
@Configuration
public class TodoInfraConfig {

}

11.2.8.1.2.3.2. MyBatis3用のブランクプロジェクトを作成した場合のtodo-infra

MyBatis3用のブランクプロジェクトを作成した場合、以下のような設定となっている。

package com.example.todo.config.app;

import javax.sql.DataSource;

import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

import com.example.todo.config.app.mybatis.MybatisConfig;

/**
 * Bean definitions for infrastructure layer.
 */
@Configuration
@MapperScan("com.example.todo.domain.repository") // (1)
@Import({ TodoEnvConfig.class }) // (2)
public class TodoInfraConfig {

    /**
     * Configure {@link SqlSessionFactory} bean.
     * @param dataSource DataSource
     * @see com.example.todo.config.app.TodoEnvConfig#dataSource()
     * @return Bean of configured {@link SqlSessionFactoryBean}
     */
    // (3)
    @Bean("sqlSessionFactory")
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        // (4)
        bean.setDataSource(dataSource);
        // (5)
        bean.setConfiguration(MybatisConfig.configuration());
        return bean;
    }
}

項番

説明

(1)
Mapperインタフェースをスキャンするために@MapperScanを定義し、Mapperインタフェースが格納されている基底パッケージを指定する。

(2)
環境依存するコンポーネント(データソースやトランザクションマネージャなど)を定義するBean定義ファイルをimportする。

指定されたパッケージ配下に格納されている Mapperインタフェースがスキャンされ、スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)が自動的に生成される。

(3)
SqlSessionFactoryを生成するためのコンポーネントとして、SqlSessionFactoryBeanをbean定義する。

(4)
dataSourceプロパティに、設定済みのデータソースのbeanを指定する。

MyBatis3の処理の中でSQLを発行する際は、ここで指定したデータソースからコネクションが取得される。
(5)
configurationプロパティに、MyBatisの設定をしたConfigurationクラスを指定する。

ここで指定したクラスはSqlSessionFactoryを生成する時に読み込まれる。

11.2.8.1.2.3.3. JPA用のブランクプロジェクトを作成した場合のtodo-infra

JPA用のブランクプロジェクトを作成した場合、以下のような設定となっている。

package com.example.todo.config.app;

import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;

/**
 * Bean definitions for infrastructure layer.
 */
@Configuration
@EnableJpaRepositories("com.example.todo.domain.repository") // (1)
@Import({ TodoEnvConfig.class }) // (2)
public class TodoInfraConfig {

    /**
     * Database property.
     */
    @Value("${database}")
    private Database database;

    /**
     * Configure {@link HibernateJpaVendorAdapter} bean.
     * @return Bean of configured {@link HibernateJpaVendorAdapter}
     */
    // (3)
    @Bean("jpaVendorAdapter")
    public HibernateJpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter bean = new HibernateJpaVendorAdapter();
        bean.setDatabase(database);
        return bean;
    }

    /**
     * Configure {@link LocalContainerEntityManagerFactoryBean} bean.
     * @param dataSource DataSource
     * @return Bean of configured {@link LocalContainerEntityManagerFactoryBean}
     */
    // (4)
    @Bean("entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(
            DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
        bean.setPackagesToScan("com.example.todo.domain.model");
        bean.setDataSource(dataSource);
        bean.setJpaVendorAdapter(jpaVendorAdapter());
        bean.setJpaPropertyMap(jpaPropertyMap());
        return bean;
    }

    /**
     * Configure {@link LocalContainerEntityManagerFactoryBean}.JpaPropertyMap.
     * @return configured JpaPropertyMap
     */
    // (5)
    private Map<String, ?> jpaPropertyMap() {
        Map<String, Object> jpaPropertyMap = new HashMap<>();
        jpaPropertyMap.put("hibernate.connection.charSet", "UTF-8");
        jpaPropertyMap.put("hibernate.format_sql", false);
        jpaPropertyMap.put("hibernate.use_sql_comments", true);
        jpaPropertyMap.put("hibernate.jdbc.batch_size", 30);
        jpaPropertyMap.put("hibernate.jdbc.fetch_size", 100);
        return jpaPropertyMap;
    }
}

項番

説明

(1)
Spring Data JPAを使用して、Repositoryインタフェースから実装クラスを自動生成する。
@EnableJpaRepositoriesアノテーションに、Repositoryを格納するパッケージを指定する。
(2)
環境依存するコンポーネント(データソースやトランザクションマネージャなど)を定義するBean定義ファイルをimportする。
(3)
JPAの実装ベンダの設定を行う。
JPA実装として、Hibernateを使うため、HibernateJpaVendorAdapterを定義している。
(4)
EntityManagerの定義を行う。
(5)
Hibernateに関する詳細な設定を行う。

11.2.8.1.2.4. todo-infra.properties

todo-infra.propertiesには、Todoアプリのインフラストラクチャ層における環境依存値の設定を行う。

O/R Mapperに依存しないブランクプロジェクトを作成した際は、todo-infra.propertiesは作成されない。

作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/todo-infra.propertiesは、以下のような設定となっている。

# (1)
database=H2
database.url=jdbc:h2:mem:todo;DB_CLOSE_DELAY=-1
database.username=sa
database.password=
database.driverClassName=org.h2.Driver
# (2)
# connection pool
cp.maxActive=96
cp.maxIdle=16
cp.minIdle=0
cp.maxWait=60000

項番

説明

(1)
データベースに関する設定を行う。
本チュートリアルでは、データベースのセットアップの手間を省くため、H2 Databaseを使用する。
(2)
コネクションプールに関する設定。

Note

これらの設定値は、todo-env.xmlまたはTodoEnvConfig.javaから参照されている。


11.2.8.1.2.5. todo-env

TodoEnvConfig.javaには、デプロイする環境によって設定が異なるコンポーネントの設定を行う。

作成したブランクプロジェクトのsrc/main/java/com/example/todo/config/app/TodoEnvConfig.javaは、以下のような設定となっている。

ここでは、MyBatis3用のブランクプロジェクトに格納されるファイルを例に説明する。なお、データベースにアクセスしないブランクプロジェクトを作成した際は、TodoEnvConfig.javaは作成されない。

package com.example.todo.config.app;

import java.time.Duration;

import javax.sql.DataSource;

import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
import org.springframework.transaction.TransactionManager;
import org.terasoluna.gfw.common.time.ClockFactory;
import org.terasoluna.gfw.common.time.DefaultClockFactory;

/* REMOVE THIS LINE IF YOU USE JPA
import org.springframework.orm.jpa.JpaTransactionManager;
import jakarta.persistence.EntityManagerFactory;
REMOVE THIS LINE IF YOU USE JPA */

/**
 * Define settings for the environment.
 */
@Configuration
public class TodoEnvConfig {

    /**
     * DataSource.driverClassName property.
     */
    @Value("${database.driverClassName}")
    private String driverClassName;

    /**
     * DataSource.url property.
     */
    @Value("${database.url}")
    private String url;

    /**
     * DataSource.username property.
     */
    @Value("${database.username}")
    private String username;

    /**
     * DataSource.password property.
     */
    @Value("${database.password}")
    private String password;

    /**
     * DataSource.maxTotal property.
     */
    @Value("${cp.maxActive}")
    private Integer maxActive;

    /**
     * DataSource.maxIdle property.
     */
    @Value("${cp.maxIdle}")
    private Integer maxIdle;

    /**
     * DataSource.minIdle property.
     */
    @Value("${cp.minIdle}")
    private Integer minIdle;

    /**
     * DataSource.maxWaitMillis property.
     */
    @Value("${cp.maxWait}")
    private Integer maxWait;

    /**
     * Property databaseName.
     */
    @Value("${database}")
    private String database;

    /**
     * Configure {@link ClockFactory}.
     * @return Bean of configured {@link DefaultClockFactory}
     */
    @Bean("dateFactory")
    public ClockFactory dateFactory() {
        return new DefaultClockFactory();
    }

    /**
     * Configure {@link DataSource} bean.
     * @return Bean of configured {@link BasicDataSource}
     */
    // (1)
    @Bean(name = "dataSource", destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource bean = new BasicDataSource();
        bean.setDriverClassName(driverClassName);
        bean.setUrl(url);
        bean.setUsername(username);
        bean.setPassword(password);
        bean.setDefaultAutoCommit(false);
        bean.setMaxTotal(maxActive);
        bean.setMaxIdle(maxIdle);
        bean.setMinIdle(minIdle);
        bean.setMaxWait(Duration.ofMillis(maxWait));
        return bean;
    }

    /**
     * Configuration to set up database during initialization.
     * @return Bean of configured {@link DataSourceInitializer}
     */
    // (2)
    @Bean
    public DataSourceInitializer dataSourceInitializer() {
        DataSourceInitializer bean = new DataSourceInitializer();
        bean.setDataSource(dataSource());
        // (3)
        ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
        databasePopulator.addScript(new ClassPathResource("/database/"
                + database + "-schema.sql"));
        databasePopulator.addScript(new ClassPathResource("/database/"
                + database + "-dataload.sql"));
        databasePopulator.setSqlScriptEncoding("UTF-8");
        databasePopulator.setIgnoreFailedDrops(true);
        bean.setDatabasePopulator(databasePopulator);
        return bean;
    }

    // (4)
    /* REMOVE THIS LINE IF YOU USE JPA
    /**
     * Configure {@link TransactionManager} bean for use with JPA.
     * @param entityManagerFactory EntityManager used within a transaction
     * @return Bean of configured {@link JpaTransactionManager}
     *REMOVE THIS COMMENT IF YOU USE JPA/
    @Bean("transactionManager")
    public TransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager bean = new JpaTransactionManager();
        bean.setEntityManagerFactory(entityManagerFactory);
        return bean;
    }
    REMOVE THIS LINE IF YOU USE JPA */

    /**
     * Configure {@link TransactionManager} bean.
     * @return Bean of configured {@link DataSourceTransactionManager}
     */
    @Bean("transactionManager")
    public TransactionManager transactionManager() {
        DataSourceTransactionManager bean = new DataSourceTransactionManager();
        bean.setDataSource(dataSource());
        bean.setRollbackOnCommitFailure(true);
        return bean;
    }

}

項番

説明

(1)
実データソースの設定。
(2)
データベース初期化の設定。
データベースを初期化するSQLファイルを実行するための設定を行っている。

この設定は通常、開発中のみでしか使用しない(環境に依存する設定)ため、TodoEnvConfig.java に定義されている。
(3)
データベースを初期化するSQLファイルの設定。
データベースを初期化するための、DDL文が記載されているSQLファイルとDML文が記載されているSQLファイルを指定している。

ブランクプロジェクトの設定ではtodo-infra.propertiesdatabase=H2 と定義されているため、H2-schema.sql 及びH2-dataload.sql が実行される。
(4)
トランザクションマネージャの設定。
id属性には、transactionManagerを指定する。

ブランクプロジェクトでは、JDBCのAPIを使用してトランザクションを制御するクラス(org.springframework.jdbc.datasource.DataSourceTransactionManager)が設定されている。

Note

JPA用のブランクプロジェクトを作成した場合は、トランザクションマネージャには、JPAのAPIを使用してトランザクションを制御するクラス(org.springframework.orm.jpa.JpaTransactionManager)が設定されている。

@Bean("transactionManager")
public TransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
    JpaTransactionManager bean = new JpaTransactionManager();
    bean.setEntityManagerFactory(entityManagerFactory);
    return bean;
}

11.2.8.1.2.6. spring-mvc

SpringMvcConfig.javaには、Spring MVCに関する定義を行う。

作成したブランクプロジェクトのsrc/main/java/com/example/todo/config/web/SpringMvcConfig.javaは、以下のような設定となっている。
なお、チュートリアルで使用しないコンポーネントについての説明は割愛する。
package com.example.todo.config.web;

import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.regex.Pattern;

import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.Resource;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.http.HttpStatus;
/* REMOVE THIS LINE IF YOU USE JPA
import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor;
REMOVE THIS LINE IF YOU USE JPA */
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
/* REMOVE THIS LINE IF YOU USE JPA
import org.springframework.web.context.request.WebRequestInterceptor;
REMOVE THIS LINE IF YOU USE JPA */
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.terasoluna.gfw.common.exception.ExceptionCodeResolver;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.web.codelist.CodeListInterceptor;
import org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor;
import org.terasoluna.gfw.web.exception.SystemExceptionResolver;
import org.terasoluna.gfw.web.logging.TraceLoggingInterceptor;
import org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor;
import org.thymeleaf.dialect.IDialect;
import org.thymeleaf.extras.springsecurity6.dialect.SpringSecurityDialect;
import org.thymeleaf.spring6.SpringTemplateEngine;
import org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver;
import org.thymeleaf.spring6.view.ThymeleafViewResolver;
import org.thymeleaf.templateresolver.ITemplateResolver;

/**
* Configure SpringMVC.
*/
// (1)
@ComponentScan(basePackages = { "com.example.todo.app" })
@EnableAspectJAutoProxy
@EnableWebMvc
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {

    /**
     * Configure {@link PropertySourcesPlaceholderConfigurer} bean.
     * @param properties Property files to be read
     * @return Bean of configured {@link PropertySourcesPlaceholderConfigurer}
     */
    // (2)
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
            @Value("classpath*:/META-INF/spring/*.properties") Resource... properties) {
        PropertySourcesPlaceholderConfigurer bean = new PropertySourcesPlaceholderConfigurer();
        bean.setLocations(properties);
        return bean;
    }

    /**
     * {@inheritDoc}
     */
    // (3)
    @Override
    public void addArgumentResolvers(
            List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(pageableHandlerMethodArgumentResolver());
        argumentResolvers.add(authenticationPrincipalArgumentResolver());
    }

    /**
     * Configure {@link PageableHandlerMethodArgumentResolver} bean.
     * @return Bean of configured {@link PageableHandlerMethodArgumentResolver}
     */
    @Bean
    public PageableHandlerMethodArgumentResolver pageableHandlerMethodArgumentResolver() {
        return new PageableHandlerMethodArgumentResolver();
    }

    /**
     * Configure {@link AuthenticationPrincipalArgumentResolver} bean.
     * @return Bean of configured {@link AuthenticationPrincipalArgumentResolver}
     */
    @Bean
    public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() {
        return new AuthenticationPrincipalArgumentResolver();
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureDefaultServletHandling(
            DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    /**
     * {@inheritDoc}
     */
    // (4)
    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**").addResourceLocations(
                "/resources/", "classpath:META-INF/resources/").setCachePeriod(
                        60 * 60);
    }

    /**
     * {@inheritDoc}
     */
    // (5)
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        addInterceptor(registry, traceLoggingInterceptor());
        addInterceptor(registry, transactionTokenInterceptor());
        addInterceptor(registry, codeListInterceptor());
        /* REMOVE THIS LINE IF YOU USE JPA
        addWebRequestInterceptor(registry, openEntityManagerInViewInterceptor());
          REMOVE THIS LINE IF YOU USE JPA */
    }

    /**
     * Common processes used in #addInterceptors.
     * @param registry {@link InterceptorRegistry}
     * @param interceptor {@link HandlerInterceptor}
     */
    private void addInterceptor(InterceptorRegistry registry,
            HandlerInterceptor interceptor) {
        registry.addInterceptor(interceptor).addPathPatterns("/**")
                .excludePathPatterns("/resources/**");
    }

    /* REMOVE THIS LINE IF YOU USE JPA
    /**
     * Common processes used in #addInterceptors.
     * @param registry {@link InterceptorRegistry}
     * @param interceptor {@link WebRequestInterceptor}
     *REMOVE THIS COMMENT IF YOU USE JPA/
    private void addWebRequestInterceptor(InterceptorRegistry registry,
            WebRequestInterceptor interceptor) {
        registry.addWebRequestInterceptor(interceptor).addPathPatterns("/**")
                .excludePathPatterns("/resources/**");
    }
    REMOVE THIS LINE IF YOU USE JPA */

    /**
     * Configure {@link TraceLoggingInterceptor} bean.
     * @return Bean of configured {@link TraceLoggingInterceptor}
     */
    @Bean
    public TraceLoggingInterceptor traceLoggingInterceptor() {
        return new TraceLoggingInterceptor();
    }

    /**
     * Configure {@link TransactionTokenInterceptor} bean.
     * @return Bean of configured {@link TransactionTokenInterceptor}
     */
    @Bean
    public TransactionTokenInterceptor transactionTokenInterceptor() {
        return new TransactionTokenInterceptor();
    }

    /**
     * Configure {@link CodeListInterceptor} bean.
     * @return Bean of configured {@link CodeListInterceptor}
     */
    @Bean
    public CodeListInterceptor codeListInterceptor() {
        CodeListInterceptor codeListInterceptor = new CodeListInterceptor();
        codeListInterceptor.setCodeListIdPattern(Pattern.compile("CL_.+"));
        return codeListInterceptor;
    }

    /* REMOVE THIS LINE IF YOU USE JPA
    /**
     * Configure {@link OpenEntityManagerInViewInterceptor} bean.
     * @return Bean of configured {@link OpenEntityManagerInViewInterceptor}
     *REMOVE THIS COMMENT IF YOU USE JPA/
    @Bean
    public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
        return new OpenEntityManagerInViewInterceptor();
    }
    REMOVE THIS LINE IF YOU USE JPA */

    /**
     * {@inheritDoc}
     */
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(thymeleafViewResolver());
    }

    /**
     * Configure Thymeleaf bean.
     * @return Bean of configured ThymeleafViewResolver
     */
    // (6)
    @Bean
    public ThymeleafViewResolver thymeleafViewResolver() {
        ThymeleafViewResolver bean = new ThymeleafViewResolver();
        bean.setTemplateEngine(templateEngine());
        bean.setCharacterEncoding("UTF-8");
        bean.setForceContentType(true);
        bean.setContentType("text/html;charset=UTF-8");
        return bean;
    }

    /**
     * Configure ITemplateResolver Bean.
     * @return Bean of configured SpringResourceTemplateResolver
     */
    // (7)
    @Bean("templateResolver")
    public ITemplateResolver templateResolver() {
        SpringResourceTemplateResolver bean = new SpringResourceTemplateResolver();
        bean.setPrefix("/WEB-INF/views/");
        bean.setSuffix(".html");
        bean.setTemplateMode("HTML");
        bean.setCharacterEncoding("UTF-8");
        return bean;
    }

    /**
     * Configure SpringTemplateEngine Bean.
     * @return Bean of configured SpringTemplateEngine
     */
    @Bean("templateEngine")
    public SpringTemplateEngine templateEngine() {
        SpringTemplateEngine bean = new SpringTemplateEngine();
        bean.setTemplateResolver(templateResolver());
        bean.setEnableSpringELCompiler(true);
        Set<IDialect> set = new HashSet<>();
        set.add(new SpringSecurityDialect());
        bean.setAdditionalDialects(set);
        return bean;
    }

    /**
     * Configure {@link RequestDataValueProcessor} bean.
     * @return Bean of configured {@link CompositeRequestDataValueProcessor}
     */
    @Bean("requestDataValueProcessor")
    public RequestDataValueProcessor requestDataValueProcessor() {
        return new CompositeRequestDataValueProcessor(csrfRequestDataValueProcessor(), transactionTokenRequestDataValueProcessor());
    }

    /**
     * Configure {@link CsrfRequestDataValueProcessor} bean.
     * @return Bean of configured {@link CsrfRequestDataValueProcessor}
     */
    @Bean
    public CsrfRequestDataValueProcessor csrfRequestDataValueProcessor() {
        return new CsrfRequestDataValueProcessor();
    }

    /**
     * Configure {@link TransactionTokenRequestDataValueProcessor} bean.
     * @return Bean of configured {@link TransactionTokenRequestDataValueProcessor}
     */
    @Bean
    public TransactionTokenRequestDataValueProcessor transactionTokenRequestDataValueProcessor() {
        return new TransactionTokenRequestDataValueProcessor();
    }

    /**
     * Configure {@link SystemExceptionResolver} bean.
     * @param exceptionCodeResolver Bean defined by ApplicationContext#exceptionCodeResolver
     * @see com.example.todo.config.app.ApplicationContext#exceptionCodeResolver()
     * @return Bean of configured {@link SystemExceptionResolver}
     */
    @Bean("systemExceptionResolver")
    public SystemExceptionResolver systemExceptionResolver(
            ExceptionCodeResolver exceptionCodeResolver) {
        SystemExceptionResolver bean = new SystemExceptionResolver();
        bean.setExceptionCodeResolver(exceptionCodeResolver);
        bean.setOrder(3);

        Properties exceptionMappings = new Properties();
        exceptionMappings.setProperty("ResourceNotFoundException",
                "common/error/resourceNotFoundError");
        exceptionMappings.setProperty("BusinessException",
                "common/error/businessError");
        exceptionMappings.setProperty("InvalidTransactionTokenException",
                "common/error/transactionTokenError");
        exceptionMappings.setProperty(".DataAccessException",
                "common/error/dataAccessError");
        bean.setExceptionMappings(exceptionMappings);

        Properties statusCodes = new Properties();
        statusCodes.setProperty("common/error/resourceNotFoundError", String
                .valueOf(HttpStatus.NOT_FOUND.value()));
        statusCodes.setProperty("common/error/businessError", String.valueOf(
                HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/transactionTokenError", String
                .valueOf(HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/dataAccessError", String.valueOf(
                HttpStatus.INTERNAL_SERVER_ERROR.value()));
        bean.setStatusCodes(statusCodes);

        bean.setDefaultErrorView("common/error/systemError");
        bean.setDefaultStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return bean;
    }

    /**
     * Configure messages logging AOP.
     * @param exceptionLogger Bean defined by ApplicationContext#exceptionLogger
     * @see com.example.todo.config.app.ApplicationContext#exceptionLogger()
     * @return Bean of configured {@link HandlerExceptionResolverLoggingInterceptor}
     */
    @Bean("handlerExceptionResolverLoggingInterceptor")
    public HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor(
            ExceptionLogger exceptionLogger) {
        HandlerExceptionResolverLoggingInterceptor bean = new HandlerExceptionResolverLoggingInterceptor();
        bean.setExceptionLogger(exceptionLogger);
        return bean;
    }

    /**
     * Configure messages logging AOP advisor.
     * @param handlerExceptionResolverLoggingInterceptor Bean defined by #handlerExceptionResolverLoggingInterceptor
     * @see #handlerExceptionResolverLoggingInterceptor(ExceptionLogger)
     * @return Advisor configured for PointCut
     */
    @Bean
    public Advisor handlerExceptionResolverLoggingInterceptorAdvisor(
            HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(
                "execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))");
        return new DefaultPointcutAdvisor(pointcut, handlerExceptionResolverLoggingInterceptor);
    }
}

項番

説明

(1)
アプリケーション層のクラスを管理するcom.example.todo.appパッケージ配下をcomponent-scan対象とする。
(2)
プロパティファイルの読み込み設定を行う。
src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。
この設定により、プロパティファイルの値をBean定義ファイル内で${propertyName}形式で埋め込んだり、Javaクラスに@Value("${propertyName}")でインジェクションすることができる。
(3)
Spring MVCのアノテーションベースのデフォルト設定を行う。
(4)
静的リソース(css, images, jsなど)アクセスのための設定を行う。
mapping属性にURLのパスを、location属性に物理的なパスの設定を行う。
この設定の場合<contextPath>/resources/app/css/styles.cssに対してリクエストが来た場合、WEB-INF/resources/app/css/styles.cssを探し、見つからなければクラスパス上(src/main/resourcesやjar内)のMETA-INF/resources/app/css/styles.cssを探す。
どこにもstyles.cssが格納されていない場合は、404エラーを返す。
ここではcache-period属性で静的リソースのキャッシュ時間(3600秒=60分)も設定している。
cache-period="3600"と設定しても良いが、60分であることを明示するためにSpELを使用して cache-period="#{60 * 60}"と書く方が分かりやすい。
(5)
コントローラ処理のTraceログを出力するインターセプタを設定する。
/resources配下を除く任意のパスに適用されるように設定する。
(6)
ViewResolverの設定を行う。
画面のレンダリングをThymeleafに委譲し、forceContentType属性によりcontentType属性に指定したコンテンツタイプ(text/html;charset=UTF-8)をレスポンスに設定している。
(7)
TemplateResolverの設定を行う。
この設定により、例えばコントローラからview名としてhelloが返却された場合には/WEB-INF/views/hello.htmlがテンプレートとして処理される。

Note

JPA用のブランクプロジェクトを作成した場合は、addInterceptorsの定義として、OpenEntityManagerInViewInterceptorの定義が有効な状態となっている。

@Override
protected void addInterceptors(InterceptorRegistry registry) {
    addInterceptor(registry, traceLoggingInterceptor());
    addInterceptor(registry, transactionTokenInterceptor());
    addInterceptor(registry, codeListInterceptor());
    addWebRequestInterceptor(registry, openEntityManagerInViewInterceptor());
}

@Bean
public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
    return new OpenEntityManagerInViewInterceptor();
}

OpenEntityManagerInViewInterceptorは、EntityManagerのライフサイクルの開始と終了を行うInterceptorである。この設定を追加することで、アプリケーション層(Controllerや、Viewクラス)でのLazy Loadが、サポートされる。


11.2.8.1.2.7. spring-security

SpringSecurityConfig.javaには、Spring Securityに関する定義を行う。

作成したブランクプロジェクトのsrc/main/java/com/example/todo/config/web/SpringSecurityConfig.javaは、以下のような設定となっている。
なお、本チュートリアルではSpring Securityの設定ファイルの説明は割愛する。Spring Securityの設定ファイルについては、「Spring Securityチュートリアル」を参照されたい。
package com.example.todo.config.web;

import java.util.LinkedHashMap;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter;

/**
 * Bean definition to configure SpringSecurity.
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {

    /**
     * Configure ignore security pattern.
     * @return Bean of configured {@link WebSecurityCustomizer}
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().requestMatchers(
                new AntPathRequestMatcher("/resources/**"));
    }

    /**
     * Configure {@link SecurityFilterChain} bean.
     * @param http Builder class for setting up authentication and authorization
     * @return Bean of configured {@link SecurityFilterChain}
     * @throws Exception Exception that occurs when setting HttpSecurity
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.formLogin(Customizer.withDefaults());
        http.logout(Customizer.withDefaults());
        http.exceptionHandling(ex -> ex.accessDeniedHandler(
                accessDeniedHandler()));
        http.addFilterAfter(userIdMDCPutFilter(),
                AnonymousAuthenticationFilter.class);
        http.sessionManagement(Customizer.withDefaults());
        http.authorizeHttpRequests(authz -> authz.requestMatchers(
                new AntPathRequestMatcher("/**")).permitAll());

        return http.build();
    }

    /**
     * Configure {@link AccessDeniedHandler} bean.
     * @return Bean of configured {@link AccessDeniedHandler}
     */
    @Bean("accessDeniedHandler")
    public AccessDeniedHandler accessDeniedHandler() {
        LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> errorHandlers = new LinkedHashMap<>();

        // Invalid CSRF authenticator error handler
        AccessDeniedHandlerImpl invalidCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
        invalidCsrfTokenErrorHandler.setErrorPage(
                "/common/error/invalidCsrfTokenError");
        errorHandlers.put(InvalidCsrfTokenException.class,
                invalidCsrfTokenErrorHandler);

        // Missing CSRF authenticator error handler
        AccessDeniedHandlerImpl missingCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
        missingCsrfTokenErrorHandler.setErrorPage(
                "/common/error/missingCsrfTokenError");
        errorHandlers.put(MissingCsrfTokenException.class,
                missingCsrfTokenErrorHandler);

        // Default error handler
        AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl();
        defaultErrorHandler.setErrorPage("/common/error/accessDeniedError");

        return new DelegatingAccessDeniedHandler(errorHandlers, defaultErrorHandler);
    }

    /**
     * Configure {@link DefaultWebSecurityExpressionHandler} bean.
     * @return Bean of configured {@link DefaultWebSecurityExpressionHandler}
     */
    @Bean("webSecurityExpressionHandler")
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
        return new DefaultWebSecurityExpressionHandler();
    }

    /**
     * Configure {@link UserIdMDCPutFilter} bean.
     * @return Bean of configured {@link UserIdMDCPutFilter}
     */
    @Bean("userIdMDCPutFilter")
    public UserIdMDCPutFilter userIdMDCPutFilter() {
        return new UserIdMDCPutFilter();
    }
}

11.2.8.1.3. logback.xml

logback.xmlには、ログ出力に関する定義を行う。

作成したブランクプロジェクトのsrc/main/resources/logback.xmlは、以下のような設定となっている。
なお、チュートリアルで使用しないログ設定についての説明は割愛する。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration>
<configuration>

    <!-- (1) -->
    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder>
            <pattern><![CDATA[date:%d{yyyy-MM-dd HH:mm:ss}\tthread:%thread\tX-Track:%X{X-Track}\tlevel:%-5level\tlogger:%-48logger{48}\tmessage:%replace(%msg){'(\r\n|\r|\n)','$1  '}%n%replace(%replace(%xEx){'(\r\n|\r|\n)','$1  '}){'  $',''}%nopex]]></pattern>
        </encoder>
    </appender>

    <appender name="APPLICATION_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${app.log.dir:-log}/todo-application.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${app.log.dir:-log}/todo-application-%d{yyyyMMdd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern><![CDATA[date:%d{yyyy-MM-dd HH:mm:ss}\tthread:%thread\tX-Track:%X{X-Track}\tlevel:%-5level\tlogger:%-48logger{48}\tmessage:%replace(%msg){'(\r\n|\r|\n)','$1  '}%n%replace(%replace(%xEx){'(\r\n|\r|\n)','$1  '}){'  $',''}%nopex]]></pattern>
        </encoder>
    </appender>

    <appender name="MONITORING_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>${app.log.dir:-log}/todo-monitoring.log</file>
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <fileNamePattern>${app.log.dir:-log}/todo-monitoring-%d{yyyyMMdd}.log</fileNamePattern>
            <maxHistory>7</maxHistory>
        </rollingPolicy>
        <encoder>
            <charset>UTF-8</charset>
            <pattern><![CDATA[date:%d{yyyy-MM-dd HH:mm:ss}\tX-Track:%X{X-Track}\tlevel:%-5level\tmessage:%replace(%msg){'(\r\n|\r|\n)','$1  '}%n%replace(%replace(%xEx){'(\r\n|\r|\n)','$1  '}){'  $',''}%nopex]]></pattern>
        </encoder>
    </appender>

    <!-- Application Loggers -->
    <!-- (2) -->
    <logger name="com.example.todo">
        <level value="debug" />
    </logger>

    <logger name="com.example.todo.domain.repository">
        <level value="trace" />
    </logger>

    <!-- TERASOLUNA -->
    <logger name="org.terasoluna.gfw">
        <level value="info" />
    </logger>
    <!-- (3) -->
    <logger name="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor">
        <level value="trace" />
    </logger>
    <logger name="org.terasoluna.gfw.common.exception.ExceptionLogger">
        <level value="info" />
    </logger>
    <logger name="org.terasoluna.gfw.common.exception.ExceptionLogger.Monitoring" additivity="false">
        <level value="error" />
        <appender-ref ref="MONITORING_LOG_FILE" />
    </logger>

    <!-- 3rdparty Loggers -->
    <logger name="org.springframework">
        <level value="warn" />
    </logger>

    <logger name="org.springframework.web.servlet">
        <level value="info" />
    </logger>

    <logger name="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping">
        <level value="trace" />
    </logger>

    <logger name="org.springframework.jdbc.core.JdbcTemplate">
        <level value="trace" />
    </logger>

    <!--  REMOVE THIS LINE IF YOU USE JPA
    <logger name="org.hibernate.SQL" additivity="true">
        <level value="debug" />
    </logger>
    <logger name="org.hibernate.type">
        <level value="trace" />
    </logger>
    <logger name="org.hibernate.engine.transaction">
        <level value="debug" />
    </logger>
          REMOVE THIS LINE IF YOU USE JPA  -->
    <!--  REMOVE THIS LINE IF YOU USE MyBatis3
    <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <level value="debug" />
    </logger>
          REMOVE THIS LINE IF YOU USE MyBatis3  -->

    <root level="warn">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="APPLICATION_LOG_FILE" />
    </root>

</configuration>

項番

説明

(1)
標準出力でログを出力するアペンダを設定。
(2)
com.example.todoパッケージ以下はdebugレベル以上を出力するように設定。
(3)
spring-mvc.xmlまたはSpringMvcConfig.javaに設定したTraceLoggingInterceptorに出力されるようにtraceレベルで設定。

Note

O/R Mapperを使用するブランクプロジェクトを作成した場合は、それぞれのO/R Mapperのログを出力するロガーが有効な状態となっている。

  • JPA用のブランクプロジェクト

    <logger name="org.hibernate.SQL" additivity="true">
        <level value="debug" />
    </logger>
    <logger name="org.hibernate.type">
        <level value="trace" />
    </logger>
    <logger name="org.hibernate.engine.transaction">
        <level value="debug" />
    </logger>
    
  • MyBatis3用のブランクプロジェクト

    <logger name="com.example.todo">
        <level value="debug" />
    </logger>
    
    <logger name="com.example.todo.domain.repository">
        <level value="trace" />
    </logger>
    
    <!-- omitted -->
    
    <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <level value="debug" />
    </logger>