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

目次


11.1.1. はじめに

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

  • TERASOLUNA Server Framework for Java (5.x)による基本的なアプリケーションの開発方法
  • MavenおよびSTS(Eclipse)プロジェクトの構築方法
  • TERASOLUNA Server Framework for Java (5.x)のアプリケーションのレイヤ化に従った開発方法

11.1.1.2. 対象読者

  • SpringのDIやAOPに関する基礎的な知識がある
  • Servlet/JSPを使用してWebアプリケーションを開発したことがある
  • SQLに関する知識がある

11.1.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.7
Web Browser Google Chrome 108

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

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

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

../_images/image001.png

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

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

ルールID 説明
B01 未完了のTODOは5件までしか登録できない
B02 完了済みのTODOは完了できない

Note

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


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

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

../_images/image002.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.1.2.3.1. Show all TODO

  • TODOを全件表示する
  • 未完了のTODOに対しては「Finish」と「Delete」用のボタンが付く
  • 完了のTODOは打ち消し線で装飾する
  • TODOの件名のみ表示する

11.1.2.3.2. Create TODO

  • フォームから送信されたTODOを保存する
  • TODOの件名は1文字以上30文字以下であること
  • アプリケーションの業務要件のB01を満たさない場合はエラーコードE001でビジネス例外をスローする
  • 処理が成功した場合は、遷移先の画面で「Created successfully!」を表示する

11.1.2.3.3. Finish TODO

  • フォームから送信されたtodoIdに対応するTODOを完了済みにする
  • 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする
  • アプリケーションの業務要件のB02を満たさない場合はエラーコードE002でビジネス例外をスローする
  • 処理が成功した場合は、遷移先の画面で「Finished successfully!」を表示する

11.1.2.3.4. Delete TODO

  • フォームから送信されたtodoIdに対応するTODOを削除する
  • 該当するTODOが存在しない場合はエラーコードE404でリソース未検出例外をスローする
  • 処理が成功した場合は、遷移先の画面で「Deleted successfully!」を表示する

11.1.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.1.3. 環境構築

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

  • データベースを使用せずjava.util.Mapを使ったインメモリ実装のRepositoryImpl
  • MyBatis3を使用してデータベースにアクセスするRepositoryImpl
  • Spring Data JPAを使用してデータベースにアクセスするRepositoryImpl

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

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


11.1.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-archetype\
 -DarchetypeVersion=5.8.1.RELEASE\
 -DgroupId=com.example.todo\
 -DartifactId=todo\
 -Dversion=1.0.0-SNAPSHOT

11.1.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-archetype^
 -DarchetypeVersion=5.8.1.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT

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

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

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

11.1.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-jpa-archetype^
 -DarchetypeVersion=5.8.1.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT

11.1.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-project.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

この状態は、Dynamic Web Moduleのバージョンをあげることで解消できる。

プロジェクト名を右クリックし、「Properties」を選択 ->「Project Facets」の「Dynamic Web Module」を5.0へ変更することで解決する。

../_images/change-DynamicWebModule.png

Tip

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

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

../_images/presentation-hierarchical.png

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

../_images/presentation-hierarchical-view.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.1.3.3. プロジェクトの構成

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

Note

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

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

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


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

src
  └main
      ├java
      │  └com
      │    └example
      │      └todo
      │        ├ app ... (1)
      │        │   └todo
      │        └domain ... (2)
      │            ├model ... (3)
      │            ├repository ... (4)
      │            │   └todo
      │            └service ... (5)
      │                └todo
      ├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)
jspを格納するディレクトリ。

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

src
  └main
      ├java
      │  └com
      │    └example
      │      └todo
      │        ├ app
      │        │   └todo
      │        └domain
      │            ├model
      │            ├repository
      │            │   └todo
      │            └service
      │                └todo
      ├resources
      │  ├META-INF
      │  │  ├mybatis ... (9)
      │  │  └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.1.3.4. 設定ファイルの確認

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

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

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

Note

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


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

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

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

ブランクプロジェクトから提供されている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の設定により、WEB-INF/views/welcome/home.jspが呼び出される。

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

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Home</title>
<link rel="stylesheet" href="${pageContext.request.contextPath}/resources/app/css/styles.css">
</head>
<body>
    <div id="wrapper">
        <h1 id="title">Hello world!</h1>
        <!-- (7) -->
        <p>The time on the server is ${serverTime}.</p>
    </div>
</body>
</html>
項番 説明
(7)
ControllerでModelに設定したserverTimeを表示する。
ここでは、XSS対策を行っていないが、ユーザの入力値を表示する場合は、f:h()関数を用いて、必ずXSS対策を行うこと。

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

../_images/image031.jpg

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

../_images/image032.jpg

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

../_images/image033.jpg

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

 date:2022-11-25 17:22:36     thread:main     X-Track:        level:INFO      logger:o.springframework.web.servlet.DispatcherServlet  message:Initializing Servlet 'appServlet'
 date:2022-11-25 17:22:37     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-11-25 17:22:37     thread:main     X-Track:        level:DEBUG     logger:o.s.w.s.m.m.a.RequestMappingHandlerMapping       message:1 mappings in 'requestMappingHandlerMapping'
 date:2022-11-25 17:22:37     thread:main     X-Track:        level:INFO      logger:o.springframework.web.servlet.DispatcherServlet  message:Completed initialization in 753 ms

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

../_images/image034.png

コンソールを見ると、

  • 共通ライブラリから提供しているTraceLoggingInterceptorのTRACEログ
  • Controllerで実装したINFOログ

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

date:2022-11-25 17:24:47      thread:http-nio-8080-exec-4     X-Track:90843a0f267a4a85b70232ed9884d969        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[START CONTROLLER] HelloController.home(Locale,Model)
date:2022-11-25 17:24:47      thread:http-nio-8080-exec-4     X-Track:90843a0f267a4a85b70232ed9884d969        level:INFO      logger:com.example.todo.app.welcome.HelloController     message:Welcome home! The client locale is ja.
date:2022-11-25 17:24:47      thread:http-nio-8080-exec-4     X-Track:90843a0f267a4a85b70232ed9884d969        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[END CONTROLLER  ] HelloController.home(Locale,Model)-> view=welcome/home, model={serverTime=2022年11月25日 17:24:47 JST}
date:2022-11-25 17:24:47      thread:http-nio-8080-exec-4     X-Track:90843a0f267a4a85b70232ed9884d969        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[HANDLING TIME   ] HelloController.home(Locale,Model)-> 51,864,000 ns

Note

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


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

Todoアプリケーションを作成する。作成する順は、以下の通りである。
  • ドメイン層(+ インフラストラクチャ層)
    • Domain Object作成
    • Repository作成
    • RepositoryImpl作成
    • Service作成
  • アプリケーション層
    • Controller作成
    • Form作成
    • View作成

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

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

11.1.4.1. ドメイン層の作成

11.1.4.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/image057.png

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

../_images/image058.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 Getter and Setters…」を選択する。

../_images/image059.png

serialVersionUID以外を選択して「OK」

../_images/image060.png

11.1.4.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/image061.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.1.4.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/image062.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.1.4.1.4. Serviceの作成

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

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

項番 項目 入力値
1 Package com.example.todo.domain.service.todo
2 Name TodoService

を入力して「Finish」する。

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

../_images/image063.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/image064.png
package com.example.todo.domain.service.todo;

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

import jakarta.inject.Inject;

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;

@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.1.4.2. アプリケーション層の作成

ドメイン層の実装が完了したので、次はドメイン層を利用して、アプリケーション層の作成に取り掛かる。

11.1.4.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/image065.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.1.4.2.2. Show all TODOの実装

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

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

を行う。

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


11.1.4.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/image066.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.1.4.2.2.2. Controllerの実装

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

package com.example.todo.app.todo;

import java.util.Collection;

import jakarta.inject.Inject;

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;

@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に定義したViewResolverによって、WEB-INF/views/todo/list.jspがレンダリングされることになる。

Note

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

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


11.1.4.2.2.3. JSPの作成

JSPを作成し、Controllerから渡されたModelを表示する。

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.jsp

を入力して「Finish」する。

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

../_images/create-list-jsp.png

まず、TODOの全件表示を行うために必要なJSPの実装を行う。

<!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;
}
</style>
</head>
<body>
    <h1>Todo List</h1>
    <hr />
    <div id="todoList">
        <ul>
            <!-- (1) -->
            <c:forEach items="${todos}" var="todo">
                <li><c:choose>
                        <c:when test="${todo.finished}"><!-- (2) -->
                            <span class="strike">
                            <!-- (3) -->
                            ${f:h(todo.todoTitle)}
                            </span>
                        </c:when>
                        <c:otherwise>
                            ${f:h(todo.todoTitle)}
                         </c:otherwise>
                    </c:choose></li>
            </c:forEach>
        </ul>
    </div>
</body>
</html>
項番 説明
(1)
<c:forEach>タグを用いて、Todoのリストを全て表示する。
(2)
完了かどうか(finished)で、打ち消し線(text-decoration: line-through;)を装飾するかどうかを判断する。
(3)
文字列値を出力する際は、XSS対策のため、必ずf:h()関数を使用してHTMLエスケープを行うこと。
XSS対策についての詳細は、XSS対策を参照されたい。

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

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-note.png

11.1.4.2.3. Create TODOの実装

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

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


11.1.4.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-mapper.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/mvnBuild1.png

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


11.1.4.2.3.2. Controllerの修正

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

package com.example.todo.app.todo;

import java.util.Collection;

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

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;

@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.1.4.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.1.4.2.3.4. JSPの修正

以下を表示するために必要なJSPの実装を追加する。

  • TODOの入力フォーム
  • 「Create Todo」ボタン
  • 入力チェックエラーを表示するエリア
  • 結果メッセージを表示するエリア
<!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;
}
</style>
</head>
<body>
    <h1>Todo List</h1>
    <div id="todoForm">
        <!-- (1) -->
        <t:messagesPanel />

        <!-- (2) -->
        <form:form
           action="${pageContext.request.contextPath}/todo/create"
            method="post" modelAttribute="todoForm">
            <form:input path="todoTitle" /><!-- (3) -->
            <form:errors path="todoTitle" /><!-- (4) -->
            <form:button>Create Todo</form:button>
        </form:form>
    </div>
    <hr />
    <div id="todoList">
        <ul>
            <c:forEach items="${todos}" var="todo">
                <li><c:choose>
                        <c:when test="${todo.finished}">
                            <span class="strike">
                            ${f:h(todo.todoTitle)}
                            </span>
                        </c:when>
                        <c:otherwise>
                            ${f:h(todo.todoTitle)}
                         </c:otherwise>
                    </c:choose></li>
            </c:forEach>
        </ul>
    </div>
</body>
</html>
項番 説明
(1)
<t:messagesPanel>タグで、結果メッセージを表示する。
(2)
新規作成処理用のformを表示する。
formを表示するために、<form:form>タグを使用する。
modelAttribute属性には、ControllerでModelに追加したFormの名前を指定する。
action属性には新規作成処理を実行するためのURL(<contextPath>/todo/create)を指定する。
新規作成処理は更新系の処理なので、method属性にはPOSTメソッドを指定する。

action属性に指定する<contextPath>は、${pageContext.request.contextPath}で取得することができる。
(3)
<form:input>タグでフォームのプロパティをバインドする。
modelAttribute属性に指定したFormのプロパティ名と、path属性の値が一致している必要がある。
(4)
<form:errors>タグで、入力エラーがあった場合に表示する。path属性の値は、<form:input>タグと合わせる。

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

../_images/image068.png
../_images/image069.png

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

../_images/image070.png

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

../_images/image071.png

11.1.4.2.3.5. メッセージ表示のカスタマイズ

<t:messagesPanel>を使用した場合、以下のようなHTMLが出力される。

<div class="alert alert-success"><ul><li>Created successfully!</li></ul></div>

スタイルシート(list.jsp<style>タグ内)に、以下の修正を加えて、結果メッセージの見た目をカスタマイズする。

.alert {
    border: 1px solid;
}

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

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

メッセージは、以下のように装飾される。

../_images/image072.png
../_images/image073.png

また、<form:errors>タグのcssClass属性で、入力エラーメッセージのclassを指定できる。

JSPを次のように修正し、

<form:errors path="todoTitle" cssClass="text-error" />

スタイルシートに、以下を追加する。

.text-error {
    color: #c60f13;
}

入力エラー時のメッセージは、以下のように装飾される。

../_images/image074.png

11.1.4.2.4. Finish TODOの実装

一覧画面に「Finish」ボタンを追加し、TODOを完了させるための処理を追加する。


11.1.4.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.1.4.2.4.2. Controllerの修正

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

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

package com.example.todo.app.todo;

import java.util.Collection;

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

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;

@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, // (1)
            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, TodoFinish.class }) TodoForm form, // (3)
            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.1.4.2.4.3. JSPの修正

完了処理用のformを追加する。

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

.inline {
    display: inline-block;
}

.alert {
    border: 1px solid;
}

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

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

.text-error {
    color: #c60f13;
}
</style>
<body>
    <h1>Todo List</h1>

    <div id="todoForm">
        <t:messagesPanel />

        <form:form
            action="${pageContext.request.contextPath}/todo/create"
            method="post" modelAttribute="todoForm">
            <form:input path="todoTitle" />
            <form:errors path="todoTitle" cssClass="text-error" />
            <form:button>Create Todo</form:button>
        </form:form>
    </div>
    <hr />
    <div id="todoList">
        <ul>
            <c:forEach items="${todos}" var="todo">
                <li><c:choose>
                        <c:when test="${todo.finished}">
                            <span class="strike">${f:h(todo.todoTitle)}</span>
                        </c:when>
                        <c:otherwise>
                            ${f:h(todo.todoTitle)}
                            <!-- (1) -->
                            <form:form
                                action="${pageContext.request.contextPath}/todo/finish"
                                method="post"
                                modelAttribute="todoForm"
                                cssClass="inline">
                                <!-- (2) -->
                                <form:hidden path="todoId"
                                    value="${f:h(todo.todoId)}" />
                                <form:button>Finish</form:button>
                            </form:form>
                        </c:otherwise>
                    </c:choose></li>
            </c:forEach>
        </ul>
    </div>
</body>
</html>
項番 説明
(1)
TODOが未完了の場合は、TODOを完了させるためのリクエストを送信するformを表示する。
action属性には完了処理を実行するためのURL(<contextPath>/todo/finish)を指定する。
完了処理は更新系の処理なので、method属性にはPOSTメソッドを指定する。
なお、「Finish」ボタンをインラインブロック要素(display: inline-block;)としてTODOの横に表示させている。
(2)
<form:hidden>タグを使用して、リクエストパラメータとしてtodoIdを送信する。
value属性に値を設定する場合も、必ずf:h()関数でHTMLエスケープすること。

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

../_images/image075.png
../_images/image076.png

11.1.4.2.5. Delete TODOの実装

一覧表示画面に「Delete」ボタンを追加して、TODOを削除するための処理を追加する。


11.1.4.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.1.4.2.5.2. Controllerの修正

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

package com.example.todo.app.todo;

import java.util.Collection;

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

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.TodoDelete;
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;

@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.1.4.2.5.3. JSPの修正

削除処理用のformを追加する。

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

.inline {
    display: inline-block;
}

.alert {
    border: 1px solid;
}

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

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

.text-error {
    color: #c60f13;
}
</style>
<body>
    <h1>Todo List</h1>

    <div id="todoForm">
        <t:messagesPanel />

        <form:form
            action="${pageContext.request.contextPath}/todo/create"
            method="post" modelAttribute="todoForm">
            <form:input path="todoTitle" />
            <form:errors path="todoTitle" cssClass="text-error" />
            <form:button>Create Todo</form:button>
        </form:form>
    </div>
    <hr />
    <div id="todoList">
        <ul>
            <c:forEach items="${todos}" var="todo">
                <li><c:choose>
                        <c:when test="${todo.finished}">
                            <span class="strike">${f:h(todo.todoTitle)}</span>
                        </c:when>
                        <c:otherwise>
                            ${f:h(todo.todoTitle)}
                            <form:form
                                action="${pageContext.request.contextPath}/todo/finish"
                                method="post"
                                modelAttribute="todoForm"
                                cssClass="inline">
                                <form:hidden path="todoId"
                                    value="${f:h(todo.todoId)}" />
                                <form:button>Finish</form:button>
                            </form:form>
                        </c:otherwise>
                    </c:choose>
                    <!-- (1) -->
                    <form:form
                        action="${pageContext.request.contextPath}/todo/delete"
                        method="post" modelAttribute="todoForm"
                        cssClass="inline">
                        <!-- (2) -->
                        <form:hidden path="todoId"
                            value="${f:h(todo.todoId)}" />
                        <form:button>Delete</form:button>
                    </form:form>
                </li>
            </c:forEach>
        </ul>
    </div>
</body>
</html>
項番 説明
(1)
削除処理用のformを表示する。
action属性には削除処理を実行するためのURL(<contextPath>/todo/delete)を指定する。
削除処理は更新系の処理なので、method属性にはPOSTメソッドを指定する。
(2)
<form:hidden>タグを使用して、リクエストパラメータとしてtodoIdを送信する。
value属性に値を設定する場合も、必ずf:h()関数でHTMLエスケープすること。

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

../_images/image077.png
../_images/image078.png

11.1.4.2.6. CSSファイルの使用

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

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

ブランクプロジェクトから提供しているCSSファイル(src/main/webapp/resources/app/css/styles.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;
}

JSPから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="${pageContext.request.contextPath}/resources/app/css/styles.css" type="text/css">
</head>
<body>
    <h1>Todo List</h1>

    <div id="todoForm">
        <t:messagesPanel />

        <form:form
            action="${pageContext.request.contextPath}/todo/create"
            method="post" modelAttribute="todoForm">
            <form:input path="todoTitle" />
            <form:errors path="todoTitle" cssClass="text-error" />
            <form:button>Create Todo</form:button>
        </form:form>
    </div>
    <hr />
    <div id="todoList">
        <ul>
            <c:forEach items="${todos}" var="todo">
                <li><c:choose>
                        <c:when test="${todo.finished}">
                            <span class="strike">${f:h(todo.todoTitle)}</span>
                        </c:when>
                        <c:otherwise>
                            ${f:h(todo.todoTitle)}
                            <form:form
                                action="${pageContext.request.contextPath}/todo/finish"
                                method="post"
                                modelAttribute="todoForm"
                                cssClass="inline">
                                <form:hidden path="todoId"
                                    value="${f:h(todo.todoId)}" />
                                <form:button>Finish</form:button>
                            </form:form>
                        </c:otherwise>
                    </c:choose>
                    <form:form
                        action="${pageContext.request.contextPath}/todo/delete"
                        method="post" modelAttribute="todoForm"
                        cssClass="inline">
                        <form:hidden path="todoId"
                            value="${f:h(todo.todoId)}" />
                        <form:button>Delete</form:button>
                    </form:form>
                </li>
            </c:forEach>
        </ul>
    </div>
</body>
</html>
項番 説明
(1)
JSPファイルからスタイルシートの定義を削除し、代わりにスタイルシートを定義したCSSファイルを読み込む。

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

../_images/list-screen-css.png

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

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

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

  • MyBatis3
  • Spring Data JPA

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

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

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

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

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


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

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

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


11.1.5.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.1.5.3. MyBatis3を使用したインフラストラクチャ層の作成

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

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


11.1.5.3.1. TodoRepositoryの作成

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

11.1.5.3.2. TodoRepositoryImplの作成

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

11.1.5.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-mybatis3.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-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[START CONTROLLER] TodoController.list(Model)
date:2022-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        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-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Acquired Connection [2013925503, URL=jdbc:h2:mem:todo-jsp-mybatis3, H2 JDBC Driver] for JDBC transaction
date:2022-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        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-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        level:DEBUG     logger:c.e.t.d.repository.todo.TodoRepository.findAll   message:==> Parameters:
date:2022-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        level:DEBUG     logger:c.e.t.d.repository.todo.TodoRepository.findAll   message:<==      Total: 0
date:2022-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Initiating transaction commit
date:2022-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Committing JDBC transaction on Connection [2013925503, URL=jdbc:h2:mem:todo-jsp-mybatis3, H2 JDBC Driver]
date:2022-11-29 17:51:09      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        level:DEBUG     logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Releasing JDBC Connection [2013925503, URL=jdbc:h2:mem:todo-jsp-mybatis3, H2 JDBC Driver] after transaction
date:2022-11-29 17:51:10      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        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@78becf2c, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
date:2022-11-29 17:51:10      thread:http-nio-8080-exec-3     X-Track:9eef87761a264278a38d4c2e14a99959        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[HANDLING TIME   ] TodoController.list(Model)-> 165,863,500 ns

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

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


11.1.5.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.1.5.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.1.5.4.3. TodoRepositoryImplの作成

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


以上で、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.1.6. おわりに

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

  • TERASOLUNA Server Framework for Java (5.x)による基本的なアプリケーションの開発方法
  • MavenおよびSTS(Eclipse)プロジェクトの構築方法
  • TERASOLUNA Server Framework for Java (5.x)のアプリケーションのレイヤ化に従った開発方法
    • POJO(+ Spring)を使用したドメイン層の実装
    • POJO(+ Spring MVC)とJSPタグライブラリを使用したアプリケーション層の実装
    • MyBatis3を使用したインフラストラクチャ層の実装
    • Spring Data JPAを使用したインフラストラクチャ層の実装
    • O/R Mapperを使用しないインフラストラクチャ層の実装

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


11.1.7. Appendix

11.1.7.1. 設定ファイルの解説

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

11.1.7.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>

    <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>
            classpath*:META-INF/spring/applicationContext.xml
            classpath*:META-INF/spring/spring-security.xml
        </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>contextConfigLocation</param-name>
            <!-- ApplicationContext for Spring MVC -->
            <param-value>classpath*:META-INF/spring/spring-mvc.xml</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) -->
    <jsp-config>
        <jsp-property-group>
            <url-pattern>*.jsp</url-pattern>
            <el-ignored>false</el-ignored>
            <page-encoding>UTF-8</page-encoding>
            <scripting-invalid>false</scripting-invalid>
            <include-prelude>/WEB-INF/views/common/include.jsp</include-prelude>
        </jsp-property-group>
    </jsp-config>

    <!-- (6) -->
    <error-page>
        <error-code>500</error-code>
        <location>/WEB-INF/views/common/error/systemError.jsp</location>
    </error-page>

    <error-page>
        <error-code>404</error-code>
        <location>/WEB-INF/views/common/error/resourceNotFoundError.jsp</location>
    </error-page>

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

    <!-- (7) -->
    <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)
JSPの共通定義。

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

  • JSP内でEL式が使用可能な状態
  • JSPのページエンコーディングとしてUTF-8
  • JSP内でスクリプティングが使用可能な状態
  • 各JSPの先頭でインクルードするJSPとして、/WEB-INF/views/common/include.jsp

が設定済みである。

(6)
エラーページの定義。

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

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

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

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

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

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

が定義済みである。


11.1.7.1.2. インクルードJSP

インクルードJSPには、全てのJSPに適用するJSPの設定や、タグライブラリの設定を行う。

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

<!-- (1) -->
<%@ page session="false"%>
<!-- (2) -->
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!-- (3) -->
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<!-- (4) -->
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%>
<!-- (5) -->
<%@ taglib uri="http://terasoluna.org/tags" prefix="t"%>
<%@ taglib uri="http://terasoluna.org/functions" prefix="f"%>
項番 説明
(1)
JSP実行時にセッションを作成しないようにするための定義。
(2)
標準タグライブラリの定義。
(3)
Spring MVC用タグライブラリの定義。
(4)
Spring Security用タグライブラリの定義(本チュートリアルでは使用しない。)
(5)
共通ライブラリで提供されている、EL関数、タグライブラリの定義。

11.1.7.1.3. Bean定義ファイル

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

  • src/main/resources/META-INF/spring/applicationContext.xml
  • src/main/resources/META-INF/spring/todo-domain.xml
  • src/main/resources/META-INF/spring/todo-infra.xml
  • src/main/resources/META-INF/spring/todo-infra.properties
  • src/main/resources/META-INF/spring/todo-env.xml
  • src/main/resources/META-INF/spring/spring-mvc.xml
  • src/main/resources/META-INF/spring/spring-security.xml

Note

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

Note

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

これは、どこに何が定義されているか想像しやすく、メンテナンス性が向上するからである。

今回のチュートリアルのような小さなアプリケーションでは効果はないが、アプリケーションの規模が大きくなるにつれ、効果が大きくなる。


11.1.7.1.3.1. applicationContext.xml

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

作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/applicationContext.xml は、以下のような設定となっている。
なお、チュートリアルで使用しないコンポーネントについての説明は割愛する。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
    ">

    <!-- (1) -->
    <import resource="classpath:/META-INF/spring/todo-domain.xml" />

    <bean id="passwordEncoder" class="org.springframework.security.crypto.password.DelegatingPasswordEncoder">
        <constructor-arg name="idForEncode" value="pbkdf2@SpringSecurity_v5_8" />
        <constructor-arg name="idToPasswordEncoder">
            <map>
                <entry key="pbkdf2@SpringSecurity_v5_8">
                    <bean class="org.springframework.security.crypto.password.Pbkdf2PasswordEncoder" factory-method="defaultsForSpringSecurity_v5_8" />
                </entry>
                <entry key="bcrypt">
                    <bean class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder" />
                </entry>
                <!-- When using commented out PasswordEncoders, you need to add bcprov-jdk18on.jar to the dependency.
                <entry key="argon2@SpringSecurity_v5_8">
                    <bean class="org.springframework.security.crypto.argon2.Argon2PasswordEncoder" factory-method="defaultsForSpringSecurity_v5_8" />
                </entry>
                <entry key="scrypt@SpringSecurity_v5_8">
                    <bean class="org.springframework.security.crypto.scrypt.SCryptPasswordEncoder" factory-method="defaultsForSpringSecurity_v5_8" />
                </entry>
                -->
            </map>
        </constructor-arg>
    </bean>

    <!-- (2) -->
    <context:property-placeholder
        location="classpath*:/META-INF/spring/*.properties" />

    <!-- Message -->
    <bean id="messageSource"
        class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>i18n/application-messages</value>
            </list>
        </property>
    </bean>

    <!-- Exception Code Resolver. -->
    <bean id="exceptionCodeResolver"
        class="org.terasoluna.gfw.common.exception.SimpleMappingExceptionCodeResolver">
        <!-- Setting and Customization by project. -->
        <property name="exceptionMappings">
            <map>
                <entry key="ResourceNotFoundException" value="e.xx.fw.5001" />
                <entry key="InvalidTransactionTokenException" value="e.xx.fw.7001" />
                <entry key="BusinessException" value="e.xx.fw.8001" />
                <entry key=".DataAccessException" value="e.xx.fw.9002" />
            </map>
        </property>
        <property name="defaultExceptionCode" value="e.xx.fw.9001" />
    </bean>

    <!-- Exception Logger. -->
    <bean id="exceptionLogger"
        class="org.terasoluna.gfw.common.exception.ExceptionLogger">
        <property name="exceptionCodeResolver" ref="exceptionCodeResolver" />
    </bean>

    <!-- Filter. -->
    <bean id="exceptionLoggingFilter"
        class="org.terasoluna.gfw.web.exception.ExceptionLoggingFilter" >
        <property name="exceptionLogger" ref="exceptionLogger" />
    </bean>

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

Tip

エディタの「Configure Namespaces」タブにて、以下のようにチェックを入れると、チェックしたXMLスキーマが有効になり、XML編集時にCtrl+Spaceを使用して入力を補完することができる。

「Namespace Versions」にはバージョンなしのxsdファイルを選択することを推奨する。

バージョンなしのxsdファイルを選択することで、常にjarに含まれる最新のxsdが使用されるため、Springのバージョンアップを意識する必要がなくなる。

../_images/image021.jpg
../_images/image023.png

11.1.7.1.3.2. todo-domain.xml

todo-domain.xmlには、Todoアプリのドメイン層に関わる設定を行う。

作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/todo-domain.xmlは、以下のような設定となっている。
なお、チュートリアルで使用しないコンポーネントについての説明は割愛する。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
    ">

    <!-- (1) -->
    <import resource="classpath:META-INF/spring/todo-infra.xml" />
    <import resource="classpath*:META-INF/spring/**/*-codelist.xml" />

    <!-- (2) -->
    <context:component-scan base-package="com.example.todo.domain" />

    <!-- AOP. -->
    <bean id="resultMessagesLoggingInterceptor"
        class="org.terasoluna.gfw.common.exception.ResultMessagesLoggingInterceptor">
        <property name="exceptionLogger" ref="exceptionLogger" />
    </bean>
    <aop:config>
        <aop:advisor advice-ref="resultMessagesLoggingInterceptor"
            pointcut="@within(org.springframework.stereotype.Service)" />
    </aop:config>

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

Note

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

<tx:annotation-driven />

11.1.7.1.3.3. todo-infra.xml

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

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

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

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

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
    ">

</beans>

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

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:mybatis="http://mybatis.org/schema/mybatis-spring"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://mybatis.org/schema/mybatis-spring http://mybatis.org/schema/mybatis-spring.xsd
    ">

    <!-- (1) -->
    <import resource="classpath:/META-INF/spring/todo-env.xml" />

    <!-- (2) -->
    <!-- define the SqlSessionFactory -->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!-- (3) -->
        <property name="dataSource" ref="dataSource" />
        <!-- (4) -->
        <property name="configLocation" value="classpath:/META-INF/mybatis/mybatis-config.xml" />
    </bean>

    <!-- (5) -->
    <!-- scan for Mappers -->
    <mybatis:scan base-package="com.example.todo.domain.repository" />

</beans>
項番 説明
(1)
環境依存するコンポーネント(データソースやトランザクションマネージャなど)を定義するBean定義ファイルをimportする。
(2)
SqlSessionFactoryを生成するためのコンポーネントとして、SqlSessionFactoryBeanをbean定義する。
(3)
dataSourceプロパティに、設定済みのデータソースのbeanを指定する。

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

ここで指定したファイルはSqlSessionFactoryを生成する時に読み込まれる。
(5)
Mapperインタフェースをスキャンするために<mybatis:scan>を定義し、base-package属性には、
Mapperインタフェースが格納されている基底パッケージを指定する。

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

Note

mybatis-config.xmlは、MyBatis3自体の動作設定を行う設定ファイルである。

ブランクプロジェクトでは、デフォルトで以下の設定が行われている。

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

    <!-- See https://mybatis.org/mybatis-3/configuration.html#settings -->
    <settings>
        <setting name="mapUnderscoreToCamelCase" value="true" />
        <setting name="lazyLoadingEnabled" value="true" />
        <setting name="defaultFetchSize" value="100" />
<!--
        <setting name="defaultExecutorType" value="REUSE" />
        <setting name="jdbcTypeForNull" value="NULL" />
        <setting name="localCacheScope" value="STATEMENT" />
-->
    </settings>

    <typeAliases>
        <package name="com.example.todo.domain.model" />
        <package name="com.example.todo.domain.repository" />
<!--
        <package name="com.example.todo.infra.mybatis.typehandler" />
-->
    </typeAliases>

    <typeHandlers>
<!--
        <package name="com.example.todo.infra.mybatis.typehandler" />
-->
    </typeHandlers>

</configuration>

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

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

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jpa="http://www.springframework.org/schema/data/jpa"
    xmlns:util="http://www.springframework.org/schema/util"
    xsi:schemaLocation="
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/data/jpa https://www.springframework.org/schema/data/jpa/spring-jpa.xsd
    ">

    <!-- (1) -->
    <import resource="classpath:/META-INF/spring/todo-env.xml" />

    <!-- (2) -->
    <jpa:repositories base-package="com.example.todo.domain.repository"></jpa:repositories>

    <!-- (3) -->
    <bean id="jpaVendorAdapter"
        class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
        <property name="showSql" value="false" />
        <property name="database" value="${database}" />
    </bean>

    <!-- (4) -->
    <bean id="entityManagerFactory"
        class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
        <!-- (5) -->
        <property name="packagesToScan" value="com.example.todo.domain.model" />
        <property name="dataSource" ref="dataSource" />
        <property name="jpaVendorAdapter" ref="jpaVendorAdapter" />
        <!-- (6) -->
        <property name="jpaPropertyMap">
            <util:map>
                <entry key="hibernate.hbm2ddl.auto" value="" />
                <entry key="hibernate.ejb.naming_strategy"
                    value="org.hibernate.cfg.ImprovedNamingStrategy" />
                <entry key="hibernate.connection.charSet" value="UTF-8" />
                <entry key="hibernate.show_sql" value="false" />
                <entry key="hibernate.format_sql" value="false" />
                <entry key="hibernate.use_sql_comments" value="true" />
                <entry key="hibernate.jdbc.batch_size" value="30" />
                <entry key="hibernate.jdbc.fetch_size" value="100" />
            </util:map>
        </property>
    </bean>

</beans>
項番 説明
(1)
環境依存するコンポーネント(データソースやトランザクションマネージャなど)を定義するBean定義ファイルをimportする。
(2)
Spring Data JPAを使用して、Repositoryインタフェースから実装クラスを自動生成する。
<jpa:repository>タグのbase-package属性に、Repositoryを格納するパッケージを指定する。
(3)
JPAの実装ベンダの設定を行う。
JPA実装として、Hibernateを使うため、HibernateJpaVendorAdapterを定義している。
(4)
EntityManagerの定義を行う。
(5)
JPAのエンティティとして扱うクラスが格納されているパッケージ名を指定する。
(6)
Hibernateに関する詳細な設定を行う。

11.1.7.1.3.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から参照されている。


11.1.7.1.3.5. todo-env.xml

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

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

ここでは、MyBatis3用のブランクプロジェクトに格納されるファイルを例に説明する。
なお、データベースにアクセスしないブランクプロジェクトを作成した際は、todo-env.xmlは作成されない。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="
        http://www.springframework.org/schema/jdbc https://www.springframework.org/schema/jdbc/spring-jdbc.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
    ">

    <bean id="dateFactory" class="org.terasoluna.gfw.common.time.DefaultClockFactory" />

    <!-- (1) -->
    <bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource"
        destroy-method="close">
        <property name="driverClassName" value="${database.driverClassName}" />
        <property name="url" value="${database.url}" />
        <property name="username" value="${database.username}" />
        <property name="password" value="${database.password}" />
        <property name="defaultAutoCommit" value="false" />
        <property name="maxTotal" value="${cp.maxActive}" />
        <property name="maxIdle" value="${cp.maxIdle}" />
        <property name="minIdle" value="${cp.minIdle}" />
        <property name="maxWaitMillis" value="${cp.maxWait}" />
    </bean>


    <!-- (2) -->
    <jdbc:initialize-database data-source="dataSource"
        ignore-failures="ALL">
        <!-- (3) -->
        <jdbc:script location="classpath:/database/${database}-schema.sql" encoding="UTF-8" />
        <jdbc:script location="classpath:/database/${database}-dataload.sql" encoding="UTF-8" />
    </jdbc:initialize-database>

    <!--  REMOVE THIS LINE IF YOU USE JPA
    <bean id="transactionManager"
        class="org.springframework.orm.jpa.JpaTransactionManager">
        <property name="entityManagerFactory" ref="entityManagerFactory" />
    </bean>
          REMOVE THIS LINE IF YOU USE JPA  -->
    <!-- (4) -->
    <bean id="transactionManager"
        class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource" />
        <property name="rollbackOnCommitFailure" value="true" />
    </bean>
</beans>
項番 説明
(1)
実データソースの設定。
(2)
データベース初期化の設定。
データベースを初期化するSQLファイルを実行するための設定を行っている。

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

ブランクプロジェクトの設定ではtodo-infra.propertiesdatabase=H2と定義されているため、H2-schema.sql及びH2-dataload.sqlが実行される。
(4)
トランザクションマネージャの設定。
id属性には、transactionManagerを指定する。
別の名前を指定する場合は、<tx:annotation-driven>タグにもトランザクションマネージャ名を指定する必要がある。

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

Note

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

<bean id="transactionManager"
    class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

11.1.7.1.3.6. spring-mvc.xml

spring-mvc.xmlには、Spring MVCに関する定義を行う。

作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/spring-mvc.xmlは、以下のような設定となっている。
なお、チュートリアルで使用しないコンポーネントについての説明は割愛する。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:mvc="http://www.springframework.org/schema/mvc"
    xmlns:util="http://www.springframework.org/schema/util"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd
        http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd
        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd
    ">

    <!-- (1) -->
    <context:property-placeholder
        location="classpath*:/META-INF/spring/*.properties" />

    <!-- (2) -->
    <mvc:annotation-driven>
        <mvc:argument-resolvers>
            <bean
                class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" />
            <bean
                class="org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver" />
        </mvc:argument-resolvers>
    </mvc:annotation-driven>

    <mvc:default-servlet-handler />

    <!-- (3) -->
    <context:component-scan base-package="com.example.todo.app" />

    <!-- (4) -->
    <mvc:resources mapping="/resources/**"
        location="/resources/,classpath:META-INF/resources/"
        cache-period="#{60 * 60}" />

    <mvc:interceptors>
        <!-- (5) -->
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean
                class="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean
                class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor" />
        </mvc:interceptor>
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean class="org.terasoluna.gfw.web.codelist.CodeListInterceptor">
                <property name="codeListIdPattern" value="CL_.+" />
            </bean>
        </mvc:interceptor>
        <!--  REMOVE THIS LINE IF YOU USE JPA
        <mvc:interceptor>
            <mvc:mapping path="/**" />
            <mvc:exclude-mapping path="/resources/**" />
            <bean
                class="org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor" />
        </mvc:interceptor>
            REMOVE THIS LINE IF YOU USE JPA  -->
    </mvc:interceptors>

    <!-- (6) -->
    <!-- Settings View Resolver. -->
    <mvc:view-resolvers>
        <mvc:jsp prefix="/WEB-INF/views/" />
    </mvc:view-resolvers>

    <bean id="requestDataValueProcessor"
        class="org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor">
        <constructor-arg>
            <util:list>
                <bean
                    class="org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor" />
                <bean
                    class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" />
            </util:list>
        </constructor-arg>
    </bean>

    <!-- Setting Exception Handling. -->
    <!-- Exception Resolver. -->
    <bean id="systemExceptionResolver"
        class="org.terasoluna.gfw.web.exception.SystemExceptionResolver">
        <property name="exceptionCodeResolver" ref="exceptionCodeResolver" />
        <!-- Setting and Customization by project. -->
        <property name="order" value="3" />
        <property name="exceptionMappings">
            <map>
                <entry key="ResourceNotFoundException" value="common/error/resourceNotFoundError" />
                <entry key="BusinessException" value="common/error/businessError" />
                <entry key="InvalidTransactionTokenException" value="common/error/transactionTokenError" />
                <entry key=".DataAccessException" value="common/error/dataAccessError" />
            </map>
        </property>
        <property name="statusCodes">
            <map>
                <entry key="common/error/resourceNotFoundError" value="404" />
                <entry key="common/error/businessError" value="409" />
                <entry key="common/error/transactionTokenError" value="409" />
                <entry key="common/error/dataAccessError" value="500" />
            </map>
        </property>
        <property name="excludedExceptions">
            <array>
            </array>
        </property>
        <property name="defaultErrorView" value="common/error/systemError" />
        <property name="defaultStatusCode" value="500" />
    </bean>
    <!-- Setting AOP. -->
    <bean id="handlerExceptionResolverLoggingInterceptor"
        class="org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor">
        <property name="exceptionLogger" ref="exceptionLogger" />
    </bean>
    <aop:config>
        <aop:advisor advice-ref="handlerExceptionResolverLoggingInterceptor"
            pointcut="execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))" />
    </aop:config>

</beans>
項番 説明
(1)
プロパティファイルの読み込み設定を行う。
src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。
この設定により、プロパティファイルの値をBean定義ファイル内で${propertyName}形式で埋め込んだり、Javaクラスに@Value("${propertyName}")でインジェクションすることができる。
(2)
Spring MVCのアノテーションベースのデフォルト設定を行う。
(3)
アプリケーション層のクラスを管理するcom.example.todo.appパッケージ配下をcomponent-scan対象とする。
(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の設定を行う。
この設定により、例えばコントローラからview名としてhelloが返却された場合には/WEB-INF/views/hello.jspが実行される。

Note

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

<mvc:interceptor>
    <mvc:mapping path="/**" />
    <mvc:exclude-mapping path="/resources/**" />
    <bean
        class="org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor" />
</mvc:interceptor>

OpenEntityManagerInViewInterceptorは、EntityManagerのライフサイクルの開始と終了を行うInterceptorである。

この設定を追加することで、アプリケーション層(Controllerや、Viewクラス)でのLazy Loadが、サポートされる。


11.1.7.1.3.7. spring-security.xml

spring-security.xmlには、Spring Securityに関する定義を行う。

作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/spring-security.xmlは、以下のような設定となっている。
なお、本チュートリアルではSpring Securityの設定ファイルの説明は割愛する。Spring Securityの設定ファイルについては、「Spring Securityチュートリアル」を参照されたい。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:sec="http://www.springframework.org/schema/security"
    xsi:schemaLocation="
        http://www.springframework.org/schema/security https://www.springframework.org/schema/security/spring-security.xsd
        http://www.springframework.org/schema/beans https://www.springframework.org/schema/beans/spring-beans.xsd
    ">

    <sec:http pattern="/resources/**" request-matcher="ant" security="none"/>
    <sec:http request-matcher="ant">
        <sec:form-login/>
        <sec:logout/>
        <sec:access-denied-handler ref="accessDeniedHandler"/>
        <sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/>
        <sec:session-management />
        <sec:intercept-url pattern="/**" access="permitAll" />
    </sec:http>

    <sec:authentication-manager />

    <!-- CSRF Protection -->
    <bean id="accessDeniedHandler"
        class="org.springframework.security.web.access.DelegatingAccessDeniedHandler">
        <constructor-arg index="0">
            <map>
                <entry
                    key="org.springframework.security.web.csrf.InvalidCsrfTokenException">
                    <bean
                        class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                        <property name="errorPage"
                            value="/WEB-INF/views/common/error/invalidCsrfTokenError.jsp" />
                    </bean>
                </entry>
                <entry
                    key="org.springframework.security.web.csrf.MissingCsrfTokenException">
                    <bean
                        class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                        <property name="errorPage"
                            value="/WEB-INF/views/common/error/missingCsrfTokenError.jsp" />
                    </bean>
                </entry>
            </map>
        </constructor-arg>
        <constructor-arg index="1">
            <bean
                class="org.springframework.security.web.access.AccessDeniedHandlerImpl">
                <property name="errorPage"
                    value="/WEB-INF/views/common/error/accessDeniedError.jsp" />
            </bean>
        </constructor-arg>
    </bean>

    <bean id="webSecurityExpressionHandler" class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />

    <!-- Put UserID into MDC -->
    <bean id="userIdMDCPutFilter" class="org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter">
    </bean>

</beans>

11.1.7.1.4. 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に設定した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>