結論
@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=上限年齢は下限年齢より大きい値を指定してください。