チュートリアル(Todoアプリケーション)
********************************************************************************
.. contents:: 目次
:depth: 3
:local:
はじめに
================================================================================
このチュートリアルで学ぶこと
--------------------------------------------------------------------------------
* TERASOLUNA Global Frameworkによる基本的なアプリケーションの開発方法およびEclipseプロジェクトの構築方法
* TERASOLUNA Global Frameworkの :doc:`../Overview/ApplicationLayering` に従った開発方法
対象読者
--------------------------------------------------------------------------------
* SpringのDIやAOPに関する基礎的な知識がある
* Servlet/JSPを使用してWebアプリケーションを開発したことがある
* SQLに関する知識がある
検証環境
--------------------------------------------------------------------------------
このチュートリアルは以下の環境で動作確認している。他の環境で実施する際は本書をベースに適宜読み替えて設定していくこと。
.. list-table::
:header-rows: 1
:widths: 15 85
* - 種別
- 名前
* - OS
- Windows7 64bit
* - JVM
- Java 1.6
* - IDE
- Spring Tool Suite Version: 3.2.0.RELEASE, Build Id: 201303060821 (以下STS) Build Maven 3.0.4 (STS付属)
* - Application Server
- VMWare vFabric tc Server Developer Edition v2.8 (STS付属)
* - Web Browser
- Google Chrome 27.0.1453.94 m
|
作成するアプリケーションの説明
================================================================================
アプリケーションの概要
--------------------------------------------------------------------------------
TODOを管理するアプリケーションを作成する。TODOの一覧表示、TODOの登録、TODOの完了、TODOの削除を行える。
.. figure:: ./images/image001.png
:width: 60%
.. _app-requirement:
アプリケーションの業務要件
--------------------------------------------------------------------------------
.. list-table::
:header-rows: 1
:widths: 10 90
* - ルールID
- 説明
* - B01
- 未完のTODOは5件までしか登録できない
* - B02
- 完了済みのTODOは完了できない
|
.. note::
本要件は学習のためのもので、現実的なTODO管理アプリケーションとしては適切ではない。
|
アプリケーションの画面遷移
--------------------------------------------------------------------------------
.. figure:: ./images/image002.png
:width: 60%
.. list-table::
:header-rows: 1
:widths: 10 20 15 15 40
* - 項番
- プロセス名
- HTTPメソッド
- URL
- 説明
* - 1
- Show all TODO
- GET
- /todo/list
-
* - 2
- Create TODO
- POST
- /todo/create
- 作成完了後1へリダイレクト
* - 3
- Finish TODO
- POST
- /todo/finish
- 作成完了後1へリダイレクト
* - 4
- Delete TODO
- POST
- /todo/delete
- 作成完了後1へリダイレクト
Show all TODO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* TODOを全件表示する
* 未完了のTODOに対しては”Finish”と”Delete”用のボタンが付く
* 完了のTODOは打ち消し線で装飾する
* TODOの件名のみ
Create TODO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* フォームから送信されたTODOを保存する
* TODOの件名は1文字以上30文字以下であること
* :ref:`app-requirement` のB01を満たさない場合はエラーコードE001でビジネス例外をスローする
Finish TODO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* フォームから送信されたtodoIdに対応するTODOを完了済みにする
* :ref:`app-requirement` のB02を満たさない場合はエラーコードE002でビジネス例外をスローする
* 該当するTODOが存在しない場合はエラーコードE404でビジネス例外をスローする
Delete TODO
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
* フォームから送信されたtodoIdに対応するTODOを削除する
* 該当するTODOが存在しない場合はエラーコードE404でビジネス例外をスローする
エラーメッセージ一覧
--------------------------------------------------------------------------------
.. list-table::
:header-rows: 1
:widths: 15 45 40
* - エラーコード
- メッセージ
- 置換パラメータ
* - 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
|
環境構築
================================================================================
プロジェクトの作成
--------------------------------------------------------------------------------
「File」->「Other」->「Maven」->「Maven Project」を選択して「Next」。
.. figure:: ./images/image004.jpg
:width: 60%
「Create a simple project」にチェックを入れて「Next」。
.. figure:: ./images/image006.jpg
:width: 60%
.. list-table::
:widths: 25 75
:stub-columns: 1
* - Group Id:
- org.terasoluna.tutorial
* - Artifact Id:
- todo
* - Packaging:
- war
で「Finish」
.. figure:: ./images/image008.jpg
:width: 60%
以下のようなプロジェクトが作成される。
.. figure:: ./images/image009.png
:width: 40%
|
.. note::
パッケージ構成上、Package PresentaionをHierarchicalにしたほうが見通しがよい。
.. figure:: ./images/presentation-hierarchical.png
:width: 80%
Mavenの設定
--------------------------------------------------------------------------------
pom.xmlを以下のように変更する。
Mavenの知識がない場合は、pom.xmlをコピーするだけで、解説は読み飛ばしてよい。
.. code-block:: xml
:emphasize-lines: 9-83
4.0.0
org.terasoluna.tutorial
todo
0.0.1-SNAPSHOT
war
org.terasoluna.gfw
terasoluna-gfw-parent
1.0.0.RELEASE
true
false
terasoluna-gfw-releases
http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-releases/
false
true
terasoluna-gfw-snapshots
http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-snapshots/
true
false
terasoluna-gfw-3rdparty
http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-3rdparty/
org.terasoluna.gfw
terasoluna-gfw-web
org.terasoluna.gfw
terasoluna-gfw-security-web
org.terasoluna.gfw
terasoluna-gfw-recommended-dependencies
pom
org.apache.tomcat
tomcat-servlet-api
7.0.40
provided
org.apache.tomcat
tomcat-jsp-api
7.0.40
provided
pom.xmlを編集した後、プロジェクト名を右クリックし、「Maven」->「Update Project」をクリックし、
.. figure:: ./images/update-project.png
:width: 60%
「OK」ボタンをクリックする。
以下のように"JRE System Library"のバージョンが"[JavaSE-1.6]"になっていることを確認する。
.. figure:: ./images/check-jre.jpg
:width: 30%
|
.. note::
JDKのバージョンを7に変更したい場合は、pom.xmlの ```` に ``1.7`` を設定した後、
「Update Project」を実施すること。
.. code-block:: xml
:emphasize-lines: 4-6
1.7
Mavenの知識がある場合は、以下の解説を確認すること。
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | TERASOLUNA Global Frameworkの親pomファイルを指定する。
| これにより、terasoluna-parentで定義されているライブラリは、versionを指定しなくても、dependencyに追加することができる。
* - | (2)
- | TERASOLUNA Global Frameworkを使うためのMavenレポジトリのURLを指定する。
* - | (3)
- | TERASOLUNA Global Frameworkの共通ライブラリ(Web用)をdependencyに追加する。
* - | (4)
- | TERASOLUNA Global Frameworkの共通ライブラリ(セキュリティWeb用)をdependencyに追加する。
* - | (5)
- | TERASOLUNA Global Frameworkで推奨されるライブラリ群を追加する。
| terasoluna-gfw-recommended-dependenciesはただのpomファイルであるため ``pom`` を記述する必要がある。
* - | (6)
- | Servlet/JSP APIをdependencyに追加する。Servlet3に対応する必要がある。
| これらはscope=provided(本来APサーバーから提供される)であり、warには含まれないが、eclipse上でコンパイルするためには明示的にdependencyに追加する必要がある。
| (尚、dependency名がtomcat-xxxとなっているが、内包するクラスのパッケージはjavax.servletであるためtomcatに依存しているわけではない。)
|
.. note:: Proxyサーバーを介してインターネットアクセスする必要がある場合は、
/.m2/settings.xmlに以下のような設定を行う。
(Windows7の場合C:\\Users\\\\.m2\settings.xml)
.. code-block:: xml
true
[Proxy Server Protocol (http)]
[Proxy Server Port]
[Proxy Server Host]
[Username]
[Password]
|
プロジェクト構成
--------------------------------------------------------------------------------
今後作成していくプロジェクトの構成について、以下に示す。
.. code-block:: console
src
└main
├java
│ └todo
│ ├ app ... アプリケーション層を格納
│ │ └todo ... todo管理業務に関わるクラスを格納
│ └domain ... ドメイン層を格納
│ ├model ... Domain Objectを格納
│ ├repository ... Repositoryを格納
│ │ └todo ... Todo用Repository
│ └service ... Serviceを格納
│ └todo ... TODO業務Service
├resources
│ └META-INF
│ └spring ... spring関連の設定ファイルを格納
└wepapp
└WEB-INF
└views ... jspを格納
順番に作成していくので、最初に上記構成を用意する必要はない。
|
.. note::
:ref:`前節の「プロジェクト構成」 ` ではマルチプロジェクトにすることを推奨していたが、
本チュートリアルでは、学習容易性を重視しているためシングルプロジェクト構成にしている。ただし、実プロジェクトで適用する場合は、
マルチプロジェクト構成を強く推奨する。
|
設定ファイルの作成
--------------------------------------------------------------------------------
web.xmlの設定
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/main/webapp/WEB-INF/web.xmlを作成して、サーブレットやフィルタの定義を行う。
WEB-INFフォルダは「New」->「Folder」で新規作成すること。
.. figure:: ./images/image010.jpg
:width: 40%
「New」->「File」でweb.xmlを作成し、
.. figure:: ./images/image011.jpg
:width: 40%
内容は以下のように記述する。
.. code-block:: xml
org.springframework.web.context.ContextLoaderListener
contextConfigLocation
classpath*:META-INF/spring/applicationContext.xml
CharacterEncodingFilter
org.springframework.web.filter.CharacterEncodingFilter
encoding
UTF-8
forceEncoding
true
CharacterEncodingFilter
/*
appServlet
org.springframework.web.servlet.DispatcherServlet
contextConfigLocation
classpath*:META-INF/spring/spring-mvc.xml
1
appServlet
/
*.jsp
false
UTF-8
false
/WEB-INF/views/common/include.jsp
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | Servlet3.0を使用するための宣言。
* - | (2)
- | ``ContextLoaderListener`` の定義。このリスナーが作成する ``ApplicationContext`` がルートコンテキストとなる。
| Bean定義ファイルのパスをclasspath直下のMETA-INF/spring/applicationContext.xmlとする。
* - | (3)
- | ``CharacterEncodingFilter`` の定義。リクエストとレスポンスの文字コードをUTF-8にするための設定。
* - | (4)
- | Spring MVCのエントリポイントとなるDispatcherServletの定義。
| Spring MVCで使用するBean定義ファイルのパスをclasspath直下のMETA-INF/spring/spring-mvc.xmlとする。
| ここで作成される ``ApplicationContext`` は(2)で作成される ``ApplicatnionContext`` の子となる。
* - | (5)
- | JSP共通でincludeするJSPの定義。任意のJSP(*.jsp)に対して、/WEB-INF/views/common/include.jspをincludeする。
.. figure:: ./images/image013.png
:width: 40%
|
共通JSPの設定
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/main/webapp/WEB-INF/views/common/include.jspに各JSP共通でincludeする内容を記述する。taglibの定義を共通的に行う。
views/commonフォルダ、include.jspファイルを作成し、以下のように記述する。
.. code-block:: jsp
<%@ page session="false"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%>
<%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%>
<%@ taglib uri="http://terasoluna.org/functions" prefix="f"%>
<%@ taglib uri="http://terasoluna.org/tags" prefix="t"%>
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 標準タグライブラリを定義する。
* - | (2)
- | Spring MVC用タグライブラリを定義する。
* - | (3)
- | Spring Security用タグライブラリを定義する。(ただし本チュートリアルでは使用しない。)
* - | (4)
- | 共通ライブラリで提供されている、EL関数、タグライブラリを定義する。
.. figure:: ./images/image014.png
:width: 40%
|
Bean定義ファイルの設定
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Bean定義ファイルは、以下4種類のファイルを作成する。
* applicationContext.xml
* todo-domain.xml
* todo-infra.xml
* spring-mvc.xml
上から順に説明する。
applicationContext.xml
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
src/main/resources/META-INF/spring/applicationContext.xmlに、Todoアプリ全体に関わる設定を行う。
META-INF/springフォルダを作成し、「New」->「Spring Bean Configuration File」でapplicationContext.xmlを作成する。
.. figure:: ./images/image016.jpg
:width: 40%
.. code-block:: xml
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 次に説明する、ドメイン層に関するBean定義ファイルをimportする。
* - | (2)
- | プロパティファイルの読み込み設定を行う。
| src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。
| この設定により、プロパティファイルの値をBean定義ファイル内で${propertyName}形式で埋め込んだり、Javaクラスに@Value("${propertyName}")でインジェクションすることができる。
* - | (3)
- | Bean変換用ライブラリDozerのMapperを定義する。
| (今回は使用しないが、マッピング用XMLファイルを定義する場合はsrc/main/resources/META-INF/dozer/xxx-mapping.xmlという形式でマッピングファイルを作成すること。
| マッピングファイルに関して `Dozerマニュアル `_ を参照されたい。)
.. figure:: ./images/image018.png
:width: 40%
|
.. note::
上記内容をコピーせず手入力を行う場合は、「namespace」タブを開き、「Configure Namspecse」で「beans」と「context」にチェックを入れること。
また「Namespace Versions」でバージョンなしのxsdファイルを選択することを推奨する。
.. figure:: ./images/image021.jpg
:width: 60%
:align: center
これにより、XML編集時にCtrl+Spaceを使用して入力を補完することができる。
.. figure:: ./images/image023.png
:width: 60%
:align: center
またバージョンを指定しないことにより、常にjarに含まれる最新のxsdが使用される。
|
todo-domain.xml
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
src/main/resources/META-INF/spring/todo-domain.xmlに、ドメイン層に関わる設定を行う。
META-INF/spring直下において、「New」->「Spring Bean Configuration File」でtodo-domain.xmlを作成する。
.. code-block:: xml
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 次に説明する、インフラストラクチャ層に関するBean定義ファイルをimportする。
* - | (2)
- | ドメイン層のクラスを管理するtodo.domainパッケージ配下をcomponent-scan対象とする。
| これにより、todo.domainパッケージ配下のクラスに ``@Repository`` , ``@Service`` , ``@Controller``, ``@Component`` などのアノテーションを付けることで、DI対象にできる。
.. figure:: ./images/image024.png
:width: 40%
|
todo-infra.xml
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
src/main/resources/META-INF/spring/todo-infra.xmlに、インフラストラクチャ層に関するBean定義を行う。
ここではDBの設定などを行うが、本節ではDBを使用しないため、以下のように空定義で良い。次節でBean定義を行う。
META-INF/spring直下において、「New」->「Spring Bean Configuration File」でtodo-infra.xmlを作成する。
.. code-block:: xml
.. figure:: ./images/image025.png
:width: 40%
|
.. note:: todo-domain.xml, todo-infra.xmlの内容もすべてapplicationContext.xmlに記述すればよいように思えるかもしれないが、
役割(層)ごとにファイルを分割することを推奨する。どこに何が定義されているか想像しやすく、メンテナンス性が向上するからである。
今回のチュートリアルのような小さなアプリケーションでは効果がない。しかし、アプリケーションの規模が大きくなるにつれ、効果が大きくなる。
|
spring-mvc.xml
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
src/main/resources/META-INF/spring/spring-mvc.xmlに、Spring MVCに関する定義を行う。
.. code-block:: xml
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | Spring MVCのアノテーションベースのデフォルト設定を行う。
* - | (2)
- | アプリケーション層のクラスを管理するtodo.appパッケージ配下をcomponent-scan対象とする。
* - | (3)
- | 静的リソース(css, images, jsなど)アクセスのための設定を行う。
| mapping属性にURLのパスを、location属性に物理的なパスの設定を行う。
| この設定の場合/rerources/css/styles.cssに対してリクエストが来た場合、WEB-INF/resources/css/styles.cssを探し、見つからなければクラスパス上(src/main/resourcesやjar内)のresources/css/style.cssを探す。
| WEB-INF/resources/css/styles.cssが見つからなければ、404エラーを返す。
| ここではcache-period属性で静的リソースのキャッシュ時間(3600秒=60分)も設定している。
| ``cache-period="3600"`` と設定しても良いが、60分であることを明示するために `SpEL `_ を使用して ``cache-period="#{60 * 60}"`` と書く方が分かりやすい。
| 尚、本チュートリアルでは静的リソースは使用しない。
* - | (4)
- | コントローラ処理のTraceログを出力するインターセプタを設定する。/resources以下を除く任意のパスに適用されるように設定する。
* - | (5)
- | ViewResolverの設定を行う。この設定により、例えばコントローラからview名”hello”が返却された場合には/WEB-INF/views/hello.jspが実行される。
.. figure:: ./images/image026.png
:width: 40%
|
.. note:: 上記内容をコピーせず手入力を行う場合は、todo-domain.xmlで説明した操作に加え、「mvc」と「util」にもチェックを入れること。
.. figure:: ./images/image028.png
:width: 60%
:align: center
|
logback.xmlの設定
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
src/main/resources/logback.xmlに、logbackによるログの出力設定を行う。
src/main/resources/直下において、「New」->「File」でlogback.xmlを作成する。
.. code-block:: xml
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 標準出力でログを出力するアペンダを設定する。
* - | (2)
- | todoパッケージ以下はdebugレベル以上を出力するように設定する。
* - | (3)
- | 共通ライブラリのログレベルをinfoにする。
* - | (4)
- | spring-mvc.xmlに設定した ``TraceLoggingInterceptor`` に出力されるようにtraceレベルで設定する。
* - | (5)
- | Springframeworkのログはwarnレベル以上を出力するように設定する。
* - | (6)
- | Springframeworkのログの中でもorg.springframework.web.servlet以下は開発中に有益なログを出力するためinfoレベル以上で設定する。
* - | (7)
- | デフォルトはwarnレベル以上を出力するように設定する。
.. figure:: ./images/image029.png
:width: 40%
|
動作確認
--------------------------------------------------------------------------------
Todoアプリケーションの開発を始める前に、SpringMVCのHelloWorldアプリケーションを作成して、動作確認を行う。「New」->「Class」で
.. list-table::
:widths: 25 75
:stub-columns: 1
* - Package:
- todo.app.hello
* - Name:
- HelloController
でtodo.app.hello.HelloControllerを作成する。
.. figure:: ./images/image030.jpg
:width: 40%
HelloControllerを以下のように編集する。
.. code-block:: java
package todo.app.hello;
import java.util.Date;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
// (1)
@Controller
public class HelloController {
// (2)
private static final Logger logger = LoggerFactory
.getLogger(HelloController.class);
// (3)
@RequestMapping("/")
public String hello(Model model) {
Date now = new Date();
// (4)
logger.debug("hello {}", now);
// (5)
model.addAttribute("now", now);
// (6)
return "hello";
}
}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | Controllerとしてcomponent-scanの対象とするため、クラスレベルに ``@Controller`` アノテーションをつける。
* - | (2)
- | ロガーの生成を行う。ロガーの実装はlogbackのものであるが、APIはSLF4Jのものであるため、``org.slf4j.Logger`` を使用すること。
* - | (3)
- | ``@RequestMapping`` で”/”(ルート)へのアクセスに対するメソッドのマッピングを設定する。
* - | (4)
- | debugログを出力する。”{}”はプレースホルダである。
* - | (5)
- | 画面へ日付を渡すためにModelに”now”という名前でDateオブジェクトを追加する。
* - | (6)
- | view名としてhelloを返す。ViewResolverの設定により、WEB-INF/views/hello.jspが出力される。
次にview(jsp)を作成する。src/main/webapp/WEB-INF/views/hello.jspを作成して、以下のように記述する。
.. code-block:: jsp
Hello World!
Hello World!
Today is
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | Controllerから渡された”now”を表示する。ここでは ```` タグを用いて日付フォーマットを行っている。
パッケージプロジェクト名”todo”を右クリックして「Run As」->「Run on Server」
.. figure:: ./images/image031.jpg
:width: 40%
実行したいAPサーバー(ここではVMWare vFabric tc Server Developer Edition v2.8)を選び
「Next」をクリック
.. figure:: ./images/image032.jpg
:width: 40%
todoが「Configured」に含まれていることを確認して「Finish」をクリックしてサーバーを起動する。
.. figure:: ./images/image033.jpg
:width: 40%
起動すると以下のようなログが出力される。”/”というパスに対して ``todo.app.hello.HelloController`` のhelloメソッドがマッピングされていることが分かる。
.. code-block:: guess
:emphasize-lines: 3
2013-06-14 14:26:54 [localhost-startStop-1] [WARN ] [org.dozer.config.GlobalSettings ] - Dozer configuration file not found: dozer.properties. Using defaults for all Dozer global properties.
2013-06-14 14:26:54 [localhost-startStop-1] [INFO ] [o.springframework.web.servlet.DispatcherServlet ] - FrameworkServlet 'appServlet': initialization started
2013-06-14 14:26:54 [localhost-startStop-1] [INFO ] [o.s.w.s.m.m.a.RequestMappingHandlerMapping ] - Mapped "{[/],methods=[],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String todo.app.hello.HelloController.hello(org.springframework.ui.Model)
2013-06-14 14:26:55 [localhost-startStop-1] [INFO ] [o.s.web.servlet.handler.SimpleUrlHandlerMapping ] - Mapped URL path [/resources/**] onto handler 'org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0'
2013-06-14 14:26:55 [localhost-startStop-1] [INFO ] [o.springframework.web.servlet.DispatcherServlet ] - FrameworkServlet 'appServlet': initialization completed in 986 ms
|
.. note:: 一行目のWARNログは無視しても良い。抑止したい場合はsrc/main/resourcesに空のdozer.propertiesを作成すること。
ブラウザでhttp://localhost:8080/todo
にアクセスすると、以下のように表示される。
.. figure:: ./images/image034.png
:width: 40%
コンソールを見ると ``TraceLoggingInterceptor`` によるTRACEログとControllerで実装したdebugログが出力されていることがわかる。
.. code-block:: guess
2013-06-14 15:40:59 [tomcat-http--3] [TRACE] [o.t.gfw.web.logging.TraceLoggingInterceptor ] - [START CONTROLLER] HelloController.hello(Model)
2013-06-14 15:40:59 [tomcat-http--3] [DEBUG] [todo.app.hello.HelloController ] - hello Fri Jun 14 15:40:59 JST 2013
2013-06-14 15:40:59 [tomcat-http--3] [TRACE] [o.t.gfw.web.logging.TraceLoggingInterceptor ] - [END CONTROLLER ] HelloController.hello(Model)-> view=hello, model={now=Fri Jun 14 15:40:59 JST 2013}
2013-06-14 15:40:59 [tomcat-http--3] [TRACE] [o.t.gfw.web.logging.TraceLoggingInterceptor ] - [HANDLING TIME ] HelloController.hello(Model)-> 15,043,704 ns
|
.. note:: ``TraceLoggingInterceptor`` はControllerの開始、終了でログを出力する。終了時にはViewとModelの情報および処理時間を出力する。
ログの確認後は、HelloController, hello.jspの2ファイルを削除しても構わない。
|
Todoアプリケーションの作成
================================================================================
| Todoアプリケーションを作成する。作成する順は、以下の通りである。
* ドメイン層(+ インフラストラクチャ層)
* Domain Object作成
* Repository作成
* Service作成
* アプリケーション層
* Controller作成
* Form作成
* View作成
なお、本節では、Todoの保存にDBを使用しない。DBを使用するRepositoryの作成は、\ :ref:`tutorial-todo_infra`\ で行う。
|
ドメイン層の作成
--------------------------------------------------------------------------------
Domain Objectの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ドメインオブジェクトに必要なプロパティは、
#. ID
#. タイトル
#. 完了フラグ
#. 作成日
である。
以下の、Domainオブジェクトを作成する。
FQCNは、todo.domain.model.Todoとする。JavaBeanとして実装すればよい。
.. list-table::
:widths: 25 75
:stub-columns: 1
* - Package:
- todo.domain.model
* - Name:
- Todo
* - Interfaces:
- java.io.Serializable
.. figure:: ./images/image057.png
:width: 40%
.. code-block:: java
package 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;
}
}
.. figure:: ./images/image058.png
:width: 40%
|
.. note::
Getter/Setterは自動生成できる。フィールドを定義した後、右クリックで「Source」->「Generate Getter and Setters…」
.. figure:: ./images/image059.png
:width: 40%
serialVersionUID以外を選択して「OK」
.. figure:: ./images/image060.png
:width: 40%
Repositoryの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
今回のアプリケーションで、必要なTODOオブジェクトに対するCRUD系操作は、
* TODOの1件取得
* TODOの全件取得
* TODOの1件削除
* TODOの1件更新
* 完了済みTODO件数の取得
である。これらの操作を定義するインタフェースTodoRepositoryを作成する。
FQCNは、todo.domain.repository.todo.TodoRepositoryとする。
.. code-block:: java
package todo.domain.repository.todo;
import java.util.Collection;
import todo.domain.model.Todo;
public interface TodoRepository {
Todo findOne(String todoId);
Collection findAll();
Todo save(Todo todo);
void delete(Todo todo);
long countByFinished(boolean finished);
}
.. figure:: ./images/image061.png
:width: 40%
\
.. note::
ここで、TodoRepositoryの汎用性を上げるため、「完了済み件数の取得」ではなく、「完了状態がxである件数」を取得するメソッドとして定義した。
RepositoryImplの作成(インフラストラクチャ層)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| 説明を単純化するため、Repositotyの実装は、Mapを使ったインメモリ実装とする。
| DBを使用したRepositoryの実装は、\ :ref:`tutorial-todo_infra`\ で説明する。
| FQCNはtodo.domain.repository.todo.TodoRepositoryImplとする。クラスレベルに、\ ``@Repository``\ アノテーションをつけること。
.. code-block:: java
package todo.domain.repository.todo;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.stereotype.Repository;
import todo.domain.model.Todo;
@Repository // (1)
public class TodoRepositoryImpl implements TodoRepository {
private static final Map TODO_MAP = new ConcurrentHashMap();
@Override
public Todo findOne(String todoId) {
return TODO_MAP.get(todoId);
}
@Override
public Collection findAll() {
return TODO_MAP.values();
}
@Override
public Todo save(Todo todo) {
return TODO_MAP.put(todo.getTodoId(), todo);
}
@Override
public void delete(Todo todo) {
TODO_MAP.remove(todo.getTodoId());
}
@Override
public long countByFinished(boolean finished) {
long count = 0;
for (Map.Entry e : TODO_MAP.entrySet()) {
Todo todo = e.getValue();
if (finished == todo.isFinished()) {
count++;
}
}
return count;
}
}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | Repositoryとして、component-scan対象とするため、クラスレベルに\ ``@Repository``\ アノテーションをつける。
Repositoryは、業務ルールを含まないので、保存先(この場合は、Map)への出し入れに終始することに注意する。
.. figure:: ./images/image062.png
:width: 40%
\
.. note::
完全に層別パッケージを分けるのであれば、インフラストラクチャ層のクラスは、todo.infrastructure以下に作成した方が良い。
ただし、通常のプロジェクトでは、インフラストラクチャ層が変更されることを前提としていない(そのような前提で進めるプロジェクトは、少ない)。
そこで、作業効率向上のために、ドメイン層のrepositotyと同じ階層に、RepositoryImplを作成しても良い。
Serviceの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
業務処理を実装する。必要な処理は、
* Todoの全件取得
* Todoの新規作成
* Todoの完了
* Todoの削除
である。まずは、TodoServiceインタフェースを作成して、これらを定義する。
FQCNは、todo.domain.serivce.todo.TodoServiceとする。
.. code-block:: java
package todo.domain.service.todo;
import java.util.Collection;
import todo.domain.model.Todo;
public interface TodoService {
Collection findAll();
Todo create(Todo todo);
Todo finish(String todoId);
void delete(String todoId);
}
必要な処理と、実装するメソッドの対応は、以下の通りである。
* Todoの全件取得→findAllメソッド
* Todoの新規作成→createメソッド
* Todoの完了→finishメソッド
* Todoの削除→deleteメソッド
.. figure:: ./images/image063.png
:width: 40%
実装クラスのFQCNを、todo.domain.service.TodoServiceImplとする。
.. code-block:: java
package todo.domain.service.todo;
import java.util.Collection;
import java.util.Date;
import java.util.UUID;
import javax.inject.Inject;
import org.springframework.stereotype.Service;
//import org.springframework.transaction.annotation.Transactional;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;
import todo.domain.model.Todo;
import todo.domain.repository.todo.TodoRepository;
@Service// (1)
// @Transactional // (2)l
public class TodoServiceImpl implements TodoService {
@Inject// (3)
protected TodoRepository todoRepository;
private static final long MAX_UNFINISHED_COUNT = 5;
// (4)
public Todo findOne(String todoId) {
Todo todo = todoRepository.findOne(todoId);
if (todo == null) {
// (5)
ResultMessages messages = ResultMessages.error();
messages.add(ResultMessage
.fromText("[E404] The requested Todo is not found. (id="
+ todoId + ")"));
// (6)
throw new ResourceNotFoundException(messages);
}
return todo;
}
@Override
public Collection findAll() {
return todoRepository.findAll();
}
@Override
public Todo create(Todo todo) {
long unfinishedCount = todoRepository.countByFinished(false);
if (unfinishedCount >= MAX_UNFINISHED_COUNT) {
ResultMessages messages = ResultMessages.error();
messages.add(ResultMessage
.fromText("[E001] The count of un-finished Todo must not be over "
+ MAX_UNFINISHED_COUNT + "."));
// (7)
throw new BusinessException(messages);
}
// (8)
String todoId = UUID.randomUUID().toString();
Date createdAt = new Date();
todo.setTodoId(todoId);
todo.setCreatedAt(createdAt);
todo.setFinished(false);
todoRepository.save(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.save(todo);
return todo;
}
@Override
public void delete(String todoId) {
Todo todo = findOne(todoId);
todoRepository.delete(todo);
}
}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | Serviceとしてcomponent-scanの対象とするため、クラスレベルに\ ``@Service``\ アノテーションをつける。
* - | (2)
- | 今回の実装では、DBを使用しないため、トランザクション管理は不要であるが、DBを使用する場合は、クラスレベルに\ ``@Transactional``\ アノテーションをつけること。
| 詳しくは、\ :ref:`tutorial-todo_infra`\ で説明する。
* - | (3)
- | \ ``@Inject``\ アノテーションで、TodoRepositoryの実装をインジェクションする。
* - | (4)
- | 1件取得は、finishメソッドでもdeleteメソッドでも使用するため、メソッドとして用意しておく(interfaceに公開しても良い)。
* - | (5)
- | 結果メッセージを格納するクラスとして、共通ライブラリで用意されているorg.terasoluna.gfw.common.message.ResultMessageを用いる。
| 今回は、Errorメッセージをスローするために、ResultMessages.error()でメッセージ種別を指定して、ResultMessageを追加している。
* - | (6)
- | 対象のデータが存在しない場合、共通ライブラリで用意されているorg.terasoluna.gfw.common.exception.ResourceNotFoundExceptionをスローする。
* - | (7)
- | 業務エラーが発生した場合、共通ライブラリで用意されているorg.terasoluna.gfw.common.exception.BusinessExceptionをスローする。
* - | (8)
- | 一意性のある値を生成するために、UUIDを使用している。DBのシーケンスを用いてもよい。
\
.. note::
本節では、説明を単純化するため、エラーメッセージをハードコードしているが、メンテナンスの観点で本来は好ましくない。
通常、メッセージは、プロパティファイルに外部化することが推奨される。
プロパティファイルに外部化する方法は、\ :doc:`../ArchitectureInDetail/PropertyManagement`\ を参照されたい。
.. figure:: ./images/image064.png
:width: 40%
ServiceのJUnit作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TBD
アプリケーション層の作成
--------------------------------------------------------------------------------
ドメイン層の実装が完了したので、次はドメイン層を利用して、アプリケーション層の作成に取り掛かる。
Controllerの作成
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
| まずは、todo管理業務にかかわる画面遷移を、制御するTodoControllerを作成する。
| FQCNはtodo.app.todo.TodoControllerとする。上位パッケージがドメイン層とは異なるので注意すること。
.. code-block:: java
package todo.app.todo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller // (1)
@RequestMapping("todo") // (2)
public class TodoController {
}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | Controllerとしてcomponent-scanの対象とするため、クラスレベルに、\ ``@Controller``\ アノテーションをつける。
* - | (2)
- | TodoControllerが扱う画面遷移のパスを、すべて/todo配下にするため、クラスレベルに@RequestMapping(“todo”)を設定する。
.. figure:: ./images/image065.png
:width: 40%
Show all TODO
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
この画面では、
* 新規作成フォームの表示
* TODOの全件表示
を行う。
Formの作成
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Formには、タイトル情報があればよいので、次のようなJavaBeanになる。FQCNは、todo.app.todo.TodoFormとする。
.. code-block:: java
package 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;
}
}
.. figure:: ./images/image066.png
:width: 40%
Controllerの実装
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
TodoControllerに、setUpFormメソッドと、listメソッドを実装する。
.. code-block:: java
:emphasize-lines: 18-32
package todo.app.todo;
import java.util.Collection;
import javax.inject.Inject;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import todo.domain.model.Todo;
import todo.domain.service.todo.TodoService;
@Controller
@RequestMapping("todo")
public class TodoController {
@Inject // (3)
protected TodoService todoService;
@ModelAttribute // (4)
public TodoForm setUpForm() {
TodoForm form = new TodoForm();
return form;
}
@RequestMapping(value = "list") // (5)
public String list(Model model) {
Collection todos = todoService.findAll();
model.addAttribute("todos", todos); // (6
return "todo/list"; // (7)
}
}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (3)
- | TodoServiceを、DIコンテナによってインジェクションさせるために、\ ``@Inject``\ アノテーションをつける。
| DIコンテナの管理するTodoSerivce型インスタンスがインジェクションされるため、結果として、TodoServiceImplインスタンスがインジェクションされる。
* - | (4)
- | Formを初期化する。\ ``@ModelAttribute``\ アノテーションをつけることで、このメソッドの返り値のformオブジェクトが、”todoForm”という名前でModelに追加される。
| TodoControllerの各処理で、model.addAttribute(“todoForm”, form)が実行されるのと同義。
* - | (5)
- | listメソッドを”/todo/list”にマッピングされるための設定。クラスレベルで@RequestMapping(“todo”)が設定されているため、ここでは@RequestMapping(value = “list”)だけで良い。
* - | (6)
- | ModelにTodoのリストを追加して、Viewに渡す。
* - | (7)
- | View名として”todo/list”を返すと、spring-mvc.xmlに定義したInternalResourceViewResolverによって、WEB-INF/views/todo/list.jspがレンダリングされることになる。
JSPの作成
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
WEB-INF/views/todo/list.jspで、Controllerから渡されたModelを表示する。
まずは、”Finish”,”Delete”ボタン以外を作成する。
.. code-block:: jsp
Todo List
Todo List
-
${f:h(todo.todoTitle)}
${f:h(todo.todoTitle)}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | タグでフォームを表示する。modelAttribute属性に、ControllerでModelに追加したformの名前を指定する。
| action属性に指定するcontextPathは、${pageContext.request.contextPath}で取得できる。
* - | (2)
- | タグでフォームのプロパティをバインドする。modelAttribute属性に指定したformのプロパティ名と、path属性の値が一致している必要がある。
* - | (3)
- | タグを用いて、Todoのリストを全て表示する。
* - | (4)
- | 完了かどうか(finished)で、打ち消し線(text-decoration: line-through;)を装飾するかどうかを判断する。
* - | (5)
- | **文字列値を出力する際は、XSS対策のため、必ずf:h()関数を使用してHTMLエスケープを行うこと。**
| XSS対策についての詳細は、\ :doc:`../Security/XSS`\ を参照されたい。
| STSで「todo」プロジェクトを右クリックし、「Run As」→「Run on Server」でWebアプリケーションを起動する。
| ブラウザで”http://localhost:8080/todo/todo/list”にアクセスすると、以下のような画面が表示される。
.. figure:: ./images/image067.png
:width: 40%
Create TODO
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
次に、一覧表示画面から”Create TODO”ボタンを押した後の、新規作成処理を実装する。
Controllerの修正
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
TodoControllerに、createメソッドを追加する。
.. code-block:: java
:emphasize-lines: 8,29-31,46-70
package todo.app.todo;
import java.util.Collection;
import javax.inject.Inject;
import javax.validation.Valid;
import org.dozer.Mapper;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
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 todo.domain.model.Todo;
import todo.domain.service.todo.TodoService;
@Controller
@RequestMapping("todo")
public class TodoController {
@Inject
protected TodoService todoService;
// (8)
@Inject
protected Mapper beanMapper;
@ModelAttribute
public TodoForm setUpForm() {
TodoForm form = new TodoForm();
return form;
}
@RequestMapping(value = "list")
public String list(Model model) {
Collection todos = todoService.findAll();
model.addAttribute("todos", todos);
return "todo/list";
}
@RequestMapping(value = "create", method = RequestMethod.POST) // (9)
public String create(@Valid TodoForm todoForm, BindingResult bindingResult, // (10)
Model model, RedirectAttributes attributes) { // (11)
// (12)
if (bindingResult.hasErrors()) {
return list(model);
}
// (13)
Todo todo = beanMapper.map(todoForm, Todo.class);
try {
todoService.create(todo);
} catch (BusinessException e) {
// (14)
model.addAttribute(e.getResultMessages());
return list(model);
}
// (15)
attributes.addFlashAttribute(ResultMessages.success().add(
ResultMessage.fromText("Created successfully!")));
return "redirect:/todo/list";
}
}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (8)
- | Formオブジェクトを、DomainObjectに変換する際に、有用なMapperをインジェクションする。
* - | (9)
- | パスが/todo/createで、HTTPメソッドがPOSTに対応するように、\ ``@RequestMapping``\ アノテーションを設定する。
* - | (10)
- | フォームの入力チェックを行うため、Formの引数に\ ``@Valid``\ アノテーションをつける。入力チェック結果は、その直後の引数BindingResultに格納される。
* - | (11)
- | 正常に作成が完了した後、リダイレクトで一覧画面に戻る。リダイレクト先への情報を格納するために、引数にRedirectAttributesを加える。
* - | (12)
- | 入力エラーがあった場合、一覧画面に戻る。Todo全件取得を再度行う必要があるので、listメソッドを再実行する。
* - | (13)
- | Mapperを用いて、TodoFormからTodoオブジェクトを作成する。変換元と変換先のプロパティ名が同じ場合は、設定不要である。
| 今回は、todoTitleプロパティのみ変換するため、Mapperを使用するメリットはほとんどない。プロパティの数が多い場合には、非常に便利である。
* - | (14)
- | 業務処理を実行して、BusinessExceptionが発生した場合、結果メッセージをModelに追加して、一覧画面に戻る。
* - | (15)
- | 正常に作成が完了したので、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。
| リダイレクトすることにより、ブラウザを再読み込みして、再び新規登録処理がPOSTされることがなくなる。なお、今回は成功メッセージであるため、ResultMessages.success()を使用している。
Formの修正
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
入力チェックのルールを定義するため、Formオブジェクトにアノテーションを追加する。
.. code-block:: java
:emphasize-lines: 3-4,8-9
package todo.app.todo;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
public class TodoForm {
@NotNull // (1)
@Size(min = 1, max = 30) // (2)
private String todoTitle;
public String getTodoTitle() {
return todoTitle;
}
public void setTodoTitle(String todoTitle) {
this.todoTitle = todoTitle;
}
}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (1)
- | 必須項目であるので、\ ``@NotNull``\ アノテーションを付ける。
* - | (2)
- | 1文字以上30文字以下であるので、\ ``@Size``\ アノテーションで、範囲を指定する。
JSPの修正
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
結果メッセージ表示用のタグを追加する。
.. code-block:: jsp
:emphasize-lines: 16,22
Todo List
Todo List
-
${f:h(todo.todoTitle)}
${f:h(todo.todoTitle)}
.. list-table::
:header-rows: 1
:widths: 10 90
* - 項番
- 説明
* - | (6)
- | タグで、結果メッセージを表示する。
* - | (7)
- | タグで、入力エラーがあった場合に表示する。path属性の値は、タグと合わせる。
フォームに適切な値を入力してsubmitすると、以下のように、成功メッセージが表示される。
.. figure:: ./images/image068.png
:width: 40%
.. figure:: ./images/image069.png
:width: 40%
6件以上登録した場合は、業務エラーとなり、エラーメッセージが表示される。
.. figure:: ./images/image070.png
:width: 40%
入力フォームを、空文字にしてsubmitすると、以下のように、エラーメッセージが表示される。
.. figure:: ./images/image071.png
:width: 40%
メッセージ表示のカスタマイズ
''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
の結果はデフォルトで、
.. code-block:: html
と出力される。
スタイルシート(list.jspの
Todo List