web-dev-qa-db-de.com

Spring Boot-Bindung und Validierungsfehlerbehandlung in REST Regler

Wenn ich folgendes Modell mit JSR-303 (Validierungsframework) -Anmerkungen habe:

public enum Gender {
    MALE, FEMALE
}

public class Profile {
    private Gender gender;

    @NotNull
    private String name;

    ...
}

und die folgenden JSON-Daten:

{ "gender":"INVALID_INPUT" }

In meinem REST-Controller möchte ich sowohl die Bindungsfehler (ungültiger Aufzählungswert für die gender-Eigenschaft) als auch Validierungsfehler behandeln (die name-Eigenschaft kann nicht null sein).

Die folgende Controller-Methode funktioniert NICHT:

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

Dies führt zu einem com.fasterxml.jackson.databind.exc.InvalidFormatException-Serialisierungsfehler, bevor die Bindung oder Validierung erfolgt.

Nach einigem Fummeln habe ich diesen benutzerdefinierten Code gefunden, der macht was ich will:

@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.

    ...
}

Grundsätzlich wird dieser Code in eine generische Map anstelle eines Modells serialisiert und dann mit benutzerdefiniertem Code an das Modell gebunden und auf Fehler geprüft.

Ich habe folgende Fragen:

  1. Ist benutzerdefinierter Code der richtige Weg, oder gibt es eine Standardmethode für Spring Boot?
  2. Wie funktioniert die Annotation @Validated? Wie kann ich meine eigene benutzerdefinierte Anmerkung erstellen, die wie @Validated funktioniert, um meinen benutzerdefinierten Bindungscode zu kapseln?
6

Dies ist der Code, den ich in einem meiner Projekte zur Validierung von REST api in Spring Boot verwendet habe. Dies ist nicht das, was Sie gefordert haben, aber es ist identisch. Prüfen Sie, ob dies hilft 

@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;
}

}

Sie können auch hier nachschauen: Spring REST Validation

4
al_mukthar

Ich habe das aufgegeben; Es ist einfach nicht möglich, die Bindungsfehler mit @RequestBody ohne viel benutzerdefinierten Code zu erhalten. Dies unterscheidet sich von Controllern, die an einfache JavaBeans-Argumente gebunden werden, da @RequestBody Jackson anstelle des Spring-Datenbinders zum Binden verwendet.

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

1

Wenn Spring MVC die http-Nachrichten nicht lesen kann (z. B. Anforderungshauptteil), wird normalerweise eine Instanz der Variable HttpMessageNotReadableException ausgelöst. Wenn der Frühling sich nicht an Ihr Modell binden konnte, sollte diese Ausnahme ausgelöst werden. Wenn SieNICHTnach jedem zu validierenden Modell in Ihren Methodenparametern eine BindingResult definieren, löst spring im Falle eines Validierungsfehlers eine MethodArgumentNotValidException-Ausnahme aus. Mit all dem können Sie ControllerAdvice erstellen, das diese beiden Ausnahmen abfängt und in der gewünschten Weise behandelt. 

@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

Sie können BindException nicht mit @RequestBody erhalten. Nicht in der Steuerung mit einem Errors-Methodenparameter, wie hier dokumentiert:

Fehler, BindingResult Für den Zugriff auf Fehler aus der Validierung und Datenbindung Für ein Befehlsobjekt (d. H. Ein @ModelAttribute-Argument) oder Fehler aus der Validierung eines @RequestBody oder @RequestPart Argumente. Sie müssen ein Fehler- oder BindingResult-Argument Unmittelbar nach dem validierten Methodenargument deklarieren.

Es gibt an, dass Sie für @ModelAttribute verbindliche AND-Validierungsfehler und für Ihren @RequestBody nur Validierungsfehler erhalten.

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

Und es wurde hier diskutiert:

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

Für mich macht es aus Anwendersicht immer noch keinen Sinn. Es ist oft sehr wichtig, die BindExceptions zu erhalten, um dem Benutzer eine korrekte Fehlermeldung anzuzeigen. Das Argument lautet, Sie sollten trotzdem eine clientseitige Validierung durchführen. Dies gilt jedoch nicht, wenn ein Entwickler die API direkt verwendet. 

Stellen Sie sich vor, Ihre clientseitige Validierung basiert auf einer API-Anforderung. Sie möchten anhand eines gespeicherten Kalenders prüfen, ob ein bestimmtes Datum gültig ist. Sie senden das Datum und die Uhrzeit an das Backend und es schlägt fehl. 

Sie können die Ausnahme ändern, die Sie mit einem ExceptionHAndler erhalten, der auf HttpMessageNotReadableException reagiert. Mit dieser Ausnahme habe ich jedoch keinen richtigen Zugriff auf das Feld, das den Fehler wie bei einer BindException ausgelöst hat. Ich muss die Ausnahmemeldung analysieren, um darauf zugreifen zu können. 

Ich sehe also keine Lösung, was irgendwie schlecht ist, weil es mit @ModelAttribute so einfach ist, verbindliche UND Validierungsfehler zu bekommen.

0
Janning

Einer der Hauptgründe für die Behebung dieses Problems ist die Standardeinstellung Eifrig fehlgeschlagen des Jackson-Datenbinders. man müsste irgendwie überzeugen damit weitermachen anstatt nur beim ersten fehler zu stolpern. Man müsste auch sammeln diese Parsing-Fehler machen, um sie letztendlich in BindingResult -Einträge umzuwandeln. Grundsätzlich müsste man catch, suppress und collect Ausnahmen analysieren, convert sie zu BindingResult Einträgen dann add diese Einträge rechts @Controller Methode BindingResult Argument.

Der catch & suppress Teil könnte gemacht werden durch:

  • benutzerdefinierte Jackson-Deserialisierer die einfach an die Standard-verwandten delegieren würden, aber auch ihre Parsing-Ausnahmen abfangen, unterdrücken und sammeln würden
  • mit AOP (aspectj version) könnte man einfach die Standard-Deserialisierer abfangen, die Ausnahmen analysieren, unterdrücken und sammeln
  • verwenden von andere Mittel, z. Wenn Sie BeanDeserializerModifier verwenden, können Sie auch die Parsing-Ausnahmen abfangen, unterdrücken und sammeln. Dies mag der einfachste Ansatz sein, erfordert jedoch einige Kenntnisse über diese jacksonspezifische Anpassungsunterstützung

Der Teil Erfassung könnte eine Variable ThreadLocal verwenden, um alle erforderlichen ausnahmebezogenen Details zu speichern. Die Konvertierung in BindingResult Einträge und die Addition rechts BindingResult Argumente könnten ziemlich einfach durch ein AOP Interceptor auf @Controller - Methoden (jede Art von AOP, einschließlich Spring-Variante).

Was ist der Gewinn

Durch diesen Ansatz erhält man die Daten Binding Fehler (zusätzlich zu den Validation) in das Argument BindingResult auf die gleiche Weise wie erwartet um sie zu bekommen, wenn ein z @ModelAttribute. Es wird auch mit mehreren Ebenen von eingebetteten Objekten funktionieren - die in der Frage vorgestellte Lösung spielt damit keine Rolle.

Lösungsdetails ( benutzerdefinierte Jackson-Deserialisierer Ansatz)

Ich habe ein kleines Projekt, das die Lösung beweist (Testklasse ausführen) erstellt, während ich hier nur die Hauptteile hervorhole:

/**
* The logic for copying the gathered binding errors 
* into the @Controller method BindingResult argument.
* 
* This is the most "complicated" part of the project.
*/
@Aspect
@Component
public class BindingErrorsHandler {
    @Before("@within(org.springframework.web.bind.annotation.RestController)")
    public void logBefore(JoinPoint joinPoint) {
        // copy the binding errors gathered by the custom
        // jackson deserializers or by other means
        Arrays.stream(joinPoint.getArgs())
                .filter(o -> o instanceof BindingResult)
                .map(o -> (BindingResult) o)
                .forEach(errors -> {
                    JsonParsingFeedBack.ERRORS.get().forEach((k, v) -> {
                        errors.addError(new FieldError(errors.getObjectName(), k, v, true, null, null, null));
                    });
                });
        // errors copied, clean the ThreadLocal
        JsonParsingFeedBack.ERRORS.remove();
    }
}

/**
 * The deserialization logic is in fact the one provided by jackson,
 * I only added the logic for gathering the binding errors.
 */
public class CustomIntegerDeserializer extends StdDeserializer<Integer> {
    /**
    * Jackson based deserialization logic. 
    */
    @Override
    public Integer deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
        try {
            return wrapperInstance.deserialize(p, ctxt);
        } catch (InvalidFormatException ex) {
            gatherBindingErrors(p, ctxt);
        }
        return null;
    }

    // ... gatherBindingErrors(p, ctxt), mandatory constructors ...
}

/**
* A simple classic @Controller used for testing the solution.
*/
@RestController
@RequestMapping("/errormixtest")
@Slf4j
public class MixBindingAndValidationErrorsController {
    @PostMapping(consumes = MediaType.APPLICATION_JSON_UTF8_VALUE)
    public Level1 post(@Valid @RequestBody Level1 level1, BindingResult errors) {
    // at the end I show some BindingResult logging for a @RequestBody e.g.:
    // {"nr11":"x","nr12":1,"level2":{"nr21":"xx","nr22":1,"level3":{"nr31":"xxx","nr32":1}}}
    // ... your whatever logic here ...

Mit diesen erhalten Sie in BindingResult so etwas:

Field error in object 'level1' on field 'nr12': rejected value [1]; codes [Min.level1.nr12,Min.nr12,Min.Java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.nr12,nr12]; arguments []; default message [nr12],5]; default message [must be greater than or equal to 5]
Field error in object 'level1' on field 'nr11': rejected value [x]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.level3.nr31': rejected value [xxx]; codes []; arguments []; default message [null]
Field error in object 'level1' on field 'level2.nr22': rejected value [1]; codes [Min.level1.level2.nr22,Min.level2.nr22,Min.nr22,Min.Java.lang.Integer,Min]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [level1.level2.nr22,level2.nr22]; arguments []; default message [level2.nr22],5]; default message [must be greater than or equal to 5]

wobei die 1. Zeile durch einen Validierung Fehler bestimmt wird (Setzen von 1 als Wert für eine @Min(5) private Integer nr12;), während die 2. Zeile durch einen binding one (setzt "x" als Wert für eine @JsonDeserialize(using = CustomIntegerDeserializer.class) private Integer nr11;). 3rd-Line-Tests Bindungsfehler mit eingebetteten Objekten: level1 Enthält ein level2, Das eine Objekteigenschaft level3 Enthält.

Beachten Sie, wie andere Ansätze einfach die Verwendung von benutzerdefinierten Jackson-Deserialisierern ersetzen könnte, während der Rest der Lösung erhalten bleibt (AOP, JsonParsingFeedBack).

0
adrhc

Gemäß diesem Beitrag https://blog.codecentric.de/de/2017/11/dynamic-validation-spring-boot-validation/ - können Sie Ihrem Controller einen zusätzlichen Parameter "Errors" hinzufügen Methode - zB.

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

um dann etwaige Validierungsfehler zu erhalten.

0
anders.norgaard