web-dev-qa-db-fra.com

Quel type d'exception à lever dans Spring RestController lorsque la validation échoue?

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);
}
13
Gregor

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.

16
Gregor

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 ConstraintViolations, 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 ConstraintViolations en une instance de BindingResult et le transmettre au constructeur de MethodArgumentNotValidException.

7
Ali Dehghani