コンテンツにスキップ

集約例外ハンドラーの設定⚓︎

集約例外ハンドラーの設定方法について解説します。集約例外ハンドラーを実装することで、複数のコントローラーで必要となる、プレゼンテーション層までで処理されなかった業務例外やシステム例外を一元的にハンドリングする機能を提供できます。

AlesInfiny Maia OSS Edition (以降、 AlesInfiny Maia)では、 RESTful API のエラーレスポンスの標準的な仕様である RFC9457 に準拠した形式でエラーレスポンスを返却します。その他の実装方針については、こちら を参照してください。

集約例外ハンドラーは web プロジェクトに実装します。本設定で利用するフォルダーの構成は以下の通りです。

root/ --------------------------------------------------- root フォルダー
└ web/src/main/java/{ プロジェクトのグループ名 }/web/controller/advice
  └ ExceptionHandlerControllerAdvice.java --------------- 集約例外ハンドラークラス

本手順では、初めに集約例外ハンドラークラスを定義し、次にそれぞれの例外をハンドリングするメソッドを定義します。 最後に、定義したメソッドに AlesInfiny Maia の実装方針に沿った、例外を適切にハンドリングする機能を実装します。

集約例外ハンドラークラスの定義⚓︎

集約例外ハンドラークラスは ResponseEntityExceptionHandler クラス の拡張クラスとして定義し、 @ControllerAdvice を付与します。

@ControllerAdvice により、付与されたクラス内で実装された機能を全てのコントローラーに追加で適用できます。

集約例外ハンドラークラスの例
1
2
3
4
@ControllerAdvice(basePackages = "プロジェクトのグループ名")
public class ExceptionHandlerControllerAdvice extends ResponseEntityExceptionHandler{
  // 例外をハンドリングするメソッド
}

例外ハンドリングするメソッドの定義⚓︎

集約例外ハンドラークラスには、ハンドリングする例外と対応したメソッドを定義します。

定義する各メソッドには @ExceptionHandler アノテーションを付与し、引数としてハンドリングする例外クラスを指定します。 指定した例外クラスとプレゼンテーション層まででハンドリングされなかった例外が合致した際に、メソッド内の処理が実行されます。

メソッドの返り値には、 ProblemDetail を型パラメータとして指定した ResponseEntity クラスを指定します。 これにより、 RFC9457 に準拠したエラーレスポンスが返却されます。

Exception クラスをハンドリングするメソッドの例
1
2
3
4
@ExceptionHandler(Exception.class)
public ResponseEntity<ProblemDetail> handleException(Exception e, HttpServletRequest req) {
  // 例外のハンドリングを行う処理
}

例外ハンドリングするメソッドの実装⚓︎

各メソッドでは、エラーログの出力やエラーレスポンスの生成を実装します。以下にそれらの実装方法を解説します。

エラーログの出力⚓︎

エラーに関するアプリケーションログを出力する処理を実装します。 ログに含める標準的なデータやログレベルは、ログ出力方針 を参照してください。

エラーレスポンスの生成⚓︎

エラーレスポンスを生成する処理を実装します。 エラーレスポンスは RFC9457 に準拠させるため、メソッド内で ProblemDetail をインスタンス化し、 ResponseEntity の body に含めます。

AlesInfiny Maia では、例外メッセージをプロパティファイルから取得し、ログ出力するために ErrorMessageBuilder クラスを実装しています。 また、プロパティファイルからエラーレスポンスに含めるメッセージを整形し ProblemDetail クラスを生成する ProblemDetailsFactory クラスを実装しています。 そして、これらを用いてエラーレスポンスを生成を実装しています。

ErrorMessageBuilder クラスおよび ProblemDetailsFactory クラスの実装例は メッセージ管理機能の設定 - メッセージの取得 およびサンプルアプリケーションを参照ください。

ProblemDetail および ErrorMessageBuilder を用いた集約例外ハンドラーの実装例
ExceptionHandlerControllerAdvice.java
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
package com.dressca.web.controller.advice;

import lombok.RequiredArgsConstructor;
import com.dressca.applicationcore.authorization.PermissionDeniedException;
import com.dressca.applicationcore.catalog.OptimisticLockingFailureException;
import com.dressca.systemcommon.constant.CommonExceptionIdConstants;
import com.dressca.systemcommon.exception.LogicException;
import com.dressca.systemcommon.exception.SystemException;
import com.dressca.systemcommon.log.AbstractStructuredLogger;
import com.dressca.web.log.ErrorMessageBuilder;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ProblemDetail;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException;
import org.springframework.security.authorization.AuthorizationDeniedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

/**
 * サーバーエラーのハンドリングを行うクラスです。
 */
@ControllerAdvice(basePackages = "com.dressca")
@RequiredArgsConstructor
public class ExceptionHandlerControllerAdvice extends ResponseEntityExceptionHandler {

  private final AbstractStructuredLogger apLog;
  private final ProblemDetailsFactory problemDetailsFactory;

  /**
   * 未認証エラーをステータスコード 401 で返却します。
   *
   * @param e 未認証エラー。
   * @return ステータースコード 401 のレスポンス。
   */
  @ExceptionHandler(AuthenticationCredentialsNotFoundException.class)
  public ResponseEntity<String> handleAuthenticationCredentialsNotFoundException(
      AuthenticationCredentialsNotFoundException e) {
    apLog.warn(ExceptionUtils.getStackTrace(e));
    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
  }

  /**
   * 認可エラーをステータスコード 404 で返却します。
   *
   * @param e 認可エラー。
   * @return ステータースコード 404 のレスポンス。
   */
  @ExceptionHandler({AuthorizationDeniedException.class, PermissionDeniedException.class})
  public ResponseEntity<String> handleAuthorizationDeniedException(Exception e) {
    apLog.warn(ExceptionUtils.getStackTrace(e));
    return ResponseEntity.notFound().build();
  }

  /**
   * 楽観ロックエラーをステータスコード 409 で返却します。
   * 
   * @param e 楽観ロックエラー。
   * @return ステータスコード 409 のレスポンス。
   */
  @ExceptionHandler(OptimisticLockingFailureException.class)
  public ResponseEntity<String> handleOptimisticLockingFailureException(
      OptimisticLockingFailureException e) {
    apLog.warn(ExceptionUtils.getStackTrace(e));
    return ResponseEntity.status(HttpStatus.CONFLICT).body(null);
  }

  /**
   * その他の業務エラーをステータースコード 500 で返却します。
   *
   * @param e 業務例外。
   * @return ステータースコード 500 のレスポンス。
   */
  @ExceptionHandler(LogicException.class)
  public ResponseEntity<ProblemDetail> handleLogicException(LogicException e) {
    ErrorMessageBuilder errorBuilder =
        new ErrorMessageBuilder(e, CommonExceptionIdConstants.E_BUSINESS, null, null);
    apLog.error(errorBuilder.createLogMessageStackTrace());
    ProblemDetail problemDetail = problemDetailsFactory.createProblemDetail(errorBuilder,
        CommonExceptionIdConstants.E_BUSINESS, HttpStatus.INTERNAL_SERVER_ERROR);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .contentType(MediaType.APPLICATION_PROBLEM_JSON).body(problemDetail);
  }

  /**
   * その他のシステムエラーをステータースコード 500 で返却します。
   *
   * @param e その他の例外。
   * @return ステータースコード 500 のレスポンス。
   */
  @ExceptionHandler(SystemException.class)
  public ResponseEntity<ProblemDetail> handleSystemException(SystemException e) {
    ErrorMessageBuilder errorBuilder =
        new ErrorMessageBuilder(e, CommonExceptionIdConstants.E_SYSTEM, null, null);
    apLog.error(errorBuilder.createLogMessageStackTrace());
    ProblemDetail problemDetail = problemDetailsFactory.createProblemDetail(errorBuilder,
        CommonExceptionIdConstants.E_SYSTEM, HttpStatus.INTERNAL_SERVER_ERROR);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .contentType(MediaType.APPLICATION_PROBLEM_JSON).body(problemDetail);
  }

  /**
   * 上記のいずれにも当てはまらない例外をステータースコード 500 で返却します。
   *
   * @param e その他の例外。
   * @return ステータースコード 500 のレスポンス。
   */
  @ExceptionHandler(Exception.class)
  public ResponseEntity<ProblemDetail> handleException(Exception e) {
    ErrorMessageBuilder errorBuilder =
        new ErrorMessageBuilder(e, CommonExceptionIdConstants.E_SYSTEM, null, null);
    apLog.error(errorBuilder.createLogMessageStackTrace());
    ProblemDetail problemDetail = problemDetailsFactory.createProblemDetail(errorBuilder,
        CommonExceptionIdConstants.E_SYSTEM, HttpStatus.INTERNAL_SERVER_ERROR);
    return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
        .contentType(MediaType.APPLICATION_PROBLEM_JSON).body(problemDetail);
  }
}