Spring MVC: Wie wird eine Validierung durchgeführt?

156

Ich würde gerne wissen, was der sauberste und beste Weg ist, um eine Formularvalidierung von Benutzereingaben durchzuführen. Ich habe einige Entwickler implementieren sehen org.springframework.validation.Validator. Eine Frage dazu: Ich habe gesehen, dass es eine Klasse bestätigt. Muss die Klasse manuell mit den Werten aus der Benutzereingabe gefüllt und dann an den Validator übergeben werden?

Ich bin verwirrt über den saubersten und besten Weg, um die Benutzereingaben zu validieren. Ich kenne die traditionelle Methode zur Verwendung request.getParameter()und anschließenden manuellen Überprüfung nulls, möchte jedoch nicht die gesamte Validierung in meinem durchführen Controller. Einige gute Ratschläge in diesem Bereich werden sehr geschätzt. Ich verwende Hibernate in dieser Anwendung nicht.

devdar
quelle

Antworten:

322

Mit Spring MVC gibt es drei verschiedene Möglichkeiten, eine Validierung durchzuführen: manuelle Verwendung von Anmerkungen oder eine Mischung aus beiden. Es gibt keinen eindeutigen "saubersten und besten Weg" zur Validierung, aber es gibt wahrscheinlich einen, der besser zu Ihrem Projekt / Problem / Kontext passt.

Lassen Sie uns einen Benutzer haben:

public class User {

    private String name;

    ...

}

Methode 1: Wenn Sie Spring 3.x + und eine einfache Validierung durchführen müssen, verwenden Sie javax.validation.constraintsAnmerkungen (auch als JSR-303-Anmerkungen bezeichnet).

public class User {

    @NotNull
    private String name;

    ...

}

Sie benötigen einen JSR-303-Anbieter in Ihren Bibliotheken, z. B. Hibernate Validator, der die Referenzimplementierung darstellt (diese Bibliothek hat nichts mit Datenbanken und relationaler Zuordnung zu tun, sondern führt nur eine Validierung durch :-).

Dann hätten Sie in Ihrem Controller so etwas wie:

@RequestMapping(value="/user", method=RequestMethod.POST)
public createUser(Model model, @Valid @ModelAttribute("user") User user, BindingResult result){
    if (result.hasErrors()){
      // do something
    }
    else {
      // do something else
    }
}

Beachten Sie das @Valid: Wenn der Benutzer zufällig einen Nullnamen hat, ist result.hasErrors () wahr.

Methode 2: Wenn Sie über eine komplexe Validierung verfügen (z. B. Validierungslogik für große Unternehmen, bedingte Validierung über mehrere Felder usw.) oder aus irgendeinem Grund Methode 1 nicht verwenden können, verwenden Sie die manuelle Validierung. Es wird empfohlen, den Code des Controllers von der Validierungslogik zu trennen. Erstellen Sie Ihre Validierungsklasse (n) nicht von Grund auf neu, Spring bietet eine praktische org.springframework.validation.ValidatorOberfläche (seit Spring 2).

Nehmen wir an, Sie haben

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

und Sie möchten eine "komplexe" Validierung durchführen, z. B.: Wenn das Alter des Benutzers unter 18 Jahren liegt, darf der verantwortliche Benutzer nicht null und das Alter des verantwortlichen Benutzers über 21 Jahre sein.

Sie werden so etwas tun

public class UserValidator implements Validator {

    @Override
    public boolean supports(Class clazz) {
      return User.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {
      User user = (User) target;

      if(user.getName() == null) {
          errors.rejectValue("name", "your_error_code");
      }

      // do "complex" validation here

    }

}

Dann hätten Sie in Ihrem Controller:

@RequestMapping(value="/user", method=RequestMethod.POST)
    public createUser(Model model, @ModelAttribute("user") User user, BindingResult result){
        UserValidator userValidator = new UserValidator();
        userValidator.validate(user, result);

        if (result.hasErrors()){
          // do something
        }
        else {
          // do something else
        }
}

Wenn Validierungsfehler vorliegen, ist result.hasErrors () wahr.

Hinweis: Sie können den Validator auch in einer @ InstBinder-Methode des Controllers mit "binder.setValidator (...)" festlegen (in diesem Fall wäre eine gemischte Verwendung der Methoden 1 und 2 nicht möglich, da Sie die Standardeinstellung ersetzen Validator). Oder Sie können es im Standardkonstruktor des Controllers instanziieren. Oder haben Sie einen @ Component / @ Service UserValidator, den Sie in Ihren Controller einfügen (@Autowired): sehr nützlich, da die meisten Validatoren Singletons sind + das Verspotten von Unit-Tests einfacher wird + Ihr Validator andere Spring-Komponenten aufrufen könnte.

Methode 3: Warum nicht eine Kombination beider Methoden verwenden? Überprüfen Sie die einfachen Dinge wie das Attribut "name" mit Anmerkungen (schnell, präzise und besser lesbar). Behalten Sie die umfangreichen Validierungen für Validatoren bei (wenn es Stunden dauern würde, benutzerdefinierte komplexe Validierungsanmerkungen zu codieren, oder wenn es nicht möglich ist, Anmerkungen zu verwenden). Ich habe das bei einem früheren Projekt gemacht, es hat wie ein Zauber funktioniert, schnell und einfach.

Achtung: Sie müssen Fehler Validierung Handhabung für die Ausnahmebehandlung . Lesen Sie diesen Beitrag, um zu erfahren, wann Sie sie verwenden müssen.

Verweise :

Jerome Dalbert
quelle
Kannst du mir sagen, was meine servlet.xml für diese Konfiguration haben soll? Ich möchte die Fehler an die Ansicht zurückgeben
devdar
@dev_darin Du meinst Konfiguration für die JSR-303-Validierung?
Jerome Dalbert
2
@dev_marin Für die Validierung gibt es in Spring 3.x + in "servlet.xml" oder "[Servlet-Name] -servlet.xml nichts Besonderes. Sie benötigen lediglich die Jibernate-Validator-JAR in Ihren Projektbibliotheken (oder über Maven). Das ist alles, es sollte dann funktionieren. Warnung, wenn Sie Methode 3 verwenden: Standardmäßig hat jeder Controller Zugriff auf einen JSR-303-Validator. Achten Sie daher darauf, ihn nicht mit "setValidator" zu überschreiben. Wenn Sie einen benutzerdefinierten Validator hinzufügen möchten top, instanziieren Sie es einfach und verwenden Sie es oder injizieren Sie es (wenn es sich um eine Spring-Komponente handelt). Wenn Sie nach dem Überprüfen von Google und Spring Doc immer noch Probleme haben, sollten Sie eine neue Frage stellen.
Jerome Dalbert
2
Für die Mischanwendung von Methode 1 und 2 gibt es eine Möglichkeit, @InitBinder zu verwenden. Anstelle von "binder.setValidator (...)" kann "binder.addValidators (...)" verwendet werden
jasonfungsing
1
Korrigieren Sie mich, wenn ich falsch liege, aber Sie können die Validierung über JSR-303-Annotationen (Methode 1) und die benutzerdefinierte Validierung (Methode 2) mischen, wenn Sie die Annotation @InitBinder verwenden. Verwenden Sie einfach binder.addValidators (userValidator) anstelle von binder.setValidator (userValidator). Beide Validierungsmethoden werden wirksam.
SebastianRiemer
31

Es gibt zwei Möglichkeiten, Benutzereingaben zu validieren: Anmerkungen und das Erben der Validator-Klasse von Spring. Für einfache Fälle sind die Anmerkungen nett. Wenn Sie komplexe Validierungen benötigen (z. B. eine feldübergreifende Validierung, z. B. das Feld "E-Mail-Adresse überprüfen") oder wenn Ihr Modell an mehreren Stellen in Ihrer Anwendung mit unterschiedlichen Regeln validiert wird oder wenn Sie nicht in der Lage sind, Ihre zu ändern Modellieren Sie das Objekt, indem Sie Anmerkungen darauf platzieren. Der vererbungsbasierte Validator von Spring ist der richtige Weg. Ich werde Beispiele von beiden zeigen.

Der eigentliche Validierungsteil ist derselbe, unabhängig davon, welche Art von Validierung Sie verwenden:

RequestMapping(value="fooPage", method = RequestMethod.POST)
public String processSubmit(@Valid @ModelAttribute("foo") Foo foo, BindingResult result, ModelMap m) {
    if(result.hasErrors()) {
        return "fooPage";
    }
    ...
    return "successPage";
}

Wenn Sie Anmerkungen verwenden, Foosieht Ihre Klasse möglicherweise folgendermaßen aus:

public class Foo {

    @NotNull
    @Size(min = 1, max = 20)
    private String name;

    @NotNull
    @Min(1)
    @Max(110)
    private Integer age;

    // getters, setters
}

Anmerkungen oben sind javax.validation.constraintsAnmerkungen. Sie können auch den Ruhezustand verwenden org.hibernate.validator.constraints, aber es sieht nicht so aus, als würden Sie den Ruhezustand verwenden.

Wenn Sie alternativ Spring's Validator implementieren, erstellen Sie alternativ eine Klasse wie folgt:

public class FooValidator implements Validator {

    @Override
    public boolean supports(Class<?> clazz) {
        return Foo.class.equals(clazz);
    }

    @Override
    public void validate(Object target, Errors errors) {

        Foo foo = (Foo) target;

        if(foo.getName() == null) {
            errors.rejectValue("name", "name[emptyMessage]");
        }
        else if(foo.getName().length() < 1 || foo.getName().length() > 20){
            errors.rejectValue("name", "name[invalidLength]");
        }

        if(foo.getAge() == null) {
            errors.rejectValue("age", "age[emptyMessage]");
        }
        else if(foo.getAge() < 1 || foo.getAge() > 110){
            errors.rejectValue("age", "age[invalidAge]");
        }
    }
}

Wenn Sie den obigen Validator verwenden, müssen Sie den Validator auch an den Spring-Controller binden (nicht erforderlich, wenn Sie Anmerkungen verwenden):

@InitBinder("foo")
protected void initBinder(WebDataBinder binder) {
    binder.setValidator(new FooValidator());
}

Siehe auch Spring-Dokumente .

Hoffentlich hilft das.

stephen.hanson
quelle
Muss ich bei Verwendung von Spring's Validator das Pojo vom Controller aus einstellen und dann validieren?
Devdar
Ich bin mir nicht sicher, ob ich deine Frage verstehe. Wenn Sie das Controller-Code-Snippet sehen, bindet Spring das übermittelte Formular automatisch an den FooParameter in der Handler-Methode. Könntest Du das erläutern?
stephen.hanson
ok, was ich sage ist, wenn der Benutzer die Benutzereingaben sendet, erhält der Controller die http-Anfrage. Von dort aus verwenden Sie die request.getParameter (), um alle Benutzerparameter abzurufen, setzen dann die Werte im POJO und übergeben sie die Klasse zum Validierungsobjekt. Die Validierungsklasse sendet die Fehler mit den gefundenen Fehlern an die Ansicht zurück. Ist das so?
Devdar
1
Es würde so passieren, aber es gibt einen einfacheren Weg ... Wenn Sie JSP und eine <form: form commandName = "user"> -Übertragung verwenden, werden die Daten automatisch in den Benutzer @ModelAttribute ("user") im Controller eingefügt Methode. Siehe das Dokument: static.springsource.org/spring/docs/3.0.x/…
Jerome Dalbert
+1, weil dies das erste Beispiel ist, das ich gefunden habe und das @ModelAttribute verwendet; ohne es funktioniert keines der Tutorials, die ich gefunden habe.
Riccardo Cossu
12

Ich möchte die nette Antwort von Jerome Dalbert erweitern. Ich fand es sehr einfach, eigene Anmerkungsprüfer auf JSR-303-Weise zu schreiben. Sie sind nicht auf die Validierung "eines Felds" beschränkt. Sie können Ihre eigene Anmerkung auf Textebene erstellen und eine komplexe Validierung durchführen (siehe Beispiele unten). Ich bevorzuge diesen Weg, weil ich nicht verschiedene Arten der Validierung (Spring und JSR-303) mischen muss, wie es Jerome tut. Auch diese Validatoren sind "Spring-fähig", sodass Sie @ Inject / @ Autowire sofort verwenden können.

Beispiel für die Validierung benutzerdefinierter Objekte:

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { YourCustomObjectValidator.class })
public @interface YourCustomObjectValid {

    String message() default "{YourCustomObjectValid.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};
}

public class YourCustomObjectValidator implements ConstraintValidator<YourCustomObjectValid, YourCustomObject> {

    @Override
    public void initialize(YourCustomObjectValid constraintAnnotation) { }

    @Override
    public boolean isValid(YourCustomObject value, ConstraintValidatorContext context) {

        // Validate your complex logic 

        // Mark field with error
        ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
        cvb.addNode(someField).addConstraintViolation();

        return true;
    }
}

@YourCustomObjectValid
public YourCustomObject {
}

Beispiel für die Gleichheit generischer Felder:

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import javax.validation.Constraint;
import javax.validation.Payload;

@Target({ TYPE, ANNOTATION_TYPE })
@Retention(RUNTIME)
@Constraint(validatedBy = { FieldsEqualityValidator.class })
public @interface FieldsEquality {

    String message() default "{FieldsEquality.message}";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    /**
     * Name of the first field that will be compared.
     * 
     * @return name
     */
    String firstFieldName();

    /**
     * Name of the second field that will be compared.
     * 
     * @return name
     */
    String secondFieldName();

    @Target({ TYPE, ANNOTATION_TYPE })
    @Retention(RUNTIME)
    public @interface List {
        FieldsEquality[] value();
    }
}




import java.lang.reflect.Field;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ReflectionUtils;

public class FieldsEqualityValidator implements ConstraintValidator<FieldsEquality, Object> {

    private static final Logger log = LoggerFactory.getLogger(FieldsEqualityValidator.class);

    private String firstFieldName;
    private String secondFieldName;

    @Override
    public void initialize(FieldsEquality constraintAnnotation) {
        firstFieldName = constraintAnnotation.firstFieldName();
        secondFieldName = constraintAnnotation.secondFieldName();
    }

    @Override
    public boolean isValid(Object value, ConstraintValidatorContext context) {
        if (value == null)
            return true;

        try {
            Class<?> clazz = value.getClass();

            Field firstField = ReflectionUtils.findField(clazz, firstFieldName);
            firstField.setAccessible(true);
            Object first = firstField.get(value);

            Field secondField = ReflectionUtils.findField(clazz, secondFieldName);
            secondField.setAccessible(true);
            Object second = secondField.get(value);

            if (first != null && second != null && !first.equals(second)) {
                    ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
          cvb.addNode(firstFieldName).addConstraintViolation();

          ConstraintViolationBuilder cvb = context.buildConstraintViolationWithTemplate(context.getDefaultConstraintMessageTemplate());
          cvb.addNode(someField).addConstraintViolation(secondFieldName);

                return false;
            }
        } catch (Exception e) {
            log.error("Cannot validate fileds equality in '" + value + "'!", e);
            return false;
        }

        return true;
    }
}

@FieldsEquality(firstFieldName = "password", secondFieldName = "confirmPassword")
public class NewUserForm {

    private String password;

    private String confirmPassword;

}
michal.kreuzman
quelle
1
Ich habe mich auch gefragt, ob ein Controller normalerweise einen Validator hat, und ich habe gesehen, wo Sie mehrere Validatoren haben können. Wenn Sie jedoch einen Validierungssatz für ein Objekt definiert haben, ist die Operation, die Sie für das Objekt durchführen möchten, anders, z. B. Speichern / Aktualisieren für a Das Speichern eines bestimmten Validierungssatzes ist erforderlich, und ein Update eines anderen Validierungssatzes ist erforderlich. Gibt es eine Möglichkeit, die Validator-Klasse so zu konfigurieren, dass sie die gesamte Validierung basierend auf der Operation enthält, oder müssen Sie mehrere Validatoren verwenden?
Devdar
1
Sie können auch eine Annotationsvalidierung für die Methode durchführen lassen. Sie können also Ihre eigene "Domain-Validierung" erstellen, wenn ich Ihre Frage verstehe. Dazu Sie angeben müssen , ElementType.METHODin @Target.
michal.kreuzman
Ich verstehe, was du sagst. Kannst du mich auch auf ein Beispiel für ein klareres Bild verweisen?
Devdar
4

Wenn Sie dieselbe Fehlerbehandlungslogik für verschiedene Methodenhandler haben, erhalten Sie viele Handler mit folgendem Codemuster:

if (validation.hasErrors()) {
  // do error handling
}
else {
  // do the actual business logic
}

Angenommen, Sie erstellen RESTful-Services und möchten 400 Bad Requestfür jeden Validierungsfehlerfall zusammen mit Fehlermeldungen zurückkehren . Dann wäre der Fehlerbehandlungsteil für jeden einzelnen REST-Endpunkt, der validiert werden muss, gleich. Die gleiche Logik in jedem einzelnen Handler zu wiederholen, ist nicht so trocken !

Eine Möglichkeit, dieses Problem zu lösen, besteht darin, die unmittelbar BindingResultnach jeder zu validierenden Bean zu löschen . Nun würde Ihr Handler so aussehen:

@RequestMapping(...)
public Something doStuff(@Valid Somebean bean) { 
    // do the actual business logic
    // Just the else part!
}

Auf diese Weise MethodArgumentNotValidExceptionwird a von Spring geworfen , wenn die gebundene Bohne nicht gültig war . Sie können eine definieren ControllerAdvice, die diese Ausnahme mit derselben Fehlerbehandlungslogik behandelt:

@ControllerAdvice
public class ErrorHandlingControllerAdvice {
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public SomeErrorBean handleValidationError(MethodArgumentNotValidException ex) {
        // do error handling
        // Just the if part!
    }
}

Sie können den Basiswert weiterhin BindingResultmit der getBindingResultMethode von untersuchen MethodArgumentNotValidException.

Ali Dehghani
quelle
1

Hier finden Sie ein vollständiges Beispiel für die Spring Mvc-Validierung

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import com.technicalkeeda.bean.Login;

public class LoginValidator implements Validator {
    public boolean supports(Class aClass) {
        return Login.class.equals(aClass);
    }

    public void validate(Object obj, Errors errors) {
        Login login = (Login) obj;
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userName",
                "username.required", "Required field");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "userPassword",
                "userpassword.required", "Required field");
    }
}


public class LoginController extends SimpleFormController {
    private LoginService loginService;

    public LoginController() {
        setCommandClass(Login.class);
        setCommandName("login");
    }

    public void setLoginService(LoginService loginService) {
        this.loginService = loginService;
    }

    @Override
    protected ModelAndView onSubmit(Object command) throws Exception {
        Login login = (Login) command;
        loginService.add(login);
        return new ModelAndView("loginsucess", "login", login);
    }
}
Vicky
quelle
0

Fügen Sie diese Bean in Ihre Konfigurationsklasse ein.

 @Bean
  public Validator localValidatorFactoryBean() {
    return new LocalValidatorFactoryBean();
  }

und dann können Sie verwenden

 <T> BindingResult validate(T t) {
    DataBinder binder = new DataBinder(t);
    binder.setValidator(validator);
    binder.validate();
    return binder.getBindingResult();
}

zum manuellen Validieren einer Bean. Dann erhalten Sie alle Ergebnisse in BindingResult und können von dort abrufen.

Praveen Jain
quelle