5.1. RESTful Web Service¶
目次
- Overview
- Architecture
- How to design
- How to use
- How to extend
- Appendix
5.1.1. Overview¶
本節では、RESTful Web Serviceの基本的な概念とSpring MVCを使った開発について説明する。
RESTful Web Serviceのアーキテクチャ、設計、実装に対する具体的な説明については、
- RESTful Web Serviceの基本的なアーキテクチャについて説明している。
- RESTful Web Serviceの設計を行う際に考慮すべき点などを説明している。
- RESTful Web Serviceのアプリケーション構成やAPIの実装方法について説明している。
を参照されたい。
5.1.1.1. RESTful Web Serviceとは¶
5.1.1.2. RESTful Web Serviceの開発について¶
Macchinetta Server Framework (1.x)では、Spring MVCの機能を利用してRESTful Web Serviceの開発を行う。
項番 機能概要 Note
例外ハンドリングについて
例外ハンドリングについては、Spring MVCから汎用的な機能の提供がないため、プロジェクト毎に実装が必要となる。 例外ハンドリングの詳細については、「例外のハンドリングの実装」を参照されたい。
項番 処理レイヤ 説明 HttpMessageConverterを使用して、リクエストBODYに指定されているJSON形式の電文をResourceオブジェクトに変換する。Validatorを使用して、Resourceオブジェクトに格納されて値に対して入力チェックを行う。HttpMessageConverterを使用して、REST APIから返却されたResourceオブジェクトをJSON形式の電文に変換する。
5.1.1.2.1. RESTful Web Serviceのモジュールの構成¶
- アプリケーション層のモジュール - 項番 - モジュール名 - 説明 (1)ControllerクラスREST APIを提供するクラス。Controllerクラスはリソース単位に作成し、リソース毎のREST APIのエンドポイント(URI)の指定を行う。リソースに対するCRUD処理は、ドメイン層のServiceに委譲する事で実現する。(2)ResourceクラスREST APIの入出力となるJSON(またはXML)を表現するJava Bean。このクラスには、Bean Validationのアノテーションの指定や、JSONやXMLのフォーマットを制御するためのアノテーションの指定を行う。(3)Validatorクラス(Optional)入力値の相関チェックを実装するクラス。入力値の相関チェックが不要な場合は、本クラスを作成する必要はないため、オプションの扱いとしている。入力値の相関チェックについては、「入力チェック」を参照されたい。(4)Helperクラス(Optional)Controllerで行う処理を補助するための処理を実装するクラス。本クラスは、Controllerの処理をシンプルに保つことを目的として作成するクラスである。具体的には、ResourceオブジェクトとDomainObjectのモデル変換処理などを行うメソッドを実装する。モデル変換が単純な値のコピーのみで済む場合は、Helperクラスは作成せずに「Beanマッピング(Dozer)」を使用すればよいため、オプションの扱いにしている。
- ドメイン層のモジュール - 項番 - 説明 (5)ドメイン層で実装するモジュールは、アプリケーションの種類に依存しないため、本節での説明は割愛する。各モジュールの役割については「アプリケーションのレイヤ化」を、ドメイン層の開発については「ドメイン層の実装」を参照されたい。
- インフラストラクチャ層のモジュール - 項番 - 説明 (6)インフラストラクチャ層で実装するモジュールは、アプリケーションの種類に依存しないため、本節での説明は割愛する。各モジュールの役割については「アプリケーションのレイヤ化」を、インフラストラクチャ層の開発については「インフラストラクチャ層の実装」を参照されたい。
5.1.1.2.2. REST APIの実装サンプル¶
Note
詳細な説明を読む前に、まずは「チュートリアル(Todoアプリケーション REST編)」を実践する事を強く推奨する。
チュートリアルでは”習うより慣れろ”を目的としており、 詳細な説明の前に実際に手を動かすことでMacchinetta Server Framework (1.x)によるRESTful Web Serviceの開発を体感する事が出来る。 RESTful Web Serviceの開発を体感した後に、詳細な説明を読むことで、RESTful Web Serviceの開発に対する理解度がより深まる事が期待できる。
特にRESTful Web Serviceの開発経験がない場合は、「チュートリアルの実践」 → 「アーキテクチャ、設計、開発に関する詳細な説明(次節以降で説明)」 → 「チュートリアルの振り返り(再実践)」というプロセスを踏むことを推奨する。
- 実装サンプルで扱うリソース
実装サンプルで扱うリソース(Todoリソース)は、以下のJSON形式とする。
{ "todoId" : "9aef3ee3-30d4-4a7c-be4a-bc184ca1d558", "todoTitle" : "Hello World!", "finished" : false, "createdAt" : "2014-02-25T02:21:48.493+0000" }
- Resourceクラスの実装サンプル
上記で示したTodoリソースを表現するJavaBeanとして、Resourceクラスを作成する。
package todo.api.todo; import java.io.Serializable; import java.util.Date; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class TodoResource implements Serializable { private static final long serialVersionUID = 1L; private String todoId; @NotNull @Size(min = 1, max = 30) 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; } }
- Controllerクラス(REST API)の実装サンプル
Todoリソースに対して、以下の5つのREST API(Controllerのハンドラメソッド)を作成する。
/api/v1/todos/api/v1/todos/api/v1/todos/{todoId}/api/v1/todos/{todoId}/api/v1/todos/{todoId}
5.1.2. Architecture¶
以下の5つのアーキテクチャは、アプリケーションの特性に関係なく適用すべきアーキテクチャである。
項番 アーキテクチャ アーキテクチャの概要 
以下の2つのアーキテクチャは、アプリケーションの特性に応じて、適用するアーキテクチャである。
項番 アーキテクチャ アーキテクチャの概要 
5.1.2.1. Web上のリソースとして公開¶
例えば、ショッピングサイトを提供するWebシステムであれば、以下のような情報がWeb上のリソースとして公開する事になる。
- 商品の情報
- 在庫の情報
- 注文の情報
- 会員の情報
- 会員毎の認証の情報(ログインIDとパスワードなど)
- 会員毎の注文履歴の情報
- 会員毎の認証履歴の情報
- and more …
5.1.2.2. URIによるリソースの識別¶
- http://example.com/api/v1/items「items」の部分が「リソースの種類を表す名詞」となり、リソースの数が複数になる場合は、複数系の名詞を使用する。上記例では、商品情報である事を表す名詞の複数系を指定しており、商品情報を一括で操作する際に使用するURIとなる。これは、ファイルシステムに置き換えると、ディレクトリに相当する。
- http://example.com/api/v1/items/I312-535-01216「I312-535-01216」の部分が「リソースを識別するための値」となり、リソース毎に異なる値となる。上記例では、商品情報を一意に識別するための値として商品IDを指定しており、特定の商品情報を操作する際に使用するURIとなる。これは、ファイルシステムに置き換えると、ディレクトリの中に格納されているファイルに相当する。
Warning
RESTful Web Serviceに割り当てるURIには、下記で示すような操作を表す動詞を含んではいけない。
- http://example.com/api/v1/items?get&itemId=I312-535-01216
- http://example.com/api/v1/items?delete&itemId=I312-535-01216
上記例では、 URIの中にgetやdeleteという動詞を含んでいるため、RESTful Web Serviceに割り当てるURIとして適切ではない。
RESTful Web Serviceでは、リソースに対する操作はHTTPメソッド(GET,POST,PUT,DELETE)を使用して表現する。
5.1.2.3. HTTPメソッドによるリソースの操作¶
以下に、HTTPメソッドに割り当てられる、リソースに対する操作の対応付けと、それぞれの操作が保証すべき事後条件について説明する。
項番 HTTPメソッド リソースに対する操作 操作が保証すべき事後条件 Note
安全性とべき等性の保証について
HTTPメソッドを使ってリソースの操作を行う場合、事後条件として、「安全性」と「べき等性」の保証を行う事が求められる。
【安全性とは】
ある数字に1を何回掛けても、数字がかわらない事(10に1を何回掛けても結果は10のままである事)を保証する。 これは、同じ操作を何回行ってもリソースの状態が変わらない事を保証する事である。【べき等性とは】
数字に0を何回掛けても0になる事(10に0を1回掛けても何回掛けても結果は共に0になる事)を保証する。 これは、一度操作を行えば、その後で同じ操作を何回行ってもリソースの状態が変わらない事を保証する事である。 ただし、別のクライアントが同じリソースの状態を変更している場合は、べき等性を保障する必要はなく、事前条件に対するエラーとして扱ってもよい。Tip
クライアントがリソースに割り当てるURIを指定してリソースを作成する場合
リソースを作成する際に、クライアントによってリソースに割り当てるURIを指定する場合は、作成するリソースに割り当てるURIに対して、PUTメソッドを呼び出すことで実現する。
PUTメソッドを使用してリソースを作成する場合、
- 指定されたURIにリソースが存在しない場合はリソースを作成
- 既にリソースが存在する場合はリソースの状態を更新
するのが一般的な動作である。
以下に、PUTとPOSTメソッドを使ってリソースを作成する際の処理イメージの違いについて説明する。
【PUTメソッドを使用してリソースを作成する際の処理イメージ】
【POSTメソッドを使用してリソースを作成する際の処理イメージ】
5.1.2.4. 適切なフォーマットの使用¶
リソースのフォーマットは、JSON又はXMLなどのデータ構造を示すためのフォーマットを使用する。
- 拡張子によって切り替えを行う。 レスポンスのフォーマットは、拡張子を指定する事で切り替える事ができる。本ガイドラインでは、拡張子による切り替えを推奨する。推奨する理由は、レスポンスするフォーマット指定が簡単であるという点と、レスポンスするフォーマットがURIに含まれ、直感的なURIになるという点である。
Note
拡張子で切り替える場合のURI例
- http://example.com/api/v1/items.json
- http://example.com/api/v1/items.xml
- http://example.com/api/v1/items/I312-535-01216.json
- http://example.com/api/v1/items/I312-535-01216.xml
- リクエストのAcceptヘッダのMIMEタイプによって切り替えを行う。 - RESTful Web Serviceで使用される代表的なMIMEタイプを以下に示す。 - 項番 - フォーマット - MIMEタイプ (1)JSONapplication/json(2)XMLapplication/xml
5.1.2.5. 適切なHTTPステータスコードの使用¶
クライアントへ返却するレスポンスには、適切なHTTPステータスコードを設定する。
200 OKを応答し、処理結果はエンティティボディ(HTML)の中で表現するという事が一般的であった。
項番 潜在的な問題点 
Note
HTTPのメッセージ構文を規定するRFC 7230では、HTTPステータスコードの説明句(
reason-phrase)の出力は必須ではなく、クライアントは無視すべきであると規定されている。 例えば、RFC 7230に準拠した実装のTomcat 8.5では、説明句が出力されない。RFC 7230(Hypertext Transfer Protocol – HTTP/1.1)の3.1.2 Status Line を参照されたい。
5.1.2.6. ステートレスなクライアント/サーバ間の通信¶
Note
アプリケーションの状態とは
Webページの遷移状態、入力値、プルダウン/チェックボックス/ラジオボタンなどの選択状態、認証状態などの事である。
Note
CSRF対策との関連
本ガイドラインに記載されているCSRF対策をRESTful Web Serviceに対して行った場合、CSRF対策用のトークン値がHTTPセッションに保存されるため、クライアントとサーバ間の「ステートレス性」を保つ事が出来ないという点を補足しておく。
そのため、CSRF対策を行う場合は、システムの可用性を考慮する必要がある。
高い可用性が求められるシステムでは、
- APサーバをクラスタ化し、セッションをレプリケーションする。
- セッションの保存先をAPサーバのメモリ以外にする。
等の対策が必要となる。 ただし、上記対策は性能への影響があるため、性能要件も考慮する必要がある。
CSRF対策については、CSRF対策を参照されたい。
Todo
TBD
高い可用性が求められる場合は、「CSRF対策用のトークン値をAPサーバのメモリ(HTTPセッション)以外に保存する」アーキテクチャを検討した方がよい。
具体的なアーキテクチャについては、現在検討中であり、次版以降に記載する予定である。
5.1.3. How to design¶
本説では、RESTful Web Serviceの設計について説明する。
5.1.3.1. リソースの抽出¶
まず、Web上に公開するリソースを抽出する。
リソースを抽出する際の注意点を以下に示す。
項番 リソース抽出時の注意点 
5.1.3.2. URIの割り当て¶
抽出したリソースに対して、リソースを識別するためのURIを割り当てる。
URIは、以下の形式を推奨する。
- http(s)://{ドメイン名(:ポート番号)}/{REST APIであることを示す値}/{APIバージョン}/{リソースを識別するためのパス}
- http(s)://{REST APIであることを示すドメイン名(:ポート番号)}/{APIバージョン}/{リソースを識別するためのパス}
具体例は以下の通り。
- http://example.com/api/v1/members/M000000001
- http://api.example.com/v1/members/M000000001
5.1.3.2.1. REST APIであることを示すためのURIの割り当て¶
RESTful Web Service(REST API)向けのURIであること明確にするために、URI内のドメイン又はパスに apiを含めることを推奨する。
具体的には、以下のようなURIとする。
- http://example.com/api/...
- http://api.example.com/...
5.1.3.2.2. APIバージョンを識別するためのURIの割り当て¶
RESTful Web Serviceは、複数のバージョンで稼働が必要になる可能性があるため、クライアントに公開するURIには、APIバージョンを識別するための値を含めるようにする事を推奨する。
具体的には、以下のような形式のURIとする。
- http://example.com/api/{APIバージョン}/{リソースを識別するためのパス}
- http://api.example.com/{APIバージョン}/{リソースを識別するためのパス}
Todo
TBD
URIの中にAPIバージョンを含めるべきかは、現在検討中である。
5.1.3.2.3. リソースを識別するためのパスの割り当て¶
項番 URIの形式 URIの具体例 説明 
項番 URIの形式 URIの具体例 説明 
項番 URIの形式 URIの具体例 説明 
5.1.3.3. HTTPメソッドの割り当て¶
リソース毎に割り当てたURIに対して、以下のHTTPメソッドを割り当て、リソースに対するCRUD操作をREST APIとして公開する。
5.1.3.3.1. リソースコレクションのURIに対するHTTPメソッドの割り当て¶
項番 HTTPメソッド 実装するREST APIの概要 
5.1.3.3.2. 特定リソースのURIに対するHTTPメソッドの割り当て¶
項番 HTTPメソッド 実装するREST APIの概要 
5.1.3.4. リソースのフォーマット¶
5.1.3.4.1. JSONのフィールド名¶
{ "memberId" : "M000000001" }
5.1.3.4.2. NULLとブランク文字¶
{ "dateOfBirth" : null, "address1" : "" }
5.1.3.4.3. 日時のフォーマット¶
具体的には、以下の3つの形式となる。
- yyyy-MM-dd
{ "dateOfBirth" : "1977-03-12" }
- yyyy-MM-dd’T’HH:mm:ss.SSSZ
{ "lastModifiedAt" : "2014-03-12T22:22:36.637+09:00" }
- yyyy-MM-dd’T’HH:mm:ss.SSS’Z’ (UTC用の形式)
{ "lastModifiedAt" : "2014-03-12T13:11:27.356Z" }
5.1.3.4.4. ハイパーメディアリンクの形式¶
{ "links" : [ { "rel" : "ownerMember", "href" : "http://example.com/api/v1/memebers/M000000001" } ] }
relとhrefという2つのフィールドを持ったLinkオブジェクトをコレクション形式で保持する。
relには、なんのリンクか識別するためのリンク名を指定する。
hrefには、リソースにアクセスするためのURIを指定する。- Linkオブジェクトをコレクション形式で保持するフィールドは、
linksとする。
5.1.3.4.5. エラー応答時のフォーマット¶
エラーを検知した際に応答するフォーマット例を以下に示す。
{ "code" : "e.ex.fw.7001", "message" : "Validation error occurred on item in the request body.", "details" : [ { "code" : "ExistInCodeList", "message" : "\"genderCode\" must exist in code list of CL_GENDER.", "target" : "genderCode" } ] }
上記のフォーマット例では、
- エラーコード(code)
- エラーメッセージ(message)
- エラー詳細リスト(details)
5.1.3.5. HTTPステータスコード¶
HTTPステータスコードは、以下の指針に則って応答する。
項番 方針 
5.1.3.5.1. リクエストが成功した場合のHTTPステータスコード¶
リクエストが成功した場合は、状況に応じて以下のHTTPステータスコードを応答する。
Tip
"200 OKと204 No Contentの違いは、レスポンスボディにリソースの情報を出力する/しないの違いとなる。
5.1.3.5.2. リクエストが失敗した原因がクライアント側にある場合のHTTPステータスコード¶
リクエストが失敗した原因がクライアント側にある場合は、状況に応じて以下のHTTPステータスコードを応答する。
リソースを扱う個々のREST APIで意識する必要があるステータスコードは以下の通り。
5.1.3.5.3. リクエストが失敗した原因がサーバ側にある場合のHTTPステータスコード¶
リクエストが失敗した原因がサーバ側にある場合は、状況に応じて以下のHTTPステータスコードを応答する。
5.1.3.7. リソースの条件付き更新の制御¶
Todo
TBD
HTTPヘッダを使ったリソースの条件付き更新(排他制御)をどのように行うか記載する。
Etag/Last-Modified-Sinceなどのヘッダを使って条件付き更新の仕組みについて、次版以降に記載する予定である。
5.1.3.8. リソースの条件付き取得の制御¶
Todo
TBD
HTTPヘッダを使ったリソースの条件付き取得(304 Not Modified制御)をどのように行うか記載する。
Etag/Last-Modifiedなどのヘッダを使ったリソースの条件付き取得の仕組みについて、次版以降に記載する予定である。
5.1.3.9. リソースのキャッシュ制御¶
Todo
TBD
HTTPヘッダを使ったリソースのキャッシュ制御をどのように行うか記載する。
Cache-Control/Pragma/Expiresなどのヘッダを使ったリソースのキャッシュ制御の仕組みについて、次版以降に記載する予定である。
5.1.3.10. バージョニング¶
Todo
TBD
RESTful Web Service自体のバージョン管理及び複数バージョンの並行稼働をどのように行うかについて、次版以降に記載する予定である。
5.1.4. How to use¶
本節では、RESTful Web Serviceの具体的な作成方法について説明する。
5.1.4.1. Webアプリケーションの構成¶
項番 構成 説明 DispatcherServletを設けて構築する。DispatcherServletと、クライアントアプリケーション用のリクエストを受け取るDispatcherServletは分割して構築することを強く推奨する。Note
クライアントアプリケーション(UI層のアプリケーション)とは
ここで言うクライアントアプリケーション(UI層のアプリケーション)とは、HTML, JavaScriptなどのスクリプト, CSS(Cascading Style Sheets)といったクライアント層(UI層)のコンポーネントを応答するアプリケーションの事をさす。 JSPなどのテンプレートエンジンによって生成されるHTMLも対象となる。
Note
DispatcherServletを分割する事を推奨する理由
Spring MVCでは、
DispatcherServlet毎にアプリケーションの動作設定を定義することになる。 そのため、RESTful Web Serviceとクライアントアプリケーション(UI層のアプリケーション)のリクエストを同じDispatcherServletで受ける構成にしてしまうと、RESTful Web Service又はクライアントアプリケーション固有の動作設定を定義する事ができなくなったり、設定が煩雑又は複雑になることがある。本ガイドラインでは、上記の様な問題が起こらないようにするために、RESTful Web Serviceについてクライアントアプリケーションを同じWebアプリケーション(war)として構築する場合は、
DispatcherServletを分割することを推奨している。
RESTful Web Service専用のWebアプリケーションとして構築する際の構成イメージは以下の通り。
RESTful Web Serviceとクライアントアプリケーションを一つのWebアプリケーションとして構築する際の構成イメージは以下の通り。
5.1.4.2. pom.xmlの設定¶
terasoluna-gfw-common-dependenciesを使用していれば、依存関係の設定は不要である。
Warning
Java SE 7環境にて使用する場合の設定
terasoluna-gfw-common-dependenciesはJava SE 8を前提とした依存関係を設定している。Java SE 7環境にて使用する場合は下記のようにJava SE 8依存ライブラリをexclusionすること。 java SE 8依存ライブラリについてはアーキテクチャ概要の「利用するOSSのバージョン 」を参照
<dependency> <groupId>org.terasoluna.gfw</groupId> <artifactId>terasoluna-gfw-common-dependencies</artifactId> <exclusions> <exclusion> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </exclusion> </exclusions> </dependency>
5.1.4.3. アプリケーションの設定¶
RESTful Web Service向けのアプリケーションの設定について説明する。
Warning
StAX(Streaming API for XML)使用時のDoS攻撃対策について
XML形式のデータについてStAXを使用して解析する場合は、DTDを使ったDoS攻撃を受けないように対応する必要がある。 詳細は、CVE-2015-3192 - DoS Attack with XML Inputを参照されたい。
5.1.4.3.1. RESTful Web Serviceで必要となるSpring MVCのコンポーネントを有効化するための設定¶
- spring-mvc-rest.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 "> <!-- Load properties files for placeholder. --> <!-- (1) --> <context:property-placeholder location="classpath*:/META-INF/spring/*.properties" /> <bean id="jsonMessageConverter" class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper" ref="objectMapper" /> </bean> <bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"> <!-- (2) --> <property name="dateFormat"> <bean class="com.fasterxml.jackson.databind.util.StdDateFormat" /> </property> </bean> <!-- Register components of Spring MVC. --> <!-- (3) --> <mvc:annotation-driven> <mvc:message-converters register-defaults="false"> <ref bean="jsonMessageConverter" /> </mvc:message-converters> <!-- (4) --> <mvc:argument-resolvers> <bean class="org.springframework.data.web.PageableHandlerMethodArgumentResolver" /> </mvc:argument-resolvers> </mvc:annotation-driven> <!-- Register components of interceptor. --> <!-- (5) --> <mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**" /> <bean class="org.terasoluna.gfw.web.logging.TraceLoggingInterceptor" /> </mvc:interceptor> <!-- omitted --> </mvc:interceptors> <!-- Scan & register components of RESTful Web Service. --> <!-- (6) --> <context:component-scan base-package="com.example.project.api" /> <!-- Register components of AOP. --> <!-- (7) --> <bean id="handlerExceptionResolverLoggingInterceptor" class="org.terasoluna.gfw.web.exception.HandlerExceptionResolverLoggingInterceptor"> <property name="exceptionLogger" ref="exceptionLogger" /> </bean> <aop:config> <aop:advisor advice-ref="handlerExceptionResolverLoggingInterceptor" pointcut="execution(* org.springframework.web.servlet.HandlerExceptionResolver.resolveException(..))" /> </aop:config> </beans>
項番 説明 <context:property-placeholder>要素を使用してプロパティファイルを読み込む必要がある。プロパティファイルから値を取得する方法の詳細については、「プロパティ管理」を参照されたい。<mvc:message-converters>要素のregister-defaults属性をfalseにしているので、リソースの形式はJSONに限定される。リソースのフォーマットとしてXMLを使用する場合は、XXE Injection対策が行われているXML用のMessageConverterを指定すること。指定方法は、「XXE Injection対策の有効化」を参照されたい。TraceLoggingInterceptorを定義している。com.example.project.apiの部分はプロジェクト毎のパッケージ名となる。HandlerExceptionResolverLoggingInterceptorについては、「例外ハンドリング」を参照されたい。
Note
ObjectMapperのBean定義方法について
Jacksonのcom.fasterxml.jackson.databind.ObjectMapperのBean定義を行う場合は、
Springが提供しているJackson2ObjectMapperFactoryBeanを使用するとよい。
Jackson2ObjectMapperFactoryBeanを使用すると、JSR-310 Date and Time APIやJoda Time用の拡張モジュールを自動登録することができ、
さらにXMLのBean定義ファイル上で表現が難しかったObjectMapperのコンフィギュレーションも簡単に行うことができる。
なお、ObjectMapperを直接Bean定義するスタイルからJackson2ObjectMapperFactoryBeanを使用するスタイルに変更する場合は、
以下のコンフィギュレーションに対するデフォルト値がJacksonのデフォルト値と異なる(無効化されている)点に注意すること。
ObjectMapperの動作をJacksonのデフォルト動作にあわせたい場合は、featuresToEnableプロパティを使用して上記のコンフィギュレーションを有効化する。
<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"> <!-- ... --> <property name="featuresToEnable"> <array> <util:constant static-field="com.fasterxml.jackson.databind.MapperFeature.DEFAULT_VIEW_INCLUSION"/> <util:constant static-field="com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES"/> </array> </property> </bean>
Jackson2ObjectMapperFactoryBeanの詳細については、 Jackson2ObjectMapperFactoryBeanのJavaDocを参照されたい。
Note
jackson version 1.x.x から jackson version 2.x.xへ変更する場合の注意点
- パッケージの変更
verision package 
- 注意事項として、配下のパッケージ構成も変更されている。
- Deprecated一覧
5.1.4.3.2. RESTful Web Service用のサーブレットの設定¶
- web.xml
<!-- omitted --> <servlet> <!-- (1) --> <servlet-name>restAppServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <!-- (2) --> <param-value>classpath*:META-INF/spring/spring-mvc-rest.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- (3) --> <servlet-mapping> <servlet-name>restAppServlet</servlet-name> <url-pattern>/api/v1/*</url-pattern> </servlet-mapping> <!-- omitted -->
項番 説明 <servlet-name>要素に、RESTful Web Service用のサーブレットであることを示す名前を指定する。上記例では、サーブレット名としてrestAppServletを指定している。DispatcherServletを構築する際に使用するSpring MVCのbean定義ファイルを指定する。上記例では、Spring MVCのbean定義ファイルとして、クラスパス上にあるMETA-INF/spring/spring-mvc-rest.xmlを指定している。DispatcherServletへマッピングするサーブレットパスのパターンの指定を行う。上記例では、/api/v1/配下のサーブレットパスをRESTful Web Service用のDispatcherServletにマッピングしている。具体的には、/api/v1//api/v1/members/api/v1/members/xxxxxといったサーブレットパスが、RESTful Web Service用のDispatcherServlet(restAppServlet)にマッピングされる。Tip
@RequestMappingアノテーションのvalue属性に指定する値について
@RequestMappingアノテーションのvalue属性に指定する値は、<url-pattern>要素で指定したワイルドカード(“*”)の部分の値を指定する。例えば、
@RequestMapping(value = "members")と指定した場合、/api/v1/membersといパスに対する処理を行うメソッドとしてデプロイされる。 そのため、@RequestMappingアノテーションのvalue属性には、分割したサーブレットへマッピングするためパス(api/v1)を指定する必要はない。
@RequestMapping(value = "api/v1/members")と指定すると、/api/v1/api/v1/membersというパスに対する処理を行うメソッドとしてデプロイされてしまうので、注意すること。
5.1.4.4. REST APIの実装¶
まず、説明で使用するREST APIの仕様を以下に示す。
リソースの形式
会員情報のリソースの形式は、以下のようなJSON形式とする。下記の例では、全フィールドを表示しているが、全てのAPIのリクエストとレスポンスで使用するわけではない。例えば、passwordはリクエストのみで使用、createdAtやlastModifiedAtはレスポンスのみ使用などの違いがある。{ "memberId" : "M000000001", "firstName" : "Firstname", "lastName" : "Lastname", "genderCode" : "1", "dateOfBirth" : "1977-03-13", "emailAddress" : "user1@test.com", "telephoneNumber" : "09012345678", "zipCode" : "1710051", "address" : "Tokyo", "credential" : { "signId" : "user1@test.com", "password" : "zaq12wsx", "passwordLastChangedAt" : "2014-03-13T04:39:14.831Z", "lastModifiedAt" : "2014-03-13T04:39:14.831Z" }, "createdAt" : "2014-03-13T04:39:14.831Z", "lastModifiedAt" : "2014-03-13T04:39:14.831Z" }Note
本節では、関連リソースへのハイパーメディアリンクは設けない例となっている。 ハイパーメディアリンクを設ける場合の実装例は、「ハイパーメディアリンクの実装」を参照されたい。
リソースの項目仕様
リソース(JSON)の項目毎の仕様は以下の通りとする。
項番 項目名 型 I/O仕様 桁数 (min-max) その他の仕様 memberId String I/O 10-10 POST Membersのリクエスト時は未指定(NULL)であること。 firstName String I/O 1-128 - lastName String I/O 1-128 - genderCode I/O 1-1 0” : UNKNOWN“1” : MEN“2” : WOMENdateOfBirth Date I/O - emailAddress I/O 1-256 - telephoneNumber String I/O 0-20 - zipCode String I/O 0-20 - address String I/O 0-256 - credential I/O - POST Membersのリクエスト時は指定されていること。 credential/signId I/O 0-256 指定がない場合は、emailAddressの値を適用する。 String I 8-32 - O - O - createdAt O - lastModifiedAt O - 
REST API一覧
実装するREST APIは以下の5つのAPIとする。
GET Members GET /api/v1/membersPOST Members POST /api/v1/membersMemberリソースを一件作成する。 GET Member GET /api/v1/members/{memberId}Memberリソースを一件取得する。 PUT Member PUT /api/v1/members/{memberId}Memberリソースを一件更新する。 DELETE Member DELETE /api/v1/members/{memberId}Memberリソースを一件削除する。 Note
Spring Framework 4.3よりHEADとOPTIONSメソッドに対するREST APIが暗黙的に用意される様になったため、 開発者がこれらのREST APIを明示的に実装する必要はない。
なお、暗黙的に用意されるOPTIONS用のREST APIがレスポンスするAllowヘッダの中にはOPTIONS自体が含まれないため、 Macchinetta Server Framework 1.3.xまでの開発ガイドラインで紹介している実装例と異なる点に留意されたい。
5.1.4.4.1. REST API用パッケージの作成¶
REST API用のクラスを格納するパッケージを作成する。
apiとして、配下にリソース毎のパッケージ(リソース名の小文字)を作成する事を推奨する。Memberなので、org.terasoluna.examples.rest.api.memberというパッケージとする。Note
作成したパッケージに格納するクラスは、通常以下の4種類となる。 作成するクラスのクラス名は、以下のネーミングルールとする事を推奨する。
[リソース名]Resource
[リソース名]RestController
[リソース名]Validator(必要に応じて作成する)
[リソース名]Helper(必要に応じて作成する)説明で扱うリソースのリソース名は
Memberなので、
MemberResource
MemberRestController
MemberValidator
MemberHelperとなる。
関連リソースを扱う場合、関連リソース用のクラスも同じパッケージに配置すればよい。
commonという名前で作成し、サブパッケージは機能単位に作成する事を推奨する。errorという名前でサブパッケージを作成する。org.terasoluna.examples.rest.api.common.errorというパッケージに格納している。Note
共通部品が格納されているパッケージという事がわかれば、パッケージ名は
common以外でもよい。
5.1.4.4.2. Resourceクラスの作成¶
Note
Resourceクラスを作成する理由
DomainObjectクラス(例えばEntityクラス)があるにも関わらず、Resourceクラスを作成する理由は、 クライアントとの入出力で使用するユーザーインタフェース(UI)上の情報と業務処理で扱う情報は必ずしも一致しないためである。
これらを混同して使用すると、アプリケーション層の影響がドメイン層におよび、保守性を低下させる原因となる。 DomainObjectとResourceクラスは別々に作成し、Dozer等のBeanMapperを利用してデータ変換を行うことを推奨する。
Resourceクラスの役割は以下の通りである。
項番 役割 説明 Warning
循環参照への対策
Resourceクラス(JavaBean)をJSONやXML形式にシリアライズする際に、相互参照関係のオブジェクトをプロパティに保持していると、 循環参照となり
StackOverflowErrorやOutOfMemoryErrorなどが発生するので、注意が必要である。循環参照を回避するためには、
- Jacksonを使用してJSON形式にシリアライズする場合は、シリアライズ対象から除外するプロパティに
@com.fasterxml.jackson.annotation.JsonIgnoreアノテーション- JAXBを使用してXML形式にシリアライズする場合は、シリアライズ対象から除外するプロパティに
javax.xml.bind.annotation.XmlTransientアノテーションを付与すればよい。
以下にJacksonを使用してJSON形式にシリアライズする際の回避例を示す。
public class Order { private String orderId; private List<OrderLine> orderLines; // ... }public class OrderLine { @JsonIgnore private Order order; private String itemCode; private int quantity; // ... }
項番 説明 シリアライズ対象から除外するプロパティに対して @JsonIgnoreアノテーションを付与する。
以下にResourceクラスの作成例を示す。
- MemberResource.java
package org.terasoluna.examples.rest.api.member; import java.io.Serializable; import javax.validation.Valid; import javax.validation.constraints.NotNull; import javax.validation.constraints.Null; import javax.validation.constraints.Past; import javax.validation.constraints.Size; import org.hibernate.validator.constraints.Email; import org.hibernate.validator.constraints.NotEmpty; import org.joda.time.DateTime; import org.joda.time.LocalDate; import org.springframework.format.annotation.DateTimeFormat; import org.terasoluna.gfw.common.codelist.ExistInCodeList; // (1) public class MemberResource implements Serializable { private static final long serialVersionUID = 1L; // (2) interface PostMembers { } interface PutMember { } @Null(groups = PostMembers.class) @NotEmpty(groups = PutMember.class) @Size(min = 10, max = 10, groups = PutMember.class) private String memberId; @NotEmpty @Size(max = 128) private String firstName; @NotEmpty @Size(max = 128) private String lastName; @NotEmpty @ExistInCodeList(codeListId = "CL_GENDER") private String genderCode; @NotNull @Past private LocalDate dateOfBirth; @NotEmpty @Size(max = 256) @Email private String emailAddress; @Size(max = 20) private String telephoneNumber; @Size(max = 20) private String zipCode; @Size(max = 256) private String address; @NotNull(groups = PostMembers.class) @Null(groups = PutMember.class) @Valid // (3) private MemberCredentialResource credential; @Null private DateTime createdAt; @Null private DateTime lastModifiedAt; // omitted setter and getter }
項番 説明 
- MemberCredentialResource.java
package org.terasoluna.examples.rest.api.member; import java.io.Serializable; import javax.validation.constraints.NotNull; import javax.validation.constraints.Null; import javax.validation.constraints.Size; import com.fasterxml.jackson.annotation.JsonInclude; import org.hibernate.validator.constraints.Email; import org.joda.time.DateTime; // (4) public class MemberCredentialResource implements Serializable { private static final long serialVersionUID = 1L; @Size(max = 256) @Email private String signId; // (5) @JsonInclude(JsonInclude.Include.NON_NULL) @NotNull @Size(min = 8, max = 32) private String password; @Null private DateTime passwordLastChangedAt; @Null private DateTime lastModifiedAt; // omitted setter and getter }
項番 説明 nullの時に、JSONにフィールド自体を出力しないようにするためのアノテーションを指定している。これは、レスポンスするJSONの中にパスワードのフィールド出力しないようにするために行っている。上記例ではNULLの場合(Inclusion.NON_NULL)に限っているが、値が空の場合(Inclusion.NON_EMPTY)という指定も可能である。
- Beanのマッピング定義の追加これから説明する実装例では、EntityクラスとResourceクラスのコピーは、「Beanマッピング(Dozer)」を使って行う。上記に示したJavaBeanには、Joda-Timeのクラスであるorg.joda.time.DateTimeとorg.joda.time.LocalDateが含まれているが、「Beanマッピング(Dozer)」を使ってコピーするとJoda-Timeのオブジェクトは正しくコピーされない。そのため、正しくコピーされるようにするためには、「Dozerを使ってJoda-Timeのクラスをコピーする方法」を適用する必要がある。
5.1.4.4.3. Controllerクラスの作成¶
package org.terasoluna.examples.rest.api.member; // omitted import org.springframework.web.bind.annotation.RestController; // omitted @RequestMapping("members") // (1) @RestController // (2) public class MemberRestController { // omitted ... }
項番 説明 @RequestMappingアノテーションのvalue属性に、リソースのコレクションを表すサーブレットパスを指定する。上記例では、/api/v1/membersというサーブレットパスをマッピングしている。Controllerに対して、
@RestControllerアノテーションを付与する。
@RestControllerアノテーションを付与することで、
- クラスに
org.springframework.stereotype.Controllerアノテーションを付与- 以降で説明するControllerのメソッドに
@org.springframework.web.bind.annotation.ResponseBodyアノテーションを付与したのと同じ意味となる。
Controllerのメソッドに
@ResponseBodyを付与することで、返却したResourceオブジェクトがJSONやXMLにmarshalされ、レスポンスBODYに設定される。Tip
@RestControllerアノテーションは、Spring Framework 4.0 から追加されたアノテーションである。
@RestControllerアノテーションの登場により、Controllerの各メソッドに@ResponseBodyアノテーションを付与する必要がなくなったため、 REST API用のControllerをよりシンプルに作成出来るようになった。@RestControllerアノテーションの詳細については、こちらを参照されたい。従来通り
@Controllerアノテーションと@ResponseBodyアノテーションを組み合わせてREST API用のControllerを作成する例を以下に示す。@RequestMapping("members") @Controller public class MemberRestController { @RequestMapping(method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) @ResponseBody public Page<MemberResource> getMembers() { // ... } // ... }
5.1.4.4.4. リソースのコレクションを取得するREST APIの実装¶
URIで指定されたMemberリソースのコレクションをページ検索するREST APIの実装例を、以下に示す。
- 検索条件を受け取るためのJavaBeanの作成リソースのコレクションを取得する際に、検索条件が必要な場合は、検索条件を受け取るためのJavaBeanを作成する。
// (1) public class MembersSearchQuery implements Serializable { private static final long serialVersionUID = 1L; // (2) @NotEmpty private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } }
項番 説明 /api/v1/members?name=Johnというリクエストの場合、JavaBeanのnameプロパティにJohnという値が設定される。
- REST APIの実装Memberリソースのコレクションをページ検索する処理を実装する。
@RequestMapping("members") @RestController public class MemberRestController { // omitted @Inject MemberService memberService; @Inject Mapper beanMapper; // (3) @RequestMapping(method = RequestMethod.GET) // (4) @ResponseStatus(HttpStatus.OK) public Page<MemberResource> getMembers( // (5) @Validated MembersSearchQuery query, // (6) Pageable pageable) { // (7) Page<Member> page = memberService.searchMembers(query.getName(), pageable); // (8) List<MemberResource> memberResources = new ArrayList<>(); for (Member member : page.getContent()) { memberResources.add(beanMapper.map(member, MemberResource.class)); } Page<MemberResource> responseResource = new PageImpl<>(memberResources, pageable, page.getTotalElements()); // (9) return responseResource; } // omitted }
項番 説明 @RequestMappingアノテーションのmethod属性に、RequestMethod.GETを指定する。@org.springframework.web.bind.annotation.ResponseStatusアノテーションを付与し、応答するステータスコードを指定する。@ResponseStatusアノテーションのvalue属性には、200(OK)を設定する。Tip
ステータスコードの指定方法について
本例では、
@ResponseStatusアノテーションを使って応答するステータスコードを固定で指定しているが、Controllerのロジック内で指定する事もできる。public ResponseEntity<Page<MemberResource>> getMembers( @Validated MembersSearchQuery query, Pageable pageable) { // omitted return ResponseEntity.ok().body(responseResource); }応答するステータスコードを処理内容や処理結果に応じて変える必要がある場合は、上記実装例の様に、
org.springframework.http.ResponseEntityを使用する事になる。@Validatedアノテーションを付与する。入力チェックの詳細については、「入力チェック」を参照されたい。org.springframework.data.domain.Pageableを引数に指定する。ページ検索の詳細については、「ページネーション」を参照されたい。org.springframework.data.domain.PageImplクラスを使用することで、ページ検索時の応答として必要な項目をクライアントに返却する事ができる。上記例では、Beanマッピングライブラリを使用してEntityからResourceオブジェクトを生成している。Beanマッピングライブラリについては、「Beanマッピング(Dozer)」を参照されたい。Resourceオブジェクトを生成するためのコード量が多くなる場合は、HelperクラスにResourceオブジェクトを生成するためのメソッドを作成することを推奨する。PageImplクラスを使用した時のレスポンスは以下の様になる。ハイライトしている部分が、ページ検索固有の項目となる。{ "content" : [ { "memberId" : "M000000001", "firstName" : "John", "lastName" : "Smith", "genderCode" : "1", "dateOfBirth" : "1977-03-07", "emailAddress" : "john.smith@test.com", "telephoneNumber" : "09012345678", "zipCode" : "1710051", "address" : "Tokyo", "credential" : { "signId" : "john.smit@test.com", "passwordLastChangedAt" : "2014-03-13T10:18:08.003Z", "lastModifiedAt" : "2014-03-13T10:18:08.003Z" }, "createdAt" : "2014-03-13T10:18:08.003Z", "lastModifiedAt" : "2014-03-13T10:18:08.003Z" }, { "memberId" : "M000000002", "firstName" : "Sophia", "lastName" : "Smith", "genderCode" : "2", "dateOfBirth" : "1977-03-07", "emailAddress" : "sophia.smith@test.com", "telephoneNumber" : "09012345678", "zipCode" : "1710051", "address" : "Tokyo", "credential" : { "signId" : "sophia.smith@test.com", "passwordLastChangedAt" : "2014-03-13T10:18:08.003Z", "lastModifiedAt" : "2014-03-13T10:18:08.003Z" }, "createdAt" : "2014-03-13T10:18:08.003Z", "lastModifiedAt" : "2014-03-13T10:18:08.003Z" } ], "last" : false, "totalPages" : 13, "totalElements" : 25, "size" : 2, "number" : 1, "sort" : [ { "direction" : "DESC", "property" : "lastModifiedAt", "ignoreCase" : false, "nullHandling": "NATIVE", "ascending" : false } ], "numberOfElements" : 2, "first" : false }Note
Spring Data CommonsのAPI仕様の変更に伴う注意点
terasoluna-gfw-common 5.0.0.RELEASE以上が依存するspring-data-commons(1.9.1.RELEASE以上)では、 ページ検索機能用のインタフェース(
org.springframework.data.domain.Page)とクラス(org.springframework.data.domain.PageImplとorg.springframework.data.domain.Sort.Order)のAPI仕様が変更になっている。具体的には、
PageインタフェースとPageImplクラスでは、isFirst()とisLast()メソッドがspring-data-commons 1.8.0.RELEASEで追加、isFirstPage()とisLastPage()メソッドがspring-data-commons 1.9.0.RELEASEで削除
Sort.Orderクラスでは、nullHandlingプロパティがspring-data-commons 1.8.0.RELEASEで追加されている。
REST APIのリソースオブジェクトとして
Pageインタフェース(PageImplクラス)を使用している場合は、 JSONやXMLのフォーマットが変わってしまうため、アプリケーションの修正が必要になるケースがある。
- Beanのマッピング定義の追加単純なフィールド値のコピーのみでよい場合は、Beanのマッピング定義の追加は不要だが、上記実装例では、Memberオブジェクトの内容をMemberResourceオブジェクトにコピーする際に、credential.passwordをコピー対象外にする必要がある。特定のフィールドをコピー対象外にするためには、Beanのマッピング定義の追加が必要となる。
<!-- (11) --> <?xml version="1.0" encoding="UTF-8"?> <mappings xmlns="http://dozer.sourceforge.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://dozer.sourceforge.net http://dozer.sourceforge.net/schema/beanmapping.xsd"> <mapping type="one-way"> <class-a>org.terasoluna.examples.rest.domain.model.MemberCredential</class-a> <class-b>org.terasoluna.examples.rest.api.member.MemberCredentialResource</class-b> <!-- (12) --> <field-exclude> <a>password</a> <b>password</b> </field-exclude> </mapping> </mappings>
項番 説明 MemberオブジェクトとMemberResourceオブジェクトのマッピングルールを定義するファイルを作成する。Dozerのマッピング定義ファイルは、リソース毎に作成する事を推奨する。今回の実装例では、/xxx-web/src/main/resources/META-INF/dozer/memberResource-mapping.xmlに格納する。Memberの関連エンティティであるMemberCredentialの内容を、MemberResourceの関連リソースであるMemberCredentialResourceにコピーする際に、passwordフィールドをコピー対象外に指定している。Beanマッピングの定義方法の詳細については、「Beanマッピング(Dozer)」を参照されたい。
- リクエスト例
GET /rest-api-web/api/v1/members?name=Smith&page=0&size=2 HTTP/1.1 Accept: text/plain, application/json, application/*+json, */* User-Agent: Java/1.7.0_51 Host: localhost:8080 Connection: keep-alive
- レスポンス例
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Track: fb63a6d446f849feb8ccaa4c9a794333 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 13 Mar 2014 11:10:43 GMT {"content":[{"memberId":"M000000001","firstName":"John","lastName":"Smith","genderCode":"1","dateOfBirth":"2013-03-13","emailAddress":"user1394709042120@test.com","telephoneNumber":"09012345678","zipCode":"1710051","address":"Tokyo","credential":{"signId":"user1394709042120@test.com","passwordLastChangedAt":"2014-03-13T11:10:43.066Z","lastModifiedAt":"2014-03-13T11:10:43.066Z"},"createdAt":"2014-03-13T11:10:43.066Z","lastModifiedAt":"2014-03-13T11:10:43.066Z"},{"memberId":"M000000002","firstName":"Sophia","lastName":"Smith","genderCode":"2","dateOfBirth":"2013-03-13","emailAddress":"user1394709043663@test.com","telephoneNumber":"09012345678","zipCode":"1710051","address":"Tokyo","credential":{"signId":"user1394709043663@test.com","passwordLastChangedAt":"2014-03-13T11:10:43.678Z","lastModifiedAt":"2014-03-13T11:10:43.678Z"},"createdAt":"2014-03-13T11:10:43.678Z","lastModifiedAt":"2014-03-13T11:10:43.678Z"}],"last":true,"totalPages":1,"totalElements":2,"size":2,"number":0,"sort":null,"numberOfElements":2,"first":true}
Tip
ページ検索が不要な場合は、Resourceクラスのリストを直接扱えばよい。
Resourceクラスのリストを直接扱う場合のControllerのメソッドは以下のような定義となる。
@RequestMapping(method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) public List<MemberResource> getMembers( @Validated MembersSearchQuery query) { // omitted }Resourceクラスのリストを直接扱った場合、以下のようなJSONとなる。
[ { "memberId" : "M000000001", "firstName" : "John", "lastName" : "Smith", "genderCode" : "1", "dateOfBirth" : "1977-03-07", "emailAddress" : "john.smith@test.com", "telephoneNumber" : "09012345678", "zipCode" : "1710051", "address" : "Tokyo", "credential" : { "signId" : "john.smit@test.com", "passwordLastChangedAt" : "2014-03-13T10:18:08.003Z", "lastModifiedAt" : "2014-03-13T10:18:08.003Z" }, "createdAt" : "2014-03-13T10:18:08.003Z", "lastModifiedAt" : "2014-03-13T10:18:08.003Z" }, { "memberId" : "M000000002", "firstName" : "Sophia", "lastName" : "Smith", "genderCode" : "2", "dateOfBirth" : "1977-03-07", "emailAddress" : "sophia.smith@test.com", "telephoneNumber" : "09012345678", "zipCode" : "1710051", "address" : "Tokyo", "credential" : { "signId" : "sophia.smith@test.com", "passwordLastChangedAt" : "2014-03-13T10:18:08.003Z", "lastModifiedAt" : "2014-03-13T10:18:08.003Z" }, "createdAt" : "2014-03-13T10:18:08.003Z", "lastModifiedAt" : "2014-03-13T10:18:08.003Z" } ]
5.1.4.4.5. リソースをコレクションに追加するAPI RESTの実装¶
指定されたMemberリソースを作成し、Memberリソースをコレクションに追加するREST APIの実装例を、以下に示す。
- REST APIの実装指定されたMemberリソースを作成し、Memberリソースをコレクションに追加する処理を実装する。
@RequestMapping("members") @RestController public class MemberRestController { // omitted // (1) @RequestMapping(method = RequestMethod.POST) // (2) @ResponseStatus(HttpStatus.CREATED) public MemberResource postMember( // (3) @RequestBody @Validated({ PostMembers.class, Default.class }) MemberResource requestedResource) { // (4) Member inputMember = beanMapper.map(requestedResource, Member.class); Member createdMember = memberService.createMember(inputMember); MemberResource responseResource = beanMapper.map(createdMember, MemberResource.class); return responseResource; } // omitted }
項番 説明 @RequestMappingアノテーションのmethod属性に、RequestMethod.POSTを指定する。@ResponseStatusアノテーションを付与し、応答するステータスコードを指定する。@ResponseStatusアノテーションのvalue属性には、201(Created)を設定する。@org.springframework.web.bind.annotation.RequestBodyアノテーションを付与する。@RequestBodyアノテーションを付与することで、リクエストBodyに設定されているJSONやXMLのデータがResourceオブジェクトにunmarshalされる。入力チェックを有効化するために、引数アノテーションとして、@Validatedアノテーションを付与する。入力チェックの詳細については、「入力チェック」を参照されたい。
- リクエスト例
POST /rest-api-web/api/v1/members HTTP/1.1 Accept: text/plain, application/json, application/*+json, */* Content-Type: application/json;charset=UTF-8 User-Agent: Java/1.7.0_51 Host: localhost:8080 Connection: keep-alive Content-Length: 248 {"firstName":"John","lastName":"Smith","genderCode":"1","dateOfBirth":"2013-03-13","emailAddress":"user1394708306056@test.com","telephoneNumber":"09012345678","zipCode":"1710051","address":"Tokyo","credential":{"signId":null,"password":"zaq12wsx"}}
- レスポンス例
HTTP/1.1 201 Created Server: Apache-Coyote/1.1 X-Track: c7e9c8a9aa4f40ff87f3acdb77baccdf Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 13 Mar 2014 10:58:26 GMT {"memberId":"M000000023","firstName":"John","lastName":"Smith","genderCode":"1","dateOfBirth":"2013-03-13","emailAddress":"user1394708306056@test.com","telephoneNumber":"09012345678","zipCode":"1710051","address":"Tokyo","credential":{"signId":"user1394708306056@test.com","passwordLastChangedAt":"2014-03-13T10:58:26.324Z","lastModifiedAt":"2014-03-13T10:58:26.324Z"},"createdAt":"2014-03-13T10:58:26.324Z","lastModifiedAt":"2014-03-13T10:58:26.324Z"}
5.1.4.4.6. 指定されたリソースを取得するREST APIの実装¶
URIで指定されたMemberリソースを取得するREST APIの実装例を、以下に示す。
- REST APIの実装URIで指定されたMemberリソースを取得する処理を実装する。
@RequestMapping("members") @RestController public class MemberRestController { // omitted // (1) @RequestMapping(value = "{memberId}", method = RequestMethod.GET) // (2) @ResponseStatus(HttpStatus.OK) public MemberResource getMember( // (3) @PathVariable("memberId") String memberId) { // (4) Member member = memberService.getMember(memberId); MemberResource responseResource = beanMapper.map(member, MemberResource.class); return responseResource; } // omitted }
項番 説明 @RequestMappingアノテーションのvalue属性にパス変数(上記例では{memberId})を、method属性にRequestMethod.GETを指定する。{memberId}には、リソースを一意に識別するための値が指定される。@ResponseStatusアノテーションを付与し、応答するステータスコードを指定する。@ResponseStatusアノテーションのvalue属性には、200(OK)を設定する。@PathVariable("memberId")を指定することで、パス変数({memberId})に指定された値をメソッドの引数として受け取ることが出来る。パス変数の詳細については、 「URLのパスから値を取得する」を参照されたい。上記例だと、URIが/api/v1/members/M12345の場合、引数のmemberIdにM12345が格納される。
- リクエスト例
GET /rest-api-web/api/v1/members/M000000003 HTTP/1.1 Accept: text/plain, application/json, application/*+json, */* User-Agent: Java/1.7.0_51 Host: localhost:8080 Connection: keep-alive
- レスポンス例
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Track: 275b4e7a61f946eea47672f272315ac2 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 13 Mar 2014 11:25:13 GMT {"memberId":"M000000003","firstName":"John","lastName":"Smith","genderCode":"1","dateOfBirth":"2013-03-13","emailAddress":"user1394709913496@test.com","telephoneNumber":"09012345678","zipCode":"1710051","address":"Tokyo","credential":{"signId":"user1394709913496@test.com","passwordLastChangedAt":"2014-03-13T11:25:13.762Z","lastModifiedAt":"2014-03-13T11:25:13.762Z"},"createdAt":"2014-03-13T11:25:13.762Z","lastModifiedAt":"2014-03-13T11:25:13.762Z"}
5.1.4.4.7. 指定されたリソースを更新するREST APIの実装¶
URIで指定されたMemberリソースを更新するREST APIの実装例を、以下に示す。
- REST APIの実装URIで指定されたMemberリソースを更新する処理を実装する。
@RequestMapping("members") @RestController public class MemberRestController { // omitted // (1) @RequestMapping(value = "{memberId}", method = RequestMethod.PUT) // (2) @ResponseStatus(HttpStatus.OK) public MemberResource putMember( @PathVariable("memberId") String memberId, // (3) @RequestBody @Validated({ PutMember.class, Default.class }) MemberResource requestedResource) { // (4) Member inputMember = beanMapper.map( requestedResource, Member.class); Member updatedMember = memberService.updateMember( memberId, inputMember); MemberResource responseResource = beanMapper.map(updatedMember, MemberResource.class); return responseResource; } // omitted }
項番 説明 @RequestMappingアノテーションのvalue属性にパス変数(上記例では{memberId})を、method属性にRequestMethod.PUTを指定する。{memberId}には、リソースを一意に識別するための値が指定される。@ResponseStatusアノテーションを付与し、応答するステータスコードを指定する。@ResponseStatusアノテーションのvalue属性には、200(OK)を設定する。@RequestBodyアノテーションを付与することで、リクエストBodyに設定されているJSONやXMLのデータがResourceオブジェクトにunmarshalされる。入力チェックを有効化するために、引数アノテーションとして、@Validatedアノテーションを付与する。入力チェックの詳細については、「入力チェック」を参照されたい。
- リクエスト例
PUT /rest-api-web/api/v1/members/M000000004 HTTP/1.1 Accept: text/plain, application/json, application/*+json, */* Content-Type: application/json;charset=UTF-8 User-Agent: Java/1.7.0_51 Host: localhost:8080 Connection: keep-alive Content-Length: 221 {"memberId":"M000000004","firstName":"John","lastName":"Smith","genderCode":"1","dateOfBirth":"2013-03-08","emailAddress":"user1394710559584@test.com","telephoneNumber":"09012345678","zipCode":"1710051","address":"Tokyo"}
- レスポンス例
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Track: 5e8fea3aae044e94bf20a293e155af57 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 13 Mar 2014 11:35:59 GMT {"memberId":"M000000004","firstName":"John","lastName":"Smith","genderCode":"1","dateOfBirth":"2013-03-08","emailAddress":"user1394710559584@test.com","telephoneNumber":"09012345678","zipCode":"1710051","address":"Tokyo","credential":{"signId":"user1394710559584@test.com","passwordLastChangedAt":"2014-03-13T11:35:59.847Z","lastModifiedAt":"2014-03-13T11:35:59.847Z"},"createdAt":"2014-03-13T11:35:59.847Z","lastModifiedAt":"2014-03-13T11:36:00.122Z"}
5.1.4.4.8. 指定されたリソースを削除するREST APIの実装¶
URIで指定されたMemberリソースを削除するREST APIの実装例を、以下に示す。
- REST APIの実装URIで指定されたMemberリソースを削除する処理を実装する。
@RequestMapping("members") @RestController public class MemberRestController { // omitted // (1) @RequestMapping(value = "{memberId}", method = RequestMethod.DELETE) // (2) @ResponseStatus(HttpStatus.NO_CONTENT) public void deleteMember( @PathVariable("memberId") String memberId) { // (3) memberService.deleteMember(memberId); } // omitted }
項番 説明 @RequestMappingアノテーションのvalue属性にパス変数(上記例では{memberId})を、method属性にRequestMethod.DELETEを指定する。@ResponseStatusアノテーションを付与し、応答するステータスコードを指定する。@ResponseStatusアノテーションのvalue属性には、204(NO_CONTENT)を設定する。Note
削除したリソースの情報をレスポンスBODYに設定する場合は、ステータスコードには200(OK)を設定する。
- リクエスト例
DELETE /rest-api-web/api/v1/members/M000000005 HTTP/1.1 Accept: text/plain, application/json, application/*+json, */* User-Agent: Java/1.7.0_51 Host: localhost:8080 Connection: keep-alive
- レスポンス例
HTTP/1.1 204 No Content Server: Apache-Coyote/1.1 X-Track: e06c5bd40c864a299c48d9be3f12b2c0 Date: Thu, 13 Mar 2014 11:40:05 GMT
5.1.4.5. 例外のハンドリングの実装¶
RESTful Web Serviceで発生した例外のハンドリング方法について説明する。
org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler)が提供されている。@ControllerAdviceアノテーションを付与する事で、例外ハンドリングを共通的に行う方法を推奨する。ResponseEntityExceptionHandlerでは、Spring MVCのフレームワーク内で発生する例外を@ExceptionHandlerアノテーションを使ってハンドリングするメソッドが予め実装されている。ResponseEntityExceptionHandlerでハンドリングされる例外に対応するHTTPステータスコードは、DefaultHandlerExceptionResolverと同様の仕様で設定される。ResponseEntityExceptionHandlerのデフォルトの実装ではレスポンスBodyは空で返却されるが、レスポンスBodyにエラー情報を出力する様に拡張する事ができる。ResponseEntityExceptionHandlerを継承した例外ハンドリング用のクラスを作成し、例外ハンドリングを共通的に行う際の処理フローについて説明する。
項番 処理レイヤ 説明 HttpMessageConverterを利用して、エラーオブジェクトをJSON形式の電文に変換する。
5.1.4.5.1. レスポンスBodyにエラー情報を出力するための実装¶
- エラー情報は以下のJSON形式とする。
{ "code" : "e.ex.fw.7001", "message" : "Validation error occurred on item in the request body.", "details" : [ { "code" : "ExistInCodeList", "message" : "\"genderCode\" must exist in code list of CL_GENDER.", "target" : "genderCode" } ] }
- エラー情報を保持するJavaBeanを作成する。
package org.terasoluna.examples.rest.api.common.error; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import com.fasterxml.jackson.annotation.JsonInclude; // (1) public class ApiError implements Serializable { private static final long serialVersionUID = 1L; private final String code; private final String message; @JsonInclude(JsonInclude.Include.NON_EMPTY) private final String target; // (2) @JsonInclude(JsonInclude.Include.NON_EMPTY) private final List<ApiError> details = new ArrayList<>(); // (3) public ApiError(String code, String message) { this(code, message, null); } public ApiError(String code, String message, String target) { this.code = code; this.message = message; this.target = target; } public String getCode() { return code; } public String getMessage() { return message; } public String getTarget() { return target; } public List<ApiError> getDetails() { return details; } public void addDetail(ApiError detail) { details.add(detail); } }
項番 説明 Tip
フィールドに
@JsonInclude(JsonInclude.Include.NON_EMPTY)を指定することで、値がnullや空の場合にJSONに項目が出力されないようにする事が出来る。 項目を出力させないための条件をnullに限定したい場合は、@JsonInclude(JsonInclude.Include.NON_NULL)を指定すればよい。
- エラー情報を保持するJavaBeanを生成するためのクラスを作成する。
全ての例外ハンドリングの実装が完了した際のソースコードについては、Appendixを参照されたい。
// (4) @Component public class ApiErrorCreator { @Inject MessageSource messageSource; public ApiError createApiError(WebRequest request, String errorCode, String defaultErrorMessage, Object... arguments) { // (5) String localizedMessage = messageSource.getMessage(errorCode, arguments, defaultErrorMessage, request.getLocale()); return new ApiError(errorCode, localizedMessage); } // omitted }
項番 説明 MessageSourceより取得する。メッセージの管理方法については、「メッセージ管理」を参照されたい。Tip
上記例では、メッセージのローカライズをサポートするために
org.springframework.web.context.request.WebRequestを引数として受け取っている。 メッセージのローカライズが必要ない場合は、WebRequestは不要である。
java.util.LocaleではなくWebRequestを引数にしている理由は、エラーメッセージの中にHTTPリクエストの内容を埋め込むといった要件が追加される事を考慮したためである。 エラーメッセージの中にHTTPリクエストの内容を埋め込む要件がない場合は、Localeでもよい。
- ResponseEntityExceptionHandlerのメソッドを拡張し、レスポンスBodyにエラー情報を出力するための実装を行う。
全ての例外ハンドリングの実装が完了した際のソースコードについては、Appendixを参照されたい。
@ControllerAdvice // (6) public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler { @Inject ApiErrorCreator apiErrorCreator; @Inject ExceptionCodeResolver exceptionCodeResolver; // (7) @Override protected ResponseEntity<Object> handleExceptionInternal(Exception ex, Object body, HttpHeaders headers, HttpStatus status, WebRequest request) { final Object apiError; // (8) if (body == null) { String errorCode = exceptionCodeResolver.resolveExceptionCode(ex); apiError = apiErrorCreator.createApiError(request, errorCode, ex .getLocalizedMessage()); } else { apiError = body; } // (9) return ResponseEntity.status(status).headers(headers).body(apiError); } // omitted }
項番 説明 ResponseEntityExceptionHandlerを継承したクラスを作成し、@ControllerAdviceアノテーションを付与する。ResponseEntityExceptionHandlerのhandleExceptionInternalメソッドをオーバライドする。ExceptionCodeResolverを使用して、例外クラスをエラーコードへ変換している。ExceptionCodeResolverの設定例については、「ExceptionCodeResolverを使ったエラーコードとメッセージの解決」を参照されたい。レスポンスBodyに出力するJavaBeanの指定がある場合は、指定されたJavaBeanをそのまま使用する。この処理は、例外毎のエラーハンドリング処理にて、個別にエラー情報が生成される事を考慮した実装となっている。ResponseEntityExceptionHandlerによって適切な値が設定される。設定されるステータスコードについては、「DefaultHandlerExceptionResolverで設定されるHTTPレスポンスコードについて」を参照されたい。Tip
Spring Framework 4.0 より追加された@ControllerAdviceアノテーションの属性について
@ControllerAdviceアノテーションの属性を指定することで、@ControllerAdviceが付与されたクラスで実装したメソッドを適用するControllerを柔軟に指定できるように改善されている。 属性の詳細については、@ControllerAdviceの属性を参照されたい。Note
@ControllerAdviceアノテーションの属性使用時の注意点
@ControllerAdviceアノテーションの属性を使用することで、さまざまな粒度で例外ハンドリングを共通化することができるようになるが、 アプリケーション共通の例外ハンドラクラス(上記例のApiGlobalExceptionHandlerクラスに相当するクラス)に対しては、@ControllerAdviceアノテーションの属性を指定しない方がよい。
ApiGlobalExceptionHandlerに付与する@ControllerAdviceアノテーションに属性を指定した場合、Spring MVCが提供するフレームワーク処理の中で発生する一部の例外をハンドリングできないケースがある。具体的には、リクエストに対応するREST API(Controllerのハンドラメソッド)が見つからない時に発生する例外を
ApiGlobalExceptionHandlerクラスでハンドリングする事ができないため、 「405 Method Not Allowed」などのエラーを正しく応答する事が出来なくなってしまう。
- レスポンス例
HTTP/1.1 400 Bad Request Server: Apache-Coyote/1.1 X-Track: e60b3b6468194e22852c8bfc7618e625 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Thu, 13 Mar 2014 12:16:55 GMT Connection: close {"code":"e.ex.fw.7001","message":"Validation error occurred on item in the request body.","details":[{"code":"ExistInCodeList","message":"\"genderCode\" must exist in code list of CL_GENDER.","target":"genderCode"}]}
5.1.4.5.2. 入力エラー例外のハンドリング実装¶
入力エラー(電文不正、単項目チェックエラー、相関項目チェックエラー)を応答するための実装例について説明する。
入力エラーを応答するためには、以下の3つの例外をハンドリングする必要がある。
項番 例外 説明 Note
Spring Frameworkから提供されているアノテーションを使用してリクエストパラメータ、リクエストヘッダ、パス変数から値を取得する際に、値の型変換エラーが発生した場合、
org.springframework.beans.TypeMismatchExceptionが発生する。Controllerのハンドラメソッドの引数(
String以外の引数)に、以下のアノテーションを指定した場合、TypeMismatchExceptionが発生する可能性がある。
@org.springframework.web.bind.annotation.RequestParam
@org.springframework.web.bind.annotation.RequestHeader
@org.springframework.web.bind.annotation.Pathvariable
@org.springframework.web.bind.annotation.MatrixVariable
TypeMismatchExceptionは、ResponseEntityExceptionHandlerによって例外がハンドリングされ、400(Bad Request)となるので個別にハンドリングしなくてもよい。エラー情報に設定するエラーコードとエラーメッセージの解決方法については、「ExceptionCodeResolverを使ったエラーコードとメッセージの解決」を参照されたい。
- 入力チェックエラー用のエラー情報を生成するためのメソッドを作成する。
@Component public class ApiErrorCreator { @Inject MessageSource messageSource; // omitted // (1) public ApiError createBindingResultApiError(WebRequest request, String errorCode, BindingResult bindingResult, String defaultErrorMessage) { ApiError apiError = createApiError(request, errorCode, defaultErrorMessage); for (FieldError fieldError : bindingResult.getFieldErrors()) { apiError.addDetail(createApiError(request, fieldError, fieldError .getField())); } for (ObjectError objectError : bindingResult.getGlobalErrors()) { apiError.addDetail(createApiError(request, objectError, objectError .getObjectName())); } return apiError; } // (2) private ApiError createApiError(WebRequest request, DefaultMessageSourceResolvable messageResolvable, String target) { String localizedMessage = messageSource.getMessage(messageResolvable, request.getLocale()); return new ApiError(messageResolvable.getCode(), localizedMessage, target); } // omitted }
項番 説明 FieldError)と相関項目チェックエラー(ObjectError)を、エラーの詳細情報に追加している。項目毎のエラー情報を出力する必要がない場合は、本メソッドを用意する必要はない。FieldError)と相関項目チェックエラー(ObjectError)で同じ処理を実装する事になるので、共通メソッドとして本メソッドを作成している。
- ResponseEntityExceptionHandlerのメソッドを拡張し、レスポンスBodyに入力チェック用のエラー情報を出力するための実装を行う。
@ControllerAdvice public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler { @Inject ApiErrorCreator apiErrorCreator; @Inject ExceptionCodeResolver exceptionCodeResolver; // omitted // (3) @Override protected ResponseEntity<Object> handleMethodArgumentNotValid( MethodArgumentNotValidException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return handleBindingResult(ex, ex.getBindingResult(), headers, status, request); } // (4) @Override protected ResponseEntity<Object> handleBindException(BindException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { return handleBindingResult(ex, ex.getBindingResult(), headers, status, request); } // (5) @Override protected ResponseEntity<Object> handleHttpMessageNotReadable( HttpMessageNotReadableException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { if (ex.getCause() instanceof Exception) { return handleExceptionInternal((Exception) ex.getCause(), null, headers, status, request); } else { return handleExceptionInternal(ex, null, headers, status, request); } } // omitted // (6) protected ResponseEntity<Object> handleBindingResult(Exception ex, BindingResult bindingResult, HttpHeaders headers, HttpStatus status, WebRequest request) { String code = exceptionCodeResolver.resolveExceptionCode(ex); String errorCode = exceptionCodeResolver.resolveExceptionCode(ex); ApiError apiError = apiErrorCreator.createBindingResultApiError( request, errorCode, bindingResult, ex.getMessage()); return handleExceptionInternal(ex, apiError, headers, status, request); } // omitted }
項番 説明 ResponseEntityExceptionHandlerのhandleMethodArgumentNotValidメソッドをオーバライドし、MethodArgumentNotValidExceptionのエラーハンドリングを拡張する。上記例では、入力チェックエラーをハンドリングするための共通メソッド(6)に処理を委譲している。項目毎のエラー情報を出力する必要がない場合は、オーバライドする必要はない。ステータスコードには400(Bad Request)が設定され、指定されたリソースの項目値に不備がある事を通知する。ResponseEntityExceptionHandlerのhandleBindExceptionメソッドをオーバライドし、BindExceptionのエラーハンドリングを拡張する。上記例では、入力チェックエラーをハンドリングするための共通メソッド(6)に処理を委譲している。項目毎のエラー情報を出力する必要がない場合は、オーバライドする必要はない。ステータスコードには400(Bad Request)が設定され、指定されたリクエストパラメータに不備がある事を通知する。ResponseEntityExceptionHandlerのhandleHttpMessageNotReadableメソッドをオーバライドし、HttpMessageNotReadableExceptionのエラーハンドリングを拡張する。上記例では、細かくエラーハンドリングを行うために、原因例外を使ってエラーハンドリングしている。細かくエラーハンドリングをしなくてもよい場合は、オーバライドする必要はない。ステータスコードには400(Bad Request)が設定され、指定されたリソースのフォーマットなどに不備がある事を通知する。Tip
JSON使用時のエラーハンドリングについて
リソースのフォーマットとしてJSONを使用する場合、以下の例外が
HttpMessageNotReadableExceptionの原因例外として格納される。
項番 例外クラス 説明 
- 入力チェックエラー(単項目チェック、相関項目チェックエラー)が発生した場合、以下のようなエラー応答が行われる。
HTTP/1.1 400 Bad Request Server: Apache-Coyote/1.1 X-Track: 13522b3badf2432ba4cad0dc7aeaee80 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 19 Feb 2014 05:08:28 GMT Connection: close {"code":"e.ex.fw.7002","message":"Validation error occurred on item in the request parameters.","details":[{"code":"NotEmpty","message":"\"{0}\" may not be empty.","target":"name"}]}
- JSONエラー(フォーマットエラーなど)が発生した場合、以下のようなエラー応答が行われる。
HTTP/1.1 400 Bad Request Server: Apache-Coyote/1.1 X-Track: ca4c742a6bfd49e5bc01cd0b124738a1 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 19 Feb 2014 13:32:24 GMT Connection: close {"code":"e.ex.fw.7003","message":"Request body format error occurred."}
5.1.4.5.3. リソース未検出エラー例外のハンドリング実装¶
リソースが存在しない場合に、リソース未検出エラーを応答するための実装例について説明する。
org.terasoluna.gfw.common.exception.ResourceNotFoundExceptionを用意している。- パス変数から取得したIDに一致するリソースが見つからない場合は、ResourceNotFoundExceptionを発生させる。
public Member getMember(String memberId) { Member member = memberRepository.findOne(memberId); if (member == null) { throw new ResourceNotFoundException(ResultMessages.error().add( "e.ex.mm.5001", memberId)); } return member; }
- ResultMessages用のエラー情報を生成するためのメソッドを作成する。
@Component public class ApiErrorCreator { // omitted // (1) public ApiError createResultMessagesApiError(WebRequest request, String rootErrorCode, ResultMessages resultMessages, String defaultErrorMessage) { ApiError apiError; if (resultMessages.getList().size() == 1) { ResultMessage resultMessage = resultMessages.iterator().next(); String errorCode = resultMessage.getCode(); String errorText = resultMessage.getText(); if (errorCode == null && errorText == null) { errorCode = rootErrorCode; } apiError = createApiError(request, errorCode, errorText, resultMessage.getArgs()); } else { apiError = createApiError(request, rootErrorCode, defaultErrorMessage); for (ResultMessage resultMessage : resultMessages.getList()) { apiError.addDetail(createApiError(request, resultMessage .getCode(), resultMessage.getText(), resultMessage .getArgs())); } } return apiError; } // omitted }
項番 説明 ResultMessagesが保持しているメッセージ情報を、エラー情報に設定している。Note
上記例では、
ResultMessagesが複数のメッセージを保持する事ができるため、格納されているメッセージが1件の時と複数件の時で処理をわけている。複数件のメッセージをサポートする必要がない場合は、先頭の1件をエラー情報として生成する処理にすればよい。
- エラーハンドリングを行うクラスに、リソースが見つからない事を通知する例外をハンドリングするためのメソッドを作成する。
@ControllerAdvice public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler { @Inject ApiErrorCreator apiErrorCreator; @Inject ExceptionCodeResolver exceptionCodeResolver; // omitted // (2) @ExceptionHandler(ResourceNotFoundException.class) public ResponseEntity<Object> handleResourceNotFoundException( ResourceNotFoundException ex, WebRequest request) { return handleResultMessagesNotificationException(ex, new HttpHeaders(), HttpStatus.NOT_FOUND, request); } // omitted // (3) private ResponseEntity<Object> handleResultMessagesNotificationException( ResultMessagesNotificationException ex, HttpHeaders headers, HttpStatus status, WebRequest request) { String errorCode = exceptionCodeResolver.resolveExceptionCode(ex); ApiError apiError = apiErrorCreator.createResultMessagesApiError( request, errorCode, ex.getResultMessages(), ex.getMessage()); return handleExceptionInternal(ex, apiError, headers, status, request); } // omitted }
項番 説明 ResourceNotFoundExceptionをハンドリングするためのメソッドを追加する。メソッドアノテーションとして@ExceptionHandler(ResourceNotFoundException.class)を指定すると、ResourceNotFoundExceptionの例外をハンドリングする事ができる。上記例では、ResourceNotFoundExceptionの親クラス(ResultMessagesNotificationException)の例外をハンドリングするメソッドに処理を委譲している。ステータスコードには404(Not Found)を設定し、指定されたリソースがサーバに存在しない事を通知する。
- リソースが見つからない場合、以下のようなエラー応答が行われる。
HTTP/1.1 404 Not Found Server: Apache-Coyote/1.1 X-Track: 5ee563877f3140fd904d0acf52eba398 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 19 Feb 2014 08:46:18 GMT {"code":"e.ex.mm.5001","message":"Specified member not found. member id : M000000001"}
5.1.4.5.4. 業務エラー例外のハンドリング実装¶
ビジネスルールの違反を検知した場合に、業務エラーを応答するための実装例について説明する。
ビジネスルールのチェックはServiceの処理として行い、ビジネスルールの違反を検知した場合は、業務例外を発生させる。 業務エラーの検知方法については、「業務エラーを通知する」を参照されたい。
- エラーハンドリングを行うクラスに、業務例外をハンドリングするためのメソッドを作成する。
@ControllerAdvice public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler { // omitted // (1) @ExceptionHandler(BusinessException.class) public ResponseEntity<Object> handleBusinessException(BusinessException ex, WebRequest request) { return handleResultMessagesNotificationException(ex, new HttpHeaders(), HttpStatus.CONFLICT, request); } // omitted }
項番 説明 BusinessExceptionをハンドリングするためのメソッドを追加する。メソッドアノテーションとして@ExceptionHandler(BusinessException.class)を指定すると、BusinessExceptionの例外をハンドリングする事ができる。上記例では、BusinessExceptionの親クラス(ResultMessagesNotificationException)の例外をハンドリングするメソッドに処理を委譲している。ステータスコードには409(Conflict)を設定し、クライアントから指定されたリソース自体には不備はないが、サーバで保持しているリソースを操作するための条件が全て整っていない事を通知する。
- 業務エラーが発生した場合、以下のようなエラー応答が行われる。
HTTP/1.1 409 Conflict Server: Apache-Coyote/1.1 X-Track: 37c1a899d5f74e7a9c24662292837ef7 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 19 Feb 2014 09:03:26 GMT {"code":"e.ex.mm.8001","message":"Cannot use specified sign id. sign id : user1@test.com"}
5.1.4.5.5. 排他エラー例外のハンドリング実装¶
- エラーハンドリングを行うクラスに、排他エラーをハンドリングするためのメソッドを作成する。
@ControllerAdvice public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler { // omitted // (1) @ExceptionHandler({ OptimisticLockingFailureException.class, PessimisticLockingFailureException.class }) public ResponseEntity<Object> handleLockingFailureException(Exception ex, WebRequest request) { return handleExceptionInternal(ex, null, new HttpHeaders(), HttpStatus.CONFLICT, request); } // omitted }
項番 説明 OptimisticLockingFailureExceptionとPessimisticLockingFailureException)をハンドリングするためのメソッドを追加する。メソッドアノテーションとして@ExceptionHandler({ OptimisticLockingFailureException.class, PessimisticLockingFailureException.class })を指定すると、排他エラー(OptimisticLockingFailureExceptionとPessimisticLockingFailureException)の例外をハンドリングする事ができる。ステータスコードには409(Conflict)を設定し、クライアントから指定されたリソース自体には不備はないが、処理が競合したためリソースを操作するための条件を満たすことが出来なかった事を通知する。
- 排他エラーが発生した場合、以下のようなエラー応答が行われる。
HTTP/1.1 409 Conflict Server: Apache-Coyote/1.1 X-Track: 85200b5a51be42b29840e482ee35087f Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 19 Feb 2014 16:32:45 GMT {"code":"e.ex.fw.8002","message":"Conflict with other processing occurred."}
5.1.4.5.6. システムエラー例外のハンドリング実装¶
システム異常を検知した場合に、システムエラーを応答するための実装例について説明する。
システム異常の検知した場合は、システム例外を発生させる。 システムエラーの検知方法については、「システムエラーを通知する」を参照されたい。
- エラーハンドリングを行うクラスに、システム例外をハンドリングするためのメソッドを作成する。
@ControllerAdvice public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler { // omitted // (1) @ExceptionHandler(Exception.class) public ResponseEntity<Object> handleSystemError(Exception ex, WebRequest request) { return handleExceptionInternal(ex, null, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR, request); } // omitted }
項番 説明 Exceptionをハンドリングするためのメソッドを追加する。メソッドアノテーションとして@ExceptionHandler(Exception.class)を指定すると、Exceptionの例外をハンドリングする事ができる。上記例では、使用している依存ライブラリから発生するシステム例外もハンドリング対象としている。ステータスコードには500(Internal Server Error)を設定する。
- システムエラーが発生した場合、以下のようなエラー応答が行われる。
HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 X-Track: 3625d5a040a744e49b0a9b3763a24e9c Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 19 Feb 2014 12:22:33 GMT Connection: close {"code":"e.ex.fw.9003","message":"System error occurred."}Warning
システムエラー時のエラーメッセージについて
システムエラーが発生した場合、クライアントへ返却するメッセージは、エラー原因が特定されないシンプルなエラーメッセージを設定することを推奨する。 エラー原因が特定できるメッセージを設定してしまうと、システムの脆弱性をクライアントに公開する可能性があり、セキュリティー上問題がある。
エラー原因は、エラー解析用にログに出力する。 Blankプロジェクトのデフォルトの設定では、共通ライブラリから提供している
ExceptionLoggerによってログが出力されるようなっているため、ログを出力するための設定や実装は不要である。
5.1.4.5.7. ExceptionCodeResolverを使ったエラーコードとメッセージの解決¶
ExceptionCodeResolverを使用すると、例外クラスからエラーコードを解決する事ができる。- applicationContext.xml例外クラスとエラーコード(例外コード)のマッピングを行う。
<!-- omitted --> <bean id="exceptionCodeResolver" class="org.terasoluna.gfw.common.exception.SimpleMappingExceptionCodeResolver"> <property name="exceptionMappings"> <map> <!-- omitted --> <entry key="ResourceNotFoundException" value="e.ex.fw.5001" /> <entry key="HttpRequestMethodNotSupportedException" value="e.ex.fw.6001" /> <entry key="MediaTypeNotAcceptableException" value="e.ex.fw.6002" /> <entry key="HttpMediaTypeNotSupportedException" value="e.ex.fw.6003" /> <entry key="MethodArgumentNotValidException" value="e.ex.fw.7001" /> <entry key="BindException" value="e.ex.fw.7002" /> <entry key="JsonParseException" value="e.ex.fw.7003" /> <entry key="UnrecognizedPropertyException" value="e.ex.fw.7004" /> <entry key="JsonMappingException" value="e.ex.fw.7005" /> <entry key="TypeMismatchException" value="e.ex.fw.7006" /> <entry key="BusinessException" value="e.ex.fw.8001" /> <entry key="OptimisticLockingFailureException" value="e.ex.fw.8002" /> <entry key="PessimisticLockingFailureException" value="e.ex.fw.8002" /> <entry key="DataAccessException" value="e.ex.fw.9002" /> <!-- omitted --> </map> </property> <property name="defaultExceptionCode" value="e.ex.fw.9001" /> </bean> <!-- omitted -->
- xxx-web/src/main/resources/i18n/application-messages.propertiesアプリケーション層で発生するエラーに対して、エラーコード(例外コード)に対応するメッセージの設定を行う。
# --- # Application common messages # --- e.ex.fw.5001 = Resource not found. e.ex.fw.6001 = Request method not supported. e.ex.fw.6002 = Specified representation format not supported. e.ex.fw.6003 = Specified media type in the request body not supported. e.ex.fw.7001 = Validation error occurred on item in the request body. e.ex.fw.7002 = Validation error occurred on item in the request parameters. e.ex.fw.7003 = Request body format error occurred. e.ex.fw.7004 = Unknown field exists in JSON. e.ex.fw.7005 = Type mismatch error occurred in JSON field. e.ex.fw.7006 = Type mismatch error occurred in request parameter or header or path variable. e.ex.fw.8001 = Business error occurred. e.ex.fw.8002 = Conflict with other processing occurred. e.ex.fw.9001 = System error occurred. e.ex.fw.9002 = System error occurred. e.ex.fw.9003 = System error occurred. # omitted
- xxx-web/src/main/resources/ValidationMessages.propertiesBean Validationを使った入力チェックで発生するエラーに対して、エラーコードに対応するメッセージの設定を行う。
# --- # Bean Validation common messages # --- # for bean validation of standard javax.validation.constraints.AssertFalse.message = "{0}" must be false. javax.validation.constraints.AssertTrue.message = "{0}" must be true. javax.validation.constraints.DecimalMax.message = "{0}" must be less than ${inclusive == true ? 'or equal to ' : ''}{value}. javax.validation.constraints.DecimalMin.message = "{0}" must be greater than ${inclusive == true ? 'or equal to ' : ''}{value}. javax.validation.constraints.Digits.message = "{0}" numeric value out of bounds (<{integer} digits>.<{fraction} digits> expected). javax.validation.constraints.Future.message = "{0}" must be in the future. javax.validation.constraints.Max.message = "{0}" must be less than or equal to {value}. javax.validation.constraints.Min.message = "{0}" must be greater than or equal to {value}. javax.validation.constraints.NotNull.message = "{0}" may not be null. javax.validation.constraints.Null.message = "{0}" must be null. javax.validation.constraints.Past.message = "{0}" must be in the past. javax.validation.constraints.Pattern.message = "{0}" must match "{regexp}". javax.validation.constraints.Size.message = "{0}" size must be between {min} and {max}. # for bean validation of hibernate org.hibernate.validator.constraints.CreditCardNumber.message = "{0}" invalid credit card number. org.hibernate.validator.constraints.EAN.message = "{0}" invalid {type} barcode. org.hibernate.validator.constraints.Email.message = "{0}" not a well-formed email address. org.hibernate.validator.constraints.Length.message = "{0}" length must be between {min} and {max}. org.hibernate.validator.constraints.LuhnCheck.message = "{0}" The check digit for ${validatedValue} is invalid, Luhn Modulo 10 checksum failed. org.hibernate.validator.constraints.Mod10Check.message = "{0}" The check digit for ${validatedValue} is invalid, Modulo 10 checksum failed. org.hibernate.validator.constraints.Mod11Check.message = "{0}" The check digit for ${validatedValue} is invalid, Modulo 11 checksum failed. org.hibernate.validator.constraints.ModCheck.message = "{0}" The check digit for ${validatedValue} is invalid, ${modType} checksum failed. org.hibernate.validator.constraints.NotBlank.message = "{0}" may not be empty. org.hibernate.validator.constraints.NotEmpty.message = "{0}" may not be empty. org.hibernate.validator.constraints.ParametersScriptAssert.message = "{0}" script expression "{script}" didn't evaluate to true. org.hibernate.validator.constraints.Range.message = "{0}" must be between {min} and {max}. org.hibernate.validator.constraints.SafeHtml.message = "{0}" may have unsafe html content. org.hibernate.validator.constraints.ScriptAssert.message = "{0}" script expression "{script}" didn't evaluate to true. org.hibernate.validator.constraints.URL.message = "{0}" must be a valid URL. org.hibernate.validator.constraints.br.CNPJ.message = "{0}" invalid Brazilian corporate taxpayer registry number (CNPJ). org.hibernate.validator.constraints.br.CPF.message = "{0}" invalid Brazilian individual taxpayer registry number (CPF). org.hibernate.validator.constraints.br.TituloEleitoral.message = "{0}" invalid Brazilian Voter ID card number. # for common library org.terasoluna.gfw.common.codelist.ExistInCodeList.message = "{0}" must exist in code list of {codeListId}.
- xxx-domain/src/main/resources/i18n/domain-messages.propertiesドメイン層で発生するエラーに対して、エラーコード(例外コード)に対応するメッセージの設定を行う。
# omitted e.ex.mm.5001 = Specified member not found. member id : {0} e.ex.mm.8001 = Cannot use specified sign id. sign id : {0} # omitted
5.1.4.6. サーブレットコンテナに通知されたエラーのハンドリング実装¶
Filterでエラーが発生した場合やHttpServletResponse#sendErrorを使ってエラーレスポンスが行われた場合は、Spring MVCの例外ハンドリングの仕組みを使ってハンドリングできないため、
これらのエラーはサーブレットコンテナに通知される。
本節では、サーブレットコンテナに通知されたエラーをハンドリングする方法について説明する。
項番 処理レイヤ 説明 web.xmlのerror-pageの定義に従って、エラー処理を行う。致命的なエラーでない場合は、エラーハンドリングを行うControllerを呼び出し、エラー処理を行う。HttpMessageConverterを利用して、エラーオブジェクトをJSON形式の電文に変換する。
5.1.4.6.1. エラー応答を行うためのControllerの実装¶
サーブレットコンテナに通知されたエラーのエラー応答を行うControllerを作成する。
package org.terasoluna.examples.rest.api.common.error; import java.util.HashMap; import java.util.Map; import javax.inject.Inject; import javax.servlet.RequestDispatcher; import org.springframework.http.HttpStatus; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.WebRequest; // (1) @RequestMapping("error") @RestController public class ApiErrorPageController { @Inject ApiErrorCreator apiErrorCreator; // (2) // (3) private final Map<HttpStatus, String> errorCodeMap = new HashMap<HttpStatus, String>(); // (4) public ApiErrorPageController() { errorCodeMap.put(HttpStatus.NOT_FOUND, "e.ex.fw.5001"); } // (5) @RequestMapping public ResponseEntity<ApiError> handleErrorPage(WebRequest request) { // (6) HttpStatus httpStatus = HttpStatus.valueOf((Integer) request .getAttribute(RequestDispatcher.ERROR_STATUS_CODE, RequestAttributes.SCOPE_REQUEST)); // (7) String errorCode = errorCodeMap.get(httpStatus); // (8) ApiError apiError = apiErrorCreator.createApiError(request, errorCode, httpStatus.getReasonPhrase()); // (9) return ResponseEntity.status(httpStatus).body(apiError); } }
項番 説明 /api/v1/error」というサーブレットパスにマッピングしている。Mapを作成する。<error-code>)を使ってエラーページのハンドリングを行うケースのみを考慮した実装になっている。例外タイプ(<exception-type>)を使ってエラーページのハンドリングを行う場合は、別途考慮が必要である。
5.1.4.6.2. 致命的なエラーが発生した際に応答する静的なJSONファイルの作成¶
致命的なエラーが発生した際に応答する静的なJSONファイルを作成する。
- unhandledSystemError.json
{"code":"e.ex.fw.9999","message":"Unhandled system error occurred."}
5.1.4.6.3. サーブレットコンテナに通知されたエラーをハンドリングするための設定¶
ここでは、サーブレットコンテナに通知されたエラーをハンドリングするための設定について説明する。
- web.xml
<!-- omitted --> <!-- (1) --> <error-page> <error-code>404</error-code> <location>/api/v1/error</location> </error-page> <!-- (2) --> <error-page> <exception-type>java.lang.Exception</exception-type> <location>/WEB-INF/views/common/error/unhandledSystemError.json</location> </error-page> <!-- (3) --> <mime-mapping> <extension>json</extension> <mime-type>application/json;charset=UTF-8</mime-type> </mime-mapping> <!-- omitted -->
項番 説明 404 Not Foundが発生した際に、「/api/v1/error」というリクエストにマッピングされているController(ApiErrorPageController)を呼び出してエラー応答を行っている。/WEB-INF/views/common/error/unhandledSystemError.json」に定義されている固定のJSONを応答している。charset=UTF-8を指定しないと、クライアント側で文字化けする可能性がある。JSONファイルにマルチバイト文字を含めない場合は、この設定は必須ではないが、設定しておいた方が無難である。Note
Servletの仕様では、
<error-page>の<location>にクエリパラメータを付与したパスを指定した場合の挙動について、定義していない。そのため、APサーバによって挙動が異なる可能性がある。 よって、クエリパラメータを使用してエラー時の遷移先に情報を渡すことは推奨しない。
- 存在しないパスへリクエストを送った場合、以下のようなエラー応答が行われる。
HTTP/1.1 404 Not Found Server: Apache-Coyote/1.1 X-Track: 2ad50fb5ba2441699c91a5b01edef83f Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 19 Feb 2014 23:24:20 GMT {"code":"e.ex.fw.5001","message":"Resource not found."}
- 致命的なエラーが発生した場合、以下のようなエラー応答が行われる。
HTTP/1.1 500 Internal Server Error Server: Apache-Coyote/1.1 X-Track: 69db3854a19f439781584321d9ce8336 Content-Type: application/json Content-Length: 68 Date: Thu, 20 Feb 2014 00:13:43 GMT Connection: close {"code":"e.ex.fw.9999","message":"Unhandled system error occurred."}
5.1.4.7. セキュリティ対策¶
5.1.4.7.2. CSRF対策¶
- RESTful Web Serviceに対してCSRF対策を行う場合の設定方法については、CSRF対策を参照されたい。
- RESTful Web Serviceに対してCSRF対策を行わない場合の設定方法については、CSRF対策の無効化を参照されたい。
5.1.4.9. リソースのキャッシュ制御¶
Todo
TBD
Cache-Control/Expires/Pragmaなどのヘッダを使ったキャッシュ制御の実現方法について、次版以降に記載する予定である。
5.1.5. How to extend¶
5.1.5.1. @JsonViewを使用したレスポンスの出力制御¶
@JsonViewを使用することによって、Resourceオブジェクト内のプロパティーをグループ分けすることができる。- MemberResource.java
package org.terasoluna.examples.rest.api.member; import java.io.Serializable; import org.joda.time.DateTime; import org.joda.time.LocalDate; import com.fasterxml.jackson.annotation.JsonView; public class MemberResource implements Serializable { private static final long serialVersionUID = 1L; // (1) interface Summary { } // (2) interface Detail { } // (3) @JsonView({Summary.class, Detail.class}) private String memberId; @JsonView({Summary.class, Detail.class}) private String firstName; @JsonView({Summary.class, Detail.class}) private String lastName; // (4) @JsonView(Detail.class) private String genderCode; @JsonView(Detail.class) private LocalDate dateOfBirth; @JsonView(Detail.class) private String emailAddress; @JsonView(Detail.class) private String telephoneNumber; @JsonView(Detail.class) private String zipCode; @JsonView(Detail.class) private String address; // (5) private DateTime createdAt; private DateTime lastModifiedAt; // omitted setter and getter }
項番 説明 @JsonViewを設定しない。グループに所属しない項目を出力するかどうかは設定によって変えることができる。設定方法については後述する。
- MemberRestController.java
package org.terasoluna.examples.rest.api.member; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import org.dozer.Mapper; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; import org.terasoluna.examples.rest.domain.model.Member; import org.terasoluna.examples.rest.domain.service.member.MemberService; import com.fasterxml.jackson.annotation.JsonView; @RequestMapping("members") @RestController public class MemberRestController { @Inject MemberService memberService; @Inject Mapper beanMapper; // (1) @JsonView(Summary.class) @RequestMapping(value = "{memberId}", params = "format=summary", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) public MemberResource getMemberSummary(@PathVariable("memberId") String memberId) { Member member = memberService.getMember(memberId); MemberResource responseResource = beanMapper.map(member, MemberResource.class); return responseResource; } // (2) @JsonView(Detail.class) @RequestMapping(value = "{memberId}", params = "format=detail", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) public MemberResource getMemberDetail(@PathVariable("memberId") String memberId) { Member member = memberService.getMember(memberId); MemberResource responseResource = beanMapper.map(member, MemberResource.class); return responseResource; } }
項番 説明 @JsonViewを付けて、出力したいグループのマーカーインターフェースを設定する。概要を出力するメソッドにSummaryマーカーインターフェースを設定する。Detailマーカーインターフェースを設定する。
- Summary
{ "memberId" : "M000000001", "firstName" : "John", "lastName" : "Smith", "createdAt" : "2014-03-14T11:02:41.477Z", "lastModifiedAt" : "2014-03-14T11:02:41.477Z" }
- Detail
{ "memberId" : "M000000001", "firstName" : "John", "lastName" : "Smith", "genderCode" : "1", "dateOfBirth" : "2013-03-14", "emailAddress" : "user1394794959984@test.com", "telephoneNumber" : "09012345678", "zipCode" : "1710051", "address" : "Tokyo", "createdAt" : "2014-03-14T11:02:41.477Z", "lastModifiedAt" : "2014-03-14T11:02:41.477Z" }
@JsonViewを付けなかったプロパティーは、MapperFeature.DEFAULT_VIEW_INCLUSIONの設定を有効にすれば出力され、無効にすれば出力されない。MapperFeature.DEFAULT_VIEW_INCLUSIONを有効にした場合の出力例である。MapperFeature.DEFAULT_VIEW_INCLUSIONを有効にする場合は、以下のように設定する。<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"> <!-- ... --> <!-- (1) --> <property name="featuresToEnable"> <array> <util:constant static-field="com.fasterxml.jackson.databind.MapperFeature.DEFAULT_VIEW_INCLUSION"/> </array> </property> </bean>
項番 説明 featuresToEnable要素にMapperFeature.DEFAULT_VIEW_INCLUSIONを定義することで設定が有効となる。
MapperFeature.DEFAULT_VIEW_INCLUSIONを無効にする場合は、以下のように設定する。<bean id="objectMapper" class="org.springframework.http.converter.json.Jackson2ObjectMapperFactoryBean"> <!-- ... --> <!-- (1) --> <property name="featuresToDisable"> <array> <util:constant static-field="com.fasterxml.jackson.databind.MapperFeature.DEFAULT_VIEW_INCLUSION"/> </array> </property> </bean>
項番 説明 featuresToDisable要素にMapperFeature.DEFAULT_VIEW_INCLUSIONを定義することで設定が無効となる。
MapperFeature.DEFAULT_VIEW_INCLUSIONが無効の場合、先ほどの出力例は、以下のように出力内容が変更される。- Summary
{ "memberId" : "M000000001", "firstName" : "John", "lastName" : "Smith" }
- Detail
{ "memberId" : "M000000001", "firstName" : "John", "lastName" : "Smith", "genderCode" : "1", "dateOfBirth" : "2013-03-14", "emailAddress" : "user1394794959984@test.com", "telephoneNumber" : "09012345678", "zipCode" : "1710051", "address" : "Tokyo" }
Warning
MapperFeature.DEFAULT_VIEW_INCLUSIONを指定しない場合のデフォルト値は、ObjectMapperの設定方法によって異なるデフォルト値となるため注意が必要である。
RESTful Web Serviceで必要となるSpring MVCのコンポーネントを有効化するための設定でも記述しているが、ObjectMapperのBean定義方法をObjectMapperを直接Bean定義するスタイルにすると、デフォルト値が有効になる。Jackson2ObjectMapperFactoryBeanを利用すると、デフォルト値は無効になる。設定を明示するため、どちらのスタイルで設定する場合においても、MapperFeature.DEFAULT_VIEW_INCLUSIONの指定を記述することを推奨する。
Note
@JsonViewは以下の2つの機能を使用して作成されている。これらは、Controller内の@RequestMappingが付けられた処理メソッドで、Objectとのマッピング前後に共通的な処理を実装したい場合に、使用することができる機能である。
- org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice
- org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
@ControllerAdviceをこれらのインタフェースの実装クラスにつけることで適用することができる。@ControllerAdviceの詳細は、@ControllerAdviceの実装を参照されたい。
RequestBodyAdviceは下記のメソッドを実装することができる。
- org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdvice
項番 メソッド名 概要 trueだと適用される。
上記すべてのタイミングで処理を記述する必要がない場合は、上記のsupports以外のメソッドが何もしない状態で実装されたorg.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapterを継承し、必要な部分だけオーバーライドすることで、簡単に実装することができる。
ResponseBodyAdviceは下記のメソッドを実装することができる。
- org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice
項番 メソッド名 概要 trueだと適用される。
5.1.6. Appendix¶
5.1.6.1. JSR-310 Date and Time API / Joda Timeを使う場合の設定¶
リソースを表現するJavaBean(Resourceクラス)のプロパティとしてJSR-310 Date and Time APIを使用する場合は、
terasoluna-gfw-common-dependenciesにて依存関係が定義されているため依存関係の追加は不要である。
一方、Joda Timeのクラスを使用する場合は、
pom.xmlにJacksonから提供されている拡張モジュールを依存ライブラリに追加する。
Joda Timeのクラスを使用する場合
<dependency>
    <groupId>org.terasoluna.gfw</groupId>
    <artifactId>terasoluna-gfw-jodatime-dependencies</artifactId>
    <type>pom</type>
</dependency>
or
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-joda</artifactId>
</dependency>
Note
上記設定例は、依存ライブラリのバージョンを親プロジェクトである terasoluna-gfw-parent で管理する前提であるため、pom.xmlでのバージョンの指定は不要である。 上記のjackson-datatype-jodaはterasoluna-gfw-parentが利用しているSpring IO Platformで定義済みである。
上記以外にも、
- Java SE 7から追加されたjava.nio.file.Path
- Java SE 8から追加されたjava.util.Optional
- Hibernate ORMのLazy Load機能によってProxy化されたオブジェクト
などを扱うための拡張モジュール(jackson-datatype-xxx)が、別途Jacksonから提供されている。
5.1.6.2. RESTful Web Serviceとクライアントアプリケーションを同じWebアプリケーションとして動かす際の設定¶
5.1.6.2.1. RESTful Web Service用のDispatcherServletを設ける方法¶
DispatcherServletと、クライアントアプリケーション用のリクエストを受け取るDispatcherServletを分割する事を推奨する。DispatcherServletを分割する方法について、以下に説明する。- web.xml
<!-- omitted --> <!-- (1) --> <servlet> <servlet-name>appServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <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> <!-- (2) --> <servlet> <servlet-name>restAppServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <!-- (3) --> <param-value>classpath*:META-INF/spring/spring-mvc-rest.xml</param-value> </init-param> <load-on-startup>2</load-on-startup> </servlet> <!-- (4) --> <servlet-mapping> <servlet-name>restAppServlet</servlet-name> <url-pattern>/api/v1/*</url-pattern> </servlet-mapping> <!-- omitted -->
項番 説明 DispatcherServletとリクエストマッピング。DispatcherServlet)を追加する。<servlet-name>要素に、RESTful Web Service用サーブレットであることを示す名前を指定する。上記例では、サーブレット名としてrestAppServletを指定している。DispatcherServletを構築する際に使用するSpring MVCのbean定義ファイルを指定する。上記例では、Spring MVCのbean定義ファイルとして、クラスパス上にあるMETA-INF/spring/spring-mvc-rest.xmlを指定している。DispatcherServletへマッピングするサーブレットパスのパターンの指定を行う。上記例では、/api/v1/配下のサーブレットパスをRESTful Web Service用のDispatcherServletにマッピングしている。具体的には、/api/v1//api/v1/members/api/v1/members/xxxxxといったサーブレットパスが、RESTful Web Service用のDispatcherServlet(restAppServlet)にマッピングされる。Tip
@RequestMappingアノテーションのvalue属性に指定する値について
@RequestMappingアノテーションのvalue属性に指定する値は、<url-pattern>要素で指定したワイルドカード(“*”)の部分の値を指定する。例えば、
@RequestMapping(value = "members")と指定した場合、/api/v1/membersといパスに対する処理を行うメソッドとしてデプロイされる。 そのため、@RequestMappingアノテーションのvalue属性には、分割したサーブレットへマッピングするためパス(api/v1)を指定する必要はない。
@RequestMapping(value = "api/v1/members")と指定すると、/api/v1/api/v1/membersというパスに対する処理を行うメソッドとしてデプロイされてしまうので、注意すること。
5.1.6.3. ハイパーメディアリンクの実装¶
JSONの中に関連リソースへのハイパーメディアリンクを含める場合の実装について説明する。
5.1.6.3.1. 共通部品の実装¶
- リンク情報を保持するJavaBeanを作成する。
package org.terasoluna.examples.rest.api.common.resource; import java.io.Serializable; // (1) public class Link implements Serializable { private static final long serialVersionUID = 1L; private final String rel; private final String href; public Link(String rel, String href) { this.rel = rel; this.href = href; } public String getRel() { return rel; } public String getHref() { return href; } }
項番 説明 
- リンク情報のコレクションを保持するResourceの抽象クラスを作成する。
package org.terasoluna.examples.rest.api.common.resource; import java.net.URI; import java.util.LinkedHashSet; import java.util.Set; import com.fasterxml.jackson.annotation.JsonInclude; // (2) public abstract class AbstractLinksSupportedResource { // (3) @JsonInclude(JsonInclude.Include.NON_EMPTY) private final Set<Link> links = new LinkedHashSet<>(); public Set<Link> getLinks() { return links; } // (4) public AbstractLinksSupportedResource addLink(String rel, URI href) { links.add(new Link(rel, href.toString())); return this; } // (5) public AbstractLinksSupportedResource addSelf(URI href) { return addLink("self", href); } // (5) public AbstractLinksSupportedResource addParent(URI href) { return addLink("parent", href); } }
項番 説明 @JsonInclude(JsonInclude.Include.NON_EMPTY)を指定している。self)と、親のリソースにアクセスするためのリンク情報(parent)を追加するためのメソッドを用意している。
5.1.6.3.2. リソース毎の実装¶
- Resourceクラスにて、リンク情報のコレクションを保持するResourceの抽象クラスを継承する。
package org.terasoluna.examples.rest.api.member; // (1) public class MemberResource extends AbstractLinksSupportedResource implements Serializable { // omitted }
項番 説明 links)が取り込まれ、ハイパーメディアリンクをサポートするResourceクラスとなる。
- REST APIの処理で、ハイパーメディアリンクを追加する。
@RequestMapping("members") @RestController public class MemberRestController { // omitted @RequestMapping(value = "{memberId}", method = RequestMethod.GET) @ResponseStatus(HttpStatus.OK) public MemberResource getMember( @PathVariable("memberId") String memberId // (2) UriComponentsBuilder uriBuilder) { Member member = memberService.getMember(memberId); MemberResource responseResource = beanMapper.map(member, MemberResource.class); // (3) responseResource.addSelf(uriBuilder.path("/members").pathSegment(memberId) .build().toUri()); return responseResource; } // omitted }
項番 説明 org.springframework.web.util.UriComponentsBuilderクラスをメソッドの引数に指定する。UriComponentsBuilderクラスをControllerのメソッドの引数に指定すると、メソッド実行時に、Spring MVCによりUriComponentsBuilderクラスを継承したorg.springframework.web.servlet.support.ServletUriComponentsBuilderクラスのインスタンスが渡される。UriComponentsBuilderクラスのメソッドを呼び出し、自身のリソースにアクセスするためのURIをリソースに追加している。Controllerのメソッドの引数として渡されたServletUriComponentsBuilderのインスタンスは、web.xmlに記載の<servlet-mapping>要素の情報を元に初期化されており、リソースには依存しない。そのため、Spring Frameworkから提供される URI Template Patterns等を利用し、リクエスト情報をベースにURIを組み立てる事により、リソースに依存しない汎用的な組み立て処理を実装することが可能となる。例えば、上記例においてhttp://example.com/api/v1/members/M000000001に対してGETした場合、組み立てられるURIは、リクエストされたURIと同じ値(http://example.com/api/v1/members/M000000001)になる。必要に応じてリンク情報に設定するURIを組み立てるためのメソッドを実装すること。Tip
ServletUriComponentsBuilderでは、URIを組み立てる際に「X-Forwarded-Host」ヘッダを参照することで、クライアントとアプリケーションサーバの間にロードバランサやWebサーバがある構成を考慮している。 ただし、パスの構成を合わせておかないと期待通りのURIにならないので注意が必要である。
- レスポンス例実際に動かすと、以下のようなレスポンスとなる。
GET /rest-api-web/api/v1/members/M000000001 HTTP/1.1 Accept: text/plain, application/json, application/*+json, */* User-Agent: Java/1.7.0_51 Host: localhost:8080 Connection: keep-alive{ "links" : [ { "rel" : "self", "href" : "http://localhost:8080/rest-api-web/api/v1/members/M000000001" } ], "memberId" : "M000000001", "firstName" : "John", "lastName" : "Smith", "genderCode" : "1", "dateOfBirth" : "2013-03-14", "emailAddress" : "user1394794959984@test.com", "telephoneNumber" : "09012345678", "zipCode" : "1710051", "address" : "Tokyo", "credential" : { "signId" : "user1394794959984@test.com", "passwordLastChangedAt" : "2014-03-14T11:02:41.477Z", "lastModifiedAt" : "2014-03-14T11:02:41.477Z" }, "createdAt" : "2014-03-14T11:02:41.477Z", "lastModifiedAt" : "2014-03-14T11:02:41.477Z" }
5.1.6.4. HTTPの仕様に準拠したRESTful Web Serviceの作成¶
5.1.6.4.1. POST時のLocationヘッダの設定¶
5.1.6.4.2. リソース毎の実装¶
- REST APIの処理で、作成したリソースのURIをLocationヘッダに設定する。
@RequestMapping("members") @RestController public class MemberRestController { // omitted @RequestMapping(method = RequestMethod.POST) public ResponseEntity<MemberResource> postMembers( @RequestBody @Validated({ PostMembers.class, Default.class }) MemberResource requestedResource, // (1) UriComponentsBuilder uriBuilder) { Member creatingMember = beanMapper.map(requestedResource, Member.class); Member createdMember = memberService.createMember(creatingMember); MemberResource responseResource = beanMapper.map(createdMember, MemberResource.class); // (2) URI createdUri = uriBuilder.path("/members/{memberId}") .buildAndExpand(responseResource.getMemberId()).toUri(); // (3) return ResponseEntity.created(createdUri).body(responseResource); } // omitted }
項番 説明 org.springframework.web.util.UriComponentsBuilderクラスをメソッドの引数に指定する。UriComponentsBuilderクラスをControllerのメソッドの引数に指定すると、メソッド実行時に、Spring MVCによりUriComponentsBuilderクラスを継承したorg.springframework.web.servlet.support.ServletUriComponentsBuilderクラスのインスタンスが渡される。ServletUriComponentsBuilderのインスタンスにpathメソッドで、URI Template Patternsを用いたパスを追加し、buildAndExpandメソッドを呼び出して、作成したリソースのIDをバインドすることで、作成したリソースのURIを組み立てている。Controllerのメソッドの引数として渡されたServletUriComponentsBuilderのインスタンスは、web.xmlに記載の<servlet-mapping>要素の情報を元に初期化されており、リソースには依存しない。そのため、Spring Frameworkから提供される URITemplatePatterns等を利用し、リクエスト情報をベースにURIを組み立てる事により、リソースに依存しない汎用的な組み立て処理を実装することが可能となる。例えば、上記例においてhttp://example.com/api/v1/membersに対してPOSTした場合、組み立てられるURIは、「リクエストされたURI + “/” + 作成したリソースのID」となる。具体的には、IDにM000000001を指定した場合、http://example.com/api/v1/members/M000000001となる。必要に応じてリンク情報に設定するURIを組み立てるためのメソッドを実装すること。org.springframework.http.ResponseEntityを生成し返却する。
- ステータスコード : 201(Created)
- Locationヘッダ : 作成したリソースのURI
- レスポンスBODY : 作成したResourceオブジェクト
Tip
ServletUriComponentsBuilderでは、URIを組み立てる際に「X-Forwarded-Host」ヘッダを参照することで、クライアントとアプリケーションサーバの間にロードバランサやWebサーバがある構成を考慮している。 ただし、パスの構成を合わせておかないと期待通りのURIにならないので注意が必要である。
- レスポンス例実際に動かすと、以下のようなレスポンスヘッダとなる。
HTTP/1.1 201 Created Server: Apache-Coyote/1.1 X-Track: 693e132312d64998a7d8d6cabf3d13ef Location: http://localhost:8080/rest-api-web/api/v1/members/M000000001 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 14 Mar 2014 12:34:31 GMT
5.1.6.5. CSRF対策の無効化¶
RESTful Web Service向けのリクエストに対して、CSRF対策を行わないようにするための設定方法について説明する。
Tip
CSRF対策を行わない場合は、セッションを利用する必要がなくなる。
下記設定例では、Spring Securityの処理でセッションが使用されなくなる様にしている。
Blankプロジェクトのデフォルトの設定では、CSRF対策が有効化されているため、以下の設定を追加し、 RESTful Web Service向けのリクエストに対して、CSRF対策の処理が行われないようにする。
- spring-security.xml
<!-- omitted --> <!-- (1) --> <sec:http pattern="/api/v1/**" create-session="stateless"> <sec:http-basic/> <sec:csrf disabled="true"/> </sec:http> <sec:http> <sec:access-denied-handler ref="accessDeniedHandler"/> <sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/> <sec:form-login/> <sec:logout/> <sec:session-management /> </sec:http> <!-- omitted -->
項番 説明 <sec:http>要素のpattern属性に、REST API用のリクエストパスのURLパターンを指定している。上記例では、/api/v1/で始まるリクエストパスをREST API用のリクエストパスとして扱う。また、create-session属性をstatelessとする事で、Spring Securityの処理でセッションが使用されなくなる。CSRF対策を無効化するために、<sec:csrf>要素にdisabled="true"を指定している。
5.1.6.6. XXE Injection対策の有効化¶
Warning
XXE(XML External Entity) Injection 対策について
terasoluna-gfw-web 1.0.0.RELEASEを使用している場合は、XXE Injection対策が行われていないSpring MVC(3.2.4.RELEASE)に依存しているため、Spring-oxmから提供されているクラスを使用すること。
Spring-oxmを依存アーティファクトとして追加する。
- pom.xml
<!-- omitted --> <!-- (1) --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> <version>${org.springframework-version}</version> <!-- (2) --> </dependency> <!-- omitted -->
pom.xmlに定義されているSpringのバージョン番号を管理するためのプレースホルダ(${org.springframework-version})から取得すること。
Spring-oxmから提供されているクラスを使用してXMLとオブジェクトの相互変換を行うためのbean定義を行う。
- spring-mvc-rest.xml
<!-- omitted --> <!-- (1) --> <bean id="xmlMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="packagesToScan" value="com.examples.app" /> <!-- (2) --> </bean> <!-- omitted --> <mvc:annotation-driven> <mvc:message-converters> <!-- (3) --> <bean class="org.springframework.http.converter.xml.MarshallingHttpMessageConverter"> <property name="marshaller" ref="xmlMarshaller" /> <!-- (4) --> <property name="unmarshaller" ref="xmlMarshaller" /> <!-- (5) --> </bean> </mvc:message-converters> <!-- omitted --> </mvc:annotation-driven> <!-- omitted -->
Jaxb2Marshallerのbean定義を行う。Jaxb2Marshallerはデフォルトの状態で XXE Injection対策が行われている。packagesToScanプロパティに JAXB用のJavaBean(javax.xml.bind.annotation.XmlRootElementアノテーションなどが付与されているJavaBean)が格納されているパッケージ名を指定する。指定したパッケージ配下に格納されているJAXB用のJavaBeanがスキャンされ、marshal、unmarshal対象のJavaBeanとして登録される。<context:component-scan>の base-package属性と同じ仕組みでスキャンされる。<mvc:annotation-driven>の子要素である<mvc:message-converters>要素に、MarshallingHttpMessageConverterのbean定義を追加する。marshallerプロパティに (1)で定義したJaxb2Marshallerのbeanを指定する。unmarshallerプロパティに (1)で定義したJaxb2Marshallerのbeanを指定する。
5.1.6.7. Dozerを使ってJoda-Timeのクラスをコピーする方法¶
Dozerを使用して、Joda-Timeのクラス(org.joda.time.DateTime、org.joda.time.LocalDateなど)をコピーする方法について説明する。
- JodaDateTimeConverter.java
package org.terasoluna.examples.rest.infra.dozer.converter; import org.dozer.DozerConverter; import org.joda.time.DateTime; public class JodaDateTimeConverter extends DozerConverter<DateTime, DateTime> { public JodaDateTimeConverter() { super(DateTime.class, DateTime.class); } @Override public DateTime convertTo(DateTime source, DateTime destination) { // This method not called, because type of from/to is same. return convertFrom(source, destination); } @Override public DateTime convertFrom(DateTime source, DateTime destination) { return source; } }
- JodaLocalDateConverter.java
package org.terasoluna.examples.rest.infra.dozer.converter; import org.dozer.DozerConverter; import org.joda.time.LocalDate; public class JodaLocalDateConverter extends DozerConverter<LocalDate, LocalDate> { public JodaLocalDateConverter() { super(LocalDate.class, LocalDate.class); } @Override public LocalDate convertTo(LocalDate source, LocalDate destination) { // This method not called, because type of from/to is same. return convertFrom(source, destination); } @Override public LocalDate convertFrom(LocalDate source, LocalDate destination) { return source; } }
<!-- (1) --> <?xml version="1.0" encoding="UTF-8"?> <mappings xmlns="http://dozer.sourceforge.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://dozer.sourceforge.net http://dozer.sourceforge.net/schema/beanmapping.xsd "> <configuration> <custom-converters> <!-- (2) --> <converter type="org.terasoluna.examples.rest.infra.dozer.converter.JodaDateTimeConverter"> <class-a>org.joda.time.DateTime</class-a> <class-b>org.joda.time.DateTime</class-b> </converter> <converter type="org.terasoluna.examples.rest.infra.dozer.converter.JodaLocalDateConverter"> <class-a>org.joda.time.LocalDate</class-a> <class-b>org.joda.time.LocalDate</class-b> </converter> </custom-converters> </configuration> </mappings>
項番 説明 /xxx-domain/src/main/resources/META-INF/dozer/dozer-configration-mapping.xmlに格納する。org.joda.time.DateTimeとorg.joda.time.LocalDate)に対するカスタムコンバータの定義を追加している。Note
ドメイン層でもDozerを使用する場合は、Dozerの動作設定を定義するファイルは、ドメイン層用のプロジェクト(
xxx-domain)に格納する事を推奨する。アプリケーション層のみでDozerを使う場合は、アプリケーション層用のプロジェクト(
xxx-web)に格納してもよい。
5.1.6.8. アプリケーション層のソースコード¶
以下のファイルは、除外している。
- JavaBean
- 設定ファイル
5.1.6.8.1. MemberRestController.java¶
java/org/terasoluna/examples/rest/api/member/MemberRestController.java
package org.terasoluna.examples.rest.api.member;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import javax.validation.groups.Default;
import org.dozer.Mapper;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import org.terasoluna.examples.rest.api.member.MemberResource.PostMembers;
import org.terasoluna.examples.rest.api.member.MemberResource.PutMember;
import org.terasoluna.examples.rest.domain.model.Member;
import org.terasoluna.examples.rest.domain.service.member.MemberService;
@RequestMapping("members")
@RestController
public class MemberRestController {
    @Inject
    MemberService memberService;
    @Inject
    Mapper beanMapper;
    @RequestMapping(method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public Page<MemberResource> getMembers(@Validated MembersSearchQuery query,
            Pageable pageable) {
        Page<Member> page = memberService.searchMembers(query.getName(), pageable);
        List<MemberResource> memberResources = new ArrayList<>();
        for (Member member : page.getContent()) {
            memberResources.add(beanMapper.map(member, MemberResource.class));
        }
        Page<MemberResource> responseResource =
            new PageImpl<>(memberResources, pageable, page.getTotalElements());
        return responseResource;
    }
    @RequestMapping(method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public List<MemberResource> getMembers() {
        List<Member> members = memberService.findAll();
        List<MemberResource> memberResources = new ArrayList<>();
        for (Member member : members) {
            memberResources.add(beanMapper.map(member, MemberResource.class));
        }
        return memberResources;
    }
    @RequestMapping(method = RequestMethod.POST)
    @ResponseStatus(HttpStatus.CREATED)
    public MemberResource postMembers(@RequestBody @Validated({
            PostMembers.class, Default.class }) MemberResource requestedResource) {
        Member creatingMember = beanMapper.map(requestedResource, Member.class);
        Member createdMember = memberService.createMember(creatingMember);
        MemberResource responseResource = beanMapper.map(createdMember,
                MemberResource.class);
        return responseResource;
    }
    @RequestMapping(value = "{memberId}", method = RequestMethod.GET)
    @ResponseStatus(HttpStatus.OK)
    public MemberResource getMember(@PathVariable("memberId") String memberId) {
        Member member = memberService.getMember(memberId);
        MemberResource responseResource = beanMapper.map(member,
                MemberResource.class);
        return responseResource;
    }
    @RequestMapping(value = "{memberId}", method = RequestMethod.PUT)
    @ResponseStatus(HttpStatus.OK)
    public MemberResource putMember(
            @PathVariable("memberId") String memberId,
            @RequestBody @Validated({
            PutMember.class, Default.class }) MemberResource requestedResource) {
        Member updatingMember = beanMapper.map(requestedResource, Member.class);
        Member updatedMember = memberService.updateMember(memberId,
                updatingMember);
        MemberResource responseResource = beanMapper.map(updatedMember,
                MemberResource.class);
        return responseResource;
    }
    @RequestMapping(value = "{memberId}", method = RequestMethod.DELETE)
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteMember(@PathVariable("memberId") String memberId) {
        memberService.deleteMember(memberId);
    }
}
5.1.6.8.2. ApiErrorCreator.java¶
java/org/terasoluna/examples/rest/api/common/error/ApiErrorCreator.java
package org.terasoluna.examples.rest.api.common.error;
import javax.inject.Inject;
import org.springframework.context.MessageSource;
import org.springframework.context.support.DefaultMessageSourceResolvable;
import org.springframework.stereotype.Component;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.validation.ObjectError;
import org.springframework.web.context.request.WebRequest;
import org.terasoluna.gfw.common.message.ResultMessage;
import org.terasoluna.gfw.common.message.ResultMessages;
@Component
public class ApiErrorCreator {
    @Inject
    MessageSource messageSource;
    public ApiError createApiError(WebRequest request, String errorCode,
            String defaultErrorMessage, Object... arguments) {
        String localizedMessage = messageSource.getMessage(errorCode,
                arguments, defaultErrorMessage, request.getLocale());
        return new ApiError(errorCode, localizedMessage);
    }
    public ApiError createBindingResultApiError(WebRequest request,
            String errorCode, BindingResult bindingResult,
            String defaultErrorMessage) {
        ApiError apiError = createApiError(request, errorCode,
                defaultErrorMessage);
        for (FieldError fieldError : bindingResult.getFieldErrors()) {
            apiError.addDetail(createApiError(request, fieldError, fieldError
                    .getField()));
        }
        for (ObjectError objectError : bindingResult.getGlobalErrors()) {
            apiError.addDetail(createApiError(request, objectError, objectError
                    .getObjectName()));
        }
        return apiError;
    }
    private ApiError createApiError(WebRequest request,
            DefaultMessageSourceResolvable messageResolvable, String target) {
        String localizedMessage = messageSource.getMessage(messageResolvable,
                request.getLocale());
        return new ApiError(messageResolvable.getCode(), localizedMessage, target);
    }
    public ApiError createResultMessagesApiError(WebRequest request,
            String rootErrorCode, ResultMessages resultMessages,
            String defaultErrorMessage) {
        ApiError apiError;
        if (resultMessages.getList().size() == 1) {
            ResultMessage resultMessage = resultMessages.iterator().next();
            String errorCode = resultMessage.getCode();
            String errorText = resultMessage.getText();
            if (errorCode == null && errorText == null) {
                errorCode = rootErrorCode;
            }
            apiError = createApiError(request, errorCode, errorText,
                    resultMessage.getArgs());
        } else {
            apiError = createApiError(request, rootErrorCode,
                    defaultErrorMessage);
            for (ResultMessage resultMessage : resultMessages.getList()) {
                apiError.addDetail(createApiError(request, resultMessage
                        .getCode(), resultMessage.getText(), resultMessage
                        .getArgs()));
            }
        }
        return apiError;
    }
}
5.1.6.8.3. ApiGlobalExceptionHandler.java¶
java/org/terasoluna/examples/rest/api/common/error/ApiGlobalExceptionHandler.java
package org.terasoluna.examples.rest.api.common.error;
import javax.inject.Inject;
import org.springframework.dao.OptimisticLockingFailureException;
import org.springframework.dao.PessimisticLockingFailureException;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.exception.ExceptionCodeResolver;
import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
import org.terasoluna.gfw.common.exception.ResultMessagesNotificationException;
@ControllerAdvice
public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler {
    @Inject
    ApiErrorCreator apiErrorCreator;
    @Inject
    ExceptionCodeResolver exceptionCodeResolver;
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(Exception ex,
            Object body, HttpHeaders headers, HttpStatus status,
            WebRequest request) {
        final Object apiError;
        if (body == null) {
            String errorCode = exceptionCodeResolver.resolveExceptionCode(ex);
            apiError = apiErrorCreator.createApiError(request, errorCode, ex
                    .getLocalizedMessage());
        } else {
            apiError = body;
        }
        return ResponseEntity.status(status).headers(headers).body(apiError);
    }
    @Override
    protected ResponseEntity<Object> handleMethodArgumentNotValid(
            MethodArgumentNotValidException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        return handleBindingResult(ex, ex.getBindingResult(), headers, status,
                request);
    }
    @Override
    protected ResponseEntity<Object> handleBindException(BindException ex,
            HttpHeaders headers, HttpStatus status, WebRequest request) {
        return handleBindingResult(ex, ex.getBindingResult(), headers, status,
                request);
    }
    private ResponseEntity<Object> handleBindingResult(Exception ex,
            BindingResult bindingResult, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        String errorCode = exceptionCodeResolver.resolveExceptionCode(ex);
        ApiError apiError = apiErrorCreator.createBindingResultApiError(
                request, errorCode, bindingResult, ex.getMessage());
        return handleExceptionInternal(ex, apiError, headers, status, request);
    }
    @Override
    protected ResponseEntity<Object> handleHttpMessageNotReadable(
            HttpMessageNotReadableException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        if (ex.getCause() instanceof Exception) {
            return handleExceptionInternal((Exception) ex.getCause(), null,
                    headers, status, request);
        } else {
            return handleExceptionInternal(ex, null, headers, status, request);
        }
    }
    @ExceptionHandler(ResourceNotFoundException.class)
    public ResponseEntity<Object> handleResourceNotFoundException(
            ResourceNotFoundException ex, WebRequest request) {
        return handleResultMessagesNotificationException(ex, new HttpHeaders(),
                HttpStatus.NOT_FOUND, request);
    }
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<Object> handleBusinessException(BusinessException ex,
            WebRequest request) {
        return handleResultMessagesNotificationException(ex, new HttpHeaders(),
                HttpStatus.CONFLICT, request);
    }
    private ResponseEntity<Object> handleResultMessagesNotificationException(
            ResultMessagesNotificationException ex, HttpHeaders headers,
            HttpStatus status, WebRequest request) {
        String errorCode = exceptionCodeResolver.resolveExceptionCode(ex);
        ApiError apiError = apiErrorCreator.createResultMessagesApiError(
                request, errorCode, ex.getResultMessages(), ex.getMessage());
        return handleExceptionInternal(ex, apiError, headers, status, request);
    }
    @ExceptionHandler({ OptimisticLockingFailureException.class,
            PessimisticLockingFailureException.class })
    public ResponseEntity<Object> handleLockingFailureException(Exception ex,
            WebRequest request) {
        return handleExceptionInternal(ex, null, new HttpHeaders(),
                HttpStatus.CONFLICT, request);
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleSystemError(Exception ex,
            WebRequest request) {
        return handleExceptionInternal(ex, null, new HttpHeaders(),
                HttpStatus.INTERNAL_SERVER_ERROR, request);
    }
}
5.1.6.9. REST API実装時に作成したドメイン層のクラスのソースコード¶
以下のファイルは、除外している。
- Entity以外のJavaBean
- Dozer以外の設定ファイル
5.1.6.9.1. Member.java¶
java/org/terasoluna/examples/rest/domain/model/Member.java
package org.terasoluna.examples.rest.domain.model;
import java.io.Serializable;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
public class Member implements Serializable {
    private static final long serialVersionUID = 1L;
    private String memberId;
    private String firstName;
    private String lastName;
    private Gender gender;
    private LocalDate dateOfBirth;
    private String emailAddress;
    private String telephoneNumber;
    private String zipCode;
    private String address;
    private DateTime createdAt;
    private DateTime lastModifiedAt;
    private long version;
    private MemberCredential credential;
    public String getMemberId() {
        return memberId;
    }
    public void setMemberId(String memberId) {
        this.memberId = memberId;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public Gender getGender() {
        return gender;
    }
    public void setGender(Gender gender) {
        this.gender = gender;
    }
    public String getGenderCode() {
        if (gender == null) {
            return null;
        } else {
            return gender.getCode();
        }
    }
    public void setGenderCode(String genderCode) {
        this.gender = Gender.getByCode(genderCode);
    }
    public LocalDate getDateOfBirth() {
        return dateOfBirth;
    }
    public void setDateOfBirth(LocalDate dateOfBirth) {
        this.dateOfBirth = dateOfBirth;
    }
    public String getEmailAddress() {
        return emailAddress;
    }
    public void setEmailAddress(String emailAddress) {
        this.emailAddress = emailAddress;
    }
    public String getTelephoneNumber() {
        return telephoneNumber;
    }
    public void setTelephoneNumber(String telephoneNumber) {
        this.telephoneNumber = telephoneNumber;
    }
    public String getZipCode() {
        return zipCode;
    }
    public void setZipCode(String zipCode) {
        this.zipCode = zipCode;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public DateTime getCreatedAt() {
        return createdAt;
    }
    public void setCreatedAt(DateTime createdAt) {
        this.createdAt = createdAt;
    }
    public DateTime getLastModifiedAt() {
        return lastModifiedAt;
    }
    public void setLastModifiedAt(DateTime lastModifiedAt) {
        this.lastModifiedAt = lastModifiedAt;
    }
    public long getVersion() {
        return version;
    }
    public void setVersion(long version) {
        this.version = version;
    }
    public MemberCredential getCredential() {
        return credential;
    }
    public void setCredential(MemberCredential credential) {
        this.credential = credential;
    }
}
5.1.6.9.2. MemberCredentia.java¶
java/org/terasoluna/examples/rest/domain/model/MemberCredential.java
package org.terasoluna.examples.rest.domain.model;
import java.io.Serializable;
import org.joda.time.DateTime;
public class MemberCredential implements Serializable {
    private static final long serialVersionUID = 1L;
    private String memberId;
    private String signId;
    private String password;
    private String previousPassword;
    private DateTime passwordLastChangedAt;
    private DateTime lastModifiedAt;
    private long version;
    public String getMemberId() {
        return memberId;
    }
    public void setMemberId(String memberId) {
        this.memberId = memberId;
    }
    public String getSignId() {
        return signId;
    }
    public void setSignId(String signId) {
        this.signId = signId;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    public String getPreviousPassword() {
        return previousPassword;
    }
    public void setPreviousPassword(String previousPassword) {
        this.previousPassword = previousPassword;
    }
    public DateTime getPasswordLastChangedAt() {
        return passwordLastChangedAt;
    }
    public void setPasswordLastChangedAt(DateTime passwordLastChangedAt) {
        this.passwordLastChangedAt = passwordLastChangedAt;
    }
    public DateTime getLastModifiedAt() {
        return lastModifiedAt;
    }
    public void setLastModifiedAt(DateTime lastModifiedAt) {
        this.lastModifiedAt = lastModifiedAt;
    }
    public long getVersion() {
        return version;
    }
    public void setVersion(long version) {
        this.version = version;
    }
}
5.1.6.9.3. Gender.java¶
java/org/terasoluna/examples/rest/domain/model/Gender.java
package org.terasoluna.examples.rest.domain.model;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import org.springframework.util.Assert;
public enum Gender {
    UNKNOWN("0"), MEN("1"), WOMEN("2");
    private static final Map<String, Gender> genderMap;
    static {
        Map<String, Gender> map = new HashMap<>();
        for (Gender gender : values()) {
            map.put(gender.code, gender);
        }
        genderMap = Collections.unmodifiableMap(map);
    }
    private final String code;
    private Gender(String code) {
        this.code = code;
    }
    public static Gender getByCode(String code) {
        Gender gender = genderMap.get(code);
        Assert.notNull(gender, "gender code is invalid. code : " + code);
        return gender;
    }
    public String getCode() {
        return code;
    }
}
5.1.6.9.4. MemberRepository.java¶
java/org/terasoluna/examples/rest/domain/repository/member/MemberRepository.java
package org.terasoluna.examples.rest.domain.repository.member;
import java.util.List;
import org.apache.ibatis.session.RowBounds;
import org.terasoluna.examples.rest.domain.model.Member;
public interface MemberRepository {
    Member findOne(String memberId);
    List<Member> findAll();
    long countByContainsName(String name);
    List<Member> findPageByContainsName(String name, RowBounds rowBounds);
    void createMember(Member creatingMember);
    void createCredential(Member creatingMember);
    boolean updateMember(Member updatingMember);
    void deleteMember(String memberId);
    void deleteCredential(String memberId);
}
5.1.6.9.5. MemberService.java¶
java/org/terasoluna/examples/rest/domain/service/member/MemberService.java
package org.terasoluna.examples.rest.domain.service.member;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.terasoluna.examples.rest.domain.model.Member;
public interface MemberService {
    List<Member> findAll();
    Page<Member> searchMembers(String name, Pageable pageable);
    Member getMember(String memberId);
    Member createMember(Member creatingMember);
    Member updateMember(String memberId, Member updatingMember);
    void deleteMember(String memberId);
}
5.1.6.9.6. MemberServiceImpl.java¶
java/org/terasoluna/examples/rest/domain/service/member/MemberServiceImpl.java
package org.terasoluna.examples.rest.domain.service.member;
import java.util.ArrayList;
import java.util.List;
import javax.inject.Inject;
import org.apache.ibatis.session.RowBounds;
import org.dozer.Mapper;
import org.joda.time.DateTime;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.Pageable;
import org.springframework.orm.ObjectOptimisticLockingFailureException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.terasoluna.examples.rest.domain.message.DomainMessageCodes;
import org.terasoluna.examples.rest.domain.model.Member;
import org.terasoluna.examples.rest.domain.model.MemberCredential;
import org.terasoluna.examples.rest.domain.repository.member.MemberRepository;
import org.terasoluna.gfw.common.date.jodatime.JodaTimeDateFactory;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
import org.terasoluna.gfw.common.message.ResultMessages;
@Transactional
@Service
public class MemberServiceImpl implements MemberService {
    @Inject
    MemberRepository memberRepository;
    @Inject
    JodaTimeDateFactory dateFactory;
    @Inject
    PasswordEncoder passwordEncoder;
    @Inject
    Mapper beanMapper;
    @Override
    @Transactional(readOnly = true)
    public List<RestMember> findAll() {
        return restMemberRepository.findAll();
    }
    @Override
    @Transactional(readOnly = true)
    public Page<Member> searchMembers(String name, Pageable pageable) {
        List<Member> members = null;
        // Count Members by search criteria
        long total = memberRepository.countByContainsName(name);
        if (0 < total) {
             RowBounds rowBounds = new RowBounds(pageable.getOffset(), pageable.getPageSize());
             members = memberRepository.findPageByContainsName(name, rowBounds);
        } else {
            members = new ArrayList<Member>();
        }
        return new PageImpl<Member>(members, pageable, total);
    }
    @Override
    @Transactional(readOnly = true)
    public Member getMember(String memberId) {
        // find member
        Member member = memberRepository.findOne(memberId);
        if (member == null) {
            // If member is not exists
            throw new ResourceNotFoundException(ResultMessages.error().add(
                            DomainMessageCodes.E_EX_MM_5001, memberId));
        }
        return member;
    }
    @Override
    public Member createMember(Member creatingMember) {
        MemberCredential creatingCredential = creatingMember
                            .getCredential();
        // get processing current date time
        DateTime currentDateTime = dateFactory.newDateTime();
        creatingMember.setCreatedAt(currentDateTime);
        creatingMember.setLastModifiedAt(currentDateTime);
        // decide sign id(email-address)
        String signId = creatingCredential.getSignId();
        if (!StringUtils.hasLength(signId)) {
            signId = creatingMember.getEmailAddress();
            creatingCredential.setSignId(signId.toLowerCase());
        }
        // encrypt password
        String rawPassword = creatingCredential.getPassword();
        creatingCredential.setPassword(passwordEncoder.encode(rawPassword));
        creatingCredential.setPasswordLastChangedAt(currentDateTime);
        creatingCredential.setLastModifiedAt(currentDateTime);
        // save member & member credential
        try {
            // Registering member details
            memberRepository.createMember(creatingMember);
            // //Registering credential details
            memberRepository.createCredential(creatingMember);
            return creatingMember;
        } catch (DuplicateKeyException e) {
            // If sign id is already used
            throw new BusinessException(ResultMessages.error().add(
                            DomainMessageCodes.E_EX_MM_8001,
                            creatingCredential.getSignId()), e);
        }
    }
    @Override
    public Member updateMember(String memberId, Member updatingMember) {
        // get member
        Member member = getMember(memberId);
        // override updating member attributes
        beanMapper.map(updatingMember, member, "member.update");
        // get processing current date time
        DateTime currentDateTime = dateFactory.newDateTime();
        member.setLastModifiedAt(currentDateTime);
        // save updating member
        boolean updated = memberRepository.updateMember(member);
        if (!updated) {
                throw new ObjectOptimisticLockingFailureException(Member.class,
                                member.getMemberId());
        }
        return member;
    }
    @Override
    public void deleteMember(String memberId) {
        // First Delete from credential (Child)
        memberRepository.deleteCredential(memberId);
        // Delete member
        memberRepository.deleteMember(memberId);
    }
}
5.1.6.9.7. DomainMessageCodes.java¶
java/org/terasoluna/examples/rest/domain/message/DomainMessageCodes.java
package org.terasoluna.examples.rest.domain.message;
/**
 * Message codes of domain layer message.
 * @author DomainMessageCodesGenerator
 */
public class DomainMessageCodes {
    private DomainMessageCodes() {
        // NOP
    }
    /** e.ex.mm.5001=Specified member not found. member id : {0} */
    public static final String E_EX_MM_5001 = "e.ex.mm.5001";
    /** e.ex.mm.8001=Cannot use specified sign id. sign id : {0} */
    public static final String E_EX_MM_8001 = "e.ex.mm.8001";
}
5.1.6.9.8. GenderTypeHandler.java¶
java/org/terasoluna/examples/infra/mybatis/typehandler/GenderTypeHandler.java
package org.terasoluna.examples.infra.mybatis.typehandler;
import java.sql.CallableStatement;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import org.terasoluna.examples.domain.model.Gender;
import org.apache.ibatis.type.JdbcType;
import org.apache.ibatis.type.BaseTypeHandler;
public class GenderTypeHandler extends BaseTypeHandler<Gender> {
    @Override
    public Gender getNullableResult(ResultSet rs, String columnName) throws SQLException {
            return getByCode(rs.getString(columnName));
    }
    @Override
    public Gender getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
            return getByCode(rs.getString(columnIndex));
    }
    @Override
    public Gender getNullableResult(CallableStatement cs, int columnIndex)
                    throws SQLException {
            return getByCode(cs.getString(columnIndex));
    }
    @Override
    public void setNonNullParameter(PreparedStatement ps, int i,
                    Gender parameter, JdbcType jdbcType) throws SQLException {
            ps.setString(i, parameter.getCode());
    }
    private Gender getByCode(String byCode) {
            if (byCode == null) {
                return null;
            } else {
                return Gender.getByCode(byCode);
            }
    }
}
5.1.6.9.9. member-mapping.xml¶
Memberオブジェクトにコピーする際に、「Beanマッピング(Dozer)」を使って行っている。memberId、credential、createdAt、version)をコピー対象外にする必要がある。resources/META-INF/dozer/member-mapping.xml
<?xml version="1.0" encoding="UTF-8"?>
<mappings xmlns="http://dozer.sourceforge.net" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:schemaLocation="http://dozer.sourceforge.net
          http://dozer.sourceforge.net/schema/beanmapping.xsd">
    <mapping map-id="member.update">
        <class-a>org.terasoluna.examples.rest.domain.model.Member</class-a>
        <class-b>org.terasoluna.examples.rest.domain.model.Member</class-b>
        <field-exclude>
            <a>memberId</a>
            <b>memberId</b>
        </field-exclude>
        <field-exclude>
            <a>credential</a>
            <b>credential</b>
        </field-exclude>
        <field-exclude>
            <a>createdAt</a>
            <b>createdAt</b>
        </field-exclude>
        <field-exclude>
            <a>lastModifiedAt</a>
            <b>lastModifiedAt</b>
        </field-exclude>
        <field-exclude>
            <a>version</a>
            <b>version</b>
        </field-exclude>
    </mapping>
</mappings>
5.1.6.9.10. mybatis-config.xml¶
TypeHandler を用意する必要がある。TypeHandler の実装例、「Joda-Time用のTypeHandlerの実装」を使って行っている。resources/META-INF/mybatis/mybatis-config.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org/DTD Config 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <settings>
        <setting name="jdbcTypeForNull" value="NULL" />
        <setting name="mapUnderscoreToCamelCase" value="true" />
    </settings>
    <typeAliases>
        <package name="org.terasoluna.examples.infra.mybatis.typehandler" />
    </typeAliases>
    <typeHandlers>
       <package name="org.terasoluna.examples.infra.mybatis.typehandler" />
    </typeHandlers>
</configuration>
5.1.6.9.11. MemberRepository.xml¶
resources/org/terasoluna/examples/rest/domain/repository/member/MemberRepository.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper
    namespace="org.terasoluna.examples.rest.domain.repository.member.MemberRepository">
    <resultMap id="MemberResultMap" type="Member">
        <id property="memberId" column="member_id" />
        <result property="firstName" column="first_name" />
        <result property="lastName" column="last_name" />
        <result property="gender" column="gender" />
        <result property="dateOfBirth" column="date_of_birth" />
        <result property="emailAddress" column="email_address" />
        <result property="telephoneNumber" column="telephone_number" />
        <result property="zipCode" column="zip_code" />
        <result property="address" column="address" />
        <result property="createdAt" column="created_at" />
        <result property="lastModifiedAt" column="last_modified_at" />
        <result property="version" column="version" />
        <result property="credential.memberId" column="member_id" />
        <result property="credential.signId" column="sign_id" />
        <result property="credential.password" column="password" />
        <result property="credential.previousPassword" column="previous_password" />
        <result property="credential.passwordLastChangedAt" column="password_last_changed_at" />
        <result property="credential.lastModifiedAt" column="credential_last_modified_at" />
        <result property="credential.version" column="credential_version" />
    </resultMap>
    <sql id="selectMember">
        SELECT
         member.member_id as member_id
         ,member.first_name as first_name
         ,member.last_name as last_name
         ,member.gender as gender
         ,member.date_of_birth as date_of_birth
         ,member.email_address as email_address
         ,member.telephone_number as telephone_number
         ,member.zip_code as zip_code
         ,member.address as address
         ,member.created_at as created_at
         ,member.last_modified_at as last_modified_at
         ,member.version as version
         ,credential.sign_id as sign_id
         ,credential.password as password
         ,credential.previous_password as previous_password
         ,credential.password_last_changed_at as password_last_changed_at
         ,credential.last_modified_at as credential_last_modified_at
         ,credential.version as credential_version
        FROM
         t_member member
         INNER JOIN t_member_credential credential ON credential.member_id = member.member_id
    </sql>
    <sql id="whereMember">
        WHERE
            member.first_name LIKE #{nameContainingCondition} ESCAPE '~'
            OR member.last_name LIKE #{nameContainingCondition} ESCAPE '~'
    </sql>
    <select id="findAll" resultMap="RestMemberResultMap">
        <include refid="selectRestMember" />
        ORDER BY member_id ASC
    </select>
    <select id="findOne" parameterType="string" resultMap="MemberResultMap">
        <include refid="selectMember" />
        WHERE
        member.member_id = #{memberId}
    </select>
    <select id="countByContainsName" parameterType="string" resultType="_long">
        <bind name="nameContainingCondition"
        value="@org.terasoluna.gfw.common.query.QueryEscapeUtils@toStartingWithCondition(_parameter)" />
        SELECT
        COUNT(*)
        FROM
        t_member member
        <include refid="whereMember" />
    </select>
    <select id="findPageByContainsName" parameterType="string"
        resultMap="MemberResultMap">
        <bind name="nameContainingCondition"
        value="@org.terasoluna.gfw.common.query.QueryEscapeUtils@toStartingWithCondition(_parameter)" />
        <include refid="selectMember" />
        <include refid="whereMember" />
        ORDER BY member_id ASC
    </select>
    <insert id="createMember" parameterType="Member">
        <selectKey keyProperty="memberId" resultType="string" order="BEFORE">
            SELECT 'M'||TO_CHAR(NEXTVAL('s_member'),'FM000000000')
        </selectKey>
        INSERT INTO
        t_member
        (
        member_id
        ,first_name
        ,last_name
        ,gender
        ,date_of_birth
        ,email_address
        ,telephone_number
        ,zip_code
        ,address
        ,created_at
        ,last_modified_at
        ,version
        )
        VALUES
        (
        #{memberId}
        ,#{firstName}
        ,#{lastName}
        ,#{gender}
        ,#{dateOfBirth}
        ,#{emailAddress}
        ,#{telephoneNumber}
        ,#{zipCode}
        ,#{address}
        ,#{createdAt}
        ,#{lastModifiedAt}
        ,1
        )
    </insert>
    <insert id="createCredential" parameterType="Member">
        INSERT INTO
        t_member_credential
        (
        member_id
        ,sign_id
        ,password
        ,previous_password
        ,password_last_changed_at
        ,last_modified_at
        ,version
        )
        VALUES
        (
        #{memberId}
        ,#{credential.signId}
        ,#{credential.password}
        ,#{credential.previousPassword}
        ,#{credential.passwordLastChangedAt}
        ,#{credential.lastModifiedAt}
        ,1
        )
    </insert>
    <update id="updateMember" parameterType="Member">
        UPDATE
            t_member
        SET
            first_name = #{firstName}
            ,last_name = #{lastName}
            ,gender = #{gender}
            ,date_of_birth = #{dateOfBirth}
            ,email_address = #{emailAddress}
            ,telephone_number = #{telephoneNumber}
            ,zip_code = #{zipCode}
            ,address = #{address}
            ,created_at = #{createdAt}
            ,last_modified_at = #{lastModifiedAt}
            ,version = version + 1
        WHERE
            member_id = #{memberId}
            AND version = #{version}
    </update>
    <delete id="deleteCredential" parameterType="string">
        DELETE FROM t_member_credential
        WHERE
        member_id = #{memberId}
    </delete>
    <delete id="deleteMember" parameterType="string">
        DELETE FROM t_member
        WHERE
        member_id = #{memberId}
    </delete>
</mapper>









