【Spring】@RestControllerAdvice を使ってREST APIのエラーハンドラを作成する

@RestControllerAdviceを使った例外処理

Spring MVCを使ってWeb APIを作成する際に、@RestControllerAdviceを使うことで共通のエラーハンドラを簡単に作成することができます。

以下のようにエラーハンドラとして@RestControllerAdviceを付与したクラスを作成し例外処理を行うメソッドを作成します。各メソッドには処理したい例外を@ExceptionHandlerアノテーションで指定します。このアノテーションの引数は配列を受け取ることができるので複数の例外を指定することも可能です。

@Slf4j  // ログ出力のためにLombokを使用
@RestControllerAdvice
public class ErrorHandlerController {

    @ExceptionHandler({AccessDeniedException.class})  // 処理したい例外
    @ResponseStatus(HttpStatus.FORBIDDEN)  // レスポンスのステータスコード、ここでは403
    public ErrorResponse handleException(AccessDeniedException e) {
        log.error("Error:", e.getMessage());
        return new ErrorResponse("notAuthorized", "The request was not authorized.");
    }

    @ExceptionHandler({EmployeeNotFoundException.class})  //  独自例外
    @ResponseStatus(HttpStatus.NOT_FOUND)  // レスポンスのステータスコード、ここでは404
    public ErrorResponse handleEmployeeNotFoundException(EmployeeNotFoundException e) {
        log.error("Error:", e.getMessage());
        return new ErrorResponse("notFound", "The Employee was not found.");
    }

    @ExceptionHandler({Exception.class})
    @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // レスポンスのステータスコード、ここでは500
    public ErrorResponse handleException(Exception e) {
        log.error("Error:", e.getMessage());
        return new ErrorResponse("systemError", "System error occurred.");
    }
}

独自例外として以下の例外を作成しています。

public class EmployeeNotFoundException extends RuntimeException {

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

エラーレスポンス用に以下のクラスを作成しています。

@Value
public class ErrorResponse {
    String errorCode;
    String message;
}

@ExceptionHandlerはソースコードの上から順に評価され、アノテーションの引数と実際に発生した例外の型が一致した最初のメソッドが実行されます。

動作検証

動作検証のために以下のようなコントローラを作成します。上記エラーハンドラの動作検証ため/employees/1へGETリクエストを送信するとEmployeeNotFoundExceptionを投げるようにしています。

@RestController
public class EmployeeController {

    @PreAuthorize("hasAuthority('GetEmployee')")
    @GetMapping("employees/{id}")
    public EmployeeResponse show(@PathVariable("id") int id) {

        // 動作検証のためidに1を指定すると例外を投げる
        if (id == 1) { 
            throw new EmployeeNotFoundException("id: " + id);
        }
        return new EmployeeResponse(id, "TestEmployee", 25);
    }
}

以下のようなレスポンスが返ってくることから想定どおりに例外処理が行えていることが確認できます。

$ http -v :8080/employees/1 Authorization:key2 # Httpieを使ってリクエストを投げる

GET /employees/1 HTTP/1.1
Accept: */*
Accept-Encoding: gzip, deflate
Authorization: key2
Connection: keep-alive
Host: localhost:8080
User-Agent: HTTPie/1.0.2



HTTP/1.1 404
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Connection: keep-alive
Content-Type: application/json
Date: Sun, 02 Aug 2020 05:54:13 GMT
Expires: 0
Keep-Alive: timeout=60
Pragma: no-cache
Transfer-Encoding: chunked
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
X-XSS-Protection: 1; mode=block

{
    "errorCode": "notFound",
    "message": "The Employee was not found."
}

ちなみに、Authorizationヘッダをつけているのは別の記事のソースコードを流用しているためです。Spring Securityを使ったWeb APIの認証認可については以下の記事を参考にしてみてください。

存在しないパスへアクセスした際の例外を処理する

デフォルトの設定ではリクエストマッピングに存在しないパスへアクセスした際の例外を@RestControllerAdviceをつけたエラーハンドラで処理できません。処理するためにはapplication.ymlに以下のように記述します。

spring:
  mvc:
    throw-exception-if-no-handler-found: true
  resources:
    add-mappings: false

参考情報

動作確認ではHttpieというCUIツールを使用しています。curlよりもWeb APIの動作確認に向いていると思います。別の記事で紹介していますので参考にしてみてください。