Spring MVC: как выполнить проверку?
Я хотел бы знать, что является самым чистым и лучшим способом для проверки формы пользовательских входов. Я видел, как некоторые разработчики реализовали org.springframework.validation.Validator
. Вопрос об этом: я видел, что он проверяет класс. Нужно ли заполнять класс вручную с помощью значений из пользовательского ввода, а затем передавать в валидатор?
Я смущен самым чистым и лучшим способом проверки ввода пользователя. Я знаю о традиционном методе использования request.getParameter()
, а затем вручную проверяю nulls
, но я не хочу делать все проверки в моем Controller
. Будем очень благодарны за некоторые полезные советы в этой области. Я не использую Hibernate в этом приложении.
Ответы
Ответ 1
С помощью Spring MVC существует три различных способа выполнения проверки: использование аннотации, вручную или их сочетание. Не существует уникального "чистого и наилучшего способа" для проверки, но, вероятно, есть тот, который лучше подходит для вашего проекта/проблемы/контекста.
Пусть есть Пользователь:
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() будет истинным.
Способ 2: Если у вас есть сложная проверка (например, логика проверки большого бизнеса, условная проверка по нескольким полям и т.д.) или по какой-либо причине вы не можете использовать метод 1, используйте ручную проверку. Эффективная практика заключается в том, чтобы отделить код контроллера от логики проверки. Не создавайте свои классы проверки с нуля, Spring предоставляет удобный интерфейс org.springframework.validation.Validator
(начиная с Spring 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() будет истинным.
Примечание. Вы также можете установить валидатор в методе @InitBinder контроллера с помощью "binder.setValidator(...)" (в этом случае использование смешанного метода 1 и 2 было бы невозможным, поскольку вы замените валидатор по умолчанию). Или вы можете создать экземпляр в конструкторе по умолчанию контроллера. Или у вас есть @Component/@Service UserValidator, который вы вводите (@Autowired) в свой контроллер: очень полезно, потому что большинство валидаторов - это синглтоны + unit test mocking становится проще + ваш валидатор может вызывать другие компоненты Spring.
Способ 3:
Почему бы не использовать комбинацию обоих методов? Подтвердите простые вещи, такие как атрибут "name", с аннотациями (это быстро сделать, кратким и читаемым). Сохраняйте тяжелую валидацию для валидаторов (когда потребуется несколько часов, чтобы закодировать специальные аннотации для проверки достоверности или просто, когда невозможно использовать аннотации). Я сделал это на прежнем проекте, он работал как шарм, быстро и легко.
Предупреждение: вы не должны допускать проверку правильности для обработки исключений. Прочитайте это сообщение, чтобы узнать, когда их использовать.
Ссылки:
Ответ 2
Существует два способа проверки ввода пользователя: аннотации и наследование класса Spring Validator. Для простых случаев аннотации хороши. Если вам нужны сложные проверки (например, проверка поля в полевых условиях, например, поле "проверить адрес электронной почты"), или если ваша модель проверена в нескольких местах вашего приложения с разными правилами или если у вас нет возможности изменять свои объект модели, поместив аннотации на него, Spring-based 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'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.
Надеюсь, это поможет.
Ответ 3
Я хотел бы продлить приятный ответ Джерома Дальберт. Мне очень легко было написать свои собственные валидаторы аннотации в 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;
}
Ответ 4
Если у вас есть такая же логика обработки ошибок для разных обработчиков методов, тогда у вас будет множество обработчиков со следующим шаблоном кода:
if (validation.hasErrors()) {
// do error handling
}
else {
// do the actual business logic
}
Предположим, вы создаете службы RESTful и хотите вернуть 400 Bad Request
вместе с сообщениями об ошибках для каждого случая ошибки проверки. Затем часть обработки ошибок будет одинаковой для каждой отдельной конечной точки REST, которая требует проверки. Повторение этой же логики в каждом обработчике не так DRY ish!
Один из способов решения этой проблемы - удалить сразу BindingResult
после каждого To-Be-Validated bean. Теперь ваш обработчик будет таким:
@RequestMapping(...)
public Something doStuff(@Valid Somebean bean) {
// do the actual business logic
// Just the else part!
}
Таким образом, если ограничение bean недействительно, a 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
.
Ответ 5
Найдите полный пример 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);
}
}
Ответ 6
Поместите этот компонент в свой класс конфигурации.
@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 и сможете получить оттуда.