Spring MVC: как выполнить проверку?
Я хотел бы знать, каков самый чистый и лучший способ выполнить проверку формы пользовательских данных. Я видел, как некоторые разработчики реализуют org.springframework.validation.Validator
, Вопрос об этом: я видел, что это подтверждает класс. Должен ли класс заполняться вручную значениями из пользовательского ввода, а затем передаваться в валидатор?
Меня смущает самый чистый и лучший способ проверки ввода пользователя. Я знаю о традиционном методе использования request.getParameter()
а затем вручную проверять наличие nulls
, но я не хочу делать все проверки в моем Controller
, Некоторые полезные советы в этой области будут с благодарностью. Я не использую Hibernate в этом приложении.
7 ответов
В 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
интерфейс (начиная с весны 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
}
}
Если есть ошибки проверки, result.hasErrors() будет true.
Примечание. Вы также можете установить валидатор в методе @InitBinder контроллера с помощью "binder.setValidator(...)" (в этом случае смешанное использование методов 1 и 2 будет невозможно, поскольку вы заменяете значение по умолчанию). валидатор). Или вы можете создать его в конструкторе контроллера по умолчанию. Или имейте @Component/@Service UserValidator, который вы вводите (@Autowired) в свой контроллер: очень полезно, потому что большинство валидаторов являются одиночными + пересмешивание модульного теста становится проще + ваш валидатор может вызывать другие компоненты Spring.
Метод 3: Почему бы не использовать комбинацию обоих методов? Проверяйте простые вещи, такие как атрибут "имя", с помощью аннотаций (это быстро, сжато и более читабельно). Сохраняйте тяжелые проверки для валидаторов (когда для написания пользовательских сложных аннотаций проверки потребуется несколько часов или просто когда невозможно использовать аннотации). Я сделал это на предыдущем проекте, он работал как шарм, быстро и легко.
Предупреждение: вы не должны путать обработку проверки с обработкой исключений. Прочтите этот пост, чтобы узнать, когда их использовать.
Рекомендации:
Существует два способа проверки ввода пользователя: аннотации и наследование класса Spring Validator. Для простых случаев аннотации хороши. Если вам нужны сложные проверки (например, проверка по нескольким полям, например, поле "проверить адрес электронной почты"), или если ваша модель проверена в нескольких местах в вашем приложении с другими правилами, или если у вас нет возможности изменить свой Чтобы смоделировать объект, разместив на нем аннотации, Validator, основанный на наследовании, - это путь. Я покажу примеры обоих.
Фактическая часть проверки одинакова независимо от того, какой тип проверки вы используете:
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'sorg.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 Docs.
Надеюсь, это поможет.
Я хотел бы расширить хороший ответ Джерома Дальберта. Я нашел очень легко написать свои собственные валидаторы аннотаций в JSR-303. Вы не ограничены в проверке "одним полем". Вы можете создать свою собственную аннотацию на уровне типа и пройти сложную проверку (см. Примеры ниже). Я предпочитаю этот способ, потому что мне не нужно смешивать разные типы валидации (Spring и JSR-303), как это делает Джером. Также эти валидаторы "Spring осведомлены", так что вы можете использовать @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;
}
Если у вас одинаковая логика обработки ошибок для разных обработчиков методов, вы получите множество обработчиков со следующим шаблоном кода:
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
,
Найти полный пример Spring Mvc Validation
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);
}
}
Поместите этот компонент в ваш класс конфигурации.
@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, и вы можете получить их оттуда.
Группы проверки
Также стоит упомянуть валидацию для некоторых более сложных случаев, когда у вас есть несколько «многошагов» в вашей бизнес-логике. В таких случаях нам нужны «группы проверки».
была добавлена аннотация для поддержки «групп проверки» в проверенном компоненте. Это можно использовать в многоэтапных формах, где на первом этапе вам необходимо, например, подтвердить имя и адрес электронной почты, а на втором этапе вам необходимо подтвердить, например, номер телефона.
С
@Проверенный пример
Допустим, у нас есть сценарий, когда у нас есть форма для регистрации пользователя. В этой форме мы хотим, чтобы пользователь указал имя и адрес электронной почты. И после того, как пользователь зарегистрировался, у нас есть еще одна форма, где мы предлагаем пользователю добавить дополнительную информацию, например, адрес электронной почты. Мы не хотим, чтобы электронная почта предоставлялась на первом этапе. Но требуется обеспечить это на втором шаге.
В этом случае мы объявим две группы. Первая группа будет
При создании:
public interface OnCreate {}
При обновлении:
public interface OnUpdate {}
Наш пользовательский класс UserAccount :
public class UserAccount {
// we will return this field after User is created
// and we want this field to be provided only on update
// so we can determine which user needs to be updated
@NotBlank(groups = OnUpdate.class)
private String id;
@NotBlank(groups = OnCreate.class)
private String name;
@NotBlank(groups = OnCreate.class)
private String email;
@NotBlank(groups = OnUpdate.class)
private String phone;
// standard constructors / setters / getters / toString
}
Мы помечаем аннотации проверки с помощью интерфейсов наших групп в зависимости от того, с какой группой эти проверки должны быть связаны.
И, наконец, наши методы контроллера :
@PostMapping(value = "/create")
public UserAccount createAccount(@Validated(OnCreate.class) @RequestBody UserAccount userAccount) {
...
}
@PatchMapping(value = "/update")
public UserAccount updateAccount(@Validated(OnUpdate.class) @RequestBody UserAccount userAccount) {
...
}
Здесь мы указываем
Теперь, в зависимости от группы проверки, мы будем выполнять проверки для определенных полей на разных этапах.