SpringではRestController
に対応するメソッドパラメータに@Valid
または@Validated
と注釈を付けるだけでRequestBody
の入力検証を行います。他の一部の検証は、受信データのいくつかの処理後にのみ実行できます。私の質問は、@Valid
アノテーションによってスローされる例外に似ているように、どのタイプの例外を使用する必要があるか、および検証結果からこの例外をどのように構成するかです。次に例を示します。
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> createOrder(@RequestBody @Validated(InputChecks.class) Order order) {
// Some processing of the Order goes here
Set<ConstraintViolation<Order>> violations = validator.validate(order, FinalChecks.class);
// What to do now with the validation errors?
orders.put(order);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(ServletUriComponentsBuilder.fromCurrentRequest().path("/" + order.getId()).build().toUri());
return new ResponseEntity<>(null, headers, HttpStatus.CREATED);
}
私にとって最も簡単な方法は、エラーオブジェクトでオブジェクトを検証し、それをMethodArgumentNotValidExceptionで使用するように見えます。
@RequestMapping(method = RequestMethod.POST)
public ResponseEntity<?> createOrder(@RequestBody @Validated(InputChecks.class) Order order)
throws NoSuchMethodException, SecurityException, MethodArgumentNotValidException {
// Some processing of the Order goes here
SpringValidatorAdapter v = new SpringValidatorAdapter(validator);
BeanPropertyBindingResult errors = new BeanPropertyBindingResult(order, "order");
v.validate(order, errors, FinalChecks.class);
if (errors.hasErrors()) {
throw new MethodArgumentNotValidException(
new MethodParameter(this.getClass().getDeclaredMethod("createOrder", Order.class), 0),
errors);
}
orders.put(order);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(ServletUriComponentsBuilder.fromCurrentRequest().path("/" + order.getId()).build().toUri());
return new ResponseEntity<>(null, headers, HttpStatus.CREATED);
}
このように、2番目の検証ステップ中に検出されたエラーは、@ validatedパラメーターの入力検証中に検出されたエラーとまったく同じ構造を持っています。
2回目の実行で検証エラーを処理するために、3つの異なるアプローチを考えることができます。最初に、検証エラーメッセージをSet
sのConstraintViolation
から抽出し、検証エラーメッセージを応答本文として、適切なHTTP応答、たとえば400 Bad Request
を返すことができます。
Set<ConstraintViolation<Order>> violations = validator.validate(order, FinalChecks.class);
if (!violations.isEmpty()) {
Set<String> validationMessages = violations
.stream()
.map(ConstraintViolation::getMessage)
.collect(Collectors.toSet());
return ResponseEntity.badRequest().body(validationMessages);
}
// the happy path
このアプローチは、いくつかのコントローラーで二重検証が必要な場合に適しています。それ以外の場合は、まったく新しいException
をスローするか、Spring関連の例外を再利用して、MethodArgumentNotValidException
と言い、それらを普遍的に処理するControllerAdvice
を定義することをお勧めします。
Set<ConstraintViolation<Order>> violations = validator.validate(order, FinalChecks.class);
if (!violations.isEmpty()) {
throw new ValidationException(violations);
}
そしてコントローラーのアドバイス:
@ControllerAdvice
public class ValidationControllerAdvice {
@ExceptionHandler(ValidationException.class)
public ResponseEntity handleValidtionErrors(ValidationException ex) {
return ResponseEntity.badRequest().body(ex.getViolations().stream()...);
}
}
MethodArgumentNotValidException
のような春の例外の1つをスローすることもできます。そのためには、Set
sのConstraintViolation
をBindingResult
のインスタンスに変換し、それをMethodArgumentNotValidException
のコンストラクターに渡す必要があります。