Dans un Spring RestController
j'ai une validation d'entrée du RequestBody
simplement en annotant le paramètre de méthode correspondant comme @Valid
ou @Validated
. Certaines autres validations ne peuvent être effectuées qu'après un certain traitement des données entrantes. Ma question est, quel type d'exceptions dois-je utiliser, afin qu'il ressemble à l'exception levée par le @Valid
annotation, et comment puis-je construire cette exception à partir du résultat de la validation. Voici un exemple:
@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);
}
Pour moi, la manière la plus simple ressemble à valider l'objet avec un objet erreurs et à l'utiliser dans une exception 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);
}
De cette façon, les erreurs trouvées lors de la deuxième étape de validation ont exactement la même structure que les erreurs trouvées lors de la validation d'entrée sur les paramètres @validated.
Pour gérer les erreurs de validation lors de la deuxième exécution, je peux penser à trois approches différentes. Tout d'abord, vous pouvez extraire les messages d'erreur de validation de Set
de ConstraintViolation
s, puis renvoyer une réponse HTTP appropriée, par exemple 400 Bad Request
, avec des messages d'erreur de validation comme corps de réponse:
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
Cette approche convient aux situations où la double validation est une exigence pour quelques contrôleurs. Sinon, il est préférable de lancer un Exception
flambant neuf ou de réutiliser les exceptions liées au ressort, par exemple MethodArgumentNotValidException
, et de définir un ControllerAdvice
qui les gère universellement:
Set<ConstraintViolation<Order>> violations = validator.validate(order, FinalChecks.class);
if (!violations.isEmpty()) {
throw new ValidationException(violations);
}
Et les conseils du contrôleur:
@ControllerAdvice
public class ValidationControllerAdvice {
@ExceptionHandler(ValidationException.class)
public ResponseEntity handleValidtionErrors(ValidationException ex) {
return ResponseEntity.badRequest().body(ex.getViolations().stream()...);
}
}
Vous pouvez également lever une des exceptions de ressort comme MethodArgumentNotValidException
. Pour ce faire, vous devez convertir le Set
de ConstraintViolation
s en une instance de BindingResult
et le transmettre au constructeur de MethodArgumentNotValidException
.