web-dev-qa-db-fra.com

Liaison Spring Boot et gestion des erreurs de validation dans REST manette

Lorsque j'ai le modèle suivant avec des annotations JSR-303 (cadre de validation):

public enum Gender {
    MALE, FEMALE
}

public class Profile {
    private Gender gender;

    @NotNull
    private String name;

    ...
}

et les données JSON suivantes:

{ "gender":"INVALID_INPUT" }

Dans mon contrôleur REST, je souhaite gérer à la fois les erreurs de liaison (valeur d'enum non valide pour la propriété gender) et les erreurs de validation (la propriété name ne peut pas être null).

La méthode de contrôleur suivante ne fonctionne pas:

@RequestMapping(method = RequestMethod.POST)
public Profile insert(@Validated @RequestBody Profile profile, BindingResult result) {
    ...
}

Cela donne une erreur de sérialisation com.fasterxml.jackson.databind.exc.InvalidFormatException avant la liaison ou la validation.

Après quelques manipulations, j'ai créé ce code personnalisé qui fait ce que je veux:

@RequestMapping(method = RequestMethod.POST)
public Profile insert(@RequestBody Map values) throws BindException {

    Profile profile = new Profile();

    DataBinder binder = new DataBinder(profile);
    binder.bind(new MutablePropertyValues(values));

    // validator is instance of LocalValidatorFactoryBean class
    binder.setValidator(validator);
    binder.validate();

    // throws BindException if there are binding/validation
    // errors, exception is handled using @ControllerAdvice.
    binder.close(); 

    // No binding/validation errors, profile is populated 
    // with request values.

    ...
}

Fondamentalement, ce code fait une sérialisation sur une carte générique au lieu d'un modèle, puis utilise un code personnalisé pour se lier au modèle et vérifier les erreurs.

J'ai les questions suivantes:

  1. Le code personnalisé est-il la bonne solution ou existe-t-il une méthode plus standard dans Spring Boot?
  2. Comment fonctionne l'annotation @Validated? Comment créer ma propre annotation personnalisée fonctionnant comme @Validated pour encapsuler mon code de liaison personnalisé?
6
Jaap van Hengstum

C’est le code que j’ai utilisé dans un de mes projets pour valider REST api au démarrage du printemps, c’est différent de ce que vous avez demandé, mais identique. Vérifiez si cela aide. 

@RequestMapping(value = "/person/{id}",method = RequestMethod.PUT)
@ResponseBody
public Object updatePerson(@PathVariable Long id,@Valid Person p,BindingResult bindingResult){
    if (bindingResult.hasErrors()) {
        List<FieldError> errors = bindingResult.getFieldErrors();
        List<String> message = new ArrayList<>();
        error.setCode(-2);
        for (FieldError e : errors){
            message.add("@" + e.getField().toUpperCase() + ":" + e.getDefaultMessage());
        }
        error.setMessage("Update Failed");
        error.setCause(message.toString());
        return error;
    }
    else
    {
        Person person = personRepository.findOne(id);
        person = p;
        personRepository.save(person);
        success.setMessage("Updated Successfully");
        success.setCode(2);
        return success;
    }

Success.Java

public class Success {
int code;
String message;

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}
}

Error.Java

public class Error {
int code;
String message;
String cause;

public int getCode() {
    return code;
}

public void setCode(int code) {
    this.code = code;
}

public String getMessage() {
    return message;
}

public void setMessage(String message) {
    this.message = message;
}

public String getCause() {
    return cause;
}

public void setCause(String cause) {
    this.cause = cause;
}

}

Vous pouvez également consulter la page: Validation de printemps REST

4
al_mukthar

J'ai abandonné ça; Il est tout simplement impossible d'obtenir les erreurs de liaison à l'aide de @RequestBody sans beaucoup de code personnalisé. Cela diffère de la liaison des contrôleurs aux arguments JavaBeans simples, car @RequestBody utilise Jackson pour se lier à la place du gestionnaire de données Spring.

Voir https://jira.spring.io/browse/SPR-6740?jql=text%20~%20%22RequestBody%20binding%22

1
Jaap van Hengstum

Généralement, lorsque Spring MVC ne parvient pas à lire les messages http (par exemple, le corps de la demande), il lève une instance de HttpMessageNotReadableException exception. Donc, si le printemps ne pouvait pas se lier à votre modèle, il devrait lever cette exception. De même, si ET NON définissez une BindingResult après chaque modèle à valider dans les paramètres de votre méthode, en cas d'erreur de validation, spring lève une exception MethodArgumentNotValidException. Avec tout cela, vous pouvez créer ControllerAdvice qui intercepte ces deux exceptions et les gère de la manière souhaitée. 

@ControllerAdvice(annotations = {RestController.class})
public class UncaughtExceptionsControllerAdvice {
    @ExceptionHandler({MethodArgumentNotValidException.class, HttpMessageNotReadableException.class})
    public ResponseEntity handleBindingErrors(Exception ex) {
        // do whatever you want with the exceptions
    }
}
1
Ali Dehghani

Vous ne pouvez pas obtenir BindException avec @RequestBody. Pas dans le contrôleur avec un paramètre de méthode Errors tel que documenté ici:

Errors, BindingResult Pour accéder aux erreurs de validation et aux données liaison pour un objet de commande (c'est-à-dire un argument @ModelAttribute) ou erreurs de validation d'un @RequestBody ou d'un @RequestPart arguments. Vous devez déclarer un argument Errors ou BindingResult immédiatement après l'argument de la méthode validée.

Il indique que pour @ModelAttribute vous obtenez des erreurs de liaison AND validation et que pour votre @RequestBody vous obtenez uniquement erreurs de validation.

https://docs.spring.io/spring/docs/current/spring-framework-reference/web.html#mvc-ann-methods

Et cela a été discuté ici:

https://github.com/spring-projects/spring-framework/issues/11406?jql=text%2520~%2520%2522RequestBody%2520binding%2522

Pour moi, cela n'a toujours pas de sens du point de vue de l'utilisateur. Il est souvent très important que les exceptions BindExceptions montrent à l'utilisateur un message d'erreur approprié. L'argument est que vous devriez quand même faire une validation côté client. Mais ce n'est pas vrai si un développeur utilise directement l'API. 

Et imaginez que votre validation côté client repose sur une requête d'API. Vous voulez vérifier si une date donnée est valide en fonction d'un calendrier enregistré. Vous envoyez la date et l'heure au serveur et tout échoue. 

Vous pouvez modifier l'exception que vous obtenez avec un ExceptionHAndler réagissant sur HttpMessageNotReadableException, mais avec cette exception, je ne dispose pas d'un accès approprié au champ sur lequel l'erreur a été générée, contrairement à une exception BindException. Je dois analyser le message d'exception pour y accéder. 

Donc, je ne vois pas de solution, ce qui est plutôt dommage car avec @ModelAttribute, il est si facile d’obtenir des erreurs de liaison ET de validation.

0
Janning