LoginSignup
2
3

More than 3 years have passed since last update.

Spring バリデーションエラーを@ControllerAdviceで一括ハンドリング

Posted at

結論

@ControllerAdviceを付与したハンドラクラスを用意すれば、各コントローラのバリデーションエラーのハンドリングを一括で処理できる。

環境

Java 11
SpringBoot 2.3.3

解説

以下、検索パラメータを指定してユーザー一覧を取得するRESTコントローラにて解説する。
リクエストの入力バリデーションを設け、バリデーションエラー検出時には所定のレスポンスボディと併せて400エラーを返すようにする。

※package、import文など省略しています。

コントローラクラス

引数に@Validatedを付与し、バリデーションが実施されるようにする。
エラー時のハンドリングはここでは特にしない。


@RestController
@RequiredArgsConstructor
public class UserController {

    @NonNull
    private final UserService userService;

    @NonNull
    private final GetUsersQueryValidator getUsersQueryValidator;

    @InitBinder
    public void initBinder(WebDataBinder binder) {
        binder.addValidators(getUsersQueryValidator);
    }

    /**
     * 検索条件を指定してユーザー情報を取得
     *
     * @param getUsersQuery 検索条件クエリパラメータ
     * @return 検索されたユーザー情報
     */
    @GetMapping(value = "/users")
    ResponseEntity<List<UserDto>> getUsers(@Validated GetUsersQuery getUsersQuery) {

        SearchUsersCondition searchUsersCondition = new SearchUsersCondition();
        searchUsersCondition.setName(getUsersQuery.getName());
        searchUsersCondition.setLowerLimitAge(getUsersQuery.getLowerLimitAge());
        searchUsersCondition.setUpperLimitAge(getUsersQuery.getUpperLimitAge());

        return ResponseEntity.ok(userService.searchUsers(searchUsersCondition));
    }
}

クエリパラメータクラス

リクエストのクエリパラメータがバインドされるクラス。
各フィールドに@NotBlank@NotNullを付与し、単項バリデーションが実施されるようにしている。

/**
 * ユーザー検索条件を指定するクエリパラメータ
 */
@Data
public class GetUsersQuery {

    /**
     * ユーザー名
     */
    @NotBlank
    private String name;

    /**
     * 下限年齢
     */
    @NotNull
    private Integer lowerLimitAge;

    /**
     * 上限年齢
     */
    @NotNull
    private Integer upperLimitAge;

}

相関バリデータクラス

クエリパラメータの下限年齢が上限年齢を上回っている時にエラーとする。

/**
 * {@link GetUsersQuery}の相関バリデータ
 */
@Component
public class GetUsersQueryValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return GetUsersQuery.class.isAssignableFrom(clazz);
    }

    /**
     * バリデーション実施
     *
     * @param target バリデーション対象
     * @param errors 検出されたエラー
     */
    @Override
    public void validate(Object target, Errors errors) {

        // 上限年齢、下限年齢のいずれかに単項エラーが発生している場合は相関バリデーションは実施しない
        if (errors.hasFieldErrors("lowerLimitAge") || errors.hasFieldErrors("upperLimitAge")) {
            return;
        }

        GetUsersQuery getUsersQuery = GetUsersQuery.class.cast(target);

        int lowerLimitAge = getUsersQuery.getLowerLimitAge();
        int upperLimitAge = getUsersQuery.getUpperLimitAge();

        // 上限年齢が下限年齢を超えていない場合はエラーとする
        if (lowerLimitAge >= upperLimitAge) {
            errors.reject("reverseLimitAge");
        }
    }
}

レスポンスボディ用エラークラス

エラー時はレスポンスボディにこの型のオブジェクトを格納する。

/**
 * リクエストボディにセットするエラー情報
 */
@Data
public class ApiError implements Serializable {

    private static final long serialVersionUID = 1L;

    private String message;
}

例外ハンドラクラス

以下のような、@ControllerAdviceを付与した例外ハンドラクラスを用意する。


/**
 * コントローラで発生した例外のハンドラ
 */
@ControllerAdvice
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {

    @Autowired
    MessageSource messageSource;

    /**
     * {@link BindException}をハンドリング
     *
     * @param bindException {@link BindException}
     * @param httpHeaders   {@link HttpHeaders}
     * @param httpStatus    {@link HttpStatus}
     * @param webRequest    {@link WebRequest}
     * @return クライアントへのレスポンス
     */
    @Override
    protected ResponseEntity<Object> handleBindException(
            BindException bindException,
            HttpHeaders httpHeaders,
            HttpStatus httpStatus,
            WebRequest webRequest
    ) {
        // レスポンスボディに格納するエラーリスト
        List<ApiError> apiErrorList = new ArrayList<>();

        List<ObjectError> objectErrorList = bindException.getAllErrors();

        for (ObjectError objectError : objectErrorList) {

            // エラーコードからメッセージ取得
            String message = messageSource.getMessage(objectError, webRequest.getLocale());

            // レスポンスボディ用エラーオブジェクトを作成しリストに格納
            ApiError apiError = new ApiError();
            apiError.setMessage(message);
            apiErrorList.add(apiError);
        }

        return new ResponseEntity<>(apiErrorList, httpHeaders, httpStatus);
    }
}

バリデーションエラー発生時はコントローラからエラー情報が格納されたBindExceptionが投げられる。
@ControllerAdviceを付与したクラスには、各コントローラ共通に適用したい処理を実装する。ResponseEntityExceptionHandlerを継承し、handleBindExceptionメソッドをオーバーライドすることで、バリデーションエラー時のレスポンスを自由にカスタムすることができる。

ここでは以下のようにカスタムしている。

  • レスポンスボディをApiError型に指定。
  • objectErrorのエラーコードからエラーメッセージに変換。

エラーコードは以下の形式でobjectErrorに格納されている。

単項バリデーション:「アノテーション名+クラス名(キャメルケース)+フィールド名」
相関バリデーション:「相関バリデータでセットしたエラーコード+クラス名(キャメルケース)」

messages.propertiesを以下のように用意すれば、メッセージへ変換される。

messages.properties

NotBlank.getUsersQuery.name=名前の入力は必須です。
NotNull.getUsersQuery.lowerLimitAge=下限年齢の入力は必須です。
NotNull.getUsersQuery.upperLimitAge=上限年齢の入力は必須です。
reverseLimitAge.getUsersQuery=上限年齢は下限年齢より大きい値を指定してください。
2
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3