11.1. チュートリアル(Todoアプリケーション JSP編)¶
11.1.1. はじめに¶
11.1.1.1. このチュートリアルで学ぶこと¶
Macchinetta Server Framework (1.x)による基本的なアプリケーションの開発方法
MavenおよびSTS(Eclipse)プロジェクトの構築方法
Macchinetta Server Framework (1.x)のアプリケーションのレイヤ化に従った開発方法
11.1.1.2. 対象読者¶
SpringのDIやAOPに関する基礎的な知識がある
Servlet/JSPを使用してWebアプリケーションを開発したことがある
SQLに関する知識がある
11.1.1.3. 検証環境¶
このチュートリアルは以下の環境で動作確認している。他の環境で実施する際は本書をベースに適宜読み替えて設定していくこと。
種別 |
名前 |
|---|---|
OS |
Windows 11 |
JVM |
Java 17 |
IDE |
Spring Tool Suite 5.0.0.RELEASE (以降「STS」と呼ぶ。設定方法はSTSの設定手順を参照されたい。) |
Build Tool |
Apache Maven 3.9.9 (以降「Maven」と呼ぶ) |
Application Server |
Apache Tomcat 10.1.50 |
Web Browser |
Google Chrome 145 |
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
の2種類を用意している。用途に応じていずれかを選択する。
チュートリアルの進行上、まずはインメモリ実装を試し、その後MyBatis3を選ぶのが円滑である。
11.1.3.1. プロジェクトの作成¶
本チュートリアルを順序通り読み進める場合は、以下のmvn archetype:generateコマンドを実行し、O/R Mapperに依存しないブランクプロジェクトを作成すること。
mvn archetype:generate -B^
-DarchetypeGroupId=com.github.macchinetta.blank^
-DarchetypeArtifactId=macchinetta-web-blank-jsp-archetype^
-DarchetypeVersion=1.11.1.RELEASE^
-DgroupId=com.example.todo^
-DartifactId=todo^
-Dversion=1.0.0-SNAPSHOT
mvn archetype:generate -B^
-DarchetypeGroupId=com.github.macchinetta.blank^
-DarchetypeArtifactId=macchinetta-web-blank-xmlconfig-jsp-archetype^
-DarchetypeVersion=1.11.1.RELEASE^
-DgroupId=com.example.todo^
-DartifactId=todo^
-Dversion=1.0.0-SNAPSHOT
プロジェクトの作成についての詳細は、「ブランクプロジェクトの作成」を参照されたい。
11.1.3.2. プロジェクトのインポート¶
作成したブランクプロジェクトをSTSへインポートする。
プロジェクトのインポートについては、「ブランクプロジェクトのSTSへのインポート」を参照されたい。
11.1.3.3. プロジェクトの構成¶
本チュートリアルで作成するプロジェクトの構成については、「O/R Mapperに依存しないプロジェクトの場合」を参照されたい。
Note
前節の「プロジェクト構成」ではマルチプロジェクトにすることを推奨していたが、本チュートリアルでは、学習容易性を重視しているためシングルプロジェクト構成にしている。
ただし、実プロジェクトで適用する場合は、マルチプロジェクト構成を強く推奨する。
マルチプロジェクトの作成方法は、「ブランクプロジェクトの作成」を参照されたい。
11.1.3.4. 設定ファイルの確認¶
チュートリアルを進める上で必要となる設定の多くは、作成したブランクプロジェクトに既に設定済みの状態である。
チュートリアルを実施するだけであれば、これらの設定の理解は必須ではないが、アプリケーションを動かすためにどのような設定が必要なのかを理解しておくことを推奨する。
アプリケーションを動かすために必要な設定(設定ファイル)の解説については、「設定ファイルの確認」を参照されたい。
Note
まず、手を動かしてTodoアプリケーションを作成したい場合は、設定ファイルの確認は読み飛ばしてもよいが、Todoアプリケーションを作成した後に一読して頂きたい。
11.1.3.5. プロジェクトの動作確認¶
Todoアプリケーションの開発を始める前に、プロジェクトの動作確認を行う。 プロジェクトの動作確認の手順については、「ブランクプロジェクトの動作確認」を参照されたい。
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.time.LocalDate;
public class Todo implements Serializable {
private static final long serialVersionUID = 1L;
private String todoId;
private String todoTitle;
private boolean finished;
private LocalDate 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 LocalDate getCreatedAt() {
return createdAt;
}
public void setCreatedAt(LocalDate 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 com.example.todo.domain.model.Todo;
public interface TodoRepository {
Todo findById(String todoId);
Collection<Todo> findAll();
void create(Todo todo);
boolean update(Todo todo);
void delete(Todo todo);
long countByFinished(boolean finished);
}
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.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 Todo findById(String todoId) {
return 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.time.LocalDate;
import java.util.Collection;
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();
LocalDate createdAt = LocalDate.now();
todo.setTodoId(todoId);
todo.setCreatedAt(createdAt);
todo.setFinished(false);
todoRepository.create(todo);
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);
return todo;
}
@Override
public void delete(String todoId) {
Todo todo = findOne(todoId);
todoRepository.delete(todo);
}
// (8)
private Todo findOne(String todoId) {
Todo todo = todoRepository.findById(todoId);
if (todo == null) {
ResultMessages messages = ResultMessages.error();
messages.add(ResultMessage
.fromText("[E404] The requested Todo is not found. (id=" + todoId + ")"));
// (9)
throw new ResourceNotFoundException(messages);
}
return todo;
}
}
項番 |
説明 |
|---|---|
(1)
|
Serviceとしてcomponent-scanの対象とするため、クラスレベルに
@Serviceアノテーションをつける。 |
(2)
|
クラスレベルに、
@Transactionalアノテーションをつけることで、公開メソッドをすべてトランザクション管理する。アノテーションを付与することで、メソッド開始時にトランザクションを開始、メソッド正常終了時にトランザクションのコミットが行われる。
また、途中で非検査例外が発生した場合は、トランザクションがロールバックされる。
データベースを使用しない場合は、
@Transactionalアノテーションは不要である。 |
(3)
|
@Injectアノテーションで、TodoRepositoryの実装をインジェクションする。 |
(4)
|
参照のみ行う処理に関しては、
readOnly=trueをつける。O/R Mapperによっては、この設定により、参照時のトランザクション制御の最適化が行われる。
データベースを使用しない場合は、
@Transactionalアノテーションは不要である。 |
(5)
|
結果メッセージを格納するクラスとして、共通ライブラリで用意されている
org.terasoluna.gfw.common.message.ResultMessageを用いる。今回は、エラーメッセージを例外に追加する際に、
ResultMessages.error()でメッセージ種別を指定して、ResultMessageを追加している。 |
(6)
|
業務エラーが発生した場合、共通ライブラリで用意されている
org.terasoluna.gfw.common.exception.BusinessExceptionをスローする。 |
(7)
|
一意性のある値を生成するために、UUIDを使用している。データベースのシーケンスを用いてもよい。
|
(8)
|
1件取得は、
finishメソッドでもdeleteメソッドでも使用するため、メソッドとして用意しておく(interfaceに公開しても良い)。 |
(9)
|
取得したデータを返す。対象のデータが存在しない場合は共通ライブラリで用意されている
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オブジェクトをデータベースに永続化するためのインフラストラクチャ層の実装方法について説明する。
本チュートリアルでは、MyBatis3を使用したインフラストラクチャ層の実装方法について説明する。
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を作成する方法について説明する。
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を使用したインフラストラクチャ層の作成が完了となる。
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.6. おわりに¶
このチュートリアルでは、以下の内容を学習した。
Macchinetta Server Framework (1.x)による基本的なアプリケーションの開発方法
MavenおよびSTS(Eclipse)プロジェクトの構築方法
Macchinetta Server Framework (1.x)のアプリケーションのレイヤ化に従った開発方法
POJO(+ Spring)を使用したドメイン層の実装
POJO(+ Spring MVC)とJSPタグライブラリを使用したアプリケーション層の実装
MyBatis3を使用したインフラストラクチャ層の実装
O/R Mapperを使用しないインフラストラクチャ層の実装
本チュートリアルで作成したTODO管理アプリケーションには、以下の改善点がある。 アプリケーションの修正を学習課題として、ガイドライン中の該当する説明を参照されたい。
プロパティ(未完了TODOの上限数)を外部化する → プロパティ管理
メッセージを外部化する → メッセージ管理
ページネーション機能を追加する → ページネーション
例外ハンドリングを加える → 例外ハンドリング
二重送信を防止する(トランザクショントークンチェックを追加する) → 二重送信防止
システム日時の取得元を変更する → システム時刻