Spring MVC: Как да извършим валидиране?

Бих искал да знам кой е най-чистият и най-добрият начин за извършване на валидиране на формуляр на въведени от потребителя данни. Виждал съм някои разработчици да прилагат org.springframework.validation.Validator. Въпрос за това: видях, че валидира клас. Трябва ли класът да се попълва ръчно със стойностите от потребителския вход и след това да се предава на валидатора?

Объркан съм относно най-чистия и най-добър начин за валидиране на въведеното от потребителя. Знам за традиционния метод за използване на request.getParameter() и след това ръчна проверка за nulls, но не искам да правя цялата проверка в моя Controller. Някои добри съвети в тази област ще бъдат високо оценени. Не използвам Hibernate в това приложение.


person devdar    schedule 27.08.2012    source източник
comment
spring.io/guides/gs/validating-form-input   -  person OrangeDog    schedule 07.11.2016


Отговори (6)


Със Spring MVC има 3 различни начина за извършване на валидиране: използване на анотация, ръчно или комбинация от двете. Няма уникален „най-чист и най-добър начин“ за валидиране, но вероятно има такъв, който пасва по-добре на вашия проект/проблем/контекст.

Нека имаме потребител:

public class User {

    private String name;

    ...

}

Метод 1: Ако имате Spring 3.x+ и проста проверка за извършване, използвайте javax.validation.constraints анотации (известни също като JSR-303 анотации).

public class User {

    @NotNull
    private String name;

    ...

}

Ще ви трябва JSR-303 доставчик във вашите библиотеки, като Hibernate Validator, който е референтният реализация (тази библиотека няма нищо общо с базите данни и релационното картографиране, тя просто прави валидиране :-).

Тогава във вашия контролер ще имате нещо като:

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

Обърнете внимание на @Valid: ако случайно потребителят има нулево име, result.hasErrors() ще бъде true.

Метод 2: Ако имате сложно валидиране (като логика за валидиране на голям бизнес, условно валидиране в множество полета и т.н.) или по някаква причина не можете да използвате метод 1, използвайте ръчно валидиране. Добра практика е да отделите кода на контролера от логиката за валидиране. Не създавайте своя клас(ове) за валидиране от нулата, Spring предоставя удобен org.springframework.validation.Validator интерфейс (от Spring 2).

Така че да кажем, че имате

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

и искате да направите някаква "сложна" проверка като: ако възрастта на потребителя е под 18 години, responseUser не трябва да е null и възрастта на responseUser трябва да е над 21.

Ще направите нещо подобно

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

    }

}

Тогава във вашия контролер ще имате:

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

Ако има грешки при валидиране, result.hasErrors() ще бъде вярно.

Забележка: Можете също да зададете валидатора в метод @InitBinder на контролера с "binder.setValidator(...)" (в който случай смесеното използване на метод 1 и 2 не би било възможно, защото замествате стандартния валидатор). Или можете да го инстанциирате в конструктора по подразбиране на контролера. Или имайте @Component/@Service UserValidator, който инжектирате (@Autowired) във вашия контролер: много полезно, тъй като повечето валидатори са сингълтони + подигравката с единичен тест става по-лесна + вашият валидатор може да извика други Spring компоненти.

Метод 3: Защо не използвате комбинация от двата метода? Валидирайте простите неща, като атрибута "име", с анотации (бързо се прави, стегнато е и по-четливо). Запазете тежките проверки за валидатори (когато ще отнеме часове за кодиране на персонализирани сложни анотации за валидиране или просто когато не е възможно да се използват анотации). Направих това на предишен проект, работи като чар, бързо и лесно.

Предупреждение: не бива да бъркате обработка на потвърждение с обработка на изключения. Прочетете тази публикация, за да знаете кога да ги използвате.

Препратки :

person Community    schedule 27.08.2012
comment
можете ли да ми кажете какво трябва да има моят servlet.xml за тази конфигурация. Искам да предам грешките обратно в изгледа - person devdar; 01.09.2012
comment
@dev_darin Имаш предвид конфигурация за валидиране на JSR-303? - person Jerome Dalbert; 01.09.2012
comment
да JSR-303 валидиране, приех го отново, съжалявам, сега разбрах, че можете да приемете само един отговор. Отговорът на steve.hanson също беше страхотен, просто се опитвах да бъда честен към двама информирани хора - person devdar; 01.09.2012
comment
@dev_marin За проверка, в Spring 3.x+, няма нищо специално в servlet.xml или [servlet-name]-servlet.xml. Просто се нуждаете от hibernate-validator jar във вашите библиотеки на проекти (или чрез Maven). Това е всичко, тогава трябва да работи. Предупреждение, ако използвате метод 3: по подразбиране всеки контролер има достъп до JSR-303 валидатор, така че внимавайте да не го замените с setValidator. Ако искате да добавите персонализиран валидатор отгоре, просто го инстанцирайте и го използвайте или го инжектирайте (ако е Spring компонент). Ако все още имате проблеми след проверка в Google и Spring doc, трябва да публикувате нов въпрос. - person Jerome Dalbert; 02.09.2012
comment
За комбинираното използване на метод 1 и 2 има начин да използвате @InitBinder. Вместо binder.setValidator(...), може да използва binder.addValidators(...) - person jasonfungsing; 05.11.2014
comment
Поправете ме, ако греша, но можете да смесвате валидиране чрез анотации JSR-303 (Метод 1) и персонализирано валидиране (Метод 2), когато използвате анотация @InitBinder. Просто използвайте binder.addValidators(userValidator) вместо binder.setValidator(userValidator) и двата метода за валидиране ще влязат в сила. - person SebastianRiemer; 20.08.2015
comment
налице ли е Bean Validation само за методи на контролер (анотирани с @RequestMapping)? Използвам това с обикновен метод, не работи. - person DiveInto; 15.06.2016
comment
Благодаря за пояснението, че няма „чист и най-добър“ начин за правене на сложни бизнес правила, това ми спестява време да търся в Google тази библия за валидиране на бизнеса, хаха - person ChengWhyNot; 19.09.2018

Има два начина за валидиране на въведеното от потребителя: анотации и чрез наследяване на класа Validator на Spring. За прости случаи анотациите са хубави. Ако имате нужда от сложни валидации (като валидиране в различни полета, напр. поле „потвърдете имейл адрес“), или ако вашият модел е валидиран на множество места в приложението ви с различни правила, или ако нямате възможност да промените своя моделира обект чрез поставяне на анотации върху него, базираният на наследяване валидатор на Spring е правилният начин. Ще покажа примери и за двете.

Действителната част за валидиране е същата, независимо кой тип валидиране използвате:

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

Ако използвате анотации, вашият Foo клас може да изглежда така:

public class Foo {

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

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

    // getters, setters
}

Анотациите по-горе са javax.validation.constraints анотации. Можете също да използвате org.hibernate.validator.constraints на Hibernate, но не изглежда, че използвате Hibernate.

Като алтернатива, ако внедрите валидатора на Spring, ще създадете клас, както следва:

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

Ако използвате горния валидатор, вие също трябва да свържете валидатора към Spring контролера (не е необходимо, ако използвате анотации):

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

Вижте също Пролетни документи.

Надявам се това да помогне.

person stephen.hanson    schedule 27.08.2012
comment
когато използвам валидатора на Spring, трябва ли да задам pojo от контролера и след това да го валидирам? - person devdar; 27.08.2012
comment
Не съм сигурен, че разбирам въпроса ви. Ако видите кодовия фрагмент на контролера, Spring автоматично обвързва изпратения формуляр към параметъра Foo в метода на манипулатора. Можете ли да поясните? - person stephen.hanson; 27.08.2012
comment
добре, това, което казвам, е, че когато потребителят изпрати, потребителят въвежда, контролерът получава http заявката, от там какво се случва е, че използвате request.getParameter(), за да получите всички потребителски параметри, след това задайте стойностите в POJO и след това преминете класа към валидиращия обект. Класът за валидиране ще изпрати грешките обратно към изгледа с грешките, ако има такива. Така ли става? - person devdar; 28.08.2012
comment
Ще се случи така, но има по-прост начин... Ако използвате JSP и ‹form:form commandName=user› подаване, данните автоматично се поставят в @ModelAttribute(user) потребител в метода на контролера. Вижте документа: static.springsource. org/spring/docs/3.0.x/ - person Jerome Dalbert; 28.08.2012
comment
+1, защото това е първият пример, който намерих, който използва @ModelAttribute; без него нито един урок, който намерих, не работи. - person Riccardo Cossu; 22.04.2014
comment
Защо, но защо да правите new CustomValidator()? Най-добре е да препратите към нашия bean и да го инжектирате с spring контекст @Autowired CustomValidator customValidator;, така че по този начин да можете да получите най-доброто от IoC контейнера. - person BendaThierry.com; 29.08.2016
comment
Връзката към статията е DOA: Грешка при установяване на връзка с база данни - person Madbreaks; 27.02.2018
comment
@Madbreaks благодаря. Наскоро свалих стария си сайт. Премахна връзката. - person stephen.hanson; 28.02.2018

Бих искал да разширя хубавия отговор на Джером Далбърт. Намерих много лесно да напишете свои собствени валидатори на анотация по начин JSR-303. Не сте ограничени да имате валидиране в „едно поле“. Можете да създадете своя собствена анотация на ниво тип и да имате сложна проверка (вижте примерите по-долу). Предпочитам този начин, защото нямам нужда от смесване на различни типове валидиране (Spring и JSR-303), както прави Jerome. Освен това тези валидатори са „съзнаващи за пролетта“, така че можете да използвате @Inject/@Autowire веднага.

Пример за проверка на потребителски обект:

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

Пример за равенство на общи полета:

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;

}
person michal.kreuzman    schedule 10.05.2013
comment
Също така се чудех, че контролерът обикновено има един валидатор и видях къде можете да имате множество валидатори, но ако имате набор от валидатори, дефинирани за един обект, обаче операцията, която искате да извършите предварително върху обекта, е различна, напр. запазване/актуализация за запазване се изисква определен набор от валидации и актуализация се изисква различен набор от валидации. Има ли начин да конфигурирате класа на валидатора да държи цялата валидация въз основа на операцията или ще трябва да използвате множество валидатори? - person devdar; 10.05.2013
comment
Можете също така да имате валидиране на анотация на метода. Така че можете да създадете свое собствено валидиране на домейн, ако разбирам въпроса ви. За целта трябва да посочите ElementType.METHOD в @Target. - person michal.kreuzman; 10.05.2013
comment
разбирам какво казваш, можеш ли да ме посочиш и с пример за по-ясна картина. - person devdar; 12.05.2013

Ако имате една и съща логика за обработка на грешки за различни манипулатори на методи, тогава ще получите много манипулатори със следния модел на код:

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

Да предположим, че създавате RESTful услуги и искате да върнете 400 Bad Request заедно със съобщения за грешка за всеки случай на грешка при валидиране. Тогава частта за обработка на грешки ще бъде една и съща за всяка отделна REST крайна точка, която изисква валидиране. Повтарянето на същата логика във всеки един манипулатор не е толкова СУХО!

Един от начините за решаване на този проблем е да изпуснете незабавния BindingResult след всеки To-Be-Validated bean. Сега вашият манипулатор ще бъде така:

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

По този начин, ако обвързаният bean не е бил валиден, MethodArgumentNotValidException ще бъде хвърлено от Spring. Можете да дефинирате ControllerAdvice, който обработва това изключение със същата логика за обработка на грешки:

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

Все още можете да изследвате основния BindingResult, като използвате getBindingResult метода на MethodArgumentNotValidException.

person Ali Dehghani    schedule 21.04.2016

Намерете пълен пример за 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);
    }
}
person Vicky    schedule 13.11.2013

Поставете този bean във вашия конфигурационен клас.

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

и след това можете да използвате

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

за ръчно валидиране на зърно. След това ще получите всички резултати в BindingResult и можете да извлечете от там.

person praveen jain    schedule 15.02.2018