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:
@Validated
? Wie kann ich meine eigene benutzerdefinierte Anmerkung erstellen, die wie @Validated
funktioniert, um meinen benutzerdefinierten Bindungscode zu kapseln?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
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
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
}
}
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:
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.
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:
AOP
(aspectj version) könnte man einfach die Standard-Deserialisierer abfangen, die Ausnahmen analysieren, unterdrücken und sammelnBeanDeserializerModifier
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ützungDer 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
).
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.