Spring MVC: Jak przeprowadzić walidację?

156

Chciałbym wiedzieć, jaki jest najczystszy i najlepszy sposób przeprowadzania walidacji formularza danych wprowadzanych przez użytkownika. Widziałem, jak niektórzy programiści wdrażali org.springframework.validation.Validator. Pytanie na ten temat: widziałem, że waliduje klasę. Czy klasa musi być wypełniona ręcznie wartościami z danych wprowadzonych przez użytkownika, a następnie przekazana do walidatora?

Jestem zdezorientowany co do najczystszego i najlepszego sposobu sprawdzania poprawności danych wejściowych użytkownika. Wiem o tradycyjnej metodzie używania, request.getParameter()a następnie ręcznego sprawdzania nulls, ale nie chcę wykonywać wszystkich weryfikacji w moim Controller. Bardzo cenne będą dobre rady w tej dziedzinie. Nie używam Hibernacji w tej aplikacji.

devdar
źródło

Odpowiedzi:

322

W Spring MVC istnieją 3 różne sposoby przeprowadzania walidacji: przy użyciu adnotacji, ręcznej lub mieszanej. Nie ma unikalnego „najczystszego i najlepszego sposobu” weryfikacji, ale prawdopodobnie jest taki, który lepiej pasuje do twojego projektu / problemu / kontekstu.

Miejmy użytkownika:

public class User {

    private String name;

    ...

}

Metoda 1: Jeśli masz Spring 3.x + i prostą weryfikację do wykonania, użyj javax.validation.constraintsadnotacji (znanych również jako adnotacje JSR-303).

public class User {

    @NotNull
    private String name;

    ...

}

Będziesz potrzebował dostawcy JSR-303 w swoich bibliotekach, takiego jak Hibernate Validator, który jest implementacją referencyjną (ta biblioteka nie ma nic wspólnego z bazami danych i mapowaniem relacyjnym, po prostu wykonuje walidację :-).

Wtedy w kontrolerze miałbyś coś takiego:

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

Zwróć uwagę na @Valid: jeśli użytkownik ma pustą nazwę, result.hasErrors () będzie true.

Metoda 2: Jeśli masz złożoną walidację (taką jak logika walidacji dużych firm, walidacja warunkowa w wielu polach itp.) Lub z jakiegoś powodu nie możesz użyć metody 1, użyj walidacji ręcznej. Dobrą praktyką jest oddzielenie kodu kontrolera od logiki walidacji. Nie twórz swoich klas walidacji od zera, Spring zapewnia wygodny org.springframework.validation.Validatorinterfejs (od Spring 2).

Powiedzmy, że masz

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

i chcesz przeprowadzić „złożoną” walidację, na przykład: jeśli użytkownik ma mniej niż 18 lat, odpowiedzialnyUser nie może być zerowy, a odpowiedzialny użytkownik musi mieć ukończone 21 lat.

Zrobisz coś takiego

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

    }

}

Wtedy w kontrolerze miałbyś:

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

Jeśli wystąpią błędy walidacji, wartość result.hasErrors () będzie true.

Uwaga: Możesz również ustawić walidator w metodzie @InitBinder kontrolera, z "binder.setValidator (...)" (w takim przypadku użycie metody 1 i 2 nie byłoby możliwe, ponieważ zastępujesz domyślną walidator). Lub możesz utworzyć instancję w domyślnym konstruktorze kontrolera. Możesz też mieć @ Component / @ Service UserValidator, który wstrzykujesz (@Autowired) do kontrolera: bardzo przydatne, ponieważ większość walidatorów to pojedyncze + testy jednostkowe stają się łatwiejsze + Twój walidator może wywoływać inne komponenty Spring.

Metoda 3: Dlaczego nie użyć kombinacji obu metod? Zweryfikuj proste rzeczy, takie jak atrybut „nazwa”, za pomocą adnotacji (jest to szybkie, zwięzłe i bardziej czytelne). Zachowaj ciężkie walidacje dla walidatorów (kiedy kodowanie niestandardowych złożonych adnotacji walidacyjnych zajmie wiele godzin lub tylko wtedy, gdy nie można użyć adnotacji). Zrobiłem to w poprzednim projekcie, zadziałało jak urok, szybko i łatwo.

Ostrzeżenie: nie należy pomyłka obsługa walidacji dla obsługi wyjątków . Przeczytaj ten post, aby dowiedzieć się, kiedy ich używać.

Bibliografia :

Jerome Dalbert
źródło
czy możesz mi powiedzieć, co powinien mieć mój plik servlet.xml dla tej konfiguracji. Chcę przekazać błędy z powrotem do widoku
devdar
@dev_darin Masz na myśli konfigurację do walidacji JSR-303?
Jerome Dalbert
2
@dev_marin Do walidacji, wiosną 3.x +, nie ma nic specjalnego w plikach "servlet.xml" lub "[nazwa-serwletu] -servlet.xml. Wystarczy hibernate-validator jar w bibliotekach projektu (lub przez Maven). To wszystko, powinno wtedy działać. Ostrzeżenie, jeśli używasz metody 3: domyślnie każdy kontroler ma dostęp do walidatora JSR-303, więc uważaj, aby nie zastąpić go wartością „setValidator”. Jeśli chcesz dodać niestandardowy walidator na do góry, po prostu utwórz jego instancję i użyj jej lub wstrzyknij (jeśli jest to komponent Spring). Jeśli nadal masz problemy po sprawdzeniu google i dokumentu Spring, powinieneś zadać nowe pytanie.
Jerome Dalbert
2
W przypadku mieszania metod 1 i 2 istnieje sposób na użycie @InitBinder. Zamiast "binder.setValidator (...)" można użyć "binder.addValidators (...)"
jasonfungsing
1
Popraw mnie, jeśli się mylę, ale możesz łączyć walidację za pomocą adnotacji JSR-303 (metoda 1) i niestandardową walidację (metoda 2), używając adnotacji @InitBinder. Po prostu użyj binder.addValidators (userValidator) zamiast binder.setValidator (userValidator), a obie metody walidacji zaczną obowiązywać.
SebastianRiemer,
31

Istnieją dwa sposoby sprawdzania poprawności danych wejściowych użytkownika: adnotacje i dziedziczenie klasy Validator Springa. W prostych przypadkach adnotacje są ładne. Jeśli potrzebujesz złożonych walidacji (takich jak walidacja między polami, np. Pole „zweryfikuj adres e-mail”) lub jeśli Twój model jest sprawdzany w wielu miejscach w aplikacji z różnymi regułami lub jeśli nie masz możliwości modyfikowania model poprzez umieszczenie na nim adnotacji, Validator oparty na dziedziczeniu Springa jest drogą do zrobienia. Pokażę przykłady obu.

Rzeczywista część walidacji jest taka sama, niezależnie od tego, jakiego typu walidacji używasz:

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

Jeśli używasz adnotacji, Twoja Fooklasa może wyglądać następująco:

public class Foo {

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

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

    // getters, setters
}

Powyższe javax.validation.constraintsadnotacje to adnotacje. Możesz także użyć Hibernacji org.hibernate.validator.constraints, ale wygląda na to, że nie używasz Hibernacji.

Alternatywnie, jeśli zaimplementujesz Validator Springa, możesz utworzyć klasę w następujący sposób:

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]");
        }
    }
}

Jeśli używasz powyższego walidatora, musisz również powiązać walidator z kontrolerem Spring (nie jest to konieczne, jeśli używasz adnotacji):

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

Zobacz także dokumentację Spring .

Mam nadzieję, że to pomoże.

stephen.hanson
źródło
Czy podczas korzystania z Walidatora Springa muszę ustawić pojo ze sterownika, a następnie je zweryfikować?
devdar
Nie jestem pewien, czy rozumiem twoje pytanie. Jeśli widzisz fragment kodu kontrolera, Spring automatycznie wiąże przesłany formularz z Fooparametrem w metodzie obsługi. Możesz wyjaśnić?
stephen.hanson
ok, mówię, gdy użytkownik przesyła dane wejściowe użytkownika, kontroler otrzymuje żądanie http, stamtąd dzieje się tak, że używasz request.getParameter (), aby uzyskać wszystkie parametry użytkownika, a następnie ustaw wartości w POJO, a następnie przejdź klasę do obiektu walidacji. Klasa walidacyjna wyśle ​​błędy z powrotem do widoku wraz z błędami, jeśli takie zostaną znalezione. Czy tak to się dzieje?
devdar
1
Tak by się stało, ale istnieje prostszy sposób ... Jeśli używasz JSP i <form: form commandName = "user">, dane są automatycznie umieszczane w użytkowniku @ModelAttribute ("user") w kontrolerze metoda. Zobacz dokumentację: static.springsource.org/spring/docs/3.0.x/…
Jerome Dalbert,
+1, ponieważ to pierwszy znaleziony przeze mnie przykład wykorzystujący @ModelAttribute; bez niego żaden z samouczków, które znalazłem, nie działa.
Riccardo Cossu
12

Chciałbym udzielić miłej odpowiedzi Jerome'a ​​Dalberta. Znalazłem bardzo łatwe pisanie własnych walidatorów adnotacji w sposób JSR-303. Nie jesteś ograniczony do walidacji „jednego pola”. Możesz stworzyć własną adnotację na poziomie typu i mieć złożoną walidację (zobacz przykłady poniżej). Wolę ten sposób, ponieważ nie potrzebuję mieszać różnych typów walidacji (Spring i JSR-303), jak robią to Jerome. Również te walidatory są "świadome wiosny", więc możesz używać @ Inject / @ Autowire po wyjęciu z pudełka.

Przykład weryfikacji obiektu niestandardowego:

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

Przykład równości pól ogólnych:

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
źródło
1
Zastanawiałem się również, że kontroler ma zwykle jeden walidator i zobaczyłem, gdzie można mieć wiele walidatorów, jednak jeśli masz zdefiniowany zestaw walidacji dla jednego obiektu, to jednak operacja, którą chcesz wykonać na obiekcie, jest inna, np. Zapis / aktualizacja dla zapisanie określonego zestawu walidacji jest wymagane i aktualizacja innego zestawu walidacji jest wymagany. Czy istnieje sposób na skonfigurowanie klasy walidatora tak, aby zawierała całą walidację w oparciu o operację, czy też będziesz musiał używać wielu walidatorów?
devdar
1
Możesz również przeprowadzić walidację adnotacji w metodzie. Możesz więc utworzyć własną „weryfikację domeny”, jeśli zrozumiem Twoje pytanie. W tym celu należy określić ElementType.METHODw @Target.
michal.kreuzman
Rozumiem, co mówisz, czy możesz również wskazać mi przykład, aby uzyskać bardziej przejrzysty obraz.
devdar
4

Jeśli masz taką samą logikę obsługi błędów dla różnych programów obsługi metod, otrzymasz wiele programów obsługi z następującym wzorcem kodu:

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

Załóżmy, że tworzysz usługi RESTful i chcesz powrócić 400 Bad Requestwraz z komunikatami o błędach dla każdego przypadku błędu walidacji. Następnie część obsługi błędów byłaby taka sama dla każdego punktu końcowego REST, który wymaga weryfikacji. Powtarzanie tej samej logiki w każdym programie obsługi nie jest takie SUCHE !

Jednym ze sposobów rozwiązania tego problemu jest porzucenie natychmiast BindingResultpo każdym ziarnie podlegającym walidacji . Teraz twój handler wyglądałby tak:

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

W ten sposób, jeśli związana fasola nie była ważna, MethodArgumentNotValidExceptionwiosną zostanie wyrzucony testament. Możesz zdefiniować a, ControllerAdvicektóry obsługuje ten wyjątek z tą samą logiką obsługi błędów:

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

Nadal możesz zbadać podstawę BindingResultprzy użyciu getBindingResultmetody MethodArgumentNotValidException.

Ali Dehghani
źródło
1

Znajdź pełny przykład weryfikacji Spring Mvc

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
źródło
0

Umieść to ziarno w swojej klasie konfiguracji.

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

a następnie możesz użyć

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

do ręcznego sprawdzania poprawności ziarna. Następnie otrzymasz wszystkie wyniki w BindingResult i możesz je pobrać.

praveen jain
źródło