4.13. Ajax¶
4.13.1. Overview¶
本章では、Ajaxを利用するアプリケーションの実装方法について説明する。
Todo
TBD
クライアント側の実装方法などについては、次版以降で詳細化する予定である。
Ajaxとは、以下の処理を非同期に行うための技術の総称である。
- ブラウザ上で行われる画面操作
- 画面操作をトリガーとしたサーバへのHTTP通信、及び通信結果のユーザインタフェースへの反映
4.13.2. How to use¶
4.13.2.1. アプリケーションの設定¶
Ajax向けのアプリケーションの設定について説明する。
Warning
StAX(Streaming API for XML)使用時のDOS攻撃対策について
XML形式のデータをStAXを使用して解析する場合は、DTDを使ったDOS攻撃を受けないように対応する必要がある。 詳細は、CVE-2015-3192 - DoS Attack with XML Inputを参照されたい。
4.13.2.1.1. Spring MVCのAjax関連の機能を有効化するための設定¶
Ajax通信時で使用されるContent-Type("application/xml"
や "application/json"
など)を、Controllerのハンドラメソッドでハンドリングできるようにする。
spring-mvc.xml
<mvc:annotation-driven /> <!-- (1) -->
項番 説明 (1)<mvc:annotation-driven>
要素が指定されていると、Ajax通信時で必要となる機能が有効化されている。そのため、Ajax通信用に特別な設定を行う必要はない。Note
Ajax通信時で必要となる機能とは、具体的には
org.springframework.http.converter.HttpMessageConverter
クラスで提供される機能の事をさす。
HttpMessageConverter
は、以下の役割をもつ。
- リクエストBodyに格納されているデータからJavaオブジェクトを生成する。
- JavaオブジェクトからレスポンスBodyに書き込むデータを生成する。
<mvc:annotation-driven>
指定時にデフォルトで有効化される HttpMessageConverter
は以下の通りである。
項番 クラス名 対象フォーマット 説明
org.springframework.http.converter.json.MappingJackson2HttpMessageConverter JSON リクエストBody又はレスポンスBodyとしてJSONを扱うためのHttpMessageConverter
。ブランクプロジェクトでは、 Jackson を同封しているため、デフォルトの状態で使用することができる。
org.springframework.http.converter.xml.Jaxb2RootElementHttpMessageConverter XML リクエストBody又はレスポンスBodyとしてXMLを扱うためのHttpMessageConverter
。JavaSE6からJAXB2.0が標準で同封されているため、デフォルトの状態で使用することができる。Note
jackson version 1.x.x から jackson version 2.x.xへ変更する場合の注意点 はこちらを参照されたい。
Warning
XXE(XML External Entity) Injection 対策について
Ajax通信でXML形式のデータを扱う場合は、XXE(XML External Entity) Injection対策を行う必要がある。 terasoluna-gfw-web 1.0.1.RELEASE以上では、XXE Injection 対策が行われているSpring MVC(3.2.10.RELEASE以上)に依存しているため、個別に対策を行う必要はない。
terasoluna-gfw-web 1.0.0.RELEASEを使用している場合は、XXE Injection対策が行われていないSpring MVC(3.2.4.RELEASE)に依存しているため、Spring-oxmから提供されているクラスを使用すること。
spring-mvc.xml
<!-- (1) --> <bean id="xmlMarshaller" class="org.springframework.oxm.jaxb.Jaxb2Marshaller"> <property name="packagesToScan" value="com.examples.app" /> <!-- (2) --> </bean> <!-- ... --> <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> <!-- ... --> </mvc:annotation-driven> <!-- ... -->
項番 説明 (1) Spring-oxmから提供されているJaxb2Marshaller
のbean定義を行う。Jaxb2Marshaller
はデフォルトの状態で XXE Injection対策が行われている。 (2)packagesToScan
プロパティに JAXB用のJavaBean(javax.xml.bind.annotation.XmlRootElement
アノテーションなどが付与されているJavaBean)が格納されているパッケージ名を指定する。指定したパッケージ配下に格納されているJAXB用のJavaBeanがスキャンされ、marshal、unmarshal対象のJavaBeanとして登録される。<context:component-scan>
の base-package属性と同じ仕組みでスキャンされる。 (3)<mvc:annotation-driven>
の子要素である<mvc:message-converters>
要素に、MarshallingHttpMessageConverter
のbean定義を追加する。 (4)marshaller
プロパティに (1)で定義したJaxb2Marshaller
のbeanを指定する。 (5)unmarshaller
プロパティに (1)で定義したJaxb2Marshaller
のbeanを指定する。Spring-oxmを依存するアーティファクトとして追加する。
pom.xml
<!-- omitted --> <!-- (1) --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-oxm</artifactId> <version>${org.springframework-version}</version> <!-- (2) --> </dependency> <!-- omitted -->
項番 説明 (1) Spring-oxm を依存アーティファクトとして追加する。 (2) Springのバージョンは、terasoluna-gfw-parent のpom.xml
に定義されているSpringのバージョン番号を管理するためのプレースフォルダ(${org.springframework-version})から取得すること。
4.13.2.2. Controllerの実装¶
以降で説明するサンプルコードの前提は以下の通りである。
- 応答データの形式にはJSONを使用する。
- クライアント側には、JQueryを使用する。バージョンは執筆時点の1.x系の最新バージョン(1.10.2)を使用する。
Warning
循環参照への対策
HttpMessageConverter
を使用してJavaBeanをJSONやXML形式にシリアライズする際に、
相互参照関係のオブジェクトをプロパティに保持していると、
循環参照となりStackOverflowError
やOutOfMemoryError
などが発生するので、注意が必要である。
循環参照を回避するためには、
- Jacksonを使用してJSON形式にシリアライズする場合は、シリアライズ対象から除外するプロパティに
@com.fasterxml.jackson.annotation.JsonIgnore
アノテーション - JAXBを使用してXML形式にシリアライズする場合は、シリアライズ対象から除外するプロパティに
javax.xml.bind.annotation.XmlTransient
アノテーション
を付与すればよい。
以下にJacksonを使用してJSON形式にシリアライズする際の回避例を示す。
public class Order { private String orderId; private List<OrderLine> orderLines; // ... }public class OrderLine { @JsonIgnore private Order order; private String itemCode; private int quantity; // ... }
項番 説明 (1)シリアライズ対象から除外するプロパティに対して @JsonIgnore
アノテーションを付与する。
4.13.2.2.1. データを取得する¶
Ajaxを使ってデータを取得する方法について説明する。
下記例は、検索ワードに一致する情報を一覧として返却するAjax通信となっている。
- リクエストデータを受け取るためのJavaBean
// (1) public class SearchCriteria implements Serializable { // omitted private String freeWord; // (2) // omitted setter/getter }
項番 説明 (1) リクエストデータを受け取るためのJavaBeanを作成する。 (2) プロパティ名は、リクエストパラメータのパラメータ名と一致させる。
- 返却するデータを格納するJavaBean
// (3) public class SearchResult implements Serializable { // omitted private List<XxxEntity> list; // omitted setter/getter }
項番 説明 (3) 返却するデータを格納するためのJavaBeanを作成する。
- Controller
@RequestMapping(value = "search", method = RequestMethod.GET) // (4) @ResponseBody // (5) public SearchResult search(@Validated SearchCriteria criteria) { // (6) SearchResult searchResult = new SearchResult(); // (7) // (8) // omitted return searchResult; // (9) }
項番 説明 (4)@RequestMapping
アノテーションの method属性にRequestMethod.GET
を指定する。 (5)@org.springframework.web.bind.annotation.ResponseBody
アノテーションを付与する。このアノテーションを付与することで、返却したオブジェクトがJSON形式にmarshalされ、レスポンスBodyに設定される。 (6) リクエストデータを受け取るためのJavaBeanを引数に指定する。入力チェックが必要な場合は、@Validated
を指定する。入力チェックのエラーハンドリングについては、「 入力エラーのハンドリング 」を参照されたい。入力チェックの詳細については、「 入力チェック 」を参照されたい。 (7) 返却するデータを格納するJavaBeanのオブジェクトを生成する。 (8) データを検索し、(7)で生成したオブジェクトに検索結果を格納する。上記例では、実装は省略している。 (9) レスポンスBodyにmarshalするためのオブジェクトを返却する。
- HTML(JSP)
<!-- omitted --> <meta name="contextPath" content="${pageContext.request.contextPath}" /> <!-- omitted --> <!-- (10) --> <form id="searchForm"> <input name="freeWord" type="text"> <button onclick="return searchByFreeWord()">Search</button> </form>
項番 説明 (10) 検索条件を入力するためのフォーム。上記例では、検索条件を入力するためのテキストボックスと検索ボタンをもっている。<!-- (11) --> <script type="text/javascript" src="${pageContext.request.contextPath}/resources/vendor/jquery/jquery-1.10.2.js"> </script>
項番 説明 (11) JQueryのJavaScriptファイルを読み込む。上記例では、JQueryのJavaScriptファイルを読み込むために、/resources/vendor/jquery/jquery-1.10.2.js
というパスに対してリクエストが送信される。Note
JQueryのJavaScriptファイルを読み込みための設定は、以下の通り。 以下はブランクプロジェクトで提供されている設定値である。
spring-mvc.xml
<!-- (12) --> <mvc:resources mapping="/resources/**" location="/resources/,classpath:META-INF/resources/" cache-period="#{60 * 60}" />
項番 説明 (12) リソースファイル(JavaScriptファイル, Stylesheetファイル, 画像ファイルなど)を公開するための設定。上記設定例では、/resources/
から始まるパスに対してリクエストがあった場合に、warファイル内の/resources/
ディレクトリ又はクラスパス内の/META-INF/resources/
ディレクトリに格納されているファイルが応答される。上記設定の場合、JQueryのJavaScriptファイルは以下の何れかのパスに配置する必要がある。
warファイル内の/resources/vendor/jquery/jquery-1.10.2.js
プロジェクト内のパスで表現すると、src/main/webapp/resources/vendor/jquery/jquery-1.10.2.js
となる。 クラスパス内の/META-INF/resources/vendor/jquery/jquery-1.10.2.js
プロジェクト内のパスで表現すると、src/main/resources/META-INF/resources/vendor/jquery/jquery-1.10.2.js
となる。
- JavaScript
var contextPath = $("meta[name='contextPath']").attr("content"); // (13) function searchByFreeWord() { $.ajax(contextPath + "/ajax/search", { type : "GET", data : $("#searchForm").serialize(), dataType : "json", // (14) }).done(function(json) { // (15) // render search result // omitted }).fail(function(xhr) { // (16) // render error message // omitted }); return false; }
項番 説明 (13) フォームに指定された検索条件をリクエストパラメータに変換し、GETメソッドで/ajax/search
に対してリクエストを送信するAjax関数。上記例では、ボタンの押下をAjax通信のトリガーとしているが、テキストボックスのキーダウンやキーアップをトリガーとすることでリアルタイム検索などを実現することができる。 (14) レスポンスとして受け取るデータ形式を指定する。上記例では"json"
を指定しているため、Acceptヘッダーに"application/json"
が設定される。 (15) Ajax通信が正常終了した時(Httpステータスコードが"200"
の時)の処理を実装する。上記例では、実装は省略している。 (16) Ajax通信が正常終了しなかった時(Httpステータスコードが"4xx"
や"5xx"
の時)の処理を実装する。上記例では、実装は省略している。エラー処理の実装例は、 フォームデータをPOSTする を参照されたい。Tip
上記例ではWebアプリケーションのコンテキストパス(
${pageContext.request.contextPath}
) をHTMLの<meta>
要素に設定しておくことで、 JavaScriptのコードからJSPのコードを排除している。
- リクエストデータ
GET /terasoluna-gfw-web-blank/ajax/search?freeWord= HTTP/1.1 Host: localhost:9999 Connection: keep-alive Accept: application/json, text/javascript, */*; q=0.01 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36 Referer: http://localhost:9999/terasoluna-gfw-web-blank/ajax/xxe Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8,ja;q=0.6 Cookie: JSESSIONID=3A486604D7DEE62032BA6C073FC6BE9F
- レスポンスデータ
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Track: a8fb8fefaaf64ee2bffc2b0f77050226 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 25 Oct 2013 13:52:55 GMT {"list":[]}
4.13.2.2.2. フォームデータをPOSTする¶
Ajaxを使ってフォームのデータをPOSTし、処理結果を取得する方法について説明する。
下記例は、2つの数値を受け取り、加算結果を返却するAjax通信となっている。
- フォームデータを受け取るためのJavaBean
// (1) public class CalculationParameters implements Serializable { // omitted private Integer number1; private Integer number2; // omitted setter/getter }
項番 説明 (1) フォームデータを受け取るためのJavaBeanを作成する。
- 処理結果を格納するJavaBean
// (2) public class CalculationResult implements Serializable { // omitted private int resultNumber; // omitted setter/getter }
項番 説明 (2) 処理結果を格納するためのJavaBeanを作成する。
- Controller
@RequestMapping("xxx") @Controller public class XxxController { @RequestMapping(value = "plusForForm", method = RequestMethod.POST) // (3) @ResponseBody public CalculationResult plusForForm( @Validated CalculationParameters params) { // (4) CalculationResult result = new CalculationResult(); int sum = params.getNumber1() + params.getNumber2(); result.setResultNumber(sum); // (5) return result; // (6) } // omitted }
項番 説明 (3)@RequestMapping
アノテーションの method属性にRequestMethod.POST
を指定する。 (4) フォームデータを受け取るためのJavaBeanを引数に指定する。入力チェックが必要な場合は、@Validated
を指定する。入力チェックのエラーハンドリングについては、「 入力エラーのハンドリング 」を参照されたい。入力チェックの詳細については、「 入力チェック 」を参照されたい。 (5) 処理結果を格納するオブジェクトに処理結果を格納する。上記例では、フォームオブジェクトから取得した2つの数値を加算した結果を格納している。 (6) レスポンスBodyにmarshalするためのオブジェクトを返却する。
- HTML(JSP)
<!-- omitted --> <meta name="contextPath" content="${pageContext.request.contextPath}" /> <sec:csrfMetaTags /> <!-- omitted --> <!-- (7) --> <form id="calculationForm"> <input name="number1" type="text">+ <input name="number2" type="text"> <button onclick="return plus()">=</button> <span id="calculationResult"></span> <!-- (8) --> </form>
項番 説明 (7) 計算対象の数値を入力するためのフォーム。 (8) 計算結果を表示するための領域。上記例では、通信成功時には計算結果が表示され、通信失敗時には計算結果がクリアされる。
- JavaScript
var contextPath = $("meta[name='contextPath']").attr("content"); // (9) var csrfToken = $("meta[name='_csrf']").attr("content"); var csrfHeaderName = $("meta[name='_csrf_header']").attr("content"); $(document).ajaxSend(function(event, xhr, options) { xhr.setRequestHeader(csrfHeaderName, csrfToken); }); // (10) function plus() { $.ajax(contextPath + "/ajax/plusForForm", { type : "POST", data : $("#calculationForm").serialize(), dataType : "json" }).done(function(json) { $("#calculationResult").text(json.resultNumber); }).fail(function(xhr) { // (11) var messages = ""; // (12) if(400 <= xhr.status && xhr.status <= 499){ // (13) var contentType = xhr.getResponseHeader('Content-Type'); if (contentType != null && contentType.indexOf("json") != -1) { // (14) json = $.parseJSON(xhr.responseText); $(json.errorResults).each(function(i, errorResult) { messages += ("<div>" + errorResult.message + "</div>"); }); } else { // (15) messages = ("<div>" + xhr.statusText + "</div>"); } }else{ // (16) messages = ("<div>" + "System error occurred." + "</div>"); } // (17) $("#calculationResult").html(messages); }); return false; }
項番 説明 (9) POSTメソッドでリクエストを行う場合、CSRFトークンをHTTPヘッダに設定して送信する必要がある。上記例では、<sec:csrfMetaTags />
を利用して<meta>
要素にCSRFトークンヘッダー名とCSRFトークン値を設定し、JavaScriptで値を取得するようにしている。CSRF対策の詳細については、 「 CSRF対策 」を参照されたい。 (10) フォームに指定された数値をリクエストパラメータに変換し、POSTメソッドで/ajax/plusForForm
に対してリクエストを送信するAjax関数。上記例では、ボタンの押下をAjax通信のトリガーとしているが、テキストボックスのロストフォーカスをトリガーとすることでリアルタイム計算を実現することができる。 (11) エラー処理の実装例を以下に示す。サーバ側のエラーハンドリング処理の実装例については、 入力エラーのハンドリング を参照されたい。 (12) HTTPのステータスコードを判定し、どのようなエラーが発生したか判定する。HTTPのステータスコードは、 XMLHttpRequestオブジェクトのstatus
フィールドに格納されている。 (13) レスポンスされたデータがJSON形式か判定を行う。上記例では、レスポンスヘッダの Content-Typeに設定されている値を参照して、レスポンスされたデータの形式をチェックしている。形式をチェックしておかないと、JSON以外の形式で応答された際に、JSONオブジェクトにデシリアライズする処理でエラーが発生することになる。サーバ側のエラーハンドリングを簡易的に行っていると、HTML形式のページが返却されることがある。 (14) レスポンスデータをJSONオブジェクトにデシリアライズする。レスポンスデータは、 XMLHttpRequestオブジェクトのresponseText
フィールドに格納されている。上記例では、デシリアライズしたJSONオブジェクトからエラー情報を取得し、エラーメッセージを組み立てている。 (15) レスポンスされたデータがJSON形式以外だった場合の処理を行う。上記例では、HTTPのステータステキストをエラーメッセージに格納している。HTTPのステータステキストは、 XMLHttpRequestオブジェクトのstatusText
フィールドに格納されている。 (16) サーバエラー時の処理を行う。上記例では、システムエラーが発生したことを通知するメッセージをエラーメッセージに格納している。 (17) エラー時の描画処理を行う。上記例では、計算結果を表示するための領域に、エラーメッセージを表示している。Warning
上記例では、Ajaxの通信処理、DOM操作処理(描画処理)、エラー処理を同じfunction内で行っているが、これらの処理は分離して実装することを推奨する。
Todo
TBD
クライアント側の実装方法については、次版以降で詳細化する予定である。
Tip
上記例では
<sec:csrfMetaTags />
を利用して、CSRFトークン値とCSRFトークンヘッダー名をHTMLの<meta>
要素に設定しておくことで、 JavaScriptのコードからJSPのコードを排除している。Ajax使用時の連携を参照されたい。尚、CSRFトークン値とCSRFトークンヘッダー名はそれぞれ
${_csrf.token}
と${_csrf.headerName}
を用いても取得可能である。
- リクエストデータ
POST /terasoluna-gfw-web-blank/ajax/plusForForm HTTP/1.1 Host: localhost:9999 Connection: keep-alive Content-Length: 19 Accept: application/json, text/javascript, */*; q=0.01 Origin: http://localhost:9999 X-CSRF-TOKEN: a5dd1858-8a4f-4ecc-88bd-a326388ab5c9 X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36 Content-Type: application/x-www-form-urlencoded; charset=UTF-8 Referer: http://localhost:9999/terasoluna-gfw-web-blank/ajax/xxe Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8,ja;q=0.6 Cookie: JSESSIONID=3A486604D7DEE62032BA6C073FC6BE9F number1=1&number2=2
- レスポンスデータ
HTTP/1.1 200 OK Server: Apache-Coyote/1.1 X-Track: c2d5066d0fa946f584536775f07d1900 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Fri, 25 Oct 2013 14:27:55 GMT {"resultNumber":3}
- エラー時のレスポンスデータ 下記のレスポンスデータは、入力エラーが発生時のものである。
HTTP/1.1 400 Bad Request Server: Apache-Coyote/1.1 X-Track: cecd7b4d746249178643b7110b0eaa74 Content-Type: application/json;charset=UTF-8 Transfer-Encoding: chunked Date: Wed, 04 Dec 2013 15:06:01 GMT Connection: close {"errorResults":[{"code":"NotNull","message":"\"number2\"maynotbenull.","itemPath":"number2"},{"code":"NotNull","message":"\"number1\"maynotbenull.","itemPath":"number1"}]}
4.13.2.2.3. フォームデータをJSONとしてPOSTする¶
Ajaxを使ってフォームのデータをJSON形式に変換してからPOSTし、処理結果を取得する方法について説明する。
「フォームデータをPOSTする」方法との差分部分について説明する。
- Controller
@RequestMapping("xxx") @Controller public class XxxController { @RequestMapping(value = "plusForJson", method = RequestMethod.POST) @ResponseBody public CalculationResult plusForJson( @Validated @RequestBody CalculationParameters params) { // (1) CalculationResult result = new CalculationResult(); int sum = params.getNumber1() + params.getNumber2(); result.setResultNumber(sum); return result; } // omitted }
項番 説明 (1) フォームデータを受け取るためのJavaBeanの引数アノテーションとして、@org.springframework.web.bind.annotation.RequestBody
アノテーションを付与する。このアノテーションを付与することで、リクエストBodyに格納されているJSON形式のデータがunmarshalされ、オブジェクトに変換される。入力チェックが必要な場合は、@Validated
を指定する。入力チェックのエラーハンドリングについては、「 入力エラーのハンドリング 」を参照されたい。入力チェックの詳細については、「 入力チェック 」を参照されたい。
- JavaScript/HTML(JSP)
// (2) function toJson($form) { var data = {}; $($form.serializeArray()).each(function(i, v) { data[v.name] = v.value; }); return JSON.stringify(data); } function plus() { $.ajax(contextPath + "/ajax/plusForJson", { type : "POST", contentType : "application/json;charset=utf-8", // (3) data : toJson($("#calculationForm")), // (2) dataType : "json", beforeSend : function(xhr) { xhr.setRequestHeader(csrfHeaderName, csrfToken); } }).done(function(json) { $("#calculationResult").text(json.resultNumber); }).fail(function(xhr) { $("#calculationResult").text(""); }); return false; }
項番 説明 (2) フォーム内のinput項目をJSON形式の文字列にするための関数。 (3) リクエストBodyにJSONを格納するので、Content-Typeのメディアタイプを"application/json"
にする。
- リクエストデータ
POST /terasoluna-gfw-web-blank/ajax/plusForJson HTTP/1.1 Host: localhost:9999 Connection: keep-alive Content-Length: 31 Accept: application/json, text/javascript, */*; q=0.01 Origin: http://localhost:9999 X-CSRF-TOKEN: 9d4f1e0c-c500-43f3-9125-a7a131ff88fa X-Requested-With: XMLHttpRequest User-Agent: Mozilla/5.0 (Windows NT 6.1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/30.0.1599.101 Safari/537.36 Content-Type: application/json;charset=UTF-8 Referer: http://localhost:9999/terasoluna-gfw-web-blank/ajax/xxe? Accept-Encoding: gzip,deflate,sdch Accept-Language: en-US,en;q=0.8,ja;q=0.6 Cookie: JSESSIONID=CECD7A6CB0431266B8D1173CCFA66B95 {"number1":"34","number2":"56"}
4.13.2.3. 入力エラーのハンドリング¶
入力値に不正な値が指定された場合のエラーハンドリング方法について説明する。
入力エラーのハンドリング方法は、大きく分けて以下の2つに分類される。
- 例外ハンドリング用のメソッドを用意してエラー処理を行う。
- Controllerのハンドラメソッドの引数として
org.springframework.validation.BindingResult
を受け取り、エラー処理を行う。
4.13.2.3.1. BindException のハンドリング¶
org.springframework.validation.BindException
は、 リクエストパラメータとして送信したデータをJavaBeanにバインドする際に、入力値に不正な値が指定された場合に発生する例外クラスである。"application/x-www-form-urlencoded"
の形式として受け取る場合は、 BindException
の例外ハンドリングが必要となる。- Controller
@RequestMapping("xxx") @Controller public class XxxController { // omitted @ExceptionHandler(BindException.class) // (1) @ResponseStatus(value = HttpStatus.BAD_REQUEST) // (2) @ResponseBody // (3) public ErrorResults handleBindException(BindException e, Locale locale) { // (4) // (5) ErrorResults errorResults = new ErrorResults(); for (FieldError fieldError : e.getBindingResult().getFieldErrors()) { errorResults.add(fieldError.getCode(), messageSource.getMessage(fieldError, locale), fieldError.getField()); } for (ObjectError objectError : e.getBindingResult().getGlobalErrors()) { errorResults.add(objectError.getCode(), messageSource.getMessage(objectError, locale), objectError.getObjectName()); } return errorResults; } // omitted }
項番 説明 (1) Controllerにエラーハンドリング用メソッドを定義する。エラーハンドリング用のメソッドには、@org.springframework.web.bind.annotation.ExceptionHandler
アノテーションを付与し、 value属性にハンドリングする例外の型を指定する。上記例では、 ハンドリング対象の例外としてBindException.class
を指定している。 (2) 応答するHTTPステータス情報を指定する。上記例では、400
(Bad Request) を指定している。 (3) 返却したオブジェクトをレスポンスBodyに書き込むため、@ResponseBody
アノテーションを付与する。 (4) エラーハンドリング用のメソッドの引数として、ハンドリング対象の例外クラスを宣言する。 (5) エラー処理を実装する。上記例では、エラー情報を返却するためのJavaBeanを生成し、返却している。Tip
エラー処理としてメッセージを生成する際に国際化を意識する必要がある場合は、
Locale
オブジェクトを引数として受け取ることができる。
- エラー情報を保持するJavaBean
// (6) public class ErrorResult implements Serializable { private static final long serialVersionUID = 1L; private String code; private String message; private String itemPath; public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } public String getItemPath() { return itemPath; } public void setItemPath(String itemPath) { this.itemPath = itemPath; } }// (7) public class ErrorResults implements Serializable { private static final long serialVersionUID = 1L; private List<ErrorResult> errorResults = new ArrayList<ErrorResult>(); public List<ErrorResult> getErrorResults() { return errorResults; } public void setErrorResults(List<ErrorResult> errorResults) { this.errorResults = errorResults; } public ErrorResults add(String code, String message) { ErrorResult errorResult = new ErrorResult(); errorResult.setCode(code); errorResult.setMessage(message); errorResults.add(errorResult); return this; } public ErrorResults add(String code, String message, String itemPath) { ErrorResult errorResult = new ErrorResult(); errorResult.setCode(code); errorResult.setMessage(message); errorResult.setItemPath(itemPath); errorResults.add(errorResult); return this; } }
項番 説明 (6) エラー情報を1件保持するためのJavaBean。 (7) エラー情報を1件保持するJavaBeanを複数件保持するためのJavaBean。(6)のJavaBeanをリストとして保持している。
4.13.2.3.2. MethodArgumentNotValidException のハンドリング¶
org.springframework.web.bind.MethodArgumentNotValidException
は、 @RequestBody
アノテーションを使用してリクエストBodyに格納されているデータをJavaBeanにバインドする際に、入力値に不正な値が指定された場合に発生する例外クラスである。"application/json"
や "application/xml"
などの形式として受け取る場合は、 MethodArgumentNotValidException
の例外ハンドリングが必要となる。- Controller
@ExceptionHandler(MethodArgumentNotValidException.class) // (1) @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ResponseBody public ErrorResults handleMethodArgumentNotValidException( MethodArgumentNotValidException e, Locale locale) { // (1) ErrorResults errorResults = new ErrorResults(); // implement error handling. // omitted return errorResults; }
項番 説明 (1) エラーハンドリング対象の例外としてMethodArgumentNotValidException.class
を指定する。上記以外はBindException
と同様。
4.13.2.3.3. HttpMessageNotReadableException のハンドリング¶
org.springframework.http.converter.HttpMessageNotReadableException
は、 @RequestBody
アノテーションを使用してリクエストBodyに格納されているデータをJavaBeanにバインドする際に、Bodyに格納されているデータからJavaBeanを生成できなかった場合に発生する例外クラスである。"application/json"
や "application/xml"
などの形式として受け取る場合は、 MethodArgumentNotValidException
の例外ハンドリングが必要となる。Note
具体的なエラー原因は、使用する
HttpMessageConverter
や利用するライブラリの実装によって異なる。JSON形式のデータをJacksonを使ってJavaBeanに変換する
MappingJackson2HttpMessageConverter
の実装では、Integer項目に数値以外の文字列を指定すると、HttpMessageNotReadableException
が発生する。
- Controller
@ExceptionHandler(HttpMessageNotReadableException.class) // (1) @ResponseStatus(value = HttpStatus.BAD_REQUEST) @ResponseBody public ErrorResults handleHttpMessageNotReadableException( HttpMessageNotReadableException e, Locale locale) { // (1) ErrorResults errorResults = new ErrorResults(); // implement error handling. // omitted return errorResults; }
項番 説明 (1) エラーハンドリング対象の例外としてHttpMessageNotReadableException.class
を指定する。上記以外はBindException
と同様。
4.13.2.3.4. BindingResult を使用したハンドリング¶
BindingResult
をハンドラメソッドの引数として受け取ることでエラーハンドリングすることができる。BindingResult
を指定しない場合は、前述した例外をハンドリングする方法でエラー処理を実装する必要がある。- Controller
@RequestMapping(value = "plus", method = RequestMethod.POST) @ResponseBody public CalculationResult plus( @Validated @RequestBody CalculationParameters params, BindingResult bResult) { // (1) CalculationResult result = new CalculationResult(); if (bResult.hasErrors()) { // (2) // (3) // implement error handling. // omitted return result; // (4) } int sum = params.getNumber1() + params.getNumber2(); result.setResultNumber(sum); return result; }
項番 説明 (1) ハンドラメソッドの引数としてBindingResult
を宣言する。BindingResult
は入力チェック対象のJavaBeanの直後に宣言する必要がある。 (2) 入力値のエラー有無を判定する。 (3) 入力値にエラーがある場合は、入力エラー時のエラー処理を行う。上記例ではエラー処理は省略しているが、エラーメッセージの設定などが行われる想定である。 (4) 処理結果を返却する。Note
上記例では、正常時及びエラー時共にレスポンスのHTTPステータスコードは
200
(OK) が返却される。 HTTPステータスコードを処理結果によってわける必要がある場合は、org.springframework.http.ResponseEntity
を返却値とすることで実現可能である。 別のアプローチとしては、ハンドラメソッドの引数としてBindingResult
を指定せず、前述した例外をハンドリングする方法でエラー処理を実装する方法がある。@RequestMapping(value = "plus", method = RequestMethod.POST) @ResponseBody public ResponseEntity<CalculationResult> plus( @Validated @RequestBody CalculationParameters params, BindingResult bResult) { CalculationResult result = new CalculationResult(); if (bResult.hasErrors()) { // implement error handling. // omitted // (1) return ResponseEntity.badRequest().body(result); } // omitted // (2) return ResponseEntity.ok().body(result); }
項番 説明 (1) 入力エラー時の応答データとHTTPステータスを返却する。 (2) 正常終了時の応答データとHTTPステータスを返却する。
4.13.2.4. 業務エラーのハンドリング¶
業務エラーのエラーハンドリング方法について説明する。
業務エラーのハンドリング方法は大きく分けて以下の2つに分類される。
- 業務例外ハンドリング用のメソッドを用意してエラー処理を行う。
- Controllerのハンドラメソッド内で業務例外をcatchしてエラー処理を行う。
4.13.2.4.1. 例外ハンドリング用のメソッドで業務例外をハンドリング¶
- Controller
@ExceptionHandler(BusinessException.class) // (1) @ResponseStatus(value = HttpStatus.CONFLICT) // (2) @ResponseBody public ErrorResults handleHttpBusinessException(BusinessException e, // (1) Locale locale) { ErrorResults errorResults = new ErrorResults(); // implement error handling. // omitted return errorResults; }
項番 説明 (1) エラーハンドリング対象の例外としてBusinessException.class
を指定する。上記以外は入力エラーのBindException
のハンドリング方法と同様。 (2) 応答するHTTPステータス情報を指定する。上記例では、409
(Conflict) を指定している。
4.13.2.4.2. ハンドラメソッド内で業務例外をハンドリング¶
- Controller
@RequestMapping(value = "plus", method = RequestMethod.POST) @ResponseBody public ResponseEntity<CalculationResult> plusForJson( @Validated @RequestBody CalculationParameters params) { CalculationResult result = new CalculationResult(); // omitted // (1) try { // call service method. // omitted // (2) } catch (BusinessException e) { // (3) // implement error handling. // omitted return ResponseEntity.status(HttpStatus.CONFLICT).body(result); } // omitted return ResponseEntity.ok().body(result); }
項番 説明 (1) 業務例外が発生するメソッド呼び出しを try句で囲む。 (2) 業務例外をcatchする。 (3) 業務例外エラー時のエラー処理を行う。上記例ではエラー処理は省略しているが、エラーメッセージの設定などが行われる想定である。