3. チュートリアル(Todoアプリケーション)¶
3.1. はじめに¶
3.1.1. このチュートリアルで学ぶこと¶
- TERASOLUNA Global Frameworkによる基本的なアプリケーションの開発方法およびEclipseプロジェクトの構築方法
- TERASOLUNA Global Frameworkの アプリケーションのレイヤ化 に従った開発方法
3.1.2. 対象読者¶
- SpringのDIやAOPに関する基礎的な知識がある
- Servlet/JSPを使用してWebアプリケーションを開発したことがある
- SQLに関する知識がある
3.1.3. 検証環境¶
このチュートリアルは以下の環境で動作確認している。他の環境で実施する際は本書をベースに適宜読み替えて設定していくこと。
種別 名前 OS Windows7 64bit JVM Java 1.7 IDE Spring Tool Suite Version: 3.5.0.RELEASE Build Id: 201404011654 (以下STS) Build Maven 3.0.4 (STS付属) Application Server VMWare vFabric tc Server Developer Edition v2.9 (STS付属) Web Browser Google Chrome 27.0.1453.94 m
3.2. 作成するアプリケーションの説明¶
3.2.2. アプリケーションの業務要件¶
アプリケーションの業務要件は、以下の通りとする。
ルールID 説明 B01 未完のTODOは5件までしか登録できない B02 完了済みのTODOは完了できない Note
本要件は学習のためのもので、現実的なTODO管理アプリケーションとしては適切ではない。
3.2.3. アプリケーションの処理仕様¶
アプリケーションの処理仕様と画面遷移は、以下の通りとする。
3.2.3.1. Show all TODO¶
- TODOを全件表示する
- 未完了のTODOに対しては”Finish”と”Delete”用のボタンが付く
- 完了のTODOは打ち消し線で装飾する
- TODOの件名のみ
3.2.3.2. Create TODO¶
- フォームから送信されたTODOを保存する
- TODOの件名は1文字以上30文字以下であること
- アプリケーションの業務要件 のB01を満たさない場合はエラーコードE001でビジネス例外をスローする
3.2.3.3. Finish TODO¶
- フォームから送信されたtodoIdに対応するTODOを完了済みにする
- アプリケーションの業務要件 のB02を満たさない場合はエラーコードE002でビジネス例外をスローする
- 該当するTODOが存在しない場合はエラーコードE404でビジネス例外をスローする
3.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
3.3. 環境構築¶
3.3.1. プロジェクトの作成¶
本チュートリアルでは、インフラストラクチャ層のRepositoryImplの実装として、
- データベースを使用せずMapを使ったインメモリ実装のRepositoryImpl
- Spring Data JPAの使用してデータベースにアクセスするRepositoryImpl
- TERASOLUNA DAO(MyBatis2)を使用してデータベースにアクセスするRepositoryImpl
の3種類を用意している。
まず、mvn archetype:generate
を利用して、実装するインフラストラクチャ層向けのブランクプロジェクトを作成する。
ここでは、Windows上にブランクプロジェクトを作成する手順となっている。
- データベースを使用せずMapを使ったインメモリ実装のRepositoryImpl用のプロジェクトを作成する場合は、O/R Mapperに依存しないブランクプロジェクトを作成するために、コマンドプロンプトで以下のコマンドを実行する。
mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -B^ -DarchetypeCatalog=http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-releases^ -DarchetypeGroupId=org.terasoluna.gfw.blank^ -DarchetypeArtifactId=terasoluna-gfw-web-blank-archetype^ -DarchetypeVersion=1.0.6.RELEASE^ -DgroupId=todo^ -DartifactId=todo^ -Dversion=1.0.0-SNAPSHOT
- Spring Data JPAの使用してデータベースにアクセスするRepositoryImpl用のプロジェクトを作成する場合は、JPA用のブランクプロジェクトを作成するために、コマンドプロンプトで以下のコマンドを実行する。
mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -B^ -DarchetypeCatalog=http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-releases^ -DarchetypeGroupId=org.terasoluna.gfw.blank^ -DarchetypeArtifactId=terasoluna-gfw-web-blank-jpa-archetype^ -DarchetypeVersion=1.0.6.RELEASE^ -DgroupId=todo^ -DartifactId=todo^ -Dversion=1.0.0-SNAPSHOT
- TERASOLUNA DAO(MyBatis2)を使用してデータベースにアクセスするRepositoryImpl用のプロジェクトを作成する場合は、TERASOLUNA DAO(MyBatis2)用のブランクプロジェクトを作成するために、コマンドプロンプトで以下のコマンドを実行する。
mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -B^ -DarchetypeCatalog=http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-releases^ -DarchetypeGroupId=org.terasoluna.gfw.blank^ -DarchetypeArtifactId=terasoluna-gfw-web-blank-mybatis2-archetype^ -DarchetypeVersion=1.0.6.RELEASE^ -DgroupId=todo^ -DartifactId=todo^ -Dversion=1.0.0-SNAPSHOT
コンソール上に以下のようなログが表示されれば、ブランクプロジェクトの作成は成功となる。
C:\work>mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -B^ More? -DarchetypeCatalog=http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-releases^ More? -DarchetypeGroupId=org.terasoluna.gfw.blank^ More? -DarchetypeArtifactId=terasoluna-gfw-web-blank-archetype^ More? -DarchetypeVersion=1.0.6.RELEASE^ More? -DgroupId=todo^ More? -DartifactId=todo^ More? -Dversion=1.0.0-SNAPSHOT [INFO] Scanning for projects... [INFO] [INFO] ------------------------------------------------------------------------ [INFO] Building Maven Stub Project (No POM) 1 [INFO] ------------------------------------------------------------------------ [INFO] [INFO] >>> maven-archetype-plugin:2.4:generate (default-cli) > generate-sources @ standalone-pom >>> [INFO] [INFO] <<< maven-archetype-plugin:2.4:generate (default-cli) < generate-sources @ standalone-pom <<< [INFO] [INFO] --- maven-archetype-plugin:2.4:generate (default-cli) @ standalone-pom --- [INFO] Generating project in Batch mode [INFO] Archetype repository not defined. Using the one from [org.terasoluna.gfw.blank:terasoluna-gfw-web-blank-archetype:1.0.0.RELEASE -> http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-releases] found in catalog http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-releases [INFO] ---------------------------------------------------------------------------- [INFO] Using following parameters for creating project from Archetype: terasoluna-gfw-web-blank-archetype:1.0.6.RELEASE [INFO] ---------------------------------------------------------------------------- [INFO] Parameter: groupId, Value: todo [INFO] Parameter: artifactId, Value: todo [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT [INFO] Parameter: package, Value: todo [INFO] Parameter: packageInPathFormat, Value: todo [INFO] Parameter: package, Value: todo [INFO] Parameter: version, Value: 1.0.0-SNAPSHOT [INFO] Parameter: groupId, Value: todo [INFO] Parameter: artifactId, Value: todo [INFO] project created from Archetype in dir: C:\work\todo [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.730 s [INFO] Finished at: 2017-02-24T10:30:02+09:00 [INFO] Final Memory: 12M/232M [INFO] ------------------------------------------------------------------------ C:\work>
STSのメニューから、[File] -> [Import] -> [Maven] -> [Existing Maven Projects] -> [Next]を選択し、archetypeで作成したプロジェクトを選択する。
Root Directoryに C:\work\todo
を設定し、Projectsにtodoのpom.xmlが選択された状態で、 [Finish] を押下する。
Package Explorerに、次のようなプロジェクトが生成される( 要インターネット接続 )。
Note
Bash上で
mvn archetype:generate
を実行する場合は以下のように^
を\
に置き換えて実行する必要がある。mvn org.apache.maven.plugins:maven-archetype-plugin:2.4:generate -B\ -DarchetypeCatalog=http://repo.terasoluna.org/nexus/content/repositories/terasoluna-gfw-releases\ -DarchetypeGroupId=org.terasoluna.gfw.blank\ -DarchetypeArtifactId=terasoluna-gfw-web-blank-archetype\ -DarchetypeVersion=1.0.6.RELEASE\ -DgroupId=todo\ -DartifactId=todo\ -Dversion=1.0.0-SNAPSHOT
3.3.2. Mavenの設定¶
Note
Proxyサーバーを介してインターネットアクセスする必要がある場合は、
<HOME>/.m2/settings.xml
に以下のような設定を行う必要がある。 (Windows7の場合C:\Users\<YourName>.m2\settings.xml)<settings> <proxies> <proxy> <active>true</active> <protocol>[Proxy Server Protocol (http)]</protocol> <port>[Proxy Server Port]</port> <host>[Proxy Server Host]</host> <username>[Username]</username> <password>[Password]</password> </proxy> </proxies> </settings>Note
インポート後にビルドエラーが発生する場合は、プロジェクト名を右クリックし、「Maven」->「Update Project」をクリックし、 「OK」ボタンをクリックすることでエラーが解消されるケースがある。
Warning
O/R Mapperを使用するブランクプロジェクトの場合、H2 Databaseがdependencyとして定義されているが、 この設定は簡易的なアプリケーションを簡単に作成するためのものであり、実際のアプリケーション開発で使用されることは想定していない。
以下の定義は、実際のアプリケーション開発を行う際は削除すること。
<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <version>${com.h2database.version}</version> <scope>runtime</scope> </dependency>
3.3.3. プロジェクト構成¶
チュートリアルで作成していくプロジェクトの構成について、以下に示す。
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
前節の「プロジェクト構成」 ではマルチプロジェクトにすることを推奨していたが、 本チュートリアルでは、学習容易性を重視しているためシングルプロジェクト構成にしている。
ただし、実プロジェクトで適用する場合は、マルチプロジェクト構成を強く推奨する。
3.3.4. 設定ファイルの確認¶
チュートリアルを進める上で必要となる設定の多くは、作成したブランクプロジェクトに既に設定済みの状態である。
本節では、アプリケーションを動かすためにどのような設定ファイルが必要なのかを理解するために、 設定ファイルの確認をしていく。
Note
まず、手を動かしてTodoアプリを作成したい場合は、本節を読み飛ばしてもよいが、 Todoアプリを作成した後に必ず一読して頂きたい。
3.3.4.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="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd" version="3.0"> <!-- (2) --> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <listener> <listener-class>org.terasoluna.gfw.web.logging.HttpSessionEventLoggingListener</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> <!-- (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> </session-config> </web-app>
項番 説明 (1) Servlet3.0を使用するための宣言。 (2) サーブレットコンテキストリスナーの定義。ブランクプロジェクトでは、
- アプリケーション全体で使用される
ApplicationContext
を作成するためのContextLoaderListener
- HttpSessionに対する操作をログ出力するための
HttpSessionEventLoggingListener
が設定済みである。
(3) サーブレットフィルタの定義。ブランクプロジェクトでは、
- 共通ライブラリから提供しているサーブレットフィルタ
- Spring Frameworkから提供されている文字エンコーディングを指定するための
CharacterEncodingFilter
- Spring Securityから提供されている認証・認可用のサーブレットフィルタ
が設定済みである。
(4) Spring MVCのエントリポイントとなるDispatcherServletの定義。DispatcherServletの中で使用するApplicationContext
を、(2)で作成したApplicatnionContext
の子として作成する。(2)で作成したApplicatnionContext
を親にすることで、(2)で読み込まれたコンポーネントも使用することができる。 (5) JSPの共通定義。ブランクプロジェクトでは、
- JSP内でEL式が使用可能な状態
- JSPのページエンコーディングとしてUTF-8
- JSP内でスクリプティングが使用可能な状態
- 各JSPの先頭でインクルードするJSPとして、
/WEB-INF/views/common/include.jsp
が設定済みである。
(6) エラーページの定義。ブランクプロジェクトでは、
- サーブレットコンテナにHTTPステータスコードとして、
404
又は500
が応答- サーブレットコンテナに例外が通知
された際の遷移先が定義済みである。
(7) セッション管理の定義。ブランクプロジェクトでは、
- セッションタイムアウトとして、30分
が定義済みである。
3.3.4.2. インクルードJSPの確認¶
インクルードJSPには、全てのJSPに適用するJSPの設定や、タグライブラリの設定を行う。
作成したブランクプロジェクトのsrc/main/webapp/WEB-INF/views/common/include.jsp
は、以下のような設定となっている。
<!-- (1) --> <%@ page session="false"%> <!-- (2) --> <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%> <%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%> <!-- (3) --> <%@ taglib uri="http://www.springframework.org/tags" prefix="spring"%> <%@ taglib uri="http://www.springframework.org/tags/form" prefix="form"%> <!-- (4) --> <%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec"%> <!-- (5) --> <%@ taglib uri="http://terasoluna.org/tags" prefix="t"%> <%@ taglib uri="http://terasoluna.org/functions" prefix="f"%>
項番 説明 (1) JSP実行時にセッションを作成しないようにするための定義。 (2) 標準タグライブラリの定義。 (3) Spring MVC用タグライブラリの定義。 (4) Spring Security用タグライブラリの定義(本チュートリアルでは使用しない。) (5) 共通ライブラリで提供されている、EL関数、タグライブラリの定義。
3.3.4.3. Bean定義ファイルの確認¶
作成したブランクプロジェクトには、以下のBean定義ファイルとプロパティファイルが作成される。
src/main/resources/META-INF/spring/applicationContext.xml
src/main/resources/META-INF/spring/todo-domain.xml
src/main/resources/META-INF/spring/todo-infra.xml
src/main/resources/META-INF/spring/todo-infra.properties
src/main/resources/META-INF/spring/todo-env.xml
src/main/resources/META-INF/spring/spring-mvc.xml
Note
O/R Mapperに依存しないブランクプロジェクトを作成した場合は、
todo-infra.properties
とtodo-env.xml
は作成されない。Note
本ガイドラインでは、Bean定義ファイルを役割(層)ごとにファイルを分割することを推奨している。
これは、どこに何が定義されているか想像しやすく、メンテナンス性が向上するからである。 今回のチュートリアルのような小さなアプリケーションでは効果はないが、アプリケーションの規模が大きくなるにつれ、効果が大きくなる。
3.3.4.3.1. applicationContext.xmlの確認¶
applicationContext.xml
には、Todoアプリ全体に関わる設定を行う。
src/main/resources/META-INF/spring/applicationContext.xml
は、以下のような設定となっている。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://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.bcrypt.BCryptPasswordEncoder" /> <!-- (2) --> <context:property-placeholder location="classpath*:/META-INF/spring/*.properties" /> <!-- (3) --> <bean class="org.dozer.spring.DozerBeanMapperFactoryBean"> <property name="mappingFiles" value="classpath*:/META-INF/dozer/**/*-mapping.xml" /> </bean> <!-- Message --> <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource"> <property name="basenames"> <list> <value>i18n/application-messages</value> </list> </property> </bean> <!-- Exception Code Resolver. --> <bean id="exceptionCodeResolver" class="org.terasoluna.gfw.common.exception.SimpleMappingExceptionCodeResolver"> <!-- Setting and Customization by project. --> <property name="exceptionMappings"> <map> <entry key="ResourceNotFoundException" value="e.xx.fw.5001" /> <entry key="InvalidTransactionTokenException" value="e.xx.fw.7001" /> <entry key="BusinessException" value="e.xx.fw.8001" /> <entry key=".DataAccessException" value="e.xx.fw.9002" /> </map> </property> <property name="defaultExceptionCode" value="e.xx.fw.9001" /> </bean> <!-- Exception Logger. --> <bean id="exceptionLogger" class="org.terasoluna.gfw.common.exception.ExceptionLogger"> <property name="exceptionCodeResolver" ref="exceptionCodeResolver" /> </bean> <!-- Filter. --> <bean id="exceptionLoggingFilter" class="org.terasoluna.gfw.web.exception.ExceptionLoggingFilter" > <property name="exceptionLogger" ref="exceptionLogger" /> </bean> </beans>
項番 説明 (1) ドメイン層に関するBean定義ファイルをimportする。 (2) プロパティファイルの読み込み設定を行う。src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。この設定により、プロパティファイルの値をBean定義ファイル内で${propertyName}形式で埋め込んだり、Javaクラスに@Value(“${propertyName}”)でインジェクションすることができる。 (3) Bean変換用ライブラリDozerのMapperを定義する。マッピングファイルに関して Dozerマニュアル を参照されたい。)
3.3.4.3.2. todo-domain.xmlの確認¶
todo-domain.xml
には、Todoアプリのドメイン層に関わる設定を行う。
src/main/resources/META-INF/spring/todo-domain.xml
は、以下のような設定となっている。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.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="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) ドメイン層のクラスを管理するtodo.domainパッケージ配下をcomponent-scan対象とする。これにより、todo.domainパッケージ配下のクラスに@Repository
,@Service
などのアノテーションを付けることで、Spring Framerowkが管理するBeanとして登録される。登録されたクラス(Bean)は、ControllerやServiceクラスにDIする事で、利用する事が出来る。Note
O/R Mapperに依存するブランクプロジェクトを作成した場合は、
@Transactional
アノテーションによるトランザクション管理を有効にするために、<tx:annotation-driven>
タグを設定されている。<tx:annotation-driven />
3.3.4.3.3. todo-infra.xmlの確認¶
todo-infra.xml
には、Todoアプリのインフラストラクチャ層に関わる設定を行う。
作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/todo-infra.xml
は、
以下のような設定となっている。
todo-infra.xml
は、インフラストラクチャ層によって設定が大きく異なるため、
ブランクプロジェクト毎に説明を行う。
作成したブランクプロジェクト以外の説明は読み飛ばしてもよい。
3.3.4.3.3.1. O/R Mapperに依存しないブランクプロジェクトを作成した場合¶
O/R Mapperに依存しないブランクプロジェクトを作成した場合、以下のように空定義のファイルが作成される。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> </beans>
3.3.4.3.3.2. JPA用のブランクプロジェクトを作成した場合¶
JPA用のブランクプロジェクトを作成した場合、以下のような設定となっている。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jpa="http://www.springframework.org/schema/data/jpa" xmlns:util="http://www.springframework.org/schema/util" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd"> <!-- (1) --> <import resource="classpath:/META-INF/spring/todo-env.xml" /> <!-- (2) --> <jpa:repositories base-package="todo.domain.repository"></jpa:repositories> <!-- (3) --> <bean id="jpaVendorAdapter" class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"> <property name="showSql" value="false" /> <property name="database" value="${database}" /> </bean> <!-- (4) --> <bean class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean" id="entityManagerFactory"> <!-- (5) --> <property name="packagesToScan" value="todo.domain.model" /> <property name="dataSource" ref="dataSource" /> <property name="jpaVendorAdapter" ref="jpaVendorAdapter" /> <!-- (6) --> <property name="jpaPropertyMap"> <util:map> <entry key="hibernate.hbm2ddl.auto" value="none" /> <entry key="hibernate.ejb.naming_strategy" value="org.hibernate.cfg.ImprovedNamingStrategy" /> <entry key="hibernate.connection.charSet" value="UTF-8" /> <entry key="hibernate.show_sql" value="false" /> <entry key="hibernate.format_sql" value="false" /> <entry key="hibernate.use_sql_comments" value="true" /> <entry key="hibernate.jdbc.batch_size" value="30" /> <entry key="hibernate.jdbc.fetch_size" value="100" /> </util:map> </property> </bean> </beans>
項番 説明 (1) 環境依存するコンポーネント(データソースやトランザクションマネージャなど)を定義するBean定義ファイルをimportする。 (2) Spring Data JPAを使用して、Repositoryインタフェースから実装クラスを自動生成する。<jpa:repository>タグのbase-package属性で、対象のRepositoryを含むパッケージを指定する。 (3) JPAの実装ベンダの設定を行う。JPA実装として、Hibernateを使うため、HibernateJpaVendorAdapterを定義している。 (4) EntityManagerの定義を行う。 (5) JPAのエンティティとして扱うクラスが格納されているパッケージ名を指定する。 (6) Hibernateに関する詳細な設定を行う。
3.3.4.3.3.3. TERASOLUNA DAO(MyBatis2)用のブランクプロジェクトを作成した場合¶
TERASOLUNA DAO(MyBatis2)用のブランクプロジェクトを作成した場合、以下のような設定となっている。
<?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 http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- (1) --> <import resource="classpath:/META-INF/spring/todo-env.xml" /> <!-- (2) --> <bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"> <!-- (3) --> <property name="configLocations" value="classpath*:/META-INF/mybatis/config/*sqlMapConfig.xml" /> <!-- (4) --> <property name="mappingLocations" value="classpath*:/META-INF/mybatis/sql/**/*-sqlmap.xml" /> <property name="dataSource" ref="dataSource" /> </bean> <!-- (5) --> <bean id="queryDAO" class="jp.terasoluna.fw.dao.ibatis.QueryDAOiBatisImpl"> <property name="sqlMapClient" ref="sqlMapClient" /> </bean> <bean id="updateDAO" class="jp.terasoluna.fw.dao.ibatis.UpdateDAOiBatisImpl"> <property name="sqlMapClient" ref="sqlMapClient" /> </bean> <bean id="spDAO" class="jp.terasoluna.fw.dao.ibatis.StoredProcedureDAOiBatisImpl"> <property name="sqlMapClient" ref="sqlMapClient" /> </bean> <bean id="queryRowHandleDAO" class="jp.terasoluna.fw.dao.ibatis.QueryRowHandleDAOiBatisImpl"> <property name="sqlMapClient" ref="sqlMapClient" /> </bean> </beans>
項番 説明 (1) 環境依存するコンポーネント(データソースやトランザクションマネージャなど)を定義するBean定義ファイルをimportする。 (2) SqlMapClientの定義を行う。 (3) SqlMap設定ファイルのパスを設定する。ここでは、META-INF/mybatis/config以下の、*sqlMapConfig.xmlを読み込む。 (4) SqlMapファイルのパスを設定する。ここでは、META-INF/mybatis/sql以下の、任意のフォルダの*-sqlmap.xmlを読み込む。 (5) TERASOLUNA DAOの定義を行う。Note
sqlMapConfig.xml
は、MyBatis2自体の動作設定を行う設定ファイルである。ブランクプロジェクトでは、デフォルトで以下の設定が行われている。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE sqlMapConfig PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-config-2.dtd"> <sqlMapConfig> <settings useStatementNamespaces="true" /> </sqlMapConfig>
useStatementNamespaces
をtrue
にすることで、SQLIDにネームスペースを指定することが出来る。
3.3.4.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
から参照されている。
3.3.4.3.5. todo-env.xmlの確認¶
todo-env.xml
には、デプロイする環境によって設定が異なるコンポーネントの設定を行う。
データベースにアクセスしないブランクプロジェクトを作成した際は、todo-env.xml
は作成されない。
作成したブランクプロジェクトのsrc/main/resources/META-INF/spring/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:jee="http://www.springframework.org/schema/jee" xmlns:jdbc="http://www.springframework.org/schema/jdbc" xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="dateFactory" class="org.terasoluna.gfw.common.date.DefaultDateFactory" /> <!-- (1) --> <bean id="realDataSource" class="org.apache.commons.dbcp.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="maxActive" value="${cp.maxActive}" /> <property name="maxIdle" value="${cp.maxIdle}" /> <property name="minIdle" value="${cp.minIdle}" /> <property name="maxWait" value="${cp.maxWait}" /> </bean> <!-- (2) --> <bean id="dataSource" class="net.sf.log4jdbc.Log4jdbcProxyDataSource"> <constructor-arg index="0" ref="realDataSource" /> </bean> <jdbc:initialize-database data-source="dataSource" ignore-failures="ALL"> <jdbc:script location="classpath:/database/${database}-schema.sql" /> <jdbc:script location="classpath:/database/${database}-dataload.sql" /> </jdbc:initialize-database> <!-- (3) --> <bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager"> <property name="entityManagerFactory" ref="entityManagerFactory" /> </bean> <!-- REMOVE THIS LINE IF YOU USE MyBatis2 <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource" /> </bean> REMOVE THIS LINE IF YOU USE MyBatis2 --> </beans>
項番 説明 (1) 実データソースの設定。 (2) データソースの設定。JDBC関連のログを出力する機能をもったデータソースを指定している。net.sf.log4jdbc.Log4jdbcProxyDataSource
を使用すると、SQLなどのJDBC関連のログを出力できるため、デバッグに役立つ情報を出力することができる。 (3) トランザクションマネージャの設定。idは、transactionManagerにすること。別の名前を指定する場合は、<tx:annotation-driven>タグにも、トランザクションマネージャ名を指定する必要がある。Note
使用するトランザクションマネージャは、使用するO/R Mapperによって切り替える必要がある。
作成したブランクプロジェクトが、
- JPA用のブランクプロジェクトを作成した場合は、 JPAのAPIを使用してトランザクションを制御するクラス(
org.springframework.orm.jpa.JpaTransactionManager
)- TERASOLUNA DAO(MyBatis2)用のブランクプロジェクトを作成した場合は、 JDBCのAPIを使用してトランザクションを制御するクラス(
org.springframework.jdbc.datasource.DataSourceTransactionManager
)が設定されている。
Note
JavaEEコンテナ上にアプリケーションをデプロイする場合は、 JTAのAPIを利用してトランザクションを制御するクラス(
org.springframework.transaction.jta.JtaTransactionManager
)を使用したほうがよい。JtaTransactionManager
を使う場合は、<tx:jta-transaction-manager />
を指定してトランザクションマネージャの定義を行う。トランザクションマネージャの設定が、アプリケーションをデプロイする環境によって変わらないプロジェクト(例えば、Tomcatを使用する場合など)は、 :file:`todo-infra.xml`に定義してもよい。
3.3.4.3.6. spring-mvc.xmlの確認¶
spring-mvc.xml
には、Spring MVCに関する定義を行う。
src/main/resources/META-INF/spring/spring-mvc.xml
は、以下のような設定となっている。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:util="http://www.springframework.org/schema/util" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://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" /> </mvc:argument-resolvers> <!-- workarround to CVE-2016-5007. --> <mvc:path-matching path-matcher="pathMatcher" /> </mvc:annotation-driven> <mvc:default-servlet-handler /> <!-- (3) --> <context:component-scan base-package="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/**" /> <mvc:exclude-mapping path="/**/*.html" /> <bean class="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor" /> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/**" /> <mvc:exclude-mapping path="/resources/**" /> <mvc:exclude-mapping path="/**/*.html" /> <bean class="org.terasoluna.gfw.web.token.transaction.TransactionTokenInterceptor" /> </mvc:interceptor> <mvc:interceptor> <mvc:mapping path="/**" /> <mvc:exclude-mapping path="/resources/**" /> <mvc:exclude-mapping path="/**/*.html" /> <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/**" /> <mvc:exclude-mapping path="/**/*.html" /> <bean class="org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor" /> </mvc:interceptor> REMOVE THIS LINE IF YOU USE JPA --> </mvc:interceptors> <!-- (6) --> <!-- Settings View Resolver. --> <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/" /> <property name="suffix" value=".jsp" /> </bean> <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" factory-method="create" /> <bean class="org.terasoluna.gfw.web.token.transaction.TransactionTokenRequestDataValueProcessor" /> </util:list> </constructor-arg> </bean> <!-- Setting Exception Handling. --> <!-- Exception Resolver. --> <bean 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="defaultErrorView" value="common/error/systemError" /> <property name="defaultStatusCode" value="500" /> </bean> <!-- Setting AOP. --> <bean id="handlerExceptionResolverLoggingInterceptor" class="org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor"> <property name="exceptionLogger" ref="exceptionLogger" /> </bean> <aop:config> <aop:advisor advice-ref="handlerExceptionResolverLoggingInterceptor" pointcut="execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))" /> </aop:config> <!-- Setting PathMatcher. --> <bean id="pathMatcher" class="org.springframework.util.AntPathMatcher"> <property name="trimTokens" value="false" /> </bean> </beans>
項番 説明 (1) プロパティファイルの読み込み設定を行う。src/main/resources/META-INF/spring直下の任意のプロパティファイルを読み込む。この設定により、プロパティファイルの値をBean定義ファイル内で${propertyName}形式で埋め込んだり、Javaクラスに@Value(“${propertyName}”)でインジェクションすることができる。 (2) Spring MVCのアノテーションベースのデフォルト設定を行う。 (3) アプリケーション層のクラスを管理するtodo.appパッケージ配下をcomponent-scan対象とする。 (4) 静的リソース(css, images, jsなど)アクセスのための設定を行う。mapping属性にURLのパスを、location属性に物理的なパスの設定を行う。この設定の場合<contextPath>/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分)も設定している。 (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/**" /> <mvc:exclude-mapping path="/**/*.html" /> <bean class="org.springframework.orm.jpa.support.OpenEntityManagerInViewInterceptor" /> </mvc:interceptor>
OpenEntityManagerInViewInterceptor
は、EntityManagerのライフサイクルの開始と終了を行うInterceptorである。 この設定を追加することで、アプリケーション層(Contollerや、Viewクラス)でのLazy Loadが、サポートされる。
3.3.4.4. logback.xmlの確認¶
logback.xml
には、ログ出力に関する定義を行う。
src/main/resources/logback.xml
は、以下のような設定となっている。<?xml version="1.0" encoding="UTF-8"?> <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:%msg%n]]></pattern> </encoder> </appender> <appender name="APPLICATION_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>log/todo-application.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>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:%msg%n]]></pattern> </encoder> </appender> <appender name="MONITORING_LOG_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>log/todo-monitoring.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>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:%msg%n]]></pattern> </encoder> </appender> <!-- Application Loggers --> <!-- (2) --> <logger name="todo"> <level value="debug" /> </logger> <!-- TERASOLUNA --> <logger name="org.terasoluna.gfw"> <level value="info" /> </logger> <!-- (3) --> <logger name="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor"> <level value="trace" /> </logger> <logger name="org.terasoluna.gfw.common.exception.ExceptionLogger"> <level value="info" /> </logger> <logger name="org.terasoluna.gfw.common.exception.ExceptionLogger.Monitoring" additivity="false"> <level value="error" /> <appender-ref ref="MONITORING_LOG_FILE" /> </logger> <!-- 3rdparty Loggers --> <logger name="org.springframework"> <level value="warn" /> </logger> <logger name="org.springframework.web.servlet"> <level value="info" /> </logger> <!-- REMOVE THIS LINE IF YOU USE JPA <logger name="org.hibernate.engine.transaction"> <level value="debug" /> </logger> REMOVE THIS LINE IF YOU USE JPA --> <!-- REMOVE THIS LINE IF YOU USE MyBatis2 <logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <level value="debug" /> </logger> REMOVE THIS LINE IF YOU USE MyBatis2 --> <logger name="jdbc.sqltiming"> <level value="debug" /> </logger> <!-- only for development --> <logger name="jdbc.resultsettable"> <level value="debug" /> </logger> <root level="warn"> <appender-ref ref="STDOUT" /> <appender-ref ref="APPLICATION_LOG_FILE" /> </root> </configuration>
項番 説明 (1) 標準出力でログを出力するアペンダを設定。 (2) todoパッケージ以下はdebugレベル以上を出力するように設定。 (3) spring-mvc.xmlに設定したTraceLoggingInterceptor
に出力されるようにtraceレベルで設定。Note
O/R Mapperを使用するブランクプロジェクトを作成した場合は、トランザクション制御関連のログを出力するロガーが有効な状態となっている。
- JPA用のブランクプロジェクト
<logger name="org.hibernate.engine.transaction"> <level value="debug" /> </logger>
- TERASOLUNA DAO(MyBatis2)用のブランクプロジェクト
<logger name="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <level value="debug" /> </logger>
3.3.5. ブランクプロジェクトの動作確認¶
Todoアプリケーションの開発を始める前に、ブランクプロジェクトの動作確認を行う。
ブランクプロジェクトでは、トップページを表示するためのControllerとJSPの実装が用意されているため、 トップページを表示する事で動作確認を行う事ができる。
ブランクプロジェクトから提供されているController(src/main/java/todo/app/welcome/HelloControllerr.java
)は、
以下のような実装となっている。
package 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.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; /** * Handles requests for the application home page. */ // (1) @Controller public class HelloControllerr { // (2) private static final Logger logger = LoggerFactory .getLogger(HelloControllerr.class); /** * Simply selects the home view to render by returning its name. */ // (3) @RequestMapping(value = "/", method = {RequestMethod.GET, RequestMethod.POST}) 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)@RequestMapping
アノテーションを使用して、”/”(ルート)へのアクセスに対するメソッドとしてマッピングを行っている。 (4) メソッドが呼ばれたことを通知するためのログをinfoレベルで出力している。 (5) 画面に表示するための日付文字列を、”serverTime”という属性名でModelに設定している。 (6) view名として”welcome/home”を返す。ViewResolverの設定により、WEB-INF/views/welcome/home.jspが呼び出される。
ブランクプロジェクトから提供されているJSP(src/main/webapp/WEB-INF/views/welcome/home.jsp
)は、
以下のような実装となっている。
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Home</title> <link rel="stylesheet" href="${pageContext.request.contextPath}/resources/app/css/styles.css"> </head> <body> <div id="wrapper"> <h1>Hello world!</h1> <!-- (1) --> <p>The time on the server is ${serverTime}.</p> </div> </body> </html>
項番 説明 (1) ControllerでModelに設定した”serverTime”を表示する。ここでは、XSS対策を行っていないが、ユーザの入力値を表示する場合は、f:h()
関数を用いて、必ずXSS対策を行うこと。
パッケージプロジェクト名”todo”を右クリックして「Run As」->「Run on Server」
実行したいAPサーバー(ここではVMWare vFabric tc Server Developer Edition v2.9)を選び 「Next」をクリック
todoが「Configured」に含まれていることを確認して「Finish」をクリックしてサーバーを起動する。
起動すると以下のようなログが出力される。
”/”というパスに対して todo.app.welcome.HelloControllerr
のhelloメソッドがマッピングされていることが分かる。
date:2014-08-25 12:35:34 thread:localhost-startStop-1 X-Track: level:INFO logger:o.springframework.web.servlet.DispatcherServlet message:FrameworkServlet 'appServlet': initialization started date:2014-08-25 12:35:35 thread:localhost-startStop-1 X-Track: level:INFO logger:o.s.w.s.m.m.a.RequestMappingHandlerMapping message:Mapped "{[/],methods=[GET || POST],params=[],headers=[],consumes=[],produces=[],custom=[]}" onto public java.lang.String todo.app.welcome.HelloControllerr.home(java.util.Locale,org.springframework.ui.Model) date:2014-08-25 12:35:36 thread:localhost-startStop-1 X-Track: level:INFO logger:o.s.web.servlet.handler.SimpleUrlHandlerMapping message:Mapped URL path [/**] onto handler 'org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler#0' date:2014-08-25 12:35:36 thread:localhost-startStop-1 X-Track: level:INFO logger:o.s.web.servlet.handler.SimpleUrlHandlerMapping message:Mapped URL path [/resources/**] onto handler 'org.springframework.web.servlet.resource.ResourceHttpRequestHandler#0' date:2014-08-25 12:35:36 thread:localhost-startStop-1 X-Track: level:INFO logger:o.springframework.web.servlet.DispatcherServlet message:FrameworkServlet 'appServlet': initialization completed in 1862 ms
ブラウザでhttp://localhost:8080/todo にアクセスすると、以下のように表示される。
コンソールを見ると TraceLoggingInterceptor
によるTRACEログとControllerで実装したinfoログが出力されていることがわかる。
date:2014-08-25 12:35:41 thread:tomcat-http--3 X-Track:94bc5651406e4333a4b47b2276707b5c level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[START CONTROLLER] HelloControllerr.home(Locale,Model) date:2014-08-25 12:35:41 thread:tomcat-http--3 X-Track:94bc5651406e4333a4b47b2276707b5c level:INFO logger:todo.app.welcome.HelloControllerr message:Welcome home! The client locale is en. date:2014-08-25 12:35:41 thread:tomcat-http--3 X-Track:94bc5651406e4333a4b47b2276707b5c level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[END CONTROLLER ] HelloControllerr.home(Locale,Model)-> view=welcome/home, model={serverTime=August 25, 2014 12:35:41 PM UTC} date:2014-08-25 12:35:41 thread:tomcat-http--3 X-Track:94bc5651406e4333a4b47b2276707b5c level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[HANDLING TIME ] HelloControllerr.home(Locale,Model)-> 41,090,439 nsNote
TraceLoggingInterceptor
はControllerの開始、終了でログを出力する。終了時にはViewとModelの情報および処理時間が出力される。
3.4. Todoアプリケーションの作成¶
- ドメイン層(+ インフラストラクチャ層)
- Domain Object作成
- Repository作成
- RepositoryImpl作成
- Service作成
- アプリケーション層
- Controller作成
- Form作成
- View作成
RepositoryImplの作成は、選択したインフラストラクチャ層の種類に応じて実装方法が異なる。
本節では、データベースを使用せずMapを使ったインメモリ実装のRepositoryImplを作成する方法について説明を行っているので、 データベースを使用する場合は、「データベースアクセスを伴うインフラストラクチャ層の作成」に記載されている内容で読み替えて、Todoアプリケーションを作成して頂きたい。
3.4.1. ドメイン層の作成¶
3.4.1.1. Domain Objectの作成¶
ドメインオブジェクトに必要なプロパティは、
- ID
- タイトル
- 完了フラグ
- 作成日
である。
todo.domain.model.Todo
とする。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; } }
3.4.1.2. Repositoryの作成¶
今回のアプリケーションで、必要なTODOオブジェクトに対するCRUD系操作は、
- TODOの1件取得
- TODOの全件取得
- TODOの1件削除
- TODOの1件更新
- 完了済みTODO件数の取得
である。
todo.domain.repository.todo.TodoRepository
とする。package todo.domain.repository.todo; import java.util.Collection; import todo.domain.model.Todo; public interface TodoRepository { Todo findOne(String todoId); Collection<Todo> findAll(); Todo save(Todo todo); void delete(Todo todo); long countByFinished(boolean finished); }Note
ここでは、TodoRepositoryの汎用性を上げるため、「完了済み件数の取得する」メソッド(
long countFinished()
)ではなく、 「完了状態がxである件数を取得する」メソッド(long countByFinished(boolean)
)として定義している。
long countByFinished(boolean)
の引数としてtrue
を渡すと「完了済みの件数」、false
を渡すと「未完了の件数」が取得できる仕様としている。
3.4.1.3. RepositoryImplの作成(インフラストラクチャ層)¶
todo.domain.repository.todo.TodoRepositoryImpl
とする。Note
RepositoryImplには、業務ロジックは含めず、Domainオブジェクトの保存先への出し入れ(CRUD操作)に終始することが実装ポイントである。
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<String, Todo> TODO_MAP = new ConcurrentHashMap<String, Todo>(); @Override public Todo findOne(String todoId) { return TODO_MAP.get(todoId); } @Override public Collection<Todo> 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 (Todo todo : TODO_MAP.values()) { if (finished == todo.isFinished()) { count++; } } return count; } }
項番 説明 (1) Repositoryとして、component-scan対象とするため、クラスレベルに@Repository
アノテーションをつける。Note
本チュートリアルでは、インフラストラクチャ層に属するクラス(RepositoryImpl)をドメイン層のパッケージ(
todo.domain
)に格納しているが、 完全に層別にパッケージを分けるのであれば、インフラストラクチャ層のクラスは、todo.infra
以下に作成した方が良い。ただし、通常のプロジェクトでは、インフラストラクチャ層が変更されることを前提としていない(そのような前提で進めるプロジェクトは、少ない)。 そこで、作業効率向上のために、ドメイン層のRepositotyインタフェースと同じ階層に、RepositoryImplを作成しても良い。
3.4.1.4. Serviceの作成¶
業務処理を実装する。必要な処理は、
- Todoの全件取得
- Todoの新規作成
- Todoの完了
- Todoの削除
である。
todo.domain.serivce.todo.TodoService
とする。必要な処理と、実装するメソッドの対応は、以下の通りである。
- Todoの全件取得→findAllメソッド
- Todoの新規作成→createメソッド
- Todoの完了→finishメソッド
- Todoの削除→deleteメソッド
実装クラスのFQCNは、todo.domain.service.todo.TodoServiceImpl
とする。
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) public class TodoServiceImpl implements TodoService { private static final long MAX_UNFINISHED_COUNT = 5; @Inject// (3) TodoRepository todoRepository; // (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 @Transactional(readOnly = true) // (7) public Collection<Todo> 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 + ".")); // (8) throw new BusinessException(messages); } // (9) 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); } }
項番 説明 (1) Serviceとしてcomponent-scanの対象とするため、クラスレベルに@Service
アノテーションをつける。 (2) クラスレベルに、@Transactional
アノテーションをつけることで、公開メソッドをすべてトランザクション管理する。アノテーションを付与することで、メソッド開始時にトランザクションを開始、メソッド正常終了時にトランザクションをコミットが行われる。また、途中で非検査例外が発生した場合は、トランザクションをロールバックされる。データベースを使用しない場合は、@Transactional
アノテーションは不要である。 (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) 参照のみ行う処理に関しては、readOnly=trueをつける。O/R Mapperによっては、この設定により、参照時のトランザクション制御の最適化が行われる(JPAを使用する場合、効果はない)。データベースを使用しない場合は、@Transactional
アノテーションは不要である。 (8) 業務エラーが発生した場合、共通ライブラリで用意されているorg.terasoluna.gfw.common.exception.BusinessExceptionをスローする。 (9) 一意性のある値を生成するために、UUIDを使用している。データベースのシーケンスを用いてもよい。Note
本節では、説明を単純化するため、エラーメッセージをハードコードしているが、メンテナンスの観点で本来は好ましくない。 通常、メッセージは、プロパティファイルに外部化することが推奨される。 プロパティファイルに外部化する方法は、プロパティ管理を参照されたい。
3.4.2. アプリケーション層の作成¶
ドメイン層の実装が完了したので、次はドメイン層を利用して、アプリケーション層の作成に取り掛かる。
3.4.2.1. Controllerの作成¶
todo.app.todo.TodoController
とする。package 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”)を設定する。
3.4.2.2. Show all TODO¶
この画面では、
- 新規作成フォームの表示
- TODOの全件表示
を行う。
3.4.2.2.1. Formの作成¶
todo.app.todo.TodoForm
とする。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; } }
3.4.2.2.2. Controllerの実装¶
TodoControllerに、setUpFormメソッドと、listメソッドを実装する。
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) TodoService todoService; @ModelAttribute // (4) public TodoForm setUpForm() { TodoForm form = new TodoForm(); return form; } @RequestMapping(value = "list") // (5) public String list(Model model) { Collection<Todo> todos = todoService.findAll(); model.addAttribute("todos", todos); // (6) return "todo/list"; // (7) } }
項番 説明 (3) TodoServiceを、DIコンテナによってインジェクションさせるために、@Inject
アノテーションをつける。DIコンテナの管理するTodoSerivce型インスタンスがインジェクションされるため、結果として、TodoServiceImplインスタンスがインジェクションされる。 (4) Formを初期化する。@ModelAttribute
アノテーションをつけることで、このメソッドの返り値のformオブジェクトが、”todoForm”という名前でModelに追加される。TodoControllerの各処理で、model.addAttribute(“todoForm”, form)が実行されるのと同義。 (5) listメソッドを”<contextPath>/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
がレンダリングされることになる。
3.4.2.2.3. JSPの作成¶
src/main/webapp/WEB-INF/views/todo/list.jsp
を作成し、Controllerから渡されたModelを表示する。
まずは、”Finish”,”Delete”ボタン以外を作成する。
<!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) --> <form:form action="${pageContext.request.contextPath}/todo/create" method="post" modelAttribute="todoForm"> <!-- (2) --> <form:input path="todoTitle" /> <input type="submit" value="Create Todo" /> </form:form> </div> <hr /> <div id="todoList"> <ul> <!-- (3) --> <c:forEach items="${todos}" var="todo"> <li><c:choose> <c:when test="${todo.finished}"><!-- (4) --> <span class="strike"> <!-- (5) --> ${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) <form:form>タグでフォームを表示する。modelAttribute属性に、ControllerでModelに追加したformの名前を指定する。action属性に指定するcontextPathは、${pageContext.request.contextPath}で取得できる。 (2) <form:input>タグでフォームのプロパティをバインドする。modelAttribute属性に指定したformのプロパティ名と、path属性の値が一致している必要がある。 (3) <c:forEach>タグを用いて、Todoのリストを全て表示する。 (4) 完了かどうか(finished)で、打ち消し線(text-decoration: line-through;)を装飾するかどうかを判断する。 (5) 文字列値を出力する際は、XSS対策のため、必ずf:h()関数を使用してHTMLエスケープを行うこと。XSS対策についての詳細は、XSS対策を参照されたい。
3.4.2.3. Create TODO¶
次に、一覧表示画面から”Create TODO”ボタンを押した後の、新規作成処理を実装する。
3.4.2.3.1. Controllerの修正¶
TodoController
に、createメソッドを追加する。
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 TodoService todoService; // (8) @Inject Mapper beanMapper; @ModelAttribute public TodoForm setUpForm() { TodoForm form = new TodoForm(); return form; } @RequestMapping(value = "list") public String list(Model model) { Collection<Todo> 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"; } }
項番 説明 (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()を使用している。
3.4.2.3.2. Formの修正¶
入力チェックのルールを定義するため、Formオブジェクトにアノテーションを追加する。
package todo.app.todo; import java.io.Serializable; import javax.validation.constraints.NotNull; import javax.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) 1文字以上30文字以下であるので、@Size
アノテーションで、範囲を指定する。
3.4.2.3.3. 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> <div id="todoForm"> <!-- (6) --> <t:messagesPanel /> <form:form action="${pageContext.request.contextPath}/todo/create" method="post" modelAttribute="todoForm"> <form:input path="todoTitle" /> <form:errors path="todoTitle" /><!-- (7) --> <input type="submit" value="Create Todo" /> </form:form> </div> <hr /> <div id="todoList"> <ul> <c:forEach items="${todos}" var="todo"> <li><c:choose> <c:when test="${todo.finished}"> <span style="text-decoration: line-through;"> ${f:h(todo.todoTitle)} </span> </c:when> <c:otherwise> ${f:h(todo.todoTitle)} </c:otherwise> </c:choose></li> </c:forEach> </ul> </div> </body> </html>
項番 説明 (6) <t:messagesPanel>タグで、結果メッセージを表示する。 (7) <form:errors>タグで、入力エラーがあった場合に表示する。path属性の値は、<form:input>タグと合わせる。
フォームに適切な値を入力してsubmitすると、以下のように、成功メッセージが表示される。
6件以上登録した場合は、業務エラーとなり、エラーメッセージが表示される。
入力フォームを、空文字にしてsubmitすると、以下のように、エラーメッセージが表示される。
3.4.2.3.4. メッセージ表示のカスタマイズ¶
<t:messagesPanel>の結果はデフォルトで、
<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; }
入力エラーは、以下のように装飾される。
3.4.2.4. Finish TODO¶
一覧表示画面に”Finish”ボタンを追加して、ボタンをsubmitすると、hiddenで対象のtodoIdが送られ、Todoを完了するように実装する。
3.4.2.4.1. 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; } .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" /> <input type="submit" value="Create Todo" /> </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)} <!-- (8) --> <form:form action="${pageContext.request.contextPath}/todo/finish" method="post" modelAttribute="todoForm" cssStyle="display: inline-block;"> <!-- (9) --> <form:hidden path="todoId" value="${f:h(todo.todoId)}" /> <input type="submit" name="finish" value="Finish" /> </form:form> </c:otherwise> </c:choose></li> </c:forEach> </ul> </div> </body> </html>
項番 説明 (8) 未完了の場合に、完了用のformを表示する。<contextPath>/todo/finishに対して、POSTでtodoIdを送信する。 (9) <form:hidden>タグでtodoIdを渡す。value属性に値を設定する場合も、 必ずf:h()関数でHTMLエスケープすること。
3.4.2.4.2. Formの修正¶
完了用のフォームも、TodoFormを用いる。 TodoFormに、todoIdプロパティを追加する必要があるが、そのままだと、新規作成用の入力チェックルールが適用されてしまう。 一つのFormに、新規作成用と完了用で、別々のルールを指定するために、group属性を設定する。
package todo.app.todo; import java.io.Serializable; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class TodoForm implements Serializable { // (3) public static interface TodoCreate { }; public static interface TodoFinish { }; private static final long serialVersionUID = 1L; // (4) @NotNull(groups = { TodoFinish.class }) private String todoId; // (5) @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; } }
項番 説明 (3) グループ化したバリデーションを行うためのグループ名となるクラスを作成する。クラスは空でよいため、ここでは、インタフェースを定義する。グループ化バリデーションについては、入力チェックを参照されたい。 (4) todoIdは、完了処理には必須であるため、@NotNull
アノテーションをつける。完了時にのみ必要なルールであるので、group属性にTodoFinish.classを設定する。 (5) 新規作成用のルールは、完了処理には不要であるので、@NotNull
アノテーション、@Size
アノテーション、それぞれのgroup属性にTodoCreate.classを設定する。
3.4.2.4.3. Controllerの修正¶
完了処理をTodoControllerに追加する。 グループ化したバリデーションを実行するために、@Valid アノテーションの代わりに、@Validated アノテーションを使用することに注意する。
package todo.app.todo; import java.util.Collection; import javax.inject.Inject; import javax.validation.groups.Default; import org.dozer.Mapper; 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.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.app.todo.TodoForm.TodoCreate; import todo.app.todo.TodoForm.TodoFinish; import todo.domain.model.Todo; import todo.domain.service.todo.TodoService; @Controller @RequestMapping("todo") public class TodoController { @Inject TodoService todoService; @Inject Mapper beanMapper; @ModelAttribute public TodoForm setUpForm() { TodoForm form = new TodoForm(); return form; } @RequestMapping(value = "list") public String list(Model model) { Collection<Todo> todos = todoService.findAll(); model.addAttribute("todos", todos); return "todo/list"; } @RequestMapping(value = "create", method = RequestMethod.POST) public String create( @Validated({ Default.class, TodoCreate.class }) TodoForm todoForm, // (16) BindingResult bindingResult, Model model, RedirectAttributes attributes) { if (bindingResult.hasErrors()) { return list(model); } Todo todo = beanMapper.map(todoForm, Todo.class); 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"; } @RequestMapping(value = "finish", method = RequestMethod.POST) // (17) public String finish( @Validated({ Default.class, TodoFinish.class }) TodoForm form, // (18) BindingResult bindingResult, Model model, RedirectAttributes attributes) { // (19) if (bindingResult.hasErrors()) { return list(model); } try { todoService.finish(form.getTodoId()); } catch (BusinessException e) { // (20) model.addAttribute(e.getResultMessages()); return list(model); } // (21) attributes.addFlashAttribute(ResultMessages.success().add( ResultMessage.fromText("Finished successfully!"))); return "redirect:/todo/list"; } }
項番 説明 (16) グループ化したバリデーションを実施するために、@Valid
アノテーションから@Validated
アノテーションに変更する。valueには、対象のグループクラスを複数指定できる。Default.classはバリデーションルールにgroupが指定されていない場合のグループである。@Validated
アノテーションを使用する際は、Default.classも指定しておくのがよい。 (17) パスが、/todo/finishで、HTTPメソッドがPOSTに対応するように、@RequestMapping
アノテーションを設定する。 (18) Finish用のグループとして、TodoFinish.classを指定する。 (19) 入力エラーがあった場合、一覧画面に戻る。 (20) 業務処理を実行して、BusinessExceptionが発生した場合は、結果メッセージをModelに追加して、一覧画面に戻る。 (21) 正常に作成が完了したので、結果メッセージをflashスコープに追加して、一覧画面でリダイレクトする。Note
Create用、Finish用に、別々のFormを作成しても良い。その場合は、必要なパラメータだけが、Formのプロパティになる。 ただし、クラス数が増え、プロパティも重複することが多いので、仕様変更が発生した場合に、修正コストが高くなる。 また、同一のController内で、複数のFormオブジェクトを、
@ModelAttribute
メソッドによって初期化すると、 毎回すべてのFormが初期化されてしまうので、不要なインスタンスが生成されてしまう。そのため、 基本的に、一つのControllerで利用するFormは、できるだけ集約し、グループ化したバリデーションの設定を行うことを推奨する。
Todoを新規作成した後に、FinishボタンをSubmitすると、以下のように打ち消し線が入り、完了したことがわかる。
3.4.2.5. Delete TODO¶
一覧表示画面に”Delete”ボタンを追加して、ボタンをsubmitすると、hiddenで対象のtodoIdが送られ、Todoを完了するように実装する。
3.4.2.5.1. 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; } .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" /> <input type="submit" value="Create Todo" /> </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" cssStyle="display: inline-block;"> <form:hidden path="todoId" value="${f:h(todo.todoId)}" /> <input type="submit" name="finish" value="Finish" /> </form:form> </c:otherwise> </c:choose> <!-- (10) --> <form:form action="${pageContext.request.contextPath}/todo/delete" method="post" modelAttribute="todoForm" cssStyle="display: inline-block;"> <!-- (11) --> <form:hidden path="todoId" value="${f:h(todo.todoId)}" /> <input type="submit" value="Delete" /> </form:form> </li> </c:forEach> </ul> </div> </body> </html>
項番 説明 (10) 削除用のformを表示する。<contextPath>/todo/deleteに対して、POSTでtodoIdを送信する。 (11) <form:hidden>タグで、todoIdを渡す。value属性に値を設定する場合も、必ずf:h()関数でHTMLエスケープすること。
3.4.2.5.2. Formの修正¶
Delete用のグループを、TodoFormに追加する。ルールは、Finish用と同じである。
package todo.app.todo; import java.io.Serializable; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class TodoForm implements Serializable { public static interface TodoCreate { }; public static interface TodoFinish { }; // (6) public static interface TodoDelete { } private static final long serialVersionUID = 1L; // (7) @NotNull(groups = { TodoFinish.class, TodoDelete.class }) private String todoId; @NotNull(groups = { TodoCreate.class }) @Size(min = 1, max = 30, groups = { TodoCreate.class }) private String todoTitle; public String getTodoId() { return todoId; } public void setTodoId(String todoId) { this.todoId = todoId; } public String getTodoTitle() { return todoTitle; } public void setTodoTitle(String todoTitle) { this.todoTitle = todoTitle; } }
項番 説明 (6) Delete用のグループTodoDeleteを定義する。 (7) todoIdプロパティに対して、TodoDeleteグループのバリデーションを行うように設定する。
3.4.2.5.3. Controllerの修正¶
削除処理を、TodoControllerに追加する。完了処理とほぼ同じである。
package todo.app.todo; import java.util.Collection; import javax.inject.Inject; import javax.validation.groups.Default; import org.dozer.Mapper; 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.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.app.todo.TodoForm.TodoDelete; import todo.app.todo.TodoForm.TodoCreate; import todo.app.todo.TodoForm.TodoFinish; import todo.domain.model.Todo; import todo.domain.service.todo.TodoService; @Controller @RequestMapping("todo") public class TodoController { @Inject TodoService todoService; @Inject Mapper beanMapper; @ModelAttribute public TodoForm setUpForm() { TodoForm form = new TodoForm(); return form; } @RequestMapping(value = "list") public String list(Model model) { Collection<Todo> todos = todoService.findAll(); model.addAttribute("todos", todos); return "todo/list"; } @RequestMapping(value = "create", method = RequestMethod.POST) 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, Todo.class); 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"; } @RequestMapping(value = "finish", method = RequestMethod.POST) 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"; } @RequestMapping(value = "delete", method = RequestMethod.POST) // (22) 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"; } }
項番 説明 (22) Todoに対して、”Delete”ボタンをsubmitすると、以下のように、対象のTODOが削除される。
3.5. データベースアクセスを伴うインフラストラクチャ層の作成¶
本節では、Domainオブジェクトをデータベースに永続化するためのインフラストラクチャ層の作成方法について、説明する。
本チュートリアルでは、以下の2つのO/R Mapperを使用したインフラストラクチャ層の作成について、説明する。
- Spring Data JPA
- TERASOLUNA DAO(MyBatis2)
3.5.1. 共通設定¶
3.5.1.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文は以下の通り。
create table if not exists todo( todo_id varchar(36) primary key, todo_title varchar(30), finished boolean, created_at timestamp )
3.5.2. Spring Data JPAを使用したインフラストラクチャ層の作成¶
本節では、インフラストラクチャ層において、 Spring Data JPA を使用する方法について、説明する。 TERASOLUNA DAOを使用する場合は、本節を読み飛ばして、TERASOLUNA DAO(MyBatis2)を使用したインフラストラクチャ層の作成に進んでよい。
3.5.2.1. Entityの設定¶
Todoクラスを、データベースとマッピングするために、JPAのアノテーションを設定する。
package todo.domain.model; import java.io.Serializable; import java.util.Date; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.Id; import javax.persistence.Table; import javax.persistence.Temporal; import javax.persistence.TemporalType; // (1) @Entity @Table(name = "todo") public class Todo implements Serializable { private static final long serialVersionUID = 1L; // (2) @Id // (3) @Column(name = "todo_id") private String todoId; @Column(name = "todo_title") private String todoTitle; @Column(name = "finished") private boolean finished; @Column(name = "created_at") // (4) @Temporal(TemporalType.TIMESTAMP) 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)@Column
アノテーションで、対応するカラム名を設定する。 (4) Date型は、java.sql.Date, java.sql.Time, java.sql.Timestampのどれに対応するか、明示的に指定する必要がある。ここでは、Timestampを指定する。
3.5.2.2. TodoRepositoryの修正¶
Spring Data JPAのRepository機能を使用するための修正を行う。
package 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 todo.domain.model.Todo; // (1) public interface TodoRepository extends JpaRepository<Todo, String> { @Query(value = "SELECT COUNT(x) FROM Todo x WHERE x.finished = :finished") // (2) long countByFinished(@Param("finished") boolean finished); // (3) }
項番 説明 (1) JpaRepositoryを拡張したインタフェースにする。Genericsのパラメータには、順にEntityのクラス(Todo)、主キーのクラス(String)を指定する。基本的なCRUD操作(findOne, findAll, save, deleteなど)は、上位のインタフェースに定義済みであるため、TodoRepositoryでは、countByFinishedのみ定義すればよい。 (2) countByFinishedを呼び出した際に、実行されるJPQLを、@Query
アノテーションで指定する。 (3) (2)で指定したJPQLのバインド変数を@Param
アノテーションで設定する。ここでは、JPQL中の”:finished”
を埋めるためのメソッド引数に、@Param(“finished”)を付けている。
3.5.2.3. TodoRepositoryImplの作成¶
以上で、Spring Data JPAを使用したインフラストラクチャ層の作成が完了したので、Service及びアプリケーション層の作成を行う。
Service及びアプリケーション層を作成後にAPサーバーを起動し、Todoの表示を行うと、以下のようなSQLログや、トランザクションログが出力される。
date:2014-08-26 12:57:48 thread:tomcat-http--3 X-Track:2705c06e0eae416591bc1f27efc84cd6 level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[START CONTROLLER] TodoController.list(Model) date:2014-08-26 12:57:48 thread:tomcat-http--3 X-Track:2705c06e0eae416591bc1f27efc84cd6 level:DEBUG logger:o.h.e.transaction.spi.AbstractTransactionImpl message:begin date:2014-08-26 12:57:48 thread:tomcat-http--3 X-Track:2705c06e0eae416591bc1f27efc84cd6 level:DEBUG logger:o.h.e.transaction.internal.jdbc.JdbcTransaction message:initial autocommit status: false date:2014-08-26 12:57:48 thread:tomcat-http--3 X-Track:2705c06e0eae416591bc1f27efc84cd6 level:DEBUG logger:jdbc.sqltiming message: org.hibernate.engine.jdbc.internal.ResultSetReturnImpl.extract(ResultSetReturnImpl.java:56) 2. /* select generatedAlias0 from Todo as generatedAlias0 */ select todo0_.todo_id as todo1_0_, todo0_.created_at as created2_0_, todo0_.finished as finished3_0_, todo0_.todo_title as todo4_0_ from todo todo0_ {executed in 5 msec} date:2014-08-26 12:57:48 thread:tomcat-http--3 X-Track:2705c06e0eae416591bc1f27efc84cd6 level:DEBUG logger:o.h.e.transaction.spi.AbstractTransactionImpl message:committing date:2014-08-26 12:57:48 thread:tomcat-http--3 X-Track:2705c06e0eae416591bc1f27efc84cd6 level:DEBUG logger:o.h.e.transaction.internal.jdbc.JdbcTransaction message:committed JDBC Connection date:2014-08-26 12:57:48 thread:tomcat-http--3 X-Track:2705c06e0eae416591bc1f27efc84cd6 level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[END CONTROLLER ] TodoController.list(Model)-> view=todo/list, model={todoForm=todo.app.todo.TodoForm@11d3160, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors} date:2014-08-26 12:57:48 thread:tomcat-http--3 X-Track:2705c06e0eae416591bc1f27efc84cd6 level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[HANDLING TIME ] TodoController.list(Model)-> 239,725,570 ns
3.5.3. TERASOLUNA DAO(MyBatis2)を使用したインフラストラクチャ層の作成¶
本節では、インフラストラクチャ層において、TERASOLUNA DAO(MyBatis2)を使用する場合の設定方法について説明する。
Note
TERASOLUNA DAOとは、MyBatis2.3.5とSpringの連携クラスであるorg.springframework.orm.ibatis.support.SqlMapClientDaoSupportを、用途別に拡張した簡易SQLマッパーを提供するライブラリである。 以下4つのインタフェースをもつDAOが、提供されている。
- jp.terasoluna.fw.dao.QueryDAO
- jp.terasoluna.fw.dao.UpdateDAO
- jp.terasoluna.fw.dao.StoredProcedureDAO
- jp.terasoluna.fw.dao.QueryRowHandleDAO
それぞれのインタフェースに対して、
jp.terasoluna.fw.dao.ibatis.XxxDAOiBatisImpl
という実装を持つ。
3.5.3.1. RepositoryImplの作成¶
TERASOLUNA DAO(MyBatis2)を使用した場合、RepositoryImplは、TERASOLUNA DAOから提供されているクラスを呼び出すことで、 データベースにアクセスを行うように実装する。
package todo.domain.repository.todo; import java.util.Collection; import javax.inject.Inject; import jp.terasoluna.fw.dao.QueryDAO; import jp.terasoluna.fw.dao.UpdateDAO; import org.springframework.stereotype.Repository; import org.springframework.transaction.annotation.Transactional; import todo.domain.model.Todo; @Repository // (1) @Transactional // (2) public class TodoRepositoryImpl implements TodoRepository { // (3) @Inject QueryDAO queryDAO; @Inject UpdateDAO updateDAO; // (4) @Override @Transactional(readOnly = true) public Todo findOne(String todoId) { return queryDAO.executeForObject("todo.findOne", todoId, Todo.class); } @Override @Transactional(readOnly = true) public Collection<Todo> findAll() { return queryDAO.executeForObjectList("todo.findAll", null); } @Override public Todo save(Todo todo) { // (5) if (exists(todo.getTodoId())) { updateDAO.execute("todo.update", todo); } else { updateDAO.execute("todo.create", todo); } return todo; } @Transactional(readOnly = true) public boolean exists(String todoId) { long count = queryDAO.executeForObject("todo.exists", todoId, Long.class); return count > 0; } @Override public void delete(Todo todo) { updateDAO.execute("todo.delete", todo); } @Override @Transactional(readOnly = true) public long countByFinished(boolean finished) { return queryDAO.executeForObject("todo.countByFinished", finished, Long.class); } }
項番 説明 (1) Repositoryとして、component-scan対象とするため、クラスレベルに@Repositoryアノテーションをつける。 (2) クラスレベルに、@Transactional
アノテーションをつけることで、公開メソッドをすべてトランザクション管理する。Repositoryを呼び出すService側でも設定しているため、@Transactional
をつけなくともトランザクション管理になるが、propagation属性は、デフォルトのREQUIREDであるため、トランザクションがネストした場合、内側(Repository側)のトランザクションは、外側(Service側)のトランザクションに参加する。 (3)@Inject
アノテーションで、QueryDAO, UpdateDAOをインジェクションする。 (4) Repositoryのメソッド実装は、基本的には、TERASOLUNA DAOにSQLIDと、パラメータを渡すことになる。参照系の場合はQueryDAO、更新系の場合はUpdateDAOを使用する。SQLIDに対応するSQLの設定は、次に行う。 (5) saveメソッドで新規作成と、更新の両方を実装している。どちらの処理を行うか判断するために、existsメソッドを作成する。このメソッドでは、対象のtodoIdの件数を取得し、件数が0より大きいかどうかで存在を確認する。Note
saveメソッドは、新規作成でも更新でも利用できるメリットがある。 しかしながら、2回SQLが実行されるという性能面でのデメリットもある。 性能を重視する場合は、新規作成用にcreateメソッドを、更新用にupdateメソッドを作成すること。
3.5.3.2. SQLMapファイルの作成¶
src/main/resources/META-INF/mybatis/sql/todo-sqlmap.xml
を作成し、
TodoRepositoryImpl
で使用したSQLIDに対応するsqlを、以下のように記述する。
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd"> <sqlMap namespace="todo"> <resultMap id="todo" class="todo.domain.model.Todo"> <result property="todoId" column="todo_id" /> <result property="todoTitle" column="todo_title" /> <result property="finished" column="finished" /> <result property="createdAt" column="created_at" /> </resultMap> <select id="findOne" parameterClass="java.lang.String" resultMap="todo"> <![CDATA[ SELECT todo_id, todo_title, finished, created_at FROM todo WHERE todo_id = #value# ]]> </select> <select id="findAll" resultMap="todo"> <![CDATA[ SELECT todo_id, todo_title, finished, created_at FROM todo ]]> </select> <insert id="create" parameterClass="todo.domain.model.Todo"> <![CDATA[ INSERT INTO todo ( todo_id, todo_title, finished, created_at ) VALUES ( #todoId#, #todoTitle#, #finished#, #createdAt# ) ]]> </insert> <update id="update" parameterClass="todo.domain.model.Todo"> <![CDATA[ UPDATE todo SET todo_title = #todoTitle#, finished = #finished#, created_at = #createdAt# WHERE todo_id = #todoId# ]]> </update> <delete id="delete" parameterClass="todo.domain.model.Todo"> <![CDATA[ DELETE FROM todo WHERE todo_id = #todoId# ]]> </delete> <select id="countByFinished" parameterClass="java.lang.Boolean" resultClass="java.lang.Long"> <![CDATA[ SELECT COUNT(*) FROM todo WHERE finished = #value# ]]> </select> <select id="exists" parameterClass="java.lang.String" resultClass="java.lang.Long"> <![CDATA[ SELECT COUNT(*) FROM todo WHERE todo_id = #value# ]]> </select> </sqlMap>
以上で、TERASOLUNA DAOを使用したインフラストラクチャ層の作成が完了したので、Service及びアプリケーション層の作成を行う。
Service及びアプリケーション層を作成後にAPサーバーを起動し、Todoの表示を行うと、以下のようなSQLログや、トランザクションログが出力される。
date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[START CONTROLLER] TodoController.list(Model) date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Creating new transaction with name [todo.domain.repository.todo.TodoRepositoryImpl.findAll]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly; '' date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Acquired Connection [net.sf.log4jdbc.ConnectionSpy@1d7541c] for JDBC transaction date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:DEBUG logger:jdbc.sqltiming message: com.ibatis.sqlmap.engine.execution.DefaultSqlExecutor.executeQuery(DefaultSqlExecutor.java:183) 4. SELECT todo_id, todo_title, finished, created_at FROM todo {executed in 0 msec} date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Initiating transaction commit date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Committing JDBC transaction on Connection [net.sf.log4jdbc.ConnectionSpy@1d7541c] date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:DEBUG logger:o.s.jdbc.datasource.DataSourceTransactionManager message:Releasing JDBC Connection [net.sf.log4jdbc.ConnectionSpy@1d7541c] after transaction date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[END CONTROLLER ] TodoController.list(Model)-> view=todo/list, model={todoForm=todo.app.todo.TodoForm@c7582d, todos=[], org.springframework.validation.BindingResult.todoForm=org.springframework.validation.BeanPropertyBindingResult: 0 errors} date:2014-08-26 13:39:08 thread:tomcat-http--9 X-Track:49fbb5426f574375b8169d39c330ec7f level:TRACE logger:o.t.gfw.web.logging.TraceLoggingInterceptor message:[HANDLING TIME ] TodoController.list(Model)-> 2,521,308 ns
3.6. おわりに¶
このチュートリアルでは、以下の内容を学習した。
- TERASOLUNA Global Frameworkによる基本的なアプリケーションの開発方法、およびEclipseプロジェクトの構築方法
- STSの使用方法
- MavenでTERASOLUNA Global Frameworkを使用する方法
- TERASOLUNA Global Frameworkのアプリケーションのレイヤ化に従った開発方法
- POJO(+ Spring)によるドメイン層の実装
- Spring MVCとJSPタグライブラリを使用したアプリケーション層の実装
- Spring Data JPAによるインフラストラクチャ層の実装
- MyBatis2によるインフラストラクチャ層の実装
ここで作成したTODO管理アプリケーションには、以下の改善点がある。 アプリケーションの修正を学習課題として、ガイドライン中の該当する説明を参照されたい。