Spring Boot の RestController での例外処理の動きを理解する

Java
JavaSpringバックエンド

この記事では、Spring Boot の RestController を利用する場合の例外処理の動きを、試行した結果とあわせて説明します。

この話題に関しては、調べればWebで多くの記事はでてきますが、断片的な情報が多く初学者には結局どの順番でどう動くのかが把握しにくかったです。
いくつもの記事を読みつつ、実際に動かし、整理しているうちに理解に繋がりましたのでそのまとめです。

対象

  • Spring Framework 初学者

環境

  • Spring Boot 2.7.2
  • Java 17

Spring MVC での例外時の動き

Spring MVC での例外処理は、HandlerExceptionResolver の仕組みによって行われます。
Spring MVC には HandlerExceptionResolver インタフェースを実装した resolver があり、デフォルトでそれらが設定されています。例外処理時は、設定されている resolver が順に実行される仕組みとなっているようです。
具体的には以下の resolver が設定されています。

  1. ExceptionHandlerExceptionResolver
    @ExceptionHandler 処理します。@ExceptionHandler には処理したい例外クラスを渡すことで、該当の例外がスローされた場合に指定した処理を行います。
  2. ResponseStatusExceptionResolver
    @ResponseStatus HTTPステータスを付与します。
  3. DefaultHandlerExceptionResolver
    → Spring MVC が定義した例外がスローされた際に処理します。

以下は Spring MVC が例外処理を行っている場所、DispatcherServlet.java の processHandlerException です。resolver として 3つ、上で記載したものがあるのが見えます。

org.springframework.web.servlet.DispatcherServlet.java
org.springframework.web.servlet.DispatcherServlet.java

初めて見た方は、これだけ見ても ? だと思いますが、この後、それぞれについて実際に動かしてみた結果と、最後にまとめとコードイメージを記載しているので、最後には各役割と実装イメージがつくと思います。

なお、Spring では カスタムで Resolver を追加することもできたり、実行順(優先度)も変えることができるようです。が、この記事では、上記 Spring MVC のデフォルトの処理を前提に検証しています。

例外処理の動き検証

[準備] プロジェクトの作成

spring initializr を利用してプロジェクトを作ります。Spring MVC を利用するので、Spring Web は必須でいれます。

Spring Initializr
Spring Initializr

[準備] 動作確認用の API を作成

検証をするベースになる api を作っておきます。

package com.example.exceptionsample;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/hello")
public class HelloController {

    @RequestMapping("/test1")
    public String test1() {
        return "hello";
    }
}

実装後、アプリケーションを動かし、http://localhost:8080/api/v1/hello にアクセスすると、hello の文言が画面に表示されます。

[準備] カスタム例外クラスの作成

検証用にカスタムの例外クラスを1つ追加します。@ResponseStatus は、検証なのでなんでもよいのですが、ここでは HttpStatus.BAD_REQUREST (HTTPステータス 400)を指定します。

package com.example.exceptionsample;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUREST)
public class MyException extends RuntimeException{

    public MyException(String message) {
        super(message);
    }
}

[準備] エラー時のレスポンスクラスの作成

もう1つ、200 以外の場合(エラー時)のレスポンス用のクラスも定義しておきます。
今回は、code と message のみを持つ、シンプルなものとしています。

package com.example.exceptionsample;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class ErrorResponse {
    private CustomError error;

    public ErrorResponse(String code, String message) {
        this.error = new CustomError(code, message);
    }

    @Getter
    @Setter
    @AllArgsConstructor
    private class CustomError {
        private String code;
        private String message;
    }
}

準備はここまでです。
ここから、デフォルトで定義されている HandlerExceptionResolver の動きを見ていきます。

[検証] ExceptionHandlerExceptionResolver

[検証1] @ExceptionHandler を @RestController クラスで定義

先程作成した HelloController.java で作成した test1 メソッドを少し書き換えた test2 を定義します。test2 メソッドでは Exception をスローさせます。
合わせて、@ExceptionHandler(Exception.class) を指定した例外処理のメソッドを定義します。これで、Exception が発生した場合に、このメソッドがキャッチするようになります。

package com.example.exceptionsample;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/hello")
public class HelloController {

    // ... test1 メソッドは変更ないのでここでは割愛

    // Exception をスローする
    @RequestMapping("/test2")
    public String test2() throws Exception {
        throw new Exception();
    }
    
    @ExceptionHandler(Exception.class)
    public ErrorResponse handleException(Exception exception) {
        return new ErrorResponse("100", "HelloController - @ExceptionHandler: Exception");
    }
}

結果

例外がキャッチされ、return で記述したレスポンスが表示されました。ちゃんと json で返却されています。ただし、HTTPステータスは 200 です。

検証1 - 実行結果
検証1 – 実行結果

[検証2] @ExceptionHandler を @RestControllerAdvice クラスで定義

HelloControllerAdvice.java を作成し、検証1で定義した @ExceptionHandler の処理を定義します。

package com.example.exceptionsample;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class HelloControllerAdvice {

    @ExceptionHandler(Exception.class)
    public ErrorResponse handleException(Exception exception) {
        return new ErrorResponse("100", "HelloControllerAdvice - @ExceptionHandler: Exception");
    }
}

結果

  • RestController クラスで定義した @ExceptionHandler がある場合は、呼ばれません
    検証1と同じ結果になります。
  • RestController クラスで定義した @ExceptionHandler をコメントアウトした場合は、呼ばれます。結果は以下の通りです。
検証2 - 実行結果
検証2 – 実行結果

なお、上記は Exception をスローしていますが、カスタムクラスの場合も同じです。
以下は、準備で作った MyException をスローするようにした、test3 メソッドを用意しています。

package com.example.exceptionsample;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/hello")
public class HelloController {

    // ... test1 , test2 はここでは割愛

    // カスタム例外(MyException)をスローする
    @RequestMapping("/test3")
    public String test3() throws MyException {
        throw new MyException("エラー");
}

HelloControllerAdvice.java に MyException をキャッチする ExceptionHandler を用意します。

@RestControllerAdvice
public class HelloControllerAdvice {

    @ExceptionHandler(MyException.class)
    public ErrorResponse handleMyException(MyException exception) {
        return new ErrorResponse("200", "HelloControllerAdvice - @ExceptionHandler: MyException");
    }

    @ExceptionHandler(Exception.class)
    public ErrorResponse handleException(Exception exception) {
        return new ErrorResponse("100", "HelloControllerAdvice - @ExceptionHandler: Exception");
    }
}

結果は以下の通りです。

検証2 - 実行結果(カスタム例外クラスの場合)
検証2 – 実行結果(カスタム例外クラスの場合)

この結果より、「自身の Controller クラスの @ExceptionHandler > ControllerAdvice クラスの @ExceptionHandler」ですね。そしてどちらも HTTPステータスは 200のままです。

[検証] @ResponseStatusExceptionResolver

[検証3] @ResponseStatus を @RestControllerAdvice クラスで定義

上の検証では例外はキャッチできましたが、 HTTPステータスは 200 でした。
ここでは、@RestControllerAdvice クラス内の @ExceptionHandler(Exception.class)@ResponseStatus も付与します。分かりやすさのため 409(HTTPStatus.CONFLICT)を指定しています。

package com.example.exceptionsample;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class HelloControllerAdvice {

    // ... MyException の ExceptionHandler は割愛

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ErrorResponse handleException(Exception exception) {
        return new ErrorResponse("100", "HelloControllerAdvice - @ExceptionHandler: Exception");
    }
}

結果

レスポンスの文字列は 検証2 と同じ結果ですが、HTTPステータスが 409 になりました
なお、もちろん @RestController でやっても同じです。

検証3 - 実行結果
検証3 – 実行結果

任意のHTTPステータスを返したい場合は @ResponseStatus を付与することで実現可能ですね

[検証] @DefaultHandlerExceptionResolver

[検証4] DefaultHandlerExceptionResolver で規定されている例外を throw

冒頭の通り、DefaultHandlerExceptionResolver は、Spring MVC 内で発生する、フレームワークが規定した例外をハンドリングします。こちら にサポートしている例外一覧があります(以下に一覧にしています)。

例外HTTP ステータスコード
HttpRequestMethodNotSupportedException405 (SC_METHOD_NOT_ALLOWED)
HttpMediaTypeNotSupportedException415 (SC_UNSUPPORTED_MEDIA_TYPE)
HttpMediaTypeNotAcceptableException406 (SC_NOT_ACCEPTABLE)
MissingPathVariableException500 (SC_INTERNAL_SERVER_ERROR)
MissingServletRequestParameterException400 (SC_BAD_REQUEST)
ServletRequestBindingException400 (SC_BAD_REQUEST)
ConversionNotSupportedException500 (SC_INTERNAL_SERVER_ERROR)
TypeMismatchException400 (SC_BAD_REQUEST)
HttpMessageNotReadableException400 (SC_BAD_REQUEST)
HttpMessageNotWritableException500 (SC_INTERNAL_SERVER_ERROR)
MethodArgumentNotValidException400 (SC_BAD_REQUEST)
MissingServletRequestPartException400 (SC_BAD_REQUEST)
BindException400 (SC_BAD_REQUEST)
NoHandlerFoundException404 (SC_NOT_FOUND)
AsyncRequestTimeoutException503 (SC_SERVICE_UNAVAILABLE)
https://spring.pleiades.io/spring-framework/docs/current/javadoc-api/org/springframework/web/servlet/mvc/support/DefaultHandlerExceptionResolver.html

今回は試しに、必須パラメータが欠落している場合に発生する org.springframework.web.bind.MissingServletRequestParameterException を発生させます。

具体的には、リクエストパラメータに hoge を追加します。
以下の記述方法の場合、hoge は必須のパラメータになります。そのため、実際にGETを実行するときにはパラメータを指定しないでリクエストをすると、MissingServletRequestParameterException が発生します。

package com.example.exceptionsample;

import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("api/v1/hello")
public class HelloController {

    // ...test1 ~ test3 は割愛

    // 必須パラメータをもつ GET API
    @RequestMapping("/test4")
    public String test4(@RequestParam("hoge") String hoge) throws Exception {
        return "hello";
    }
}

なお、この検証の前に、HelloControllerAdvice.java の @ExceptionHandler(Exception.class) 部分の処理はコメントアウトしておきます(でないと、ここがキャッチするため)。

結果

json ではありませんが、status=400 で返ってきており、必須パラメータがない旨のメッセージがあります。

検証4 - 実行結果
検証4 – 実行結果

なお、現状では当該例外に対する @ExceptionHandler は定義しておらず、またレスポンスを変更する実装も行っていないので、MissingServletRequestParameterException がキャッチされ、例外の処理はしてくれていますが、戻り値が JSON ではありません。

今回のような場合、 Spring Boot の doc によると、Accept ヘッダに `text/html` がある場合には /erorr ページに遷移しようとするようです。今は明示的に /error のパスも定義していないので、上の画面が表示されています。

試しに、Accept ヘッダから text/html ヘッダを除外して同じリクエストを実行してみると、以下のように、JSON で返却されます。

検証4 - 実行結果(Acceptヘッダから text/html を除去した場合。JSONが返却される)
検証4 – 実行結果(Acceptヘッダから text/html を除去した場合。JSONが返却される)


/error のカスタマイズしたい場合は、ErrorController インタフェースを実装することで実現できます。が、そもそも /error に行くのではなく、/test4 でのレスポンスを JSON かつ ErrorResponse の形式に合わせたいです。

参考: /error をカスタマイズするときの例もメモ。AbstractErrorController を extends する方がよいのかもしれません。

package com.example.exceptionsample;

import org.springframework.boot.web.servlet.error.ErrorController;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;

@RestController
public class MyErrorController implements ErrorController {

    @RequestMapping("/error")
    public ErrorResponse handleError(HttpServletRequest request) {
        return new ErrorResponse("999", "MyErrorController - /error ");
    }
}


結果:whitelabel page error ではなく、ErrorResponse のレスポンスが返せました。

検証 - /error のパスのレスポンスをカスタマイズ
検証 – /error のパスのレスポンスをカスタマイズ

ということで、次の検証です。

[検証5] @DefaultHandlerExceptionResolver で規定されている例外発生時のレスポンスを変更する

@ExceptionHandler@ExceptionHandler(MissingServletRequestParameterException.class) のようにするとキャッチできますが、HTTP ステータスが 200 になったりと元々設定されているものが効いてなかったりします。

Spring framework には、ResponseEntitityExceptionHandler というクラスが存在し、これで、DefaultHandlerExceptionResolver でサポートされている例外に対するレスポンスをカスタマイズすることが可能です。

具体的には以下のように、ResponseEntitityExceptionHandler が持つ handleExceptionInternal を オーバライドします。

package com.example.exceptionsample;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@RestControllerAdvice
public class HelloControllerAdvice extends ResponseEntityExceptionHandler {

    @ExceptionHandler(MyException.class)
    public ErrorResponse handleMyException(MyException exception) {
        return new ErrorResponse("200", "HelloControllerAdvice - @ExceptionHandler: MyException");
    }
    
    // 今回は使用していませんが、今回の例外に対する処理を Override することができます。
    // @Override
    // public ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
    //     return this.handleExceptionInternal(ex, "", headers, HttpStatus.BAD_REQUEST, request);
    // }

    // このメソッドは内部的に呼ばれているもので、ResponseEntityExceptionHandler で定義されている例外の共通処理を実装します。
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(
        Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ErrorResponse re = new ErrorResponse(String.valueOf(status.value()), ex.getMessage());
        return super.handleExceptionInternal(ex, re, headers, status, request);
    }

    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.CONFLICT)
    public ErrorResponse handleException(Exception exception) {
        return new ErrorResponse("100", "HelloControllerAdvice - @ExceptionHandler: Exception");
    }
}

結果

例外がキャッチされ、レスポンスも指定の形式になっています。HTTPステータスも 400 と期待した結果になりました。

検証5 - 実行結果
検証5 – 実行結果

[追加検証] ResponseStatusException を用いたレスポンス

上記までで、デフォルトでの Resolver の動きは見えて来たのですが、Spring のバージョンの変遷による例外処理の仕組みの変化を少し確認しました(間違ってたら指摘ください)。

Spring バージョンによる変遷

Spring の歴史的には例外処理はこちらを見る限り、以下のようです。

  • Spring 3.2 以前:ErrorController, HandlerExceptionResolver, @ExceptionHandlerアノテーション
  • Spring 3.2 以降:@ControllerAdviceアノテーション 登場
  • Spring 5.0 以降:ResponseStatusException 登場

当初は HTML を返すのが主流だったが、REST API の普及により、データのみを返す仕組みも提供。
その後、Controller ごとに例外の記述が必要だったが、ControllerAdvice により、アプリケーション横断で定義することが可能になった。
さらに、例外とHTTPステータスを分離しつつ、開発効率をあげるために ResponseStatusException が登場した、と理解しました。

最後、まだ検証していない ResponseStatusException を確認します。

[検証6] ResponseStatusException 利用時のレスポンス形式

まず、ResponseStatusException をスローした場合のレスポンスを確認します。わかり易さのため、また別の HTTP ステータス(502 BAD GATEWAY)を指定しています。
この場合、検証4 同様、whitelabel page error になります。

@RestController
@RequestMapping("api/v1/hello")
public class HelloController {

    // ~ test4 までは割愛

    // ResponseStatusException を利用した例外処理
    @RequestMapping("/test5")
    public ResponseStatusException test5() throws Exception {
        // 何かしら例外が発生した場合
        throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "HelloController - ResponseStatusException");
    }
}

結果

検証6 - 実行結果
検証6 – 実行結果

ResponseStatusException でレスポンスを返した場合にも、ErrorResponse のフォーマットでレスポンスを返すには、ResponseStatusException.class に対する @ExceptionHandler を設定することでカスタマイズできます。

@RestControllerAdvice
public class HelloControllerAdvice extends ResponseEntityExceptionHandler {

    // ... 記載割愛

    @ExceptionHandler(ResponseStatusException.class)
    protected ResponseEntity<Object> handleResponseStatus(ResponseStatusException ex, WebRequest request) {
        ErrorResponse re = new ErrorResponse(String.valueOf(ex.getStatus().value()), ex.getMessage());
        return this.handleExceptionInternal(ex, null, new HttpHeaders(), ex.getStatus(), request);
    }
}

結果
ResponseStatusException で指定した情報を含む形で ErrorResponse のフォーマットでレスポンスできました。HTTP ステータスも指定したものです。

検証6 - 実行結果(ResponseStatusException.class を ExceptionHandlerし任意のレスポンスを返す)
検証6 – 実行結果(ResponseStatusException.class を ExceptionHandlerし任意のレスポンスを返す)

まとめ

これまでの検証結果をまとめると、

  • @ExceptionHandler を記載することで指定した例外に対するハンドリングが可能。
    @RestController では当該コントローラでの例外をキャッチ
    @RestControllerAdvice に記載した場合はアプリケーション全体で発生した例外をキャッチ
    両方でキャッチ可能な例外がスローされた場合、前者で処理される。
  • @ResponseStatus で任意の HTTP ステータスを付与することができる。
    @ExceptionHandlerとセットで設定することで、任意の例外をキャッチし、任意の HTTP ステータスコードで返却可能。
    逆に指定しない場合は HTTP ステータスコードは 200 となる。
  • @ExceptionHandler を記載していない例外でも、DefaultHandlerExceptionResolver でサポートされている例外はキャッチされる。
    このとき、任意のフォーマットでレスポンスを返却するためには、ResponseEntityExceptionHandler を継承した RestControllerAdvice アノテーションをもつクラスを定義し、そこで handleExceptionInternal メソッドをオーバライドすることでカスタマイズが可能。
  • ResponseStatusException で例外をスローすることで、例外とHTTPステータスコードを分離して考えることが可能。この場合は、@ExceptionHandlerResponseStatusException クラスの例外をキャッチすることで、任意のフォーマットで返却することができる。

となります。
これらを踏まえた結果、以下の方針で考えていくのだろう、というのが自分の理解です。

  1. エラー時のレスポンスフォーマットは今回の検証のように共通フォーマットを定義する。
  2. 例外の捕捉は以下の種類があり、どの組み合わせを使うかプロジェクトで方針を決める。
    1. @ExceptionHandler@ResponseStatus の組み合わせで、例外のキャッチとHTTPステータスを定義し、1. のフォーマットで応答する。
      また、RestController でやるか、RestControllerAdvice で実装するか(基本は後者を利用、前者は使わざるを得ない場合があるとき)。
    2. DefaultHandlerExceptionResolver で処理できる例外はまかせる。
    3. ResponseStatusException を利用し、例外と HTTPステータスは分けて考える
  3. なにはともあれ、@ExceptionHandler(Exception.class) は実装することで、万が一漏れがあってもその他例外として、共通フォーマットで応答できるようにする。

実現イメージ

最後に、例外のスローの仕方の方針によって使わないものもあると思いますが、一旦 @RestControllerAdvice に以下のように定義しておくことで、一通り拾うことができると思いますので載せておきます。

package com.example.exceptionsample;

import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.server.ResponseStatusException;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@RestControllerAdvice
public class HelloControllerAdvice extends ResponseEntityExceptionHandler {

    // カスタム例外を処理したい場合
    // 同様の記述が @RestController にもある場合は、@RestController での実装が優先される。
    // 例外と HTTPステータスがセットな場合はこれらを定義していく 
    @ExceptionHandler(MyException.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ErrorResponse handleMyException(MyException exception) {
        return new ErrorResponse("200", "HelloControllerAdvice - @ExceptionHandler: MyException");
    }

    // DefaultHandlerExceptionResolver でサポートされている例外に対する処理
    // をオーバライドしたい場合に利用する。
    // 以下は MissingServletRequestParameterException だが、他のメソッドも必要に応じて定義
    @Override
    public ResponseEntity<Object> handleMissingServletRequestParameter(MissingServletRequestParameterException ex, HttpHeaders headers, HttpStatus status, WebRequest request) {
        return this.handleExceptionInternal(ex, "", headers, HttpStatus.BAD_REQUEST, request);
    }

    // DefaultHandlerExceptionResolver でサポートされている例外に対する処理の共通処理部
    // レスポンスフォーマットをカスタマイズしたい場合は、こちらをオーバライドする
    @Override
    protected ResponseEntity<Object> handleExceptionInternal(
            Exception ex, @Nullable Object body, HttpHeaders headers, HttpStatus status, WebRequest request) {
        ErrorResponse re = new ErrorResponse(String.valueOf(status.value()), ex.getMessage());
        return super.handleExceptionInternal(ex, re, headers, status, request);
    }

    // ResponseStatusException 例外を処理したい場合
    // レスポンスフォーマットをカスタマイズしたい場合は、こちらを定義してレスポンスを実装する
    @ExceptionHandler(ResponseStatusException.class)
    protected ResponseEntity<Object> handleResponseStatus(ResponseStatusException ex, WebRequest request) {
        ErrorResponse re = new ErrorResponse(String.valueOf(ex.getStatus().value()), ex.getMessage());
        return this.handleExceptionInternal(ex, null, new HttpHeaders(), ex.getStatus(), request);
    }

    // 未処理の例外があった場合を想定して定義
    // ここではその他サーバエラーとして 500 を返すのがよさそう
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
    public ErrorResponse handleGeneralException(Exception exception, WebRequest request) {
        return new ErrorResponse(String.valueOf(HttpStatus.INTERNAL_SERVER_ERROR), "Unexpected Error");
    }
}

確認用の Controller です。

package com.example.exceptionsample;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

@RestController
@RequestMapping("api/v1/hello")
public class HelloController {

    // 正常
    @RequestMapping("/test1")
    public String test1() throws Exception {
         return "hello";
    }

    // Exception をスローする
    @RequestMapping("/test2")
    public String test2() throws Exception {
        throw new Exception();
    }

    // カスタム例外(MyException)をスローする
    @RequestMapping("/test3")
    public String test3() throws MyException {
        throw new MyException("エラー");
    }

    // 必須パラメータをもつ GET API
    @RequestMapping("/test4")
    public String test4(@RequestParam("hoge") String hoge) throws Exception {
        return "hello";
    }

    // ResponseStatusException を利用した例外処理
    @RequestMapping("/test5")
    public ResponseStatusException test5() throws Exception {
        // 何かしら例外が発生した場合
        throw new ResponseStatusException(HttpStatus.BAD_GATEWAY, "HelloController - ResponseStatusException");
    }
}

それぞれのパターンでちゃんと例外をキャッチし ErrorResponse で返せています。

# 正常
% curl -i http://localhost:8080/api/v1/hello/test1 
HTTP/1.1 200 
Content-Type: text/plain;charset=UTF-8
Content-Length: 5
Date: Fri, 19 Aug 2022 14:05:21 GMT

hello


# Exception のスローは @ExceptionHandler(Exception.class) がキャッチ
% curl -i http://localhost:8080/api/v1/hello/test2
HTTP/1.1 500 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 19 Aug 2022 14:05:41 GMT
Connection: close

{"error":{"code":"500 INTERNAL_SERVER_ERROR","message":"Unexpected Error"}}


# MyException のスローは @ExceptionHandler(MyException.class) がキャッチ
% curl -i http://localhost:8080/api/v1/hello/test3
HTTP/1.1 400 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 19 Aug 2022 14:06:12 GMT
Connection: close

{"error":{"code":"200","message":"HelloControllerAdvice - @ExceptionHandler: MyException"}}


# MissingServletRequestParameterException のスローは ResponseEntityExceptionHandler がキャッチ
% curl -i http://localhost:8080/api/v1/hello/test4
HTTP/1.1 400 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 19 Aug 2022 14:06:37 GMT
Connection: close

{"error":{"code":"400","message":"Required request parameter 'hoge' for method parameter type String is not present"}}


# ResponseStatusException は @ExceptionHandler(ResponseStatusException.class) がキャッチ
% curl -i http://localhost:8080/api/v1/hello/test5
HTTP/1.1 502 
Content-Type: application/json
Transfer-Encoding: chunked
Date: Fri, 19 Aug 2022 14:06:56 GMT

{"error":{"code":"502","message":"502 BAD_GATEWAY \"HelloController - ResponseStatusException\""}}

思った以上に長くなりましたが、これで Spring の仕組みを活用しつつ効率的に例外のハンドリングができればと思います。

最後に。以下の書籍は出版されて年数が経っており最新ではない部分はありますが、それでもベースを理解する上では今でも有用でした(自分も何度も買うか悩んだ末 2023/1 に買いましたがやはり体系だったものごとが見れるのは助かります)。Spring は歴史があるぶん奥が深いので、迷っている方は読んで見ることをおすすめです。

コメント