Весняний MVC: Як виконати перевірку?


156

Мені хотілося б знати, який найчистіший і найкращий спосіб провести перевірку форми вводу даних користувачів. Я бачив, як деякі розробники впроваджуютьorg.springframework.validation.Validator . Питання про це: я бачив, як це підтверджує клас. Чи повинен клас вручну заповнюватись значеннями, введеними користувачем, а потім передавати валідатору?

Мене бентежить найчистіший і найкращий спосіб перевірити введення даних користувача. Я знаю про традиційний метод використання, request.getParameter()а потім перевірку вручну nulls, але я не хочу робити всі перевірки в моєму Controller. Деякі хороші поради з цього питання будуть дуже вдячні. Я не використовую сплячку в цій програмі.


Відповіді:


322

У 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: якщо у користувача є нулеве ім'я, результат.hasErrors () буде істинним.

Спосіб 2: Якщо у вас є складна перевірка (наприклад, логіка великого бізнесу, умовна перевірка в декількох полях тощо), або ви чомусь не можете використовувати метод 1, використовуйте перевірку вручну. Це відома практика відокремлення коду контролера від логіки перевірки. Не створюйте свої класи перевірки з нуля, Spring надає зручний org.springframework.validation.Validatorінтерфейс (починаючи з весни 2).

Тож скажімо, у вас є

public class User {

    private String name;

    private Integer birthYear;
    private User responsibleUser;
    ...

}

і ви хочете зробити «складну» перевірку на кшталт: якщо вік користувача до 18 років, відповідальний Користувач не повинен бути недійсним, а відповідальний Вік користувача повинен бути старше 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
        }
}

Якщо є помилки перевірки, результат.hasErrors () буде істинним.

Примітка. Ви також можете встановити валідатор у методі контролера @InitBinder, використовуючи "binder.setValidator (...)" (у цьому випадку використання методів 1 і 2 неможливо, тому що ви замінюєте за замовчуванням валідатор). Або ви можете створити його в конструкторі за замовчуванням контролера. Або мати @ Component / @ Service UserValidator, який ви вводите (@Autowired) у свій контролер: дуже корисно, тому що більшість валідаторів є одинаковими + тестування блоку стає легше + ваш валідатор може викликати інші компоненти Spring.

Спосіб 3: Чому б не використовувати комбінацію обох методів? Провіряйте прості речі, наприклад атрибут "name", з анотаціями (це швидко зробити, стисло і читати). Зберігайте валідні валідації для валідаторів (коли для кодування спеціальних приміток для підтвердження складних анотацій потрібно буде витратити години або просто тоді, коли використання анотацій неможливо). Я робив це на колишньому проекті, він працював як шарм, швидко і легко.

Попередження: ви не повинні помилитися з обробкою валідацією для обробки виключень . Прочитайте цю публікацію, щоб знати, коли ними користуватися.

Список літератури:


чи можете ви сказати мені, що повинен мати мій servlet.xml для цієї конфігурації. Я хочу передати помилки назад до перегляду
devdar

@dev_darin Ви маєте на увазі конфігурацію для перевірки JSR-303?
Джером Далберт

2
@dev_marin Для перевірки, у Spring 3.x + немає нічого особливого в "servlet.xml" або "[servlet-name] -servlet.xml. Вам просто потрібна баночка зі спячковою валідацією в бібліотеках вашого проекту (або через Maven). Це все, воно повинно працювати тоді. Попередження, якщо ви використовуєте метод 3: за замовчуванням кожен контролер має доступ до валідатора JSR-303, тому будьте обережні, щоб не перекрити його "setValidator". Якщо ви хочете додати спеціальний валідатор на зверху, просто започаткуйте його та скористайтеся ним або вставте його (якщо це компонент Spring). Якщо у вас все-таки виникають проблеми після перевірки google та Spring doc, вам слід поставити нове запитання.
Джером Далберт

2
Для комбінованого використання методу 1 і 2 існує спосіб використання @InitBinder. Замість "binder.setValidator (...)", можна використовувати "binder.addValidators (...)"
jasonfungsing

1
Виправте мене, якщо я помиляюся, але ви можете змішати перевірку за допомогою анотацій JSR-303 (метод 1) та спеціальної перевірки (метод 2) при використанні анотації @InitBinder. Просто використовуйте binder.addValidators (userValidator) замість binder.setValidator (userValidator), і обидва методи перевірки набудуть чинності.
SebastianRiemer

31

Існує два способи перевірки введення користувачів: анотації та успадкування класу 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анотації - це примітки. Ви також можете використовувати Hibernate's org.hibernate.validator.constraints, але це не схоже на те, що ви використовуєте Hibernate.

Крім того, якщо ви реалізуєте Spring Validator, ви створили клас наступним чином:

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());
}

Також дивіться весняні документи .

Сподіваюся, що це допомагає.


при використанні валідатора Spring, чи потрібно встановити pojo від контролера, а потім перевірити його?
девдар

Я не впевнений, що розумію ваше запитання. Якщо ви бачите фрагмент коду контролера, Spring автоматично прив'язує подану форму до Fooпараметра в методі обробника. Ви можете уточнити?
stephen.hanson

Добре, що я говорю, коли користувач подає користувальницькі введення Контролер отримує http-запит, звідси, що трапляється, ви використовуєте request.getParameter (), щоб отримати всі параметри користувача, а потім встановіть значення в POJO, а потім передайте клас до об’єкта перевірки. Клас перевірки поверне помилки до перегляду з помилками, якщо такі знайдені. Це так відбувається?
девдар

1
Це сталося б так, але є більш простий спосіб ... Якщо ви використовуєте JSP та подання <form: form commandName = "user">, дані автоматично вводяться в контролер користувача @ModelAttribute ("user"). метод. Дивіться документ: static.springsource.org/spring/docs/3.0.x/…
Джером Далберт

+1, тому що це перший я знайдений приклад, який використовує @ModelAttribute; без нього жоден із підручника я не знайшов творів.
Ріккардо Коссу

12

Я хотів би сказати приємну відповідь Джерома Далберта. Мені було дуже просто написати валідатори анотацій у формі 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;

}

1
Мені також було цікаво, що у контролера зазвичай є один валідатор, і я бачив, де у вас може бути кілька валідаторів, однак якщо для одного об’єкта визначений набір перевірки, однак операція, яку ви хочете виконати на попередньому об'єкті, відрізняється, наприклад, збереження / оновлення для потрібно зберегти певний набір перевірки та оновити інший набір перевірки. Чи є спосіб налаштувати клас валідатора для проведення всієї перевірки на основі операції або вам потрібно буде використовувати кілька валідаторів?
девдар

1
Ви також можете мати перевірку приміток про метод. Тож ви можете створити власну "перевірку домену", якщо я зрозумію ваше запитання. Для цього потрібно вказати ElementType.METHODв @Target.
michal.kreuzman

я розумію, що ти кажеш, чи можеш ти вказати мені на приклад для отримання більш чіткої картини.
девдар

4

Якщо у вас однакова логіка поводження з помилками для різних обробників методів, ви отримаєте безліч обробників із таким кодовим шаблоном:

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

Припустимо, ви створюєте служби RESTful і хочете повернутися 400 Bad Requestразом із повідомленнями про помилки для кожного випадку помилки перевірки. Тоді частина обробки помилок буде однаковою для кожної кінцевої точки REST, яка вимагає перевірки. Повторення такої самої логіки у кожному оброблювачем не настільки ДУШЕ !

Один із способів вирішити цю проблему - це негайно відмовитись BindingResultпісля кожного боба, що підтверджується . Тепер ваш обробник буде таким:

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

Таким чином, якщо зв'язана квасоля не була дійсною, MethodArgumentNotValidExceptionВесна буде кинута. Ви можете визначити, ControllerAdviceщо обробляє цей виняток за допомогою тієї самої логіки обробки помилок:

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

Ви все ще можете вивчити основні, BindingResultвикористовуючи getBindingResultметод MethodArgumentNotValidException.


1

Знайдіть повний приклад перевірки 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);
    }
}

0

Помістіть цей боб у свій клас конфігурації.

 @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 і зможете отримати звідти.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.