5.16. RESTful Web Service¶
Caution
本バージョンの内容は既に古くなっています。最新のガイドラインはこちらからご参照ください。
目次
- Overview
- Architecture
- How to design
- How to use
- Appendix
5.16.1. Overview¶
本節では、RESTful Web Serviceの基本的な概念とSpring MVCを使った開発について説明する。
RESTful Web Serviceのアーキテクチャ、設計、実装に対する具体的な説明については、
- RESTful Web Serviceの基本的なアーキテクチャについて説明している。
- RESTful Web Serviceの設計を行う際に考慮すべき点などを説明している。
- RESTful Web Serviceのアプリケーション構成やAPIの実装方法について説明している。
を参照されたい。
5.16.1.1. RESTful Web Serviceとは¶
5.16.1.2. RESTful Web Serviceの開発について¶
TERASOLUNA Global Frameworkでは、Spring MVCの機能を利用してRESTful Web Serviceの開発を行う。
項番 機能概要 Note
例外ハンドリングについて
例外ハンドリングについては、Spring MVCから汎用的な機能の提供がないため、プロジェクト毎に実装が必要となる。 例外ハンドリングの詳細については、「例外のハンドリングの実装」を参照されたい。
項番 処理レイヤ 説明 HttpMessageConverterを使用して、リクエストBODYに指定されているJSON形式の電文をResourceオブジェクトに変換する。Validatorを使用して、Resourceオブジェクトに格納されて値に対して入力チェックを行う。HttpMessageConverterを使用して、REST APIから返却されたResourceオブジェクトをJSON形式の電文に変換する。
5.16.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.16.1.2.2. REST APIの実装サンプル¶
Note
詳細な説明を読む前に、まずは「チュートリアル(Todoアプリケーション REST編)」を実践する事を強く推奨する。
チュートリアルでは”習うより慣れろ”を目的としており、 詳細な説明の前に実際に手を動かすことでTERASOLUNA Global Frameworkによる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.util.Date; import javax.validation.constraints.NotNull; import javax.validation.constraints.Size; public class TodoResource { 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.16.2. Architecture¶
以下の5つのアーキテクチャは、アプリケーションの特性に関係なく適用すべきアーキテクチャである。
項番 アーキテクチャ アーキテクチャの概要 
以下の2つのアーキテクチャは、アプリケーションの特性に応じて、適用するアーキテクチャである。
項番 アーキテクチャ アーキテクチャの概要 
5.16.2.1. Web上のリソースとして公開¶
例えば、ショッピングサイトを提供するWebシステムであれば、以下のような情報がWeb上のリソースとして公開する事になる。
- 商品の情報
- 在庫の情報
- 注文の情報
- 会員の情報
- 会員毎の認証の情報(ログインIDとパスワードなど)
- 会員毎の注文履歴の情報
- 会員毎の認証履歴の情報
- and more …
5.16.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.16.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.16.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.16.2.5. 適切なHTTPステータスコードの使用¶
クライアントへ返却するレスポンスには、適切なHTTPステータスコードを設定する。
Tip
HTTPの仕様について
RFC 2616(Hypertext Transfer Protocol – HTTP/1.1)の6.1.1 Status Code and Reason Phrase を参照されたい。
"200 OK"を応答し、処理結果はエンティティボディ(HTML)の中で表現するという事が一般的であった。
項番 潜在的な問題点 
5.16.2.6. ステートレスなクライアント/サーバ間の通信¶
Note
アプリケーションの状態とは
Webページの遷移状態、入力値、プルダウン/チェックボックス/ラジオボタンなどの選択状態、認証状態などの事である。
Note
CSRF対策との関連
本ガイドラインに記載されているCSRF対策をRESTful Web Serviceに対して行った場合、CSRF対策用のトークン値がHTTPセッションに保存されるため、クライアントとサーバ間の「ステートレス性」を保つ事が出来ないという点を補足しておく。
そのため、CSRF対策を行う場合は、システムの可用性を考慮する必要がある。
高い可用性が求められるシステムでは、
- APサーバをクラスタ化し、セッションをレプリケーションする。
- セッションの保存先をAPサーバのメモリ以外にする。
等の対策が必要となる。 ただし、上記対策は性能への影響があるため、性能要件も考慮する必要がある。
CSRF対策については、CSRF対策を参照されたい。
Todo
TBD
高い可用性が求められる場合は、「CSRF対策用のトークン値をAPサーバのメモリ(HTTPセッション)以外に保存する」アーキテクチャを検討した方がよい。
具体的なアーキテクチャについては、現在検討中であり、次版以降に記載する予定である。
5.16.3. How to design¶
本説では、RESTful Web Serviceの設計について説明する。
5.16.3.1. リソースの抽出¶
まず、Web上に公開するリソースを抽出する。
リソースを抽出する際の注意点を以下に示す。
項番 リソース抽出時の注意点 
5.16.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.16.3.2.1. REST APIであることを示すためのURIの割り当て¶
RESTful Web Service(REST API)向けのURIであること明確にするために、URI内のドメイン又はパスに apiを含めることを推奨する。
具体的には、以下のようなURIとする。
- http://example.com/api/...
- http://api.example.com/...
5.16.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.16.3.2.3. リソースを識別するためのパスの割り当て¶
項番 URIの形式 URIの具体例 説明 
項番 URIの形式 URIの具体例 説明 
項番 URIの形式 URIの具体例 説明 
5.16.3.3. HTTPメソッドの割り当て¶
リソース毎に割り当てたURIに対して、以下のHTTPメソッドを割り当て、リソースに対するCRUD操作をREST APIとして公開する。
Note
HEADとOPTIONSメソッドについて
以降の説明では、HEADとOPTIONSメソッドについても触れているが、REST APIとしての提供は任意とする。
HTTPの仕様に準拠したREST APIを作成する場合は、HEAD及びOPTIONSメソッドの提供も必要だが、実際に使われるケースは稀であり、必要ない事が多いためである。
5.16.3.3.1. リソースコレクションのURIに対するHTTPメソッドの割り当て¶
項番 HTTPメソッド 実装するREST APIの概要 
5.16.3.3.2. 特定リソースのURIに対するHTTPメソッドの割り当て¶
項番 HTTPメソッド 実装するREST APIの概要 
5.16.3.4. リソースのフォーマット¶
5.16.3.4.1. JSONのフィールド名¶
{ "memberId" : "M000000001" }
5.16.3.4.2. NULLとブランク文字¶
{ "dateOfBirth" : null, "address1" : "" }
5.16.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.16.3.4.4. パイパーメディアリンクの形式¶
{ "links" : [ { "rel" : "ownerMember", "href" : "http://example.com/api/v1/memebers/M000000001" } ] }
"rel"と"href"という2つのフィールドを持ったLinkオブジェクトをコレクション形式で保持する。
"rel"には、なんのリンクか識別するためのリンク名を指定する。
"href"には、リソースにアクセスするためのURIを指定する。- Linkオブジェクトをコレクション形式で保持するフィールドは、
"links"とする。
5.16.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.16.3.5. HTTPステータスコード¶
HTTPステータスコードは、以下の指針に則って応答する。
項番 方針 
5.16.3.5.1. リクエストが成功した場合のHTTPステータスコード¶
リクエストが成功した場合は、状況に応じて以下のHTTPステータスコードを応答する。
Tip
"200 OKと"204 No Content"の違いは、レスポンスボディにリソースの情報を出力する/しないの違いとなる。
5.16.3.5.2. リクエストが失敗した原因がクライアント側にある場合のHTTPステータスコード¶
リクエストが失敗した原因がクライアント側にある場合は、状況に応じて以下のHTTPステータスコードを応答する。
リソースを扱う個々のREST APIで意識する必要があるステータスコードは以下の通り。
5.16.3.5.3. リクエストが失敗した原因がサーバ側にある場合のHTTPステータスコード¶
リクエストが失敗した原因がサーバ側にある場合は、状況に応じて以下のHTTPステータスコードを応答する。
5.16.3.6. 認証・認可¶
Todo
TBD
認証及び認可制御をどのような指針で行うかについて記載する。
OAuth2の仕組みを使って認証・認可を行う仕組みについて、次版以降に記載する予定である。
5.16.3.7. リソースの条件付き更新の制御¶
Todo
TBD
HTTPヘッダを使ったリソースの条件付き更新(排他制御)をどのように行うか記載する。
Etag/Last-Modified-Sinceなどのヘッダを使って条件付き更新の仕組みについて、次版以降に記載する予定である。
5.16.3.8. リソースの条件付き取得の制御¶
Todo
TBD
HTTPヘッダを使ったリソースの条件付き取得(304 Not Modified制御)をどのように行うか記載する。
Etag/Last-Modifiedなどのヘッダを使ったリソースの条件付き取得の仕組みについて、次版以降に記載する予定である。
5.16.3.9. リソースのキャッシュ制御¶
Todo
TBD
HTTPヘッダを使ったリソースのキャッシュ制御をどのように行うか記載する。
Cache-Control/Pragma/Expiresなどのヘッダを使ったリソースのキャッシュ制御の仕組みについて、次版以降に記載する予定である。
5.16.3.10. バージョニング¶
Todo
TBD
RESTful Web Service自体のバージョン管理及び複数バージョンの並行稼働をどのように行うかについて、次版以降に記載する予定である。
5.16.4. How to use¶
本節では、RESTful Web Serviceの具体的な作成方法について説明する。
Warning
StAX(Streaming API for XML)使用時のDOS攻撃対策について
XML形式のデータをStAXを使用して解析する場合は、DTDを使ったDOS攻撃を受けないように対応する必要がある。 詳細は、CVE-2015-3192 - DoS Attack with XML Inputを参照されたい。
5.16.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.16.4.2. アプリケーションの設定¶
RESTful Web Service向けのアプリケーションの設定について説明する。
5.16.4.2.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.MappingJacksonHttpMessageConverter"> <property name="objectMapper"> <bean id="objectMapper" class="org.codehaus.jackson.map.ObjectMapper"> <!-- (2) --> <property name="dateFormat"> <bean class="org.codehaus.jackson.map.util.StdDateFormat" /> </property> </bean> </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のみを定義しているが、データアクセスとしてJPAを使う場合は、別途OpenEntityManagerInViewInterceptorの設定を追加する必要がある。OpenEntityManagerInViewInterceptorについては、「データベースアクセス(JPA編)」を参照されたい。"com.example.project.api"の部分はプロジェクト毎のパッケージ名となる。HandlerExceptionResolverLoggingInterceptorについては、「例外ハンドリング」を参照されたい。
5.16.4.2.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.16.4.3. 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
本節では、リソースのCRUD操作の説明に注力するため、HEADとOPTIONSメソッドの説明は行わない。 HTTPの仕様に準拠したRESTful Web Serviceを作成する場合は、「HTTPの仕様に準拠したRESTful Web Serviceの作成」を参照されたい。
5.16.4.3.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.16.4.3.2. Resourceクラスの作成¶
Note
Resourceクラスを作成する理由
DomainObjectクラス(例えばEntityクラス)があるにも関わらず、Resourceクラスを作成する理由は、 クライアントとの入出力で使用するユーザーインタフェース(UI)上の情報と業務処理で扱う情報は必ずしも一致しないためである。
これらを混同してして使用すると、アプリケーション層の影響がドメイン層におよび、保守性を低下させる原因となる。 DomainObjectとResourceクラスは別々に作成し、Dozer等のBeanMapperを利用してデータ変換を行うことを推奨する。
Resourceクラスの役割は以下の通りである。
項番 役割 説明 
以下に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.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 org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; 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) @JsonSerialize(include = Inclusion.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.16.4.3.3. Controllerクラスの作成¶
package org.terasoluna.examples.rest.api.member; // omitted @RequestMapping("members") // (1) @Controller public class MemberRestController { // omitted ... }
項番 説明 @RequestMappingアノテーションのvalue属性に、リソースのコレクションを表すサーブレットパスを指定する。上記例では、/api/v1/membersというサーブレットパスをマッピングしている。
5.16.4.3.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") @Controller public class MemberRestController { // omitted @Inject MemberService memberService; @Inject Mapper beanMapper; // (3) @RequestMapping(method = RequestMethod.GET) // (4) @ResponseBody // (5) @ResponseStatus(HttpStatus.OK) public Page<MemberResource> getMembers( // (6) @Validated MembersSearchQuery query, // (7) Pageable pageable) { // (8) Page<Member> page = memberService.searchMembers(query.getName(), pageable); // (9) 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()); // (10) return responseResource; } // omitted }
項番 説明 @RequestMappingアノテーションのmethod属性に、RequestMethod.GETを指定する。@org.springframework.web.bind.annotation.ResponseBodyアノテーションを付与する。このアノテーションを付与することで、返却したResourceオブジェクトがJSONやXMLにmarshalされ、レスポンスBODYに設定される。@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 new ResponseEntity(responseResource, HttpStatus.OK); }応答するステータスコードを処理内容や処理結果に応じて変える必要がある場合は、上記実装例の様に、
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" } ], "sort" : [ { "direction" : "DESC", "property" : "lastModifiedAt", "ignoreCase" : false, "ascending" : false } ], "lastPage" : false, "firstPage" : false, "totalElements" : 25, "numberOfElements" : 2, "totalPages" : 13, "size" : 2, "number" : 1 }
- 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"}],"sort":null,"firstPage":true,"lastPage":true,"totalPages":1,"numberOfElements":2,"totalElements":2,"size":2,"number":0}
Tip
ページ検索が不要な場合は、Resourceクラスのリストを直接扱えばよい。
Resourceクラスのリストを直接扱う場合のControllerのメソッドは以下のような定義となる。
@RequestMapping(method = RequestMethod.GET) @ResponseBody @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.16.4.3.5. リソースをコレクションに追加するAPI RESTの実装¶
指定されたMemberリソースを作成し、Memberリソースをコレクションに追加するREST APIの実装例を、以下に示す。
- REST APIの実装指定されたMemberリソースを作成し、Memberリソースをコレクションに追加する処理を実装する。
@RequestMapping("members") @Controller public class MemberRestController { // omitted // (1) @RequestMapping(method = RequestMethod.POST) @ResponseBody // (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.16.4.3.6. 指定されたリソースを取得するREST APIの実装¶
URIで指定されたMemberリソースを取得するREST APIの実装例を、以下に示す。
- REST APIの実装URIで指定されたMemberリソースを取得する処理を実装する。
@RequestMapping("members") @Controller public class MemberRestController { // omitted // (1) @RequestMapping(value = "{memberId}", method = RequestMethod.GET) @ResponseBody // (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.16.4.3.7. 指定されたリソースを更新するREST APIの実装¶
URIで指定されたMemberリソースを更新するREST APIの実装例を、以下に示す。
- REST APIの実装URIで指定されたMemberリソースを更新する処理を実装する。
@RequestMapping("members") @Controller public class MemberRestController { // omitted // (1) @RequestMapping(value = "{memberId}", method = RequestMethod.PUT) @ResponseBody // (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.16.4.3.8. 指定されたリソースを削除するREST APIの実装¶
URIで指定されたMemberリソースを削除するREST APIの実装例を、以下に示す。
- REST APIの実装URIで指定されたMemberリソースを削除する処理を実装する。
@RequestMapping("members") @Controller public class MemberRestController { // omitted // (1) @RequestMapping(value = "{memberId}", method = RequestMethod.DELETE) @ResponseBody // (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.16.4.4. 例外のハンドリングの実装¶
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.16.4.4.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 org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; // (1) public class ApiError implements Serializable { private static final long serialVersionUID = 1L; private final String code; private final String message; @JsonSerialize(include = Inclusion.NON_EMPTY) private final String target; // (2) @JsonSerialize(include = Inclusion.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
フィールドに
@JsonSerialize(include = Inclusion.NON_EMPTY)を指定することで、値がnullや空の場合にJSONに項目が出力されないようにする事が出来る。 項目を出力させないための条件をnullに限定したい場合は、@JsonSerialize(include = Inclusion.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 new ResponseEntity<>(apiError, headers, status); } // omitted }
項番 説明 ResponseEntityExceptionHandlerを継承したクラスを作成し、@ControllerAdviceアノテーションを付与する。ResponseEntityExceptionHandlerのhandleExceptionInternalメソッドをオーバライドする。ExceptionCodeResolverを使用して、例外クラスをエラーコードを変換している。ExceptionCodeResolverの設定例については、「ExceptionCodeResolverを使ったエラーコードとメッセージの解決」を参照されたい。レスポンスBodyに出力するJavaBeanの指定がある場合は、指定されたJavaBeanをそのまま使用する。この処理は、例外毎のエラーハンドリング処理にて、個別にエラー情報が生成される事を考慮した実装となっている。ResponseEntityExceptionHandlerによって適切な値が設定される。設定されるステータスコードについては、「DefaultHandlerExceptionResolverで設定されるHTTPレスポンスコードについて」を参照されたい。
- レスポンス例
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.16.4.4.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.16.4.4.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, null, 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.16.4.4.4. 業務エラー例外のハンドリング実装¶
ビジネスルールの違反を検知した場合に、業務エラーを応答するための実装例について説明する。
ビジネスルールのチェックはServiceの処理として行い、ビジネスルールの違反を検知した場合は、業務例外を発生させる。 業務エラーの検知方法については、「業務エラーを通知する」を参照されたい。
- エラーハンドリングを行うクラスに、業務例外をハンドリングするためのメソッドを作成する。
@ControllerAdvice public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler { // omitted // (1) @ExceptionHandler(BusinessException.class) public ResponseEntity<Object> handleBusinessException(BusinessException ex, WebRequest request) { return handleResultMessagesNotificationException(ex, null, 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.16.4.4.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, null, 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.16.4.4.6. システムエラー例外のハンドリング実装¶
システム異常を検知した場合に、システムエラーを応答するための実装例について説明する。
システム異常の検知した場合は、システム例外を発生させる。 システムエラーの検知方法については、「システムエラーを通知する」を参照されたい。
- エラーハンドリングを行うクラスに、システム例外をハンドリングするためのメソッドを作成する。
@ControllerAdvice public class ApiGlobalExceptionHandler extends ResponseEntityExceptionHandler { // omitted // (1) @ExceptionHandler(Exception.class) public ResponseEntity<Object> handleSystemError(Exception ex, WebRequest request) { return handleExceptionInternal(ex, null, null, 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.16.4.4.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 or equal to {value}. javax.validation.constraints.DecimalMin.message = "{0}" must be greater than 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.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.NotBlank.message = "{0}" may not be empty. org.hibernate.validator.constraints.NotEmpty.message = "{0}" may not be empty. 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. # for common library org.terasoluna.gfw.common.codelist.ExistInCodeList = "{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.16.4.5. サーブレットコンテナに通知されたエラーのハンドリングの実装¶
Filterでエラーが発生した場合やHttpServletResponse#sendErrorを使ってエラーレスポンスが行われた場合は、Spring MVCの例外ハンドリングの仕組みを使ってハンドリングできないため、
これらのエラーはサーブレットコンテナに通知される。
本節では、サーブレットコンテナに通知されたエラーをハンドリングする方法について説明する。
項番 処理レイヤ 説明 web.xmlのerror-pageの定義に従って、エラー処理を行う。致命的なエラーでない場合は、エラーハンドリングを行うControllerを呼び出し、エラー処理を行う。HttpMessageConverterを利用して、エラーオブジェクトをJSON形式の電文に変換する。
5.16.4.5.1. エラー応答を行うためのControllerの実装¶
サーブレットコンテナに通知されたエラーのエラー応答を行うControllerを作成する。
package org.terasoluna.examples.rest.api.common.error; import javax.inject.Inject; import javax.servlet.RequestDispatcher; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.WebRequest; // (1) @RequestMapping("error") @Controller public class ApiErrorPageController { @Inject ApiErrorCreator apiErrorCreator; // (2) // (3) @RequestMapping @ResponseBody public ResponseEntity<ApiError> handleErrorPage( @RequestParam("errorCode") String errorCode, // (4) WebRequest request) { // (5) HttpStatus httpStatus = HttpStatus.valueOf((Integer) request .getAttribute(RequestDispatcher.ERROR_STATUS_CODE, RequestAttributes.SCOPE_REQUEST)); // (6) ApiError apiError = apiErrorCreator.createApiError(request, errorCode, httpStatus.getReasonPhrase()); // (7) return new ResponseEntity<>(apiError, httpStatus); } }
項番 説明 /api/v1/error」というサーブレットパスにマッピングしている。<error-code>)を使ってエラーページのハンドリングを行うケースのみを考慮した実装になっている。したがって、例外タイプ(<exception-type>)を使ってハンドリングしたエラーページの処理を本メソッドを使って行う場合は、別途考慮が必要である。
5.16.4.5.2. 致命的なエラーが発生した際に応答する静的なJSONファイルの作成¶
致命的なエラーが発生した際に応答する静的なJSONファイルを作成する。
- unhandledSystemError.json
{"code":"e.ex.fw.9999","message":"Unhandled system error occurred."}
5.16.4.5.3. サーブレットコンテナに通知されたエラーをハンドリングするための設定¶
ここでは、サーブレットコンテナに通知されたエラーをハンドリングするための設定について説明する。
- web.xml
<!-- omitted --> <!-- (1) --> <error-page> <error-code>404</error-code> <location>/api/v1/error?errorCode=e.ex.fw.5001</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?errorCode=e.ex.fw.5001」というリクエストにマッピングされているController(ApiErrorPageController)を呼び出してエラー応答を行っている。/WEB-INF/views/common/error/unhandledSystemError.json」に定義されている固定のJSONを応答している。charset=UTF-8を指定しないと、クライアント側で文字化けする可能性がある。JSONファイルにマルチバイト文字を含めない場合は、この設定は必須ではないが、設定しておいた方が無難である。
- 存在しないパスへリクエストを送った場合、以下のようなエラー応答が行われる。
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.16.4.6. セキュリティ対策¶
5.16.4.6.2. CSRF対策¶
- RESTful Web Serviceに対してCSRF対策を行う場合の設定方法については、CSRF対策を参照されたい。
- RESTful Web Serviceに対してCSRF対策を行わない場合の設定方法については、CSRF対策の無効化を参照されたい。
5.16.4.8. リソースのキャッシュ制御¶
Todo
TBD
Cache-Control/Expires/Pragmaなどのヘッダを使ったキャッシュ制御の実現方法について、次版以降に記載する予定である。
5.16.5. Appendix¶
5.16.5.1. RESTful Web Serviceとクライアントアプリケーションを同じWebアプリケーションとして動かす際の設定¶
5.16.5.1.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.16.5.2. ハイパーメディアリンクの実装¶
JSONの中に関連リソースへのハイパーメディアリンクを含める場合の実装について説明する。
5.16.5.2.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 org.codehaus.jackson.map.annotate.JsonSerialize; import org.codehaus.jackson.map.annotate.JsonSerialize.Inclusion; // (2) public abstract class AbstractLinksSupportedResource { // (3) @JsonSerialize(include = Inclusion.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); } }
項番 説明 @JsonSerialize(include = Inclusion.NON_EMPTY)を指定している。"self")と、親のリソースにアクセスするためのリンク情報("parent")を追加するためのメソッドを用意している。
5.16.5.2.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") @Controller public class MemberRestController { // omitted @RequestMapping(value = "{memberId}", method = RequestMethod.GET) @ResponseBody @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.16.5.3. HTTPの仕様に準拠したRESTful Web Serviceの作成¶
5.16.5.3.1. POST時のLocationヘッダの設定¶
5.16.5.3.2. リソース毎の実装¶
- REST APIの処理で、作成したリソースのURIをLocationヘッダに設定する。
@RequestMapping("members") @Controller public class MemberRestController { // omitted @RequestMapping(method = RequestMethod.POST) @ResponseBody @ResponseStatus(HttpStatus.CREATED) public HttpEntity<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) HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setLocation(createdUri); // (4) return new HttpEntity<>(responseResource, responseHeaders); } // 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.HttpEntityに、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.16.5.3.3. OPTIONSメソッドのリクエストをControllerにディスパッチするための設定¶
DispatcherServletのデフォルトの設定では、OPTIONSメソッドのリクエストはControllerにディスパッチされずに、DispatcherServletが許可しているメソッドのリストがAllowヘッダに設定されてしまう。- web.xml
<!-- omitted --> <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-rest.xml</param-value> </init-param> <!-- (1) --> <init-param> <param-name>dispatchOptionsRequest</param-name> <param-value>true</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <!-- omitted -->
DispatcherServletの初期化パラメータ(dispatchOptionsRequest)の値を、trueに設定する。
5.16.5.3.4. OPTIONSメソッドの実装¶
- REST APIの実装URIで指定されたリソースでサポートされているHTTPメソッド(REST API)のリストを応答する処理を実装する。
@RequestMapping("members") @Controller public class MembersRestController { // omitted @RequestMapping(value = "{memberId}", method = RequestMethod.OPTIONS) @ResponseBody // (1) @ResponseStatus(HttpStatus.OK) public HttpEntity<Void> optionsMember( @PathVariable("memberId") String memberId) { // (2) memberService.getMember(memberId); // (3) return RestResponseUtils.createEntityOfOptions( HttpMethod.GET, HttpMethod.HEAD, HttpMethod.PUT, HttpMethod.DELETE); } // omitted }
項番 説明 @ResponseStatusアノテーションを付与し、応答するステータスコードを指定する。@ResponseStatusアノテーションのvalue属性には、200(OK)を設定する。
- ユーティリティメソッドURIで指定されたリソースでサポートされているHTTPメソッド(API)のリストを応答する処理は、全てのリソースで同じようなロジックを組む事になるため、ユーティリティメソッド化した方がよい。
public static HttpEntity<Void> createEntityOfOptions( HttpMethod... allowedMethods) { Set<HttpMethod> allowedMethodSet = new LinkedHashSet<>( Arrays.asList(allowedMethods)); if (!allowedMethodSet.contains(HttpMethod.OPTIONS)) { allowedMethodSet.add(HttpMethod.OPTIONS); } HttpHeaders responseHeaders = new HttpHeaders(); responseHeaders.setAllow(allowedMethodSet); return new HttpEntity<>(responseHeaders); }
- リクエスト例
OPTIONS /rest-api-web/api/v1/members/M000000004 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: 6d7bbc818c7f44e7942c54bc0ddc15bb Allow: GET,HEAD,PUT,DELETE,OPTIONS Content-Length: 0 Date: Mon, 17 Mar 2014 01:54:27 GMT
5.16.5.3.5. HEADメソッドの実装¶
- REST APIの実装URIで指定されたリソースのメタ情報を取得する処理を実装する。
@RequestMapping("members") @Controller public class MemberRestController { // omitted @RequestMapping(value = "{memberId}", method = { RequestMethod.GET, RequestMethod.HEAD }) // (1) @ResponseBody @ResponseStatus(HttpStatus.OK) public MemberResource getMember( @PathVariable("memberId") String memberId) { // omitted } // omitted }
項番 説明 @RequestMappingアノテーションのmethod属性にRequestMethod.HEADを追加する。HEADメソッドは、GETメソッドと同じ処理を行いヘッダ情報のみレスポンスする必要があるため、@RequestMappingアノテーションのmethod属性に、RequestMethod.HEADも指定する。レスポンスBODYを空にする処理は、Servlet APIの標準機能で行われるため、Controllerの処理としてはGETメソッドと同じ処理を行えばよい。
- リクエスト例
HEAD /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
- レスポンス例
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Track: 71093a551e624c149867b6bfec486d2c Content-Type: application/json;charset=UTF-8 Content-Length: 452 Date: Thu, 13 Mar 2014 13:25:23 GMT
5.16.5.4. 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/**" auto-config="true" use-expressions="true" create-session="stateless"> <!--<sec:custom-filter ref="csrfFilter" before="LOGOUT_FILTER"/>--> <!-- (2) --> <sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/> <!--<sec:session-management session-authentication-strategy-ref="sessionAuthenticationStrategy" />--> <!-- (3) --> </sec:http> <sec:http auto-config="true" use-expressions="true"> <sec:custom-filter ref="csrfFilter" before="LOGOUT_FILTER"/> <sec:custom-filter ref="userIdMDCPutFilter" after="ANONYMOUS_FILTER"/> <sec:session-management session-authentication-strategy-ref="sessionAuthenticationStrategy" /> </sec:http> <!-- omitted -->
項番 説明 <sec:http>要素のpattern属性に、REST API用のリクエストパスのURLパターンを指定している。上記例では、/api/v1/で始まるリクエストパスをREST API用のリクエストパスとして扱う。また、create-session属性をstatelessとする事で、Spring Securityの処理でセッションが使用されなくなる。
5.16.5.5. 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.16.5.6. 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.16.5.7. アプリケーション層のソースコード¶
以下のファイルは、除外している。
- JavaBean
- 設定ファイル
5.16.5.7.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.stereotype.Controller;
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.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
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")
@Controller
public class MemberRestController {
    @Inject
    MemberService memberService;
    @Inject
    Mapper beanMapper;
    @RequestMapping(method = RequestMethod.GET)
    @ResponseBody
    @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.POST)
    @ResponseBody
    @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)
    @ResponseBody
    @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)
    @ResponseBody
    @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)
    @ResponseBody
    @ResponseStatus(HttpStatus.NO_CONTENT)
    public void deleteMember(@PathVariable("memberId") String memberId) {
        memberService.deleteMember(memberId);
    }
}
5.16.5.7.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.16.5.7.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 new ResponseEntity<>(apiError, headers, status);
    }
    @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, null,
                HttpStatus.NOT_FOUND, request);
    }
    @ExceptionHandler(BusinessException.class)
    public ResponseEntity<Object> handleBusinessException(BusinessException ex,
            WebRequest request) {
        return handleResultMessagesNotificationException(ex, null,
                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, null, HttpStatus.CONFLICT,
                request);
    }
    @ExceptionHandler(Exception.class)
    public ResponseEntity<Object> handleSystemError(Exception ex,
            WebRequest request) {
        return handleExceptionInternal(ex, null, null,
                HttpStatus.INTERNAL_SERVER_ERROR, request);
    }
}
5.16.5.8. REST API実装時に作成したドメイン層のクラスのソースコード¶
以下のファイルは、除外している。
- Entity以外のJavaBean
- Dozer以外の設定ファイル
5.16.5.8.1. Member.java¶
java/org/terasoluna/examples/rest/domain/model/Member.java
package org.terasoluna.examples.rest.domain.model;
import java.io.Serializable;
import javax.persistence.Access;
import javax.persistence.AccessType;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import javax.persistence.Version;
import org.joda.time.DateTime;
import org.joda.time.LocalDate;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
@Table(name = "t_member")
@Entity
public class Member implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    private String memberId;
    private String firstName;
    private String lastName;
    @Transient
    private Gender gender;
    private LocalDate dateOfBirth;
    private String emailAddress;
    private String telephoneNumber;
    private String zipCode;
    private String address;
    @CreatedDate
    private DateTime createdAt;
    @LastModifiedDate
    private DateTime lastModifiedAt;
    @Version
    private long version;
    @OneToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "member_id")
    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;
    }
    @Access(AccessType.PROPERTY)
    @Column(name = "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.16.5.8.2. MemberCredentia.java¶
java/org/terasoluna/examples/rest/domain/model/MemberCredential.java
package org.terasoluna.examples.rest.domain.model;
import java.io.Serializable;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import javax.persistence.Version;
import org.joda.time.DateTime;
import org.springframework.data.annotation.LastModifiedDate;
@Table(name = "t_member_credential")
@Entity
public class MemberCredential implements Serializable {
    private static final long serialVersionUID = 1L;
    @Id
    private String memberId;
    private String signId;
    private String password;
    private String previousPassword;
    private DateTime passwordLastChangedAt;
    @LastModifiedDate
    private DateTime lastModifiedAt;
    @Version
    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.16.5.8.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.16.5.8.4. MemberRepository.java¶
java/org/terasoluna/examples/rest/domain/repository/member/MemberRepository.java
package org.terasoluna.examples.rest.domain.repository.member;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.terasoluna.examples.rest.domain.model.Member;
public interface MemberRepository extends JpaRepository<Member, String> {
    @Query("SELECT m FROM Member m"
            + " WHERE m.firstName LIKE :name% ESCAPE '~'"
            + " OR m.lastName LIKE :name% ESCAPE '~'")
    Page<Member> findPageByContainsName(@Param("name") String name,
            Pageable pageable);
}
5.16.5.8.5. MemberService.java¶
java/org/terasoluna/examples/rest/domain/service/member/MemberService.java
package org.terasoluna.examples.rest.domain.service.member;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.terasoluna.examples.rest.domain.model.Member;
public interface MemberService {
    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.16.5.8.6. MemberServiceImpl.java¶
java/org/terasoluna/examples/rest/domain/service/member/MemberServiceImpl.java
package org.terasoluna.examples.rest.domain.service.member;
import javax.inject.Inject;
import javax.inject.Named;
import org.dozer.Mapper;
import org.joda.time.DateTime;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
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.DateFactory;
import org.terasoluna.gfw.common.exception.BusinessException;
import org.terasoluna.gfw.common.exception.ResourceNotFoundException;
import org.terasoluna.gfw.common.message.ResultMessages;
import org.terasoluna.gfw.common.query.QueryEscapeUtils;
import org.terasoluna.gfw.common.sequencer.Sequencer;
@Transactional
@Service
public class MemberServiceImpl implements MemberService {
    @Inject
    MemberRepository memberRepository;
    @Inject
    @Named("memberIdSequencer")
    Sequencer<String> sequencer;
    @Inject
    DateFactory dateFactory;
    @Inject
    PasswordEncoder passwordEncoder;
    @Inject
    Mapper beanMapper;
    @Transactional(readOnly = true)
    public Page<Member> searchMembers(String name, Pageable pageable) {
        // escape to like condition value
        String escapedName = QueryEscapeUtils.toLikeCondition(name);
        // find members that matches with search criteria
        return memberRepository.findPageByContainsName(escapedName, pageable);
    }
    @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;
    }
    public Member createMember(Member creatingMember) {
        MemberCredential creatingCredential = creatingMember.getCredential();
        // get processing current date time
        DateTime currentDateTime = dateFactory.newDateTime();
        // set id
        String newMemberId = sequencer.getNext();
        creatingMember.setMemberId(newMemberId);
        creatingCredential.setMemberId(newMemberId);
        // 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);
        // save member & member credential
        try {
            return memberRepository.saveAndFlush(creatingMember);
        } catch (DataIntegrityViolationException e) {
            // If sign id is already used
            throw new BusinessException(ResultMessages.error().add(
                    DomainMessageCodes.E_EX_MM_8001,
                    creatingCredential.getSignId()), e);
        }
    }
    public Member updateMember(String memberId, Member updatingMember) {
        // get member
        Member member = getMember(memberId);
        // override updating member attributes
        beanMapper.map(updatingMember, member, "member.update");
        // save updating member
        return memberRepository.save(member);
    }
    public void deleteMember(String memberId) {
        // delete member
        memberRepository.delete(memberId);
    }
}
5.16.5.8.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.16.5.8.8. 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>









