11.1. チュートリアル(Todoアプリケーション JSP編)¶
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.27.0.RELEASE (以降「STS」と呼ぶ。設定方法はSTS4の設定手順を参照されたい。)  | 
Build Tool  | 
Apache Maven 3.9.9 (以降「Maven」と呼ぶ)  | 
Application Server  | 
Apache Tomcat 10.1.33  | 
Web Browser  | 
Google Chrome 132  | 
11.1.2. 作成するアプリケーションの説明¶
11.1.2.1. アプリケーションの概要¶
TODOを管理するアプリケーションを作成する。TODOの一覧表示、TODOの登録、TODOの完了、TODOの削除を行える。
11.1.2.2. アプリケーションの業務要件¶
アプリケーションの業務要件は、以下の通りとする。
ルールID  | 
説明  | 
|---|---|
B01  | 
未完了のTODOは5件までしか登録できない  | 
B02  | 
完了済みのTODOは完了できない  | 
Note
本要件は学習のためのもので、現実的なTODO管理アプリケーションとしては適切ではない。
11.1.2.3. アプリケーションの処理仕様¶
アプリケーションの処理仕様と画面遷移は、以下の通りとする。
項番  | 
プロセス名  | 
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を使ったインメモリ実装のRepositoryImplMyBatis3を使用してデータベースにアクセスする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-jsp-archetype\ -DarchetypeVersion=5.10.0.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-jsp-archetype^
 -DarchetypeVersion=5.10.0.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT
mvn archetype:generate -B^
 -DarchetypeGroupId=org.terasoluna.gfw.blank^
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-xmlconfig-jsp-archetype^
 -DarchetypeVersion=5.10.0.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-jsp-mybatis3-archetype^
 -DarchetypeVersion=5.10.0.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT
mvn archetype:generate -B^
 -DarchetypeGroupId=org.terasoluna.gfw.blank^
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-xmlconfig-jsp-mybatis3-archetype^
 -DarchetypeVersion=5.10.0.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-jsp-jpa-archetype^
 -DarchetypeVersion=5.10.0.RELEASE^
 -DgroupId=com.example.todo^
 -DartifactId=todo^
 -Dversion=1.0.0-SNAPSHOT
mvn archetype:generate -B^
 -DarchetypeGroupId=org.terasoluna.gfw.blank^
 -DarchetypeArtifactId=terasoluna-gfw-web-blank-xmlconfig-jsp-jpa-archetype^
 -DarchetypeVersion=5.10.0.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で作成したプロジェクトを選択する。
Root DirectoryにC:\work\todoを設定し、Projectsにtodoのpom.xmlが選択された状態で、[Finish]を押下する。
インポートが完了すると、Package Explorerに次のようなプロジェクトが表示される。
プロジェクトのJavaバージョンを変更したい場合はJavaバージョンが異なる場合の対応を参照されたい。
Note
インポート後にビルドエラーが発生する場合は、プロジェクト名を右クリックし、「Maven」->「Update Project…」をクリックし、「OK」ボタンをクリックすることでエラーが解消されるケースがある。
Tip
パッケージの表示形式は、デフォルトは「Flat」だが、「Hierarchical」にしたほうが見通しがよい。
Package Explorerの「View Menu」 (右端の下矢印)をクリックし、「Package Presentation」->「Hierarchical」を選択する。
Package PresentationをHierarchicalにすると、以下の様な表示になる。
Warning
O/R Mapperを使用するブランクプロジェクトの場合、H2 Databaseがdependencyとして定義されているが、この設定は簡易的なアプリケーションを簡単に作成するためのものであり、実際のアプリケーション開発で使用されることは想定していない。
以下の定義は、実際のアプリケーション開発を行う際は削除すること。
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency>
Note
上記設定例は、依存ライブラリのバージョンをBOMプロジェクトである terasoluna-dependencies で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。
上記の依存ライブラリはterasoluna-dependenciesが依存しているSpring Bootで管理されている。
11.1.3.3. プロジェクトの構成¶
本チュートリアルで作成するプロジェクトの構成を以下に示す。
Note
前節の「プロジェクト構成」ではマルチプロジェクトにすることを推奨していたが、本チュートリアルでは、学習容易性を重視しているためシングルプロジェクト構成にしている。
ただし、実プロジェクトで適用する場合は、マルチプロジェクト構成を強く推奨する。
マルチプロジェクトの作成方法は、「Webアプリケーション向け開発プロジェクトの作成」を参照されたい。
[O/R Mapperに依存しないブランクプロジェクト、JPA用のブランクプロジェクト用を作成した場合の構成]
src
  └main
      ├java
      │  └com
      │    └example
      │      └todo
      │        ├ app ... (1)
      │        │   ├todo
      │        │   └welcome
      │        ├domain ... (2)
      │        │  ├model ... (3)
      │        │  ├repository ... (4)
      │        │  │   └todo
      │        │  └service ... (5)
      │        │      └todo
      │        └ config ... (6)
      │            ├app
      │            └web
      ├resources
      │  └META-INF
      │      └spring ... (6)
      └webapp
          ├resources
          │  └app
          │    └css ... (7)
          └WEB-INF
              └views ... (8)
src
  └main
      ├java
      │  └com
      │    └example
      │      └todo
      │        ├ app ... (1)
      │        │   ├todo
      │        │   └welcome
      │        └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
      │        │   └welcome
      │        ├domain
      │        │  ├model
      │        │  ├repository
      │        │  │   └todo
      │        │  └service
      │        │      └todo
      │        └ config
      │            ├app
      │            │ └mybatis ... (9)
      │            └web
      ├resources
      │  ├META-INF
      │  │  └spring
      │  └com
      │    └example
      │      └todo
      │        └domain
      │            └repository ... (10)
      │                 └todo
      └webapp
          ├resources
          │  └app
          │    └css
          └WEB-INF
              └views
src
  └main
      ├java
      │  └com
      │    └example
      │      └todo
      │        ├ app
      │        │   ├todo
      │        │   └welcome
      │        └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 class="container">
            <div id="wrapper">
                <h1 id="title">Hello world!</h1>
                <!-- (7) -->
                <p>The time on the server is ${serverTime}.</p>
            </div>
            <jsp:include page="../layout/footer.jsp" />
        </div>
    </body>
</html>
項番  | 
説明  | 
|---|---|
(7) 
 | 
ControllerでModelに設定した 
serverTimeを表示する。ここでは、XSS対策を行っていないが、ユーザの入力値を表示する場合は、 
f:h()関数を用いて、必ずXSS対策を行うこと。 | 
プロジェクトを右クリックして「Run As」->「Run on Server」を選択する。
APサーバー(Tomcat v10.1 Server at localhost)を選択し、「Next」をクリックする。
todoが「Configured」に含まれていることを確認して「Finish」をクリックしてサーバーを起動する。
起動すると以下のようなログが出力される。
“/“というパスに対して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にアクセスすると、以下のように表示される。
コンソールを見ると、
共通ライブラリから提供している
TraceLoggingInterceptorのTRACEログControllerで実装したINFOログ
が出力されていることがわかる。
date:2025-01-28 14:00:49      thread:http-nio-8080-exec-2     X-Track:aae50332dd844ee28c44067d96e1f09f        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[START CONTROLLER] HelloController.home(Locale,Model)
date:2025-01-28 14:00:49      thread:http-nio-8080-exec-2     X-Track:aae50332dd844ee28c44067d96e1f09f        level:INFO      logger:com.example.todo.app.welcome.HelloController     message:Welcome home! The client locale is ja.
date:2025-01-28 14:00:49      thread:http-nio-8080-exec-2     X-Track:aae50332dd844ee28c44067d96e1f09f        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[END CONTROLLER  ] HelloController.home(Locale,Model)-> view=welcome/home, model={serverTime=2025年1月28日 14:00:49 JST}
date:2025-01-28 14:00:49      thread:http-nio-8080-exec-2     X-Track:aae50332dd844ee28c44067d96e1f09f        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[HANDLING TIME   ] HelloController.home(Locale,Model)-> 87,161,900 ns
Note
TraceLoggingInterceptorはControllerの開始、終了でログを出力する。終了時にはViewとModelの情報および処理時間が出力される。
11.1.4. Todoアプリケーションの作成¶
ドメイン層(+ インフラストラクチャ層)
Domain Object作成
Repository作成
RepositoryImpl作成
Service作成
アプリケーション層
Controller作成
Form作成
View作成
RepositoryImplの作成は、選択したインフラストラクチャ層の種類に応じて実装方法が異なる。
java.util.Mapを使ったインメモリ実装のRepositoryImplを作成する方法について説明を行う。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.model2
Name
Todo3
Interfaces
java.io.Serializable
を入力して「Finish」する。
作成したクラスは以下のディレクトリに格納される。
作成したクラスに以下のプロパティを追加する。
ID → todoId
タイトル → todoTitle
完了フラグ → finished
作成日 → createdAt
package com.example.todo.domain.model;
import java.io.Serializable;
import java.util.Date;
public class Todo implements Serializable {
    private static final long serialVersionUID = 1L;
    private String todoId;
    private String todoTitle;
    private boolean finished;
    private Date createdAt;
    public String getTodoId() {
        return todoId;
    }
    public void setTodoId(String todoId) {
        this.todoId = todoId;
    }
    public String getTodoTitle() {
        return todoTitle;
    }
    public void setTodoTitle(String todoTitle) {
        this.todoTitle = todoTitle;
    }
    public boolean isFinished() {
        return finished;
    }
    public void setFinished(boolean finished) {
        this.finished = finished;
    }
    public Date getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(Date createdAt) {
        this.createdAt = createdAt;
    }
}
Tip
Getter/SetterメソッドはSTSの機能を使って自動生成することができる。
フィールドを定義した後、エディタ上で右クリックし、「Source」->「Generate Getters and Setters…」を選択する。
serialVersionUID以外を選択して「Generate」
11.1.4.1.2. Repositoryの作成¶
TodoRepositoryインタフェースを作成する。Package Explorer上で右クリック -> New -> Interface を選択し、「New Java Interface」ダイアログを表示し、
項番
項目
入力値
1
Package
com.example.todo.domain.repository.todo2
Name
TodoRepository
を入力して「Finish」する。
作成したインタフェースは以下のディレクトリに格納される。
作成したインタフェースに、今回のアプリケーションで必要となる以下の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を作成する。Package Explorer上で右クリック -> New -> Class を選択し、「New Java Class」ダイアログを表示し、
項番
項目
入力値
1
Package
com.example.todo.domain.repository.todo2
Name
TodoRepositoryImpl3
Interfaces
com.example.todo.domain.repository.todo.TodoRepository
を入力して「Finish」する。
作成したクラスは以下のディレクトリに格納される。
作成したクラスに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<>();
    @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.todo2
Name
TodoService
を入力して「Finish」する。
作成したインタフェースは以下のディレクトリに格納される。
作成したインタフェースに以下の業務処理を行うメソッドを定義する。
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.todo2
Name
TodoServiceImpl3
Interfaces
com.example.todo.domain.service.todo.TodoService
を入力して「Finish」する。
作成したクラスは以下のディレクトリに格納される。
package com.example.todo.domain.service.todo;
import java.util.Collection;
import java.util.Date;
import java.util.UUID;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;
import com.example.todo.domain.model.Todo;
import com.example.todo.domain.repository.todo.TodoRepository;
import jakarta.inject.Inject;
@Service // (1)
@Transactional // (2)
public class TodoServiceImpl implements TodoService {
    private static final long MAX_UNFINISHED_COUNT = 5;
    @Inject // (3)
    TodoRepository todoRepository;
    @Override
    @Transactional(readOnly = true) // (4)
    public Collection<Todo> findAll() {
        return todoRepository.findAll();
    }
    @Override
    public Todo create(Todo todo) {
        long unfinishedCount = todoRepository.countByFinished(false);
        if (unfinishedCount >= MAX_UNFINISHED_COUNT) {
            // (5)
            ResultMessages messages = ResultMessages.error();
            messages.add(
                    ResultMessage.fromText("[E001] The count of un-finished Todo must not be over "
                            + MAX_UNFINISHED_COUNT + "."));
            // (6)
            throw new BusinessException(messages);
        }
        // (7)
        String todoId = UUID.randomUUID().toString();
        Date createdAt = new Date();
        todo.setTodoId(todoId);
        todo.setCreatedAt(createdAt);
        todo.setFinished(false);
        todoRepository.create(todo);
        /* REMOVE THIS LINE IF YOU USE JPA
            todoRepository.save(todo); // (8)
           REMOVE THIS LINE IF YOU USE JPA */
        return todo;
    }
    @Override
    public Todo finish(String todoId) {
        Todo todo = findOne(todoId);
        if (todo.isFinished()) {
            ResultMessages messages = ResultMessages.error();
            messages.add(ResultMessage.fromText(
                    "[E002] The requested Todo is already finished. (id=" + todoId + ")"));
            throw new BusinessException(messages);
        }
        todo.setFinished(true);
        todoRepository.update(todo);
        /* REMOVE THIS LINE IF YOU USE JPA
            todoRepository.save(todo); // (9)
           REMOVE THIS LINE IF YOU USE JPA */
        return todo;
    }
    @Override
    public void delete(String todoId) {
        Todo todo = findOne(todoId);
        todoRepository.delete(todo);
    }
    // (10)
    private Todo findOne(String todoId) {
        return todoRepository.findById(todoId).orElseThrow(() -> {
            // (11)
            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.todo2
Name
TodoController
を入力して「Finish」する。
Note
上位パッケージがドメイン層と異なるので注意すること。
作成したクラスは以下のディレクトリに格納される。
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.todo2
Name
TodoForm3
Interfaces
java.io.Serializable
を入力して「Finish」する。
作成したクラスは以下のディレクトリに格納される。
作成したクラスに以下のプロパティを追加する。
タイトル → 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 org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import com.example.todo.domain.model.Todo;
import com.example.todo.domain.service.todo.TodoService;
import jakarta.inject.Inject;
@Controller
@RequestMapping("todo")
public class TodoController {
    @Inject // (1)
    TodoService todoService;
    @ModelAttribute // (2)
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }
    @GetMapping("list") // (3)
    public String list(Model model) {
        Collection<Todo> todos = todoService.findAll();
        model.addAttribute("todos", todos); // (4)
        return "todo/list"; // (5)
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
TodoServiceを、DIコンテナによってインジェクションさせるために、@Injectアノテーションをつける。DIコンテナの管理する 
TodoService型のインスタンス(TodoServiceImplのインスタンス)がインジェクションされる。 | 
(2) 
 | 
Formを初期化する。 
@ModelAttributeアノテーションをつけることで、このメソッドの返り値のformオブジェクトが、todoFormという名前でModelに追加される。これは、 
TodoControllerの各処理で、model.addAttribute("todoForm", form)を実装するのと同義である。 | 
(3) 
 | 
/todo/listというパスにGETメソッドを使用してリクエストされた際に、一覧画面表示処理用のメソッド(listメソッド)が実行されるように@GetMappingアノテーションを設定する。クラスレベルに 
@RequestMapping(“todo”)が設定されているため、ここでは@GetMapping("list")のみで良い。 | 
(4) 
 | 
ModelにTodoのリストを追加して、Viewに渡す。 | 
(5) 
 | 
View名として 
todo/listを返すと、spring-mvc.xmlまたはSpringMvcConfig.javaに定義したViewResolverによって、WEB-INF/views/todo/list.jspがレンダリングされることになる。 | 
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/todo2
File name
list.jsp
を入力して「Finish」する。
作成したファイルは以下のディレクトリに格納される。
まず、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対策を参照されたい。 
 | 
Note
上記で表示されている画面には、TODOが1件も登録されていないため、TODOの一覧は出力されない。
以下のように、ドメイン層の作成で作成したTodoRepositoryImplを一時的に修正し初期データを登録することで、TODOの一覧が出力されることを確認できる。
なお、次節「Create TODOの実装」で実際にTODOを登録できるようになるため、一覧の出力が確認できたら削除して構わない。
TodoRepositoryImpl.javapackage 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
以下のように画面に出力される。
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.todo2
Name
TodoMapper
を入力して「Finish」する。
作成したクラスは以下のディレクトリに格納される。
作成したクラスに以下の@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」をクリックする。
ビルドが成功した後、プロジェクト名を右クリックし、「Run As」->「Maven install」をクリックする。
11.1.4.2.3.2. Controllerの修正¶
新規作成処理をTodoControllerに追加する。
package com.example.todo.app.todo;
import java.util.Collection;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;
import com.example.todo.domain.model.Todo;
import com.example.todo.domain.service.todo.TodoService;
import jakarta.inject.Inject;
import jakarta.validation.Valid;
@Controller
@RequestMapping("todo")
public class TodoController {
    @Inject
    TodoService todoService;
    // (1)
    @Inject
    TodoMapper beanMapper;
    @ModelAttribute
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }
    @GetMapping("list")
    public String list(Model model) {
        Collection<Todo> todos = todoService.findAll();
        model.addAttribute("todos", todos);
        return "todo/list";
    }
    @PostMapping("create") // (2)
    public String create(@Valid TodoForm todoForm, BindingResult bindingResult, // (3)
            Model model, RedirectAttributes attributes) { // (4)
        // (5)
        if (bindingResult.hasErrors()) {
            return list(model);
        }
        // (6)
        Todo todo = beanMapper.map(todoForm);
        try {
            todoService.create(todo);
        } catch (BusinessException e) {
            // (7)
            model.addAttribute(e.getResultMessages());
            return list(model);
        }
        // (8)
        attributes.addFlashAttribute(
                ResultMessages.success().add(ResultMessage.fromText("Created successfully!")));
        return "redirect:/todo/list";
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
FormオブジェクトをDomainObjectに変換するために、 
TodoMapperインタフェースをインジェクションする。 | 
(2) 
 | 
/todo/createというパスにPOSTメソッドを使用してリクエストされた際に、新規作成処理用のメソッド(createメソッド)が実行されるように@PostMappingアノテーションを設定する。 | 
(3) 
 | 
フォームの入力チェックを行うため、Formの引数に 
@Validアノテーションをつける。入力チェック結果は、その直後の引数BindingResultに格納される。 | 
(4) 
 | 
正常に作成が完了した後にリダイレクトし、一覧画面を表示する。 
リダイレクト先への情報を格納するために、引数に 
RedirectAttributesを加える。 | 
(5) 
 | 
入力エラーがあった場合、一覧画面に戻る。 
Todo全件取得を再度行う必要があるので、 
listメソッドを再実行する。 | 
(6) 
 | 
Mapstructを用いて、TodoFormオブジェクトからTodoオブジェクトを作成する。 | 
(7) 
 | 
業務処理を実行して、 
BusinessExceptionが発生した場合、結果メッセージをModelに追加して、一覧画面に戻る。 | 
(8) 
 | 
正常に作成が完了したので、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。 
リダイレクトすることにより、ブラウザを再読み込みして、再び新規登録処理が 
POSTされることがなくなる。(詳しくは、「PRG(Post-Redirect-Get)パターンについて」を参照されたい)なお、今回は成功メッセージであるため、 
ResultMessages.success()を使用している。 | 
11.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すると、以下のように、成功メッセージが表示される。
未完了のTODOが5件登録済みの場合は、業務エラーとなり、エラーメッセージが表示される。
入力フォームを、空文字にしてsubmitすると、以下のように、エラーメッセージが表示される。
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;
}
メッセージは、以下のように装飾される。
また、<form:errors>タグのcssClass属性で、入力エラーメッセージのclassを指定できる。
JSPを次のように修正し、
<form:errors path="todoTitle" cssClass="text-error" />
スタイルシートに、以下を追加する。
.text-error {
    color: #c60f13;
}
入力エラー時のメッセージは、以下のように装飾される。
11.1.4.2.4. Finish TODOの実装¶
一覧画面に「Finish」ボタンを追加し、TODOを完了させるための処理を追加する。
11.1.4.2.4.1. Formの修正¶
完了処理用のFormについても、TodoFormを使用する。
TodoFormにtodoIdプロパティを追加する必要があるが、単純に追加してしまうと、新規作成処理でもtodoIdプロパティのチェックが実行されてしまう。groups属性を使用して、入力チェックルールをグループ化する。Formクラスに以下のプロパティを追加する。
ID → todoId
package com.example.todo.app.todo;
import java.io.Serializable;
import jakarta.validation.constraints.NotEmpty;
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)
    @NotEmpty(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は完了処理で使用するプロパティである。そのため、 
@NotEmptyアノテーションの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 org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;
import com.example.todo.app.todo.TodoForm.TodoCreate;
import com.example.todo.app.todo.TodoForm.TodoFinish;
import com.example.todo.domain.model.Todo;
import com.example.todo.domain.service.todo.TodoService;
import jakarta.inject.Inject;
import jakarta.validation.groups.Default;
@Controller
@RequestMapping("todo")
public class TodoController {
    @Inject
    TodoService todoService;
    @Inject
    TodoMapper beanMapper;
    @ModelAttribute
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }
    @GetMapping("list")
    public String list(Model model) {
        Collection<Todo> todos = todoService.findAll();
        model.addAttribute("todos", todos);
        return "todo/list";
    }
    @PostMapping("create")
    public String create(@Validated({Default.class, // (1)
            TodoCreate.class}) TodoForm todoForm, BindingResult bindingResult, Model model,
            RedirectAttributes attributes) {
        if (bindingResult.hasErrors()) {
            return list(model);
        }
        Todo todo = beanMapper.map(todoForm);
        try {
            todoService.create(todo);
        } catch (BusinessException e) {
            model.addAttribute(e.getResultMessages());
            return list(model);
        }
        attributes.addFlashAttribute(
                ResultMessages.success().add(ResultMessage.fromText("Created successfully!")));
        return "redirect:/todo/list";
    }
    @PostMapping("finish") // (2)
    public String finish(@Validated({Default.class, // (3)
            TodoFinish.class}) TodoForm form, BindingResult bindingResult, Model model,
            RedirectAttributes attributes) {
        // (4)
        if (bindingResult.hasErrors()) {
            return list(model);
        }
        try {
            todoService.finish(form.getTodoId());
        } catch (BusinessException e) {
            // (5)
            model.addAttribute(e.getResultMessages());
            return list(model);
        }
        // (6)
        attributes.addFlashAttribute(
                ResultMessages.success().add(ResultMessage.fromText("Finished successfully!")));
        return "redirect:/todo/list";
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
グループ化した入力チェックルールを適用するために、 
@Validアノテーションを@Validatedアノテーションに変更する。value属性には、適用する入力チェックルールのグループ(グループインタフェース)を指定する。Default.classは、グループ化されていない入力チェックルールを適用するために用意されているグループインタフェースである。 | 
(2) 
 | 
/todo/finishというパスにPOSTメソッドを使用してリクエストされた際に、完了処理用のメソッド(finishメソッド)が実行されるように@PostMappingアノテーションを設定する。 | 
(3) 
 | 
適用する入力チェックのグループとして、完了処理用のグループインタフェース( 
TodoFinishインタフェース)を指定する。 | 
(4) 
 | 
入力エラーがあった場合、一覧画面に戻る。 
 | 
(5) 
 | 
業務処理を実行して、 
BusinessExceptionが発生した場合は、結果メッセージをModelに追加して、一覧画面に戻る。 | 
(6) 
 | 
正常に作成が完了した場合は、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。 
 | 
Note
新規作成処理用と完了処理用を別々のFormクラスとして作成しても良い。別々のFormクラスにした場合、入力チェックルールをグループ化する必要がないため、入力チェックルールの定義はシンプルになる。
ただし、処理毎にFormクラスを作成した場合、
クラス数が増える
プロパティが重複するため入力チェックルールを一元管理できない
ため、仕様変更が発生した場合に修正コストが高くなる可能性があるという点に注意してほしい。
また、@ModelAttributeメソッドを使用して複数のFormを初期化した場合、毎回すべてのFormが初期化されるため、不要なインスタンスが生成されることになる。
11.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」ボタン押下すると、以下のように打ち消し線が入り、完了したことがわかる。
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.NotEmpty;
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)
    @NotEmpty(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の@NotEmptyアノテーションのgroups属性には、削除処理用の入力チェックルールである事を示すTodoDeleteインタフェースを指定する。 | 
11.1.4.2.5.2. Controllerの修正¶
削除処理をTodoControllerに追加する。完了処理とほぼ同じである。
package com.example.todo.app.todo;
import java.util.Collection;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;
import com.example.todo.app.todo.TodoForm.TodoCreate;
import com.example.todo.app.todo.TodoForm.TodoDelete;
import com.example.todo.app.todo.TodoForm.TodoFinish;
import com.example.todo.domain.model.Todo;
import com.example.todo.domain.service.todo.TodoService;
import jakarta.inject.Inject;
import jakarta.validation.groups.Default;
@Controller
@RequestMapping("todo")
public class TodoController {
    @Inject
    TodoService todoService;
    @Inject
    TodoMapper beanMapper;
    @ModelAttribute
    public TodoForm setUpForm() {
        TodoForm form = new TodoForm();
        return form;
    }
    @GetMapping("list")
    public String list(Model model) {
        Collection<Todo> todos = todoService.findAll();
        model.addAttribute("todos", todos);
        return "todo/list";
    }
    @PostMapping("create")
    public String create(@Validated({Default.class, TodoCreate.class}) TodoForm todoForm,
            BindingResult bindingResult, Model model, RedirectAttributes attributes) {
        if (bindingResult.hasErrors()) {
            return list(model);
        }
        Todo todo = beanMapper.map(todoForm);
        try {
            todoService.create(todo);
        } catch (BusinessException e) {
            model.addAttribute(e.getResultMessages());
            return list(model);
        }
        attributes.addFlashAttribute(
                ResultMessages.success().add(ResultMessage.fromText("Created successfully!")));
        return "redirect:/todo/list";
    }
    @PostMapping("finish")
    public String finish(@Validated({Default.class, TodoFinish.class}) TodoForm form,
            BindingResult bindingResult, Model model, RedirectAttributes attributes) {
        if (bindingResult.hasErrors()) {
            return list(model);
        }
        try {
            todoService.finish(form.getTodoId());
        } catch (BusinessException e) {
            model.addAttribute(e.getResultMessages());
            return list(model);
        }
        attributes.addFlashAttribute(
                ResultMessages.success().add(ResultMessage.fromText("Finished successfully!")));
        return "redirect:/todo/list";
    }
    @PostMapping("delete") // (1)
    public String delete(@Validated({Default.class, TodoDelete.class}) TodoForm form,
            BindingResult bindingResult, Model model, RedirectAttributes attributes) {
        if (bindingResult.hasErrors()) {
            return list(model);
        }
        try {
            todoService.delete(form.getTodoId());
        } catch (BusinessException e) {
            model.addAttribute(e.getResultMessages());
            return list(model);
        }
        attributes.addFlashAttribute(
                ResultMessages.success().add(ResultMessage.fromText("Deleted successfully!")));
        return "redirect:/todo/list";
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
  | 
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が削除される。
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ファイルを適用すると、以下のようなレイアウトになる。
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の作成¶
11.1.5.3.2. TodoRepositoryImplの作成¶
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/todo2
File name
TodoRepository.xml
を入力して「Finish」する。
作成したファイルは以下のディレクトリに格納される。
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.todo2
Name
TodoRepository3
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の作成は不要である。作成した場合は削除すること。
Note
当ページをはじめから順に実施した場合、TodoRepositoryImplの削除が必要になるが、削除したタイミングでTodoServiceImplがコンパイルエラーとなる。
JPA用のメソッドに切り替える必要があるため、ソースコード内のコメントを参考にcreateメソッドとupdateメソッドをsaveメソッドへ変更する必要がある。
以上で、Spring Data JPAを使用したインフラストラクチャ層の作成が完了したので、Service及びアプリケーション層の作成を行う。
Service及びアプリケーション層を作成後にAPサーバーを起動し、Todoの表示を行うと、以下のようなSQLログや、トランザクションログが出力される。
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[START CONTROLLER] TodoController.list(Model)
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:DEBUG     logger:o.h.engine.transaction.internal.TransactionImpl  message:On TransactionImpl creation, JpaCompliance#isJpaTransactionComplianceEnabled == false
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:DEBUG     logger:o.h.engine.transaction.internal.TransactionImpl  message:begin
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:DEBUG     logger:org.hibernate.SQL                                message:/* <criteria> */ select t1_0.todo_id,t1_0.created_at,t1_0.finished,t1_0.todo_title from todo t1_0
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:DEBUG     logger:o.h.engine.transaction.internal.TransactionImpl  message:committing
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[END CONTROLLER  ] TodoController.list(Model)-> view=todo/list, model={todoForm=com.example.todo.app.todo.TodoForm@5fcdd91a, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors}
date:2022-11-29 17:42:58      thread:http-nio-8080-exec-4     X-Track:67bbe0a8a3e8416baf91d5dcd73273e8        level:TRACE     logger:o.t.gfw.web.logging.TraceLoggingInterceptor      message:[HANDLING TIME   ] TodoController.list(Model)-> 247,911,500 ns
11.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管理アプリケーションには、以下の改善点がある。 アプリケーションの修正を学習課題として、ガイドライン中の該当する説明を参照されたい。
プロパティ(未完了TODOの上限数)を外部化する → プロパティ管理
メッセージを外部化する → メッセージ管理
ページネーション機能を追加する → ページネーション
例外ハンドリングを加える → 例外ハンドリング
二重送信を防止する(トランザクショントークンチェックを追加する) → 二重送信防止
システム日時の取得元を変更する → システム時刻
11.1.7. Appendix¶
11.1.7.1. 設定ファイルの解説¶
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>
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>
    <listener>
        <listener-class>ch.qos.logback.classic.servlet.LogbackServletContextListener</listener-class>
    </listener>
    <!-- (2) -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <!-- Root ApplicationContext -->
        <param-value>
            com.example.todo.config.app.ApplicationContextConfig
            com.example.todo.config.web.SpringSecurityConfig
        </param-value>
    </context-param>
    <listener>
        <listener-class>org.terasoluna.gfw.web.logging.HttpSessionEventLoggingListener</listener-class>
    </listener>
    <!-- (3) -->
    <filter>
        <filter-name>MDCClearFilter</filter-name>
        <filter-class>org.terasoluna.gfw.web.logging.mdc.MDCClearFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>MDCClearFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>exceptionLoggingFilter</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>exceptionLoggingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>XTrackMDCPutFilter</filter-name>
        <filter-class>org.terasoluna.gfw.web.logging.mdc.XTrackMDCPutFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>XTrackMDCPutFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>CharacterEncodingFilter</filter-name>
        <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
        <init-param>
            <param-name>encoding</param-name>
            <param-value>UTF-8</param-value>
        </init-param>
        <init-param>
            <param-name>forceEncoding</param-name>
            <param-value>true</param-value>
        </init-param>
    </filter>
    <filter-mapping>
        <filter-name>CharacterEncodingFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <filter>
        <filter-name>springSecurityFilterChain</filter-name>
        <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    <!-- (4) -->
    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <!-- ApplicationContext for Spring MVC -->
            <param-value>com.example.todo.config.web.SpringMvcConfig</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
    <!-- (5) -->
    <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) 
 | 
サーブレットコンテキストリスナーの定義。 
ブランクプロジェクトでは、 
 が設定済みである。  | 
(3) 
 | 
サーブレットフィルタの定義。 
ブランクプロジェクトでは、 
 が設定済みである。  | 
(4) 
 | 
Spring MVCのエントリポイントとなるDispatcherServletの定義。 
DispatcherServletの中で使用する 
ApplicationContextを、(2)で作成したApplicationContextの子として作成する。(2)で作成した 
ApplicationContextを親にすることで、(2)で読み込まれたコンポーネントも使用することができる。 | 
(5) 
 | 
JSPの共通定義。 
ブランクプロジェクトでは、 
 が設定済みである。  | 
(6) 
 | 
エラーページの定義。 
ブランクプロジェクトでは、 
 された際の遷移先が定義済みである。  | 
(7) 
 | 
セッション管理の定義。 
ブランクプロジェクトでは、 
 が定義済みである。  | 
<?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) 
 | 
サーブレットコンテキストリスナーの定義。 
ブランクプロジェクトでは、 
 が設定済みである。  | 
(3) 
 | 
サーブレットフィルタの定義。 
ブランクプロジェクトでは、 
 が設定済みである。  | 
(4) 
 | 
Spring MVCのエントリポイントとなるDispatcherServletの定義。 
DispatcherServletの中で使用する 
ApplicationContextを、(2)で作成したApplicationContextの子として作成する。(2)で作成した 
ApplicationContextを親にすることで、(2)で読み込まれたコンポーネントも使用することができる。 | 
(5) 
 | 
JSPの共通定義。 
ブランクプロジェクトでは、 
 が設定済みである。  | 
(6) 
 | 
エラーページの定義。 
ブランクプロジェクトでは、 
 された際の遷移先が定義済みである。  | 
(7) 
 | 
セッション管理の定義。 
ブランクプロジェクトでは、 
 が定義済みである。  | 
11.1.7.1.2. インクルードJSP¶
インクルードJSPには、全てのJSPに適用するJSPの設定や、タグライブラリの設定を行う。
作成したブランクプロジェクトのsrc/main/webapp/WEB-INF/views/common/include.jspは、以下のような設定となっている。
<!-- prettier-ignore -->
<!-- (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/java/com/example/todo/config/app/ApplicationContextConfig.javasrc/main/java/com/example/todo/config/app/TodoCodeListConfig.javasrc/main/java/com/example/todo/config/app/TodoDomainConfig.javasrc/main/java/com/example/todo/config/app/TodoInfraConfig.javasrc/main/resources/META-INF/spring/todo-infra.propertiessrc/main/java/com/example/todo/config/app/TodoEnvConfig.javasrc/main/java/com/example/todo/config/web/SpringMvcConfig.javasrc/main/java/com/example/todo/config/web/SpringSecurityConfig.java
Note
O/R Mapperに依存しないブランクプロジェクトを作成した場合は、todo-infra.propertiesとTodoEnvConfig.javaは作成されない。
src/main/resources/META-INF/spring/applicationContext.xmlsrc/main/resources/META-INF/spring/todo-codelist.xmlsrc/main/resources/META-INF/spring/todo-domain.xmlsrc/main/resources/META-INF/spring/todo-infra.xmlsrc/main/resources/META-INF/spring/todo-infra.propertiessrc/main/resources/META-INF/spring/todo-env.xmlsrc/main/resources/META-INF/spring/spring-mvc.xmlsrc/main/resources/META-INF/spring/spring-security.xml
Note
O/R Mapperに依存しないブランクプロジェクトを作成した場合は、todo-infra.propertiesとtodo-env.xmlは作成されない。
Note
本ガイドラインでは、Bean定義ファイルを役割(層)ごとにファイルを分割することを推奨している。
これは、どこに何が定義されているか想像しやすく、メンテナンス性が向上するからである。
今回のチュートリアルのような小さなアプリケーションでは効果はないが、アプリケーションの規模が大きくなるにつれ、効果が大きくなる。
11.1.7.1.3.1. applicationContext¶
ApplicationContextConfig.javaには、Todoアプリ全体に関わる設定を行う。
src/main/java/com/example/todo/config/app/ApplicationContextConfig.java は、以下のような設定となっている。package com.example.todo.config.app;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.Map;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.annotation.Import;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.context.support.ResourceBundleMessageSource;
import org.springframework.core.io.Resource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.DelegatingPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.password.Pbkdf2PasswordEncoder;
import org.terasoluna.gfw.common.exception.ExceptionCodeResolver;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.common.exception.SimpleMappingExceptionCodeResolver;
import org.terasoluna.gfw.web.exception.ExceptionLoggingFilter;
/**
 * Application context.
 */
@Configuration
@EnableAspectJAutoProxy
@Import({TodoDomainConfig.class}) // (1)
public class ApplicationContextConfig {
    /**
     * Configure {@link PasswordEncoder} bean.
     * @return Bean of configured {@link DelegatingPasswordEncoder}
     */
    @Bean("passwordEncoder")
    public PasswordEncoder passwordEncoder() {
        Map<String, PasswordEncoder> idToPasswordEncoder = new HashMap<>();
        idToPasswordEncoder.put("pbkdf2", pbkdf2PasswordEncoder());
        idToPasswordEncoder.put("bcrypt", bCryptPasswordEncoder());
        /* When using commented out PasswordEncoders, you need to add bcprov-jdk18on.jar to the dependency.
        idToPasswordEncoder.put("argon2", argon2PasswordEncoder());
        idToPasswordEncoder.put("scrypt", sCryptPasswordEncoder());
        */
        return new DelegatingPasswordEncoder("pbkdf2", idToPasswordEncoder);
    }
    /**
     * Configure {@link Pbkdf2PasswordEncoder} bean.
     * @return Bean of configured {@link Pbkdf2PasswordEncoder}
     */
    @Bean
    public Pbkdf2PasswordEncoder pbkdf2PasswordEncoder() {
        return Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    }
    /**
     * Configure {@link BCryptPasswordEncoder} bean.
     * @return Bean of configured {@link BCryptPasswordEncoder}
     */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
    /* When using commented out PasswordEncoders, you need to add bcprov-jdk18on.jar to the dependency.
    @Bean
    public Argon2PasswordEncoder argon2PasswordEncoder() {
        return Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8();
    }
    @Bean
    public SCryptPasswordEncoder sCryptPasswordEncoder() {
        return SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8();
    }
    */
    /**
     * Configure {@link PropertySourcesPlaceholderConfigurer} bean.
     * @param properties Property files to be read
     * @return Bean of configured {@link PropertySourcesPlaceholderConfigurer}
     */
    // (2)
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
            @Value("classpath*:/META-INF/spring/*.properties") Resource... properties) {
        PropertySourcesPlaceholderConfigurer bean = new PropertySourcesPlaceholderConfigurer();
        bean.setLocations(properties);
        return bean;
    }
    /**
     * Configure {@link MessageSource} bean.
     * @return Bean of configured {@link ResourceBundleMessageSource}
     */
    @Bean("messageSource")
    public MessageSource messageSource() {
        ResourceBundleMessageSource bean = new ResourceBundleMessageSource();
        bean.setBasenames("i18n/application-messages");
        return bean;
    }
    /**
     * Configure {@link ExceptionCodeResolver} bean.
     * @return Bean of configured {@link SimpleMappingExceptionCodeResolver}
     */
    @Bean("exceptionCodeResolver")
    public ExceptionCodeResolver exceptionCodeResolver() {
        LinkedHashMap<String, String> map = new LinkedHashMap<>();
        map.put("ResourceNotFoundException", "e.xx.fw.5001");
        map.put("InvalidTransactionTokenException", "e.xx.fw.7001");
        map.put("BusinessException", "e.xx.fw.8001");
        map.put(".DataAccessException", "e.xx.fw.9002");
        SimpleMappingExceptionCodeResolver bean = new SimpleMappingExceptionCodeResolver();
        bean.setExceptionMappings(map);
        bean.setDefaultExceptionCode("e.xx.fw.9001");
        return bean;
    }
    /**
     * Configure {@link ExceptionLogger} bean.
     * @return Bean of configured {@link ExceptionLogger}
     */
    @Bean("exceptionLogger")
    public ExceptionLogger exceptionLogger() {
        ExceptionLogger bean = new ExceptionLogger();
        bean.setExceptionCodeResolver(exceptionCodeResolver());
        return bean;
    }
    /**
     * Configure {@link ExceptionLoggingFilter} bean.
     * @return Bean of configured {@link ExceptionLoggingFilter}
     */
    @Bean("exceptionLoggingFilter")
    public ExceptionLoggingFilter exceptionLoggingFilter() {
        ExceptionLoggingFilter bean = new ExceptionLoggingFilter();
        bean.setExceptionLogger(exceptionLogger());
        return bean;
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
ドメイン層に関するBean定義ファイルをimportする。 
 | 
(2) 
 | 
プロパティファイルの読み込み設定を行う。 
src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。この設定により、プロパティファイルの値をBean定義ファイル内で 
${propertyName}形式で埋め込んだり、Javaクラスに@Value("${propertyName}")でインジェクションすることができる。 | 
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"
    xmlns:aop="http://www.springframework.org/schema/aop"
    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
                        http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.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" />
        <constructor-arg name="idToPasswordEncoder">
            <map>
                <entry key="pbkdf2">
                    <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">
                    <bean class="org.springframework.security.crypto.argon2.Argon2PasswordEncoder" factory-method="defaultsForSpringSecurity_v5_8" />
                </entry>
                <entry key="scrypt">
                    <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>
    <aop:aspectj-autoproxy />
</beans>
項番  | 
説明  | 
|---|---|
(1) 
 | 
ドメイン層に関するBean定義ファイルをimportする。 
 | 
(2) 
 | 
プロパティファイルの読み込み設定を行う。 
src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。この設定により、プロパティファイルの値をBean定義ファイル内で 
${propertyName}形式で埋め込んだり、Javaクラスに@Value("${propertyName}")でインジェクションすることができる。 | 
11.1.7.1.3.2. todo-domain¶
TodoDomainConfig.javaには、Todoアプリのドメイン層に関わる設定を行う。
src/main/java/com/example/todo/config/app/TodoDomainConfig.javaは、以下のような設定となっている。package com.example.todo.config.app;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.common.exception.ResultMessagesLoggingInterceptor;
/**
 * Bean definitions for domain layer.
 */
@Configuration
@ComponentScan(basePackages = {"com.example.todo.domain"}) // (1)
@Import({TodoInfraConfig.class, TodoCodeListConfig.class}) // (2)
public class TodoDomainConfig {
    /**
     * Configure messages logging AOP.
     * @param exceptionLogger Bean defined by ApplicationContext#exceptionLogger
     * @see com.example.todo.config.app.ApplicationContext#exceptionLogger()
     * @return Bean of configured {@link ResultMessagesLoggingInterceptor}
     */
    @Bean("resultMessagesLoggingInterceptor")
    public ResultMessagesLoggingInterceptor resultMessagesLoggingInterceptor(
            ExceptionLogger exceptionLogger) {
        ResultMessagesLoggingInterceptor bean = new ResultMessagesLoggingInterceptor();
        bean.setExceptionLogger(exceptionLogger);
        return bean;
    }
    /**
     * Configure messages logging AOP advisor.
     * @param resultMessagesLoggingInterceptor Bean defined by #resultMessagesLoggingInterceptor
     * @see #resultMessagesLoggingInterceptor(ExceptionLogger)
     * @return Advisor configured for PointCut
     */
    @Bean
    public Advisor resultMessagesLoggingInterceptorAdvisor(
            ResultMessagesLoggingInterceptor resultMessagesLoggingInterceptor) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression("@within(org.springframework.stereotype.Service)");
        return new DefaultPointcutAdvisor(pointcut, resultMessagesLoggingInterceptor);
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
ドメイン層のクラスを管理するcom.example.todo.domainパッケージ配下をcomponent-scan対象とする。 
これにより、com.example.todo.domainパッケージ配下のクラスに  
@Repository , @Service などのアノテーションを付けることで、Spring Framerowkが管理するBeanとして登録される。登録されたクラス(Bean)は、ControllerやServiceクラスにDIする事で、利用する事が出来る。 
 | 
(2) 
 | 
インフラストラクチャ層に関するBean定義ファイルをimportする。 
 | 
Note
O/R Mapperに依存するブランクプロジェクトを作成した場合は、@Transactionalアノテーションによるトランザクション管理を有効にするために、@EnableTransactionManagementアノテーションが設定されている。
@EnableTransactionManagement
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¶
TodoInfraConfig.javaには、Todoアプリのインフラストラクチャ層に関わる設定を行う。
作成したブランクプロジェクトのsrc/main/java/com/example/todo/config/app/TodoInfraConfig.javaは、以下のような設定となっている。
TodoInfraConfig.javaは、インフラストラクチャ層によって設定が大きく異なるため、ブランクプロジェクト毎に説明を行う。作成したブランクプロジェクト以外の説明は読み飛ばしてもよい。
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¶
O/R Mapperに依存しないブランクプロジェクトを作成した場合、以下のように空定義のファイルが作成される。
package com.example.todo.config.app;
import org.springframework.context.annotation.Configuration;
/**
 * Bean definitions for infrastructure layer.
 */
@Configuration
public class TodoInfraConfig {
}
<?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¶
MyBatis3用のブランクプロジェクトを作成した場合、以下のような設定となっている。
package com.example.todo.config.app;
import javax.sql.DataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import com.example.todo.config.app.mybatis.MybatisConfig;
/**
 * Bean definitions for infrastructure layer.
 */
@Configuration
@MapperScan("com.example.todo.domain.repository") // (1)
@Import({TodoEnvConfig.class}) // (2)
public class TodoInfraConfig {
    /**
     * Configure {@link SqlSessionFactoryBean} bean.
     * @param dataSource DataSource
     * @see com.example.todo.config.app.TodoEnvConfig#dataSource()
     * @return Bean of configured {@link SqlSessionFactoryBean}
     */
    // (3)
    @Bean("sqlSessionFactory")
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource) {
        SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
        // (4)
        bean.setDataSource(dataSource);
        // (5)
        bean.setConfiguration(MybatisConfig.configuration());
        return bean;
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
Mapperインタフェースをスキャンするために 
@MapperScanを定義し、Mapperインタフェースが格納されている基底パッケージを指定する。 | 
(2) 
 | 
環境依存するコンポーネント(データソースやトランザクションマネージャなど)を定義するBean定義ファイルをimportする。 
指定されたパッケージ配下に格納されている Mapperインタフェースがスキャンされ、スレッドセーフなMapperオブジェクト(MapperインタフェースのProxyオブジェクト)が自動的に生成される。 
 | 
(3) 
 | 
SqlSessionFactoryを生成するためのコンポーネントとして、SqlSessionFactoryBeanをbean定義する。 | 
(4) 
 | 
dataSourceプロパティに、設定済みのデータソースのbeanを指定する。MyBatis3の処理の中でSQLを発行する際は、ここで指定したデータソースからコネクションが取得される。 
 | 
(5) 
 | 
configurationプロパティに、MyBatisの設定をしたConfigurationクラスを指定する。ここで指定したクラスは 
SqlSessionFactoryを生成する時に読み込まれる。 | 
<?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¶
JPA用のブランクプロジェクトを作成した場合、以下のような設定となっている。
package com.example.todo.config.app;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.Database;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
/**
 * Bean definitions for infrastructure layer.
 */
@Configuration
@EnableJpaRepositories("com.example.todo.domain.repository") // (1)
@Import({TodoEnvConfig.class}) // (2)
public class TodoInfraConfig {
    /**
     * Database property.
     */
    @Value("${database}")
    private Database database;
    /**
     * Configure {@link HibernateJpaVendorAdapter} bean.
     * @return Bean of configured {@link HibernateJpaVendorAdapter}
     */
    // (3)
    @Bean("jpaVendorAdapter")
    public HibernateJpaVendorAdapter jpaVendorAdapter() {
        HibernateJpaVendorAdapter bean = new HibernateJpaVendorAdapter();
        bean.setDatabase(database);
        return bean;
    }
    /**
     * Configure {@link LocalContainerEntityManagerFactoryBean} bean.
     * @param dataSource DataSource
     * @return Bean of configured {@link LocalContainerEntityManagerFactoryBean}
     */
    // (4)
    @Bean("entityManagerFactory")
    public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource dataSource) {
        LocalContainerEntityManagerFactoryBean bean = new LocalContainerEntityManagerFactoryBean();
        bean.setPackagesToScan("com.example.todo.domain.model");
        bean.setDataSource(dataSource);
        bean.setJpaVendorAdapter(jpaVendorAdapter());
        bean.setJpaPropertyMap(jpaPropertyMap());
        return bean;
    }
    /**
     * Configure {@link LocalContainerEntityManagerFactoryBean}.JpaPropertyMap.
     * @return configured JpaPropertyMap
     */
    // (5)
    private Map<String, ?> jpaPropertyMap() {
        Map<String, Object> jpaPropertyMap = new HashMap<>();
        jpaPropertyMap.put("hibernate.connection.charSet", "UTF-8");
        jpaPropertyMap.put("hibernate.format_sql", false);
        jpaPropertyMap.put("hibernate.use_sql_comments", true);
        jpaPropertyMap.put("hibernate.jdbc.batch_size", 30);
        jpaPropertyMap.put("hibernate.jdbc.fetch_size", 100);
        return jpaPropertyMap;
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
Spring Data JPAを使用して、Repositoryインタフェースから実装クラスを自動生成する。 
@EnableJpaRepositoriesアノテーションに、Repositoryを格納するパッケージを指定する。 | 
(2) 
 | 
環境依存するコンポーネント(データソースやトランザクションマネージャなど)を定義するBean定義ファイルをimportする。 
 | 
(3) 
 | 
JPAの実装ベンダの設定を行う。 
JPA実装として、Hibernateを使うため、 
HibernateJpaVendorAdapterを定義している。 | 
(4) 
 | 
EntityManagerの定義を行う。 | 
(5) 
 | 
Hibernateに関する詳細な設定を行う。 
 | 
<?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" />
    <!-- (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.connection.charSet" value="UTF-8" />
                <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またはTodoEnvConfig.javaから参照されている。
11.1.7.1.3.5. todo-env¶
TodoEnvConfig.javaには、デプロイする環境によって設定が異なるコンポーネントの設定を行う。
作成したブランクプロジェクトのsrc/main/java/com/example/todo/config/app/TodoEnvConfig.javaは、以下のような設定となっている。
ここでは、MyBatis3用のブランクプロジェクトに格納されるファイルを例に説明する。なお、データベースにアクセスしないブランクプロジェクトを作成した際は、TodoEnvConfig.javaは作成されない。
package com.example.todo.config.app;
import java.time.Duration;
import javax.sql.DataSource;
import org.apache.commons.dbcp2.BasicDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.init.DataSourceInitializer;
import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
/* REMOVE THIS LINE IF YOU USE JPA
import org.springframework.orm.jpa.JpaTransactionManager;
REMOVE THIS LINE IF YOU USE JPA */
import org.springframework.transaction.TransactionManager;
import org.terasoluna.gfw.common.time.ClockFactory;
import org.terasoluna.gfw.common.time.DefaultClockFactory;
/* REMOVE THIS LINE IF YOU USE JPA
import jakarta.persistence.EntityManagerFactory;
REMOVE THIS LINE IF YOU USE JPA */
/**
 * Define settings for the environment.
 */
@Configuration
public class TodoEnvConfig {
    /**
     * DataSource.driverClassName property.
     */
    @Value("${database.driverClassName}")
    private String driverClassName;
    /**
     * DataSource.url property.
     */
    @Value("${database.url}")
    private String url;
    /**
     * DataSource.username property.
     */
    @Value("${database.username}")
    private String username;
    /**
     * DataSource.password property.
     */
    @Value("${database.password}")
    private String password;
    /**
     * DataSource.maxTotal property.
     */
    @Value("${cp.maxActive}")
    private Integer maxActive;
    /**
     * DataSource.maxIdle property.
     */
    @Value("${cp.maxIdle}")
    private Integer maxIdle;
    /**
     * DataSource.minIdle property.
     */
    @Value("${cp.minIdle}")
    private Integer minIdle;
    /**
     * DataSource.maxWaitMillis property.
     */
    @Value("${cp.maxWait}")
    private Integer maxWait;
    /**
     * Property databaseName.
     */
    @Value("${database}")
    private String database;
    /**
     * Configure {@link ClockFactory}.
     * @return Bean of configured {@link DefaultClockFactory}
     */
    @Bean("dateFactory")
    public ClockFactory dateFactory() {
        return new DefaultClockFactory();
    }
    /**
     * Configure {@link DataSource} bean.
     * @return Bean of configured {@link BasicDataSource}
     */
    // (1)
    @Bean(name = "dataSource", destroyMethod = "close")
    public DataSource dataSource() {
        BasicDataSource bean = new BasicDataSource();
        bean.setDriverClassName(driverClassName);
        bean.setUrl(url);
        bean.setUsername(username);
        bean.setPassword(password);
        bean.setDefaultAutoCommit(false);
        bean.setMaxTotal(maxActive);
        bean.setMaxIdle(maxIdle);
        bean.setMinIdle(minIdle);
        bean.setMaxWait(Duration.ofMillis(maxWait));
        return bean;
    }
    /**
     * Configuration to set up database during initialization.
     * @return Bean of configured {@link DataSourceInitializer}
     */
    // (2)
    @Bean
    public DataSourceInitializer dataSourceInitializer() {
        DataSourceInitializer bean = new DataSourceInitializer();
        bean.setDataSource(dataSource());
        // (3)
        ResourceDatabasePopulator databasePopulator = new ResourceDatabasePopulator();
        databasePopulator.addScript(new ClassPathResource("/database/" + database + "-schema.sql"));
        databasePopulator
                .addScript(new ClassPathResource("/database/" + database + "-dataload.sql"));
        databasePopulator.setSqlScriptEncoding("UTF-8");
        databasePopulator.setIgnoreFailedDrops(true);
        bean.setDatabasePopulator(databasePopulator);
        return bean;
    }
    // @formatter:off
    /* REMOVE THIS LINE IF YOU USE JPA
    /**
     * Configure {@link TransactionManager} bean for use with JPA.
     * @param entityManagerFactory EntityManager used within a transaction
     * @return Bean of configured {@link JpaTransactionManager}
     *REMOVE THIS COMMENT IF YOU USE JPA/
    @Bean("transactionManager")
    public TransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager bean = new JpaTransactionManager();
        bean.setEntityManagerFactory(entityManagerFactory);
        return bean;
    }
    REMOVE THIS LINE IF YOU USE JPA */
    /**
     * Configure {@link TransactionManager} bean.
     * @return Bean of configured {@link DataSourceTransactionManager}
     */
    // (4)
    @Bean("transactionManager")
    public TransactionManager transactionManager() {
        DataSourceTransactionManager bean = new DataSourceTransactionManager();
        bean.setDataSource(dataSource());
        bean.setRollbackOnCommitFailure(true);
        return bean;
    }
    // @formatter:on
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
実データソースの設定。 
 | 
(2) 
 | 
データベース初期化の設定。 
データベースを初期化するSQLファイルを実行するための設定を行っている。 
この設定は通常、開発中のみでしか使用しない(環境に依存する設定)ため、 
TodoEnvConfig.java に定義されている。 | 
(3) 
 | 
データベースを初期化するSQLファイルの設定。 
データベースを初期化するための、DDL文が記載されているSQLファイルとDML文が記載されているSQLファイルを指定している。 
ブランクプロジェクトの設定では 
todo-infra.propertiesにdatabase=H2と定義されているため、H2-schema.sql及びH2-dataload.sqlが実行される。 | 
(4) 
 | 
トランザクションマネージャの設定。 
id属性には、 
transactionManagerを指定する。ブランクプロジェクトでは、JDBCのAPIを使用してトランザクションを制御するクラス( 
org.springframework.jdbc.datasource.DataSourceTransactionManager)が設定されている。 | 
Note
JPA用のブランクプロジェクトを作成した場合は、トランザクションマネージャには、JPAのAPIを使用してトランザクションを制御するクラス(org.springframework.orm.jpa.JpaTransactionManager)が設定されている。
@Bean("transactionManager") public TransactionManager transactionManager(EntityManagerFactory entityManagerFactory) { JpaTransactionManager bean = new JpaTransactionManager(); bean.setEntityManagerFactory(entityManagerFactory); return bean; }
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="maxWait">
            <bean class="java.time.Duration" factory-method="ofMillis">
                <constructor-arg value="${cp.maxWait}" />
            </bean>
        </property>
    </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.propertiesにdatabase=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¶
SpringMvcConfig.javaには、Spring MVCに関する定義を行う。
src/main/java/com/example/todo/config/web/SpringMvcConfig.javaは、以下のような設定となっている。package com.example.todo.config.web;
import java.util.List;
import java.util.Properties;
import java.util.regex.Pattern;
import org.springframework.aop.Advisor;
import org.springframework.aop.aspectj.AspectJExpressionPointcut;
import org.springframework.aop.support.DefaultPointcutAdvisor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.Resource;
import org.springframework.data.web.PageableHandlerMethodArgumentResolver;
import org.springframework.http.HttpStatus;
/* REMOVE THIS LINE IF YOU USE JPA
import org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor;
REMOVE THIS LINE IF YOU USE JPA */
import org.springframework.security.web.method.annotation.AuthenticationPrincipalArgumentResolver;
import org.springframework.security.web.servlet.support.csrf.CsrfRequestDataValueProcessor;
/* REMOVE THIS LINE IF YOU USE JPA
import org.springframework.web.context.request.WebRequestInterceptor;
REMOVE THIS LINE IF YOU USE JPA */
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.DefaultServletHandlerConfigurer;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewResolverRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.support.RequestDataValueProcessor;
import org.terasoluna.gfw.common.exception.ExceptionCodeResolver;
import org.terasoluna.gfw.common.exception.ExceptionLogger;
import org.terasoluna.gfw.web.codelist.CodeListInterceptor;
import org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor;
import org.terasoluna.gfw.web.exception.SystemExceptionResolver;
import org.terasoluna.gfw.web.logging.TraceLoggingInterceptor;
import org.terasoluna.gfw.web.mvc.support.CompositeRequestDataValueProcessor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor;
import org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor;
/**
 * Configure SpringMVC.
 */
// (1)
@ComponentScan(basePackages = {"com.example.todo.app"})
@EnableAspectJAutoProxy
@EnableWebMvc
@Configuration
public class SpringMvcConfig implements WebMvcConfigurer {
    /**
     * Configure {@link PropertySourcesPlaceholderConfigurer} bean.
     * @param properties Property files to be read
     * @return Bean of configured {@link PropertySourcesPlaceholderConfigurer}
     */
    // (2)
    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer(
            @Value("classpath*:/META-INF/spring/*.properties") Resource... properties) {
        PropertySourcesPlaceholderConfigurer bean = new PropertySourcesPlaceholderConfigurer();
        bean.setLocations(properties);
        return bean;
    }
    /**
     * {@inheritDoc}
     */
    // (3)
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
        argumentResolvers.add(pageableHandlerMethodArgumentResolver());
        argumentResolvers.add(authenticationPrincipalArgumentResolver());
    }
    /**
     * Configure {@link PageableHandlerMethodArgumentResolver} bean.
     * @return Bean of configured {@link PageableHandlerMethodArgumentResolver}
     */
    @Bean
    public PageableHandlerMethodArgumentResolver pageableHandlerMethodArgumentResolver() {
        return new PageableHandlerMethodArgumentResolver();
    }
    /**
     * Configure {@link AuthenticationPrincipalArgumentResolver} bean.
     * @return Bean of configured {@link AuthenticationPrincipalArgumentResolver}
     */
    @Bean
    public AuthenticationPrincipalArgumentResolver authenticationPrincipalArgumentResolver() {
        return new AuthenticationPrincipalArgumentResolver();
    }
    /**
     * {@inheritDoc}
     */
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }
    /**
     * {@inheritDoc}
     */
    // (4)
    @Override
    public void addResourceHandlers(final ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/resources/**")
                .addResourceLocations("/resources/", "classpath:META-INF/resources/")
                .setCachePeriod(60 * 60);
    }
    /**
     * {@inheritDoc}
     */
    // (5)
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        addInterceptor(registry, traceLoggingInterceptor());
        addInterceptor(registry, transactionTokenInterceptor());
        addInterceptor(registry, codeListInterceptor());
        // @formatter:off
        /* REMOVE THIS LINE IF YOU USE JPA
        addWebRequestInterceptor(registry, openEntityManagerInViewInterceptor());
           REMOVE THIS LINE IF YOU USE JPA */
        // @formatter:on
    }
    /**
     * Common processes used in #addInterceptors.
     * @param registry {@link InterceptorRegistry}
     * @param interceptor {@link HandlerInterceptor}
     */
    private void addInterceptor(InterceptorRegistry registry, HandlerInterceptor interceptor) {
        registry.addInterceptor(interceptor).addPathPatterns("/**")
                .excludePathPatterns("/resources/**");
    }
    // @formatter:off
    /* REMOVE THIS LINE IF YOU USE JPA
    /**
     * Common processes used in #addInterceptors.
     * @param registry {@link InterceptorRegistry}
     * @param interceptor {@link WebRequestInterceptor}
     * REMOVE THIS COMMENT IF YOU USE JPA/
    private void addWebRequestInterceptor(InterceptorRegistry registry,
            WebRequestInterceptor interceptor) {
        registry.addWebRequestInterceptor(interceptor).addPathPatterns("/**")
                .excludePathPatterns("/resources/**");
    }
    REMOVE THIS LINE IF YOU USE JPA */
    // @formatter:on
    /**
     * Configure {@link TraceLoggingInterceptor} bean.
     * @return Bean of configured {@link TraceLoggingInterceptor}
     */
    @Bean
    public TraceLoggingInterceptor traceLoggingInterceptor() {
        return new TraceLoggingInterceptor();
    }
    /**
     * Configure {@link TransactionTokenInterceptor} bean.
     * @return Bean of configured {@link TransactionTokenInterceptor}
     */
    @Bean
    public TransactionTokenInterceptor transactionTokenInterceptor() {
        return new TransactionTokenInterceptor();
    }
    /**
     * Configure {@link CodeListInterceptor} bean.
     * @return Bean of configured {@link CodeListInterceptor}
     */
    @Bean
    public CodeListInterceptor codeListInterceptor() {
        CodeListInterceptor codeListInterceptor = new CodeListInterceptor();
        codeListInterceptor.setCodeListIdPattern(Pattern.compile("CL_.+"));
        return codeListInterceptor;
    }
    // @formatter:off
    /* REMOVE THIS LINE IF YOU USE JPA
    /**
     * Configure {@link OpenEntityManagerInViewInterceptor} bean.
     * @return Bean of configured {@link OpenEntityManagerInViewInterceptor}
     * REMOVE THIS COMMENT IF YOU USE JPA/
    @Bean
    public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() {
        return new OpenEntityManagerInViewInterceptor();
    }
    REMOVE THIS LINE IF YOU USE JPA */
    // @formatter:on
    /**
     * {@inheritDoc}
     */
    // (6)
    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.jsp("/WEB-INF/views/", ".jsp");
    }
    /**
     * Configure {@link RequestDataValueProcessor} bean.
     * @return Bean of configured {@link CompositeRequestDataValueProcessor}
     */
    @Bean("requestDataValueProcessor")
    public RequestDataValueProcessor requestDataValueProcessor() {
        return new CompositeRequestDataValueProcessor(csrfRequestDataValueProcessor(),
                transactionTokenRequestDataValueProcessor());
    }
    /**
     * Configure {@link CsrfRequestDataValueProcessor} bean.
     * @return Bean of configured {@link CsrfRequestDataValueProcessor}
     */
    @Bean
    public CsrfRequestDataValueProcessor csrfRequestDataValueProcessor() {
        return new CsrfRequestDataValueProcessor();
    }
    /**
     * Configure {@link TransactionTokenRequestDataValueProcessor} bean.
     * @return Bean of configured {@link TransactionTokenRequestDataValueProcessor}
     */
    @Bean
    public TransactionTokenRequestDataValueProcessor transactionTokenRequestDataValueProcessor() {
        return new TransactionTokenRequestDataValueProcessor();
    }
    /**
     * Configure {@link SystemExceptionResolver} bean.
     * @param exceptionCodeResolver Bean defined by ApplicationContext#exceptionCodeResolver
     * @see com.example.todo.config.app.ApplicationContext#exceptionCodeResolver()
     * @return Bean of configured {@link SystemExceptionResolver}
     */
    @Bean("systemExceptionResolver")
    public SystemExceptionResolver systemExceptionResolver(
            ExceptionCodeResolver exceptionCodeResolver) {
        SystemExceptionResolver bean = new SystemExceptionResolver();
        bean.setExceptionCodeResolver(exceptionCodeResolver);
        bean.setOrder(3);
        Properties exceptionMappings = new Properties();
        exceptionMappings.setProperty("ResourceNotFoundException",
                "common/error/resourceNotFoundError");
        exceptionMappings.setProperty("BusinessException", "common/error/businessError");
        exceptionMappings.setProperty("InvalidTransactionTokenException",
                "common/error/transactionTokenError");
        exceptionMappings.setProperty(".DataAccessException", "common/error/dataAccessError");
        bean.setExceptionMappings(exceptionMappings);
        Properties statusCodes = new Properties();
        statusCodes.setProperty("common/error/resourceNotFoundError",
                String.valueOf(HttpStatus.NOT_FOUND.value()));
        statusCodes.setProperty("common/error/businessError",
                String.valueOf(HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/transactionTokenError",
                String.valueOf(HttpStatus.CONFLICT.value()));
        statusCodes.setProperty("common/error/dataAccessError",
                String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR.value()));
        bean.setStatusCodes(statusCodes);
        bean.setDefaultErrorView("common/error/systemError");
        bean.setDefaultStatusCode(HttpStatus.INTERNAL_SERVER_ERROR.value());
        return bean;
    }
    /**
     * Configure messages logging AOP.
     * @param exceptionLogger Bean defined by ApplicationContext#exceptionLogger
     * @see com.example.todo.config.app.ApplicationContext#exceptionLogger()
     * @return Bean of configured {@link HandlerExceptionResolverLoggingInterceptor}
     */
    @Bean("handlerExceptionResolverLoggingInterceptor")
    public HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor(
            ExceptionLogger exceptionLogger) {
        HandlerExceptionResolverLoggingInterceptor bean =
                new HandlerExceptionResolverLoggingInterceptor();
        bean.setExceptionLogger(exceptionLogger);
        return bean;
    }
    /**
     * Configure messages logging AOP advisor.
     * @param handlerExceptionResolverLoggingInterceptor Bean defined by
     *        #handlerExceptionResolverLoggingInterceptor
     * @see #handlerExceptionResolverLoggingInterceptor(ExceptionLogger)
     * @return Advisor configured for PointCut
     */
    @Bean
    public Advisor handlerExceptionResolverLoggingInterceptorAdvisor(
            HandlerExceptionResolverLoggingInterceptor handlerExceptionResolverLoggingInterceptor) {
        AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
        pointcut.setExpression(
                "execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))");
        return new DefaultPointcutAdvisor(pointcut, handlerExceptionResolverLoggingInterceptor);
    }
}
項番  | 
説明  | 
|---|---|
(1) 
 | 
アプリケーション層のクラスを管理するcom.example.todo.appパッケージ配下をcomponent-scan対象とする。 
 | 
(2) 
 | 
プロパティファイルの読み込み設定を行う。 
src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。 
この設定により、プロパティファイルの値をBean定義ファイル内で 
${propertyName}形式で埋め込んだり、Javaクラスに@Value("${propertyName}")でインジェクションすることができる。 | 
(3) 
 | 
Spring MVCのアノテーションベースのデフォルト設定を行う。 
 | 
(4) 
 | 
静的リソース(css, images, jsなど)アクセスのための設定を行う。 
mapping属性にURLのパスを、location属性に物理的なパスの設定を行う。この設定の場合 
<contextPath>/resources/app/css/styles.cssに対してリクエストが来た場合、WEB-INF/resources/app/css/styles.cssを探し、見つからなければクラスパス上(src/main/resourcesやjar内)のMETA-INF/resources/app/css/styles.cssを探す。どこにも 
styles.cssが格納されていない場合は、404エラーを返す。ここでは 
cache-period属性で静的リソースのキャッシュ時間(3600秒=60分)も設定している。 | 
(5) 
 | 
コントローラ処理のTraceログを出力するインターセプタを設定する。 
/resources配下を除く任意のパスに適用されるように設定する。 | 
(6) 
 | 
ViewResolverの設定を行う。この設定により、例えばコントローラからView名として 
helloが返却された場合には/WEB-INF/views/hello.jspが実行される。 | 
Note
JPA用のブランクプロジェクトを作成した場合は、addInterceptorsの定義として、OpenEntityManagerInViewInterceptorの定義が有効な状態となっている。
@Override protected void addInterceptors(InterceptorRegistry registry) { addInterceptor(registry, traceLoggingInterceptor()); addInterceptor(registry, transactionTokenInterceptor()); addInterceptor(registry, codeListInterceptor()); addWebRequestInterceptor(registry, openEntityManagerInViewInterceptor()); } @Bean public OpenEntityManagerInViewInterceptor openEntityManagerInViewInterceptor() { return new OpenEntityManagerInViewInterceptor(); }
OpenEntityManagerInViewInterceptorは、EntityManagerのライフサイクルの開始と終了を行うInterceptorである。この設定を追加することで、アプリケーション層(Controllerや、Viewクラス)でのLazy Loadが、サポートされる。
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. -->
    <aop:aspectj-autoproxy />
    <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分)も設定している。 | 
(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¶
SpringSecurityConfig.javaには、Spring Securityに関する定義を行う。
src/main/java/com/example/todo/config/web/SpringSecurityConfig.javaは、以下のような設定となっている。package com.example.todo.config.web;
import static org.springframework.security.web.util.matcher.AntPathRequestMatcher.antMatcher;
import java.util.LinkedHashMap;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.AccessDeniedHandlerImpl;
import org.springframework.security.web.access.DelegatingAccessDeniedHandler;
import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler;
import org.springframework.security.web.authentication.AnonymousAuthenticationFilter;
import org.springframework.security.web.csrf.InvalidCsrfTokenException;
import org.springframework.security.web.csrf.MissingCsrfTokenException;
import org.terasoluna.gfw.security.web.logging.UserIdMDCPutFilter;
/**
 * Bean definition to configure SpringSecurity.
 */
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
    /**
     * Configure ignore security pattern.
     * @return Bean of configured {@link WebSecurityCustomizer}
     */
    @Bean
    public WebSecurityCustomizer webSecurityCustomizer() {
        return web -> web.ignoring().requestMatchers(antMatcher("/resources/**"));
    }
    /**
     * Configure {@link SecurityFilterChain} bean.
     * @param http Builder class for setting up authentication and authorization
     * @return Bean of configured {@link SecurityFilterChain}
     * @throws Exception Exception that occurs when setting HttpSecurity
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.formLogin(Customizer.withDefaults());
        http.logout(Customizer.withDefaults());
        http.exceptionHandling(ex -> ex.accessDeniedHandler(accessDeniedHandler()));
        http.addFilterAfter(userIdMDCPutFilter(), AnonymousAuthenticationFilter.class);
        http.sessionManagement(Customizer.withDefaults());
        http.authorizeHttpRequests(authz -> authz.requestMatchers(antMatcher("/**")).permitAll());
        return http.build();
    }
    /**
     * Configure {@link AccessDeniedHandler} bean.
     * @return Bean of configured {@link AccessDeniedHandler}
     */
    @Bean("accessDeniedHandler")
    public AccessDeniedHandler accessDeniedHandler() {
        LinkedHashMap<Class<? extends AccessDeniedException>, AccessDeniedHandler> errorHandlers =
                new LinkedHashMap<>();
        // Invalid CSRF authenticator error handler
        AccessDeniedHandlerImpl invalidCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
        invalidCsrfTokenErrorHandler
                .setErrorPage("/WEB-INF/views/common/error/invalidCsrfTokenError.jsp");
        errorHandlers.put(InvalidCsrfTokenException.class, invalidCsrfTokenErrorHandler);
        // Missing CSRF authenticator error handler
        AccessDeniedHandlerImpl missingCsrfTokenErrorHandler = new AccessDeniedHandlerImpl();
        missingCsrfTokenErrorHandler
                .setErrorPage("/WEB-INF/views/common/error/missingCsrfTokenError.jsp");
        errorHandlers.put(MissingCsrfTokenException.class, missingCsrfTokenErrorHandler);
        // Default error handler
        AccessDeniedHandlerImpl defaultErrorHandler = new AccessDeniedHandlerImpl();
        defaultErrorHandler.setErrorPage("/WEB-INF/views/common/error/accessDeniedError.jsp");
        return new DelegatingAccessDeniedHandler(errorHandlers, defaultErrorHandler);
    }
    /**
     * Configure {@link DefaultWebSecurityExpressionHandler} bean.
     * @return Bean of configured {@link DefaultWebSecurityExpressionHandler}
     */
    @Bean("webSecurityExpressionHandler")
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
        return new DefaultWebSecurityExpressionHandler();
    }
    /**
     * Configure {@link UserIdMDCPutFilter} bean.
     * @return Bean of configured {@link UserIdMDCPutFilter}
     */
    @Bean("userIdMDCPutFilter")
    public UserIdMDCPutFilter userIdMDCPutFilter() {
        return new UserIdMDCPutFilter();
    }
}
spring-security.xmlには、Spring Securityに関する定義を行う。
src/main/resources/META-INF/spring/spring-security.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: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="debug" />
    <logger name="com.example.todo.domain.repository" level="trace" />
    <!-- TERASOLUNA -->
    <logger name="org.terasoluna.gfw" level="info" />
    <!-- (3) -->
    <logger name="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor" level="trace" />
    <logger name="org.terasoluna.gfw.common.exception.ExceptionLogger" level="info" />
    <logger name="org.terasoluna.gfw.common.exception.ExceptionLogger.Monitoring" additivity="false" level="error">
        <appender-ref ref="MONITORING_LOG_FILE" />
    </logger>
    <!-- 3rdparty Loggers -->
    <logger name="org.springframework" level="warn" />
    <logger name="org.springframework.web.servlet" level="info" />
    <logger name="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping" level="trace" />
    <logger name="org.springframework.jdbc.core.JdbcTemplate" level="trace" />
    <!--  REMOVE THIS LINE IF YOU USE JPA
    <logger name="org.hibernate.SQL" additivity="true" level="debug" />
    <logger name="org.hibernate.orm.jdbc.bind" level="trace" />
    <logger name="org.hibernate.engine.transaction" level="debug" />
          REMOVE THIS LINE IF YOU USE JPA  -->
    <!--  REMOVE THIS LINE IF YOU USE MyBatis3
    <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" level="debug" />
          REMOVE THIS LINE IF YOU USE MyBatis3  -->
    <root level="warn">
        <appender-ref ref="STDOUT" />
        <appender-ref ref="APPLICATION_LOG_FILE" />
    </root>
</configuration>
項番  | 
説明  | 
|---|---|
(1) 
 | 
標準出力でログを出力するアペンダを設定。 
 | 
(2) 
 | 
com.example.todoパッケージ以下はdebugレベル以上を出力するように設定。 
 | 
(3) 
 | 
spring-mvc.xmlまたはSpringMvcConfig.javaに設定した 
TraceLoggingInterceptorに出力されるようにtraceレベルで設定。 | 
Note
O/R Mapperを使用するブランクプロジェクトを作成した場合は、それぞれのO/R Mapperのログを出力するロガーが有効な状態となっている。
JPA用のブランクプロジェクト
<logger name="org.hibernate.SQL" additivity="true" level="debug" /> <logger name="org.hibernate.orm.jdbc.bind" level="trace" /> <logger name="org.hibernate.engine.transaction" level="debug" />
MyBatis3用のブランクプロジェクト
<logger name="com.example.todo" level="debug" /> <logger name="com.example.todo.domain.repository" level="trace" /> <!-- omitted --> <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager" level="debug" />