4.14. Ajax


4.14.1. Overview

本章では、Ajaxを利用するアプリケーションの実装方法について説明する。

Ajaxとは、以下の処理を非同期に行うための技術の総称である。

  • ブラウザ上で行われる画面操作

  • 画面操作をトリガーとしたサーバへのHTTP通信、及び通信結果のユーザインタフェースへの反映

Ajaxを使うことで、HTTP通信中に画面の操作を継続できるため、ユーザビリティの向上を目的として使用されることが多い。
この技術の代表的な適用例としては、検索サイトにおける検索ワードのSuggestion機能やリアルタイム検索などがあげられる。

4.14.2. How to use

4.14.2.1. アプリケーションの設定

Ajax向けのアプリケーションの設定について説明する。

Warning

StAX(Streaming API for XML)使用時のDoS攻撃対策について

XML形式のデータについてStAXを使用して解析する場合は、DTDを使ったDoS攻撃を受けないように対応する必要がある。

詳細は、CVE-2015-3192 - DoS Attack with XML Inputを参照されたい。

4.14.2.1.1. Spring MVCのAjax関連の機能を有効化するための設定

Ajax通信時で使用されるContent-Type(application/xmlapplication/jsonなど)を、Controllerのハンドラメソッドでハンドリングできるようにする。

  • SpringMvcConfig.java

    @EnableAspectJAutoProxy
    @EnableWebMvc // (1)
    @Configuration
    public class SpringMvcConfig implements WebMvcConfigurer {
    
    項番
    説明
    (1)
    @EnableWebMvcアノテーションが指定されていると、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
Java SE 17でJAXBを利用するにはJAXBの削除を参照されたい。

Note

XXE(XML External Entity) 対策について

Ajax通信でXML形式のデータを扱う場合は、XXE(XML External Entity)対策を行う必要がある。

Macchinetta Server Framework (1.x)では、XXE 対策が行われているSpring MVC(3.2.10.RELEASE以上)に依存しているため、個別に対策を行う必要はない。


4.14.2.2. Controllerの実装

以降で説明するサンプルコードの前提は以下の通りである。

  • 応答データの形式にはJSONを使用する。

  • クライアント側には、JQueryを使用する。バージョンは執筆時点の1.x系の最新バージョン(1.10.2)を使用する。

Warning

循環参照への対策

HttpMessageConverterを使用してJavaBeanをJSONやXML形式にシリアライズする際に、相互参照関係のオブジェクトをプロパティに保持していると、循環参照となりStackOverflowErrorOutOfMemoryErrorなどが発生するので、注意が必要である。

循環参照を回避するためには、

  • Jacksonを使用してJSON形式にシリアライズする場合は、シリアライズ対象から除外するプロパティに@com.fasterxml.jackson.annotation.JsonIgnoreアノテーション

  • JAXBを使用してXML形式にシリアライズする場合は、シリアライズ対象から除外するプロパティにjakarta.xml.bind.annotation.XmlTransientアノテーション

を付与すればよい。

以下にJacksonを使用してJSON形式にシリアライズする際の回避例を示す。

public class Order {
    private String orderId;
    private List<OrderLine> orderLines;
    // omitted
}
public class OrderLine {
    @JsonIgnore
    private Order order;
    private String itemCode;
    private int quantity;
    // omitted
}

項番

説明

(1)

シリアライズ対象から除外するプロパティに対して@JsonIgnoreアノテーションを付与する。


4.14.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

    @GetMapping(value = "search") // (4)
    @ResponseBody // (5)
    public SearchResult search(@Validated SearchCriteria criteria) { // (6)
    
        SearchResult searchResult = new SearchResult(); // (7)
    
        // (8)
        // omitted
    
        return searchResult; // (9)
    }
    
    項番
    説明
    (4)
    @GetMappingアノテーションを指定する。
    (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ファイルを読み込みための設定は、以下の通り。

    以下はブランクプロジェクトで提供されている設定値である。

    • SpringMvcConfig.java

      @EnableAspectJAutoProxy
      @EnableWebMvc
      @Configuration
      public class SpringMvcConfig implements WebMvcConfigurer {
      
          // (12)
          @Override
          public void addResourceHandlers(final ResourceHandlerRegistry registry) {
              registry.addResourceHandler("/resources/**").addResourceLocations(
                      "/resources/", "classpath:META-INF/resources/").setCachePeriod(
                              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ステータスコードが4xx5xxの時)の処理を実装する。
上記例では、実装は省略している。
エラー処理の実装例は、フォームデータをPOSTするを参照されたい。

Tip

上記例ではWebアプリケーションのコンテキストパス(${pageContext.request.contextPath}) をHTMLの<meta>要素に設定しておくことで、JavaScriptのコードからJSPのコードを排除している。


上記検索フォームの「Search」ボタンを押下した際には、以下のような通信が発生する。
ポイントとなる部分にハイライトを設けている。
  • リクエストデータ

    GET /macchinetta-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/macchinetta-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.14.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を作成する。

    Warning

    電文からJava Beanにデシリアライズする際、プロパティにジェネリクスやインターフェイスを使用しているなどの理由で型を特定できない場合は@com.fasterxml.jackson.annotation.JsonTypeInfoアノテーションを付与する。@JsonTypeInfoアノテーションを付与したプロパティをシリアライズするとJSONに型情報が出力され、これを読み取ってデシリアライズが行われる。

    ただし、@JsonTypeInfoアノテーションのuse属性にId.CLASSId.MINIMAL_CLASSを使用すると、JSONに出力されたクラス名を元にデシリアライズが行われるため、これにより不正にリモートコードが実行される危険がある。このため、(信頼できない送信元を含み得る)不特定多数からの電文を受け付ける前提のシステムにおいては、Id.CLASSId.MINIMAL_CLASSを指定してはならない。

    なお、ObjectMapperdefaultTypingを利用すると、上記のようなデシリアライズ時の型判断をアプリケーション全体に適用することが可能である。こちらも合わせて注意されたい。


  • Controller

    @RequestMapping("xxx")
    @Controller
    public class XxxController {
    
        @PostMapping(value = "plusForForm") // (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)
    @PostMappingアノテーションを指定する。
    (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)
エラー時の描画処理を行う。
上記例では、計算結果を表示するための領域に、エラーメッセージを表示している。

Tip

<sec:csrfMetaTags />を利用して、CSRFトークン値とCSRFトークンヘッダー名をHTMLの<meta>要素に設定しておくことで、JavaScriptのコードからJSPのコードを排除している。Ajax使用時の連携を参照されたい。

尚、CSRFトークン値とCSRFトークンヘッダー名はそれぞれ${_csrf.token}${_csrf.headerName}を用いても取得可能である。

Warning

上記例では、Ajaxの通信処理、DOM操作処理(描画処理)、エラー処理を同じfunction内で行っているが、これらの処理は分離して実装することを推奨する。


上記検索フォームの「=」ボタンを押下した際には、以下のような通信が発生する。
ポイントとなる部分にハイライトを設けている。
  • リクエストデータ

    POST /macchinetta-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/macchinetta-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.14.2.2.3. フォームデータをJSONとしてPOSTする

Ajaxを使ってフォームのデータをJSON形式に変換してからPOSTし、処理結果を取得する方法について説明する。

「フォームデータをPOSTする」方法との差分部分について説明する。

  • Controller

    @RequestMapping("xxx")
    @Controller
    public class XxxController {
    
        @PostMapping(value = "plusForJson")
        @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

    // (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 /macchinetta-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/macchinetta-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.14.2.3. 入力エラーのハンドリング

入力値に不正な値が指定された場合のエラーハンドリング方法について説明する。

入力エラーのハンドリング方法は、大きく分けて以下の2つに分類される。

  • 例外ハンドリング用のメソッドを用意してエラー処理を行う。

  • Controllerのハンドラメソッドの引数としてorg.springframework.validation.BindingResultを受け取り、エラー処理を行う。


4.14.2.3.1. MethodArgumentNotValidException のハンドリング

org.springframework.web.bind.MethodArgumentNotValidExceptionは、リクエストパラメータとして送信したデータ又は@RequestBodyアノテーションを使用してリクエストBodyに格納されているデータをJavaBeanにバインドする際に、入力値に不正な値が指定された場合に発生する例外クラスである。
application/jsonapplication/xmlなどの形式として受け取る場合又はGET時のリクエストパラメータや、フォームデータをapplication/x-www-form-urlencodedの形式として受け取る場合は、MethodArgumentNotValidExceptionの例外ハンドリングが必要となる。
  • Controller

    @RequestMapping("xxx")
    @Controller
    public class XxxController {
    
        // omitted
    
        @ExceptionHandler(MethodArgumentNotValidException.class) // (1)
        @ResponseStatus(value = HttpStatus.BAD_REQUEST) // (2)
        @ResponseBody // (3)
        public ErrorResults handleMethodArgumentNotValidException(
            MethodArgumentNotValidException 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属性にハンドリングする例外の型を指定する。
    上記例では、 ハンドリング対象の例外としてMethodArgumentNotValidException.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.14.2.3.2. HttpMessageNotReadableException のハンドリング

org.springframework.http.converter.HttpMessageNotReadableExceptionは、@RequestBodyアノテーションを使用してリクエストBodyに格納されているデータをJavaBeanにバインドする際に、Bodyに格納されているデータからJavaBeanを生成できなかった場合に発生する例外クラスである。
application/jsonapplication/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を指定する。
    上記以外はMethodArgumentNotValidExceptionと同様。

4.14.2.3.3. BindingResult を使用したハンドリング

正常終了時に返却するJavaBeanと入力エラー時に返却するJavaBeanの型が同じ場合は、BindingResultをハンドラメソッドの引数として受け取ることでエラーハンドリングすることができる。
この方法は、リクエストデータの形式に関係なく使用することができる。
ハンドラメソッドの引数としてBindingResultを指定しない場合は、前述した例外をハンドリングする方法でエラー処理を実装する必要がある。
  • Controller

    @PostMapping(value = "plus")
    @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を指定せず、前述した例外をハンドリングする方法でエラー処理を実装する方法がある。

    @PostMapping(value = "plus")
    @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.14.2.4. 業務エラーのハンドリング

業務エラーのエラーハンドリング方法について説明する。

業務エラーのハンドリング方法は大きく分けて以下の2つに分類される。

  • 業務例外ハンドリング用のメソッドを用意してエラー処理を行う。

  • Controllerのハンドラメソッド内で業務例外をcatchしてエラー処理を行う。


4.14.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を指定する。
    上記以外は入力エラーのMethodArgumentNotValidExceptionのハンドリング方法と同様。
    (2)
    応答するHTTPステータス情報を指定する。
    上記例では、409(Conflict) を指定している。

4.14.2.4.2. ハンドラメソッド内で業務例外をハンドリング

業務エラーが発生する処理を try句で囲み、業務例外をcatchする。
エラー処理がリクエスト毎に異なる場合は、この方法でエラーハンドリングすることになる。
  • Controller

    @PostMapping(value = "plus")
    @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)
    業務例外エラー時のエラー処理を行う。
    上記例ではエラー処理は省略しているが、エラーメッセージの設定などが行われる想定である。