SpringMVC参数校验



使用SpringMVC时配合hibernate-validate进行参数的合法性校验【常规性校验】,能节省一定的代码量.

使用步骤

1.搭建Web工程并引入hibernate-validate依赖

<dependency>
    <groupId>org.hibernate.validator</groupId>
    <artifactId>hibernate-validator</artifactId>
    <version>6.0.7.Final</version>
</dependency> 

Maven依赖传递,自动依赖validation-api、jboss-logging、classmate

2.使用校验注解标注在属性上(dto)

 

*每个注解都有message属性,该属性用于填写校验失败时的异常描述信息,当校验失败时可以获取对应的message属性值.

 

复制代码
public class User {

    @NotNull(message="id不能为空!")
    private Integer id;
    
    @NotBlank(message="用户名不能为空!")
@Size(min=4,max=12,message="用户名的长度在4~12之间!") private String username; @NotBlank(message="密码不能为空!") private String password; @Email(message="非法邮箱!") private String email; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public String getEmail() { return email; } public void setEmail(String email) { this.email = email; } public User() { super(); } }
复制代码

 

3.控制层中使用dto接收参数并使用@Validated/@Valid注解开启对参数的校验

 *@Validated注解表示使用Spring的校验机制,支持分组校验,声明在入参上.

 *@Valid注解表示使用Hibernate的校验机制,不支持分组校验,声明在入参上.

 *在dto后面要紧跟BindingResult对象,该对象用于获取当校验失败时的异常信息.

复制代码
@RestController
public class BaseController {

    @RequestMapping("/test")
    public User test(@Validated User user, BindingResult result) {
        if (result.hasErrors()) {
            List<ObjectError> errors = result.getAllErrors();
            for (ObjectError error : errors) {
                System.out.println(error.getDefaultMessage());
            }
        }
        return user;
    }

}
复制代码

 

演示:

 

 结果:

密码不能为空!
id不能为空!
用户名的长度在4~12之间!

*校验的顺序是随机的,因此程序不能依赖校验的顺序去做相关的逻辑处理.

4.分组校验

 

每个校验注解都有group属性用于指定校验所属的组,其值是Class数组,在Controller中使用@Validated注解开启对参数的校验时当指定要进行校验的组,那么只有组相同的属性才会被进行校验(默认全匹配).

Class<?>[] groups() default { };

 

一般定义标识接口作为组资源

复制代码
public interface GroupA {

}

public interface GroupB {

}
复制代码

 

使用校验注解标注在属性上并进行分组

复制代码
public class User {

    @NotNull(message="id不能为空!",groups = {GroupA.class})
    private Integer id;
    
    @NotBlank(message="用户名不能为空!",groups = {GroupB.class})
    @Size(min=4,max=12,message="用户名的长度在4~12之间!")
    private String username;

    @NotBlank(message="密码不能为空!")
    private String password;
    
    @Email(message="非法邮箱!")
    private String email;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getEmail() {
        return email;
    }

    public void setEmail(String email) {
        this.email = email;
    }

    public User() {
        super();
    }

}
复制代码

 

 Controller中使用@Validated注解开启对参数的校验并指定校验的组,那么只有组相同的属性才会被进行校验(默认全匹配),

复制代码
@RestController
public class BaseController {

    @RequestMapping("/test")
    public User test(@Validated(value= {GroupB.class}) User user, BindingResult result) {
        if (result.hasErrors()) {
            List<ObjectError> errors = result.getAllErrors();
            for (ObjectError error : errors) {
                System.out.println(error.getDefaultMessage());
            }
        }
        return user;
    }

}
复制代码

 

演示:

 

https://blog.csdn.net/shunqixing/article/details/79751569

 

Spring MVC - @Valid on list of beans in REST service

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public List<...> myMethod(@Valid @RequestBody List<MyBean> request, BindingResult bindingResult) {

Where the MyBean class has bean validation annotations.
The validations don't seem to take place in this case, although it works well for other controllers.
 解决办法:

(1)Wrap your list inside a Java Bean //把这个List放到一个Java Bean中
(2)Call the validator manually in your bulk create method myEntityValidator. validate(targetObject, errors). //显式使用spring的Validator解决

问题原因【在Spring Validation中List不是一个Java Bean】:

As you might have guessed this cannot be achieved using Spring Validation.
Spring Validation implements Bean Validation(JSR 303/349) as opposed to Object validation.
Unfortunately a collection is not a Java Bean.
https://stackoverflow.com/questions/34011892/spring-validation-for-requestbody-parameters-bound-to-collections-in-controller/36790509#answer-36790509

来看看Java Bean的定义:

JavaBeans Components
JavaBeans components are Java classes that can be easily reused and composed together into applications. Any Java class that follows certain design conventions is a JavaBeans component.

JavaServer Pages technology directly supports using JavaBeans components with standard JSP language elements. You can easily create and initialize beans and get and set the values of their properties.

JavaBeans Component Design Conventions
JavaBeans component design conventions govern the properties of the class and govern the public methods that give access to the properties.

A JavaBeans component property can be:
Read/write, read-only, or write-only
Simple, which means it contains a single value, or indexed, which means it represents an array of values

A property does not have to be implemented by an instance variable. It must simply be accessible using public methods that conform to the following conventions:

For each readable property, the bean must have a method of the form:

PropertyClass getProperty() { ... }
For each writable property, the bean must have a method of the form:

setProperty(PropertyClass pc) { ... }
In addition to the property methods, a JavaBeans component must define a constructor that takes no parameters.

https://docs.oracle.com/javaee/5/tutorial/doc/bnair.html

解决方案1:Wrap your list inside a Java Bean
(1)具体代码实现1【问题:会影响input的json数据结构】
https://stackoverflow.com/questions/17207766/spring-mvc-valid-on-list-of-beans-in-rest-service
把上面的改成这样:

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public List<...> myMethod(@Valid @RequestBody MyBeanList request, BindingResult bindingResult) {

and we also need:

import javax.validation.Valid;
import java.util.List;

public class MyBeanList {

    @Valid
    List<MyBean> list;
    //getters and setters....
}

 

(2)具体代码实现【不改变input代码现实,但会有可读性、可维护性的难度】
继承java.util.ArrayList或实现java.util.List,实现java.util.List会有好多接口需要实现,理解和操作难度会更高

public class PersonDtoList extends ArrayList<MyBean> {
      @Valid
      public List<MyBean> getList() {
           return this;
      }
}

public void insertPersons(@RequestBody @Valid PersonDtoList array) {
}

 https://stackoverflow.com/questions/49876901/how-to-validate-a-collection-in-spring-mvc-post-webservice?noredirect=1&lq=1
实现java.util.List接口的示例:
https://stackoverflow.com/questions/28150405/validation-of-a-list-of-objects-in-spring?noredirect=1&lq=1

解决方案2:
Call the validator manually in your bulk create method myEntityValidator. validate(targetObject, errors). //显式使用spring的Validator解决
具体代码实现(1):

The solution is to create a custom Validator for Collection and a @ControllerAdvice that registers that Validator in the WebDataBinders.
Validator:

import java.util.Collection;

import org.springframework.validation.Errors;
import org.springframework.validation.ValidationUtils;
import org.springframework.validation.Validator;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;

/**
 * Spring {@link Validator} that iterates over the elements of a 
 * {@link Collection} and run the validation process for each of them
 * individually.
 *   
 * @author DISID CORPORATION S.L. (www.disid.com)
 */
public class CollectionValidator implements Validator {

  private final Validator validator;

  public CollectionValidator(LocalValidatorFactoryBean validatorFactory) {
    this.validator = validatorFactory;
  }

  @Override
  public boolean supports(Class<?> clazz) {
    return Collection.class.isAssignableFrom(clazz);
  }

  /**
   * Validate each element inside the supplied {@link Collection}.
   * 
   * The supplied errors instance is used to report the validation errors.
   * 
   * @param target the collection that is to be validated
   * @param errors contextual state about the validation process
   */
  @Override
  @SuppressWarnings("rawtypes")
  public void validate(Object target, Errors errors) {
    Collection collection = (Collection) target;
    for (Object object : collection) {
      ValidationUtils.invokeValidator(validator, object, errors);
    }
  }
}

在@ControllerAdvice中注册上面为java.util.Collection定制的CollectionValidator 

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.beanvalidation.LocalValidatorFactoryBean;
import org.springframework.web.bind.WebDataBinder;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.InitBinder;

/**
 * Controller advice that adds the {@link CollectionValidator} to the 
 * {@link WebDataBinder}.
 * 
 * @author DISID CORPORATION S.L. (www.disid.com)
 */
@ControllerAdvice
public class ValidatorAdvice {

  @Autowired
  protected LocalValidatorFactoryBean validator;


  /**
   * Adds the {@link CollectionValidator} to the supplied 
   * {@link WebDataBinder}
   * 
   * @param binder web data binder.
   */
  @InitBinder
  public void initBinder(WebDataBinder binder) {
    binder.addValidators(new CollectionValidator(validator));
  }
}

https://stackoverflow.com/questions/34011892/spring-validation-for-requestbody-parameters-bound-to-collections-in-controller/36790509#answer-36790509

另一个写法,待测试:

import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

@ControllerAdvice(annotations = Validated.class)
public class ValidatedExceptionHandler {

    @ExceptionHandler
    public ResponseEntity<Object> handle(ConstraintViolationException exception) {

        List<String> errors = exception.getConstraintViolations()
                                       .stream()
                                       .map(this::toString)
                                       .collect(Collectors.toList());

        return new ResponseEntity<>(new ErrorResponseBody(exception.getLocalizedMessage(), errors),
                                    HttpStatus.BAD_REQUEST);
    }

    private String toString(ConstraintViolation<?> violation) {
        return Formatter.format("{} {}: {}",
                                violation.getRootBeanClass().getName(),
                                violation.getPropertyPath(),
                                violation.getMessage());
    }

    public static class ErrorResponseBody {
        private String message;
        private List<String> errors;
    }
}

https://stackoverflow.com/questions/39348234/spring-boot-how-to-use-valid-with-listt?noredirect=1&lq=1



具体代码实现2:

Try direct validation. Something like this:

@Autowired
Validator validator;

@RequestMapping(method = RequestMethod.POST, value = { "/doesntmatter" })
@ResponseBody
public Object myMethod(@RequestBody List<Object> request, BindingResult bindingResult) {
    for (int i = 0; i < request.size(); i++) {
        Object o = request.get(i);
        BeanPropertyBindingResult errors = new BeanPropertyBindingResult(o, String.format("o[%d]", i));
        validator.validate(o, errors);
        if (errors.hasErrors())
            bindingResult.addAllErrors(errors);
    }
    if (bindingResult.hasErrors())
        ...

https://stackoverflow.com/questions/17207766/spring-mvc-valid-on-list-of-beans-in-rest-service



直接使用Java BeanValidation中示例代码:

JavaBean Validation - Object Association validation with @Valid

According to the Bean Validation specification, the @Valid annotation on a given object reference is used to allow cascading Validation. The associated object can itself contain cascaded references, hence it is a recursive process. This feature is also referred as 'object graph validation'.

Example
In this example, we are purposely supplying invalid values during object creation to see @Valid annotation in action.

public class ValidAnnotationExample {

    private static class DriverLicense {
        @NotNull
        @Valid
        private Driver driver;
        @Digits(integer = 7, fraction = 0)
        private int number;

        public DriverLicense(Driver driver, int number) {
            this.driver = driver;
            this.number = number;
        }
    }

    private static class Driver {
        @NotNull
        private String fullName;
        @Min(100)
        private int height;
        @Past
        @NotNull
        private Date dateOfBirth;

        public Driver(String fullName, int height, Date dateOfBirth) {
            this.dateOfBirth = dateOfBirth;
            this.fullName = fullName;
            this.height = height;
        }
    }

    public static void main(String[] args) throws ParseException {
        Driver driver = new Driver("Joseph Waters", 60,
                new Date(System.currentTimeMillis() + 100000));
        DriverLicense dl = new DriverLicense(driver, 3454343);

        Validator validator = createValidator();
        Set<ConstraintViolation<DriverLicense>> violations = validator.validate(dl);
        if (violations.size() == 0) {
            System.out.println("No violations.");
        } else {
            System.out.printf("%s violations:%n", violations.size());
            violations.stream()
                      .forEach(ValidAnnotationExample::printError);
        }
    }

    private static void printError(ConstraintViolation<?> violation) {
        System.out.println(violation.getPropertyPath()
                + " " + violation.getMessage());
    }

    public static Validator createValidator() {
        Configuration<?> config = Validation.byDefaultProvider().configure();
        ValidatorFactory factory = config.buildValidatorFactory();
        Validator validator = factory.getValidator();
        factory.close();
        return validator;
    }
}

Output:

2 violations:
driver.dateOfBirth must be in the past
driver.height must be greater than or equal to 100
https://www.logicbig.com/tutorials/java-ee-tutorial/bean-validation/cascaded-validation.html


JavaBean Validation - Collection Validation
Just like object references can be validated recursively by using @Valid (as we saw in the last example), the elements of Java Collections, arrays and Iterable can also be validated by using @Valid annotation.

public class ValidAnnotationExample {

    private static class Department {
        @NotNull
        @Valid
        private List<Employee> employees;
        @NotNull
        private String name;

        public Department(String name, List<Employee> employees) {
            this.employees = employees;
            this.name = name;
        }
    }

    private static class Employee {
        @NotNull
        private String name;
        @Pattern(regexp = "\\d{3}-\\d{3}-\\d{4}")
        private String phone;

        public Employee(String name, String phone) {
            this.name = name;
            this.phone = phone;
        }
    }

    public static void main(String[] args) throws ParseException {
        Employee e1 = new Employee(null, "333333");
        Employee e2 = new Employee("Jake", "abc");

        Department dept = new Department("Admin", Arrays.asList(e1, e2));

        Validator validator = createValidator();
        Set<ConstraintViolation<Department>> violations = validator.validate(dept);
        if (violations.size() == 0) {
            System.out.println("No violations.");
        } else {
            System.out.printf("%s violations:%n", violations.size());
            violations.stream()
                      .forEach(ValidAnnotationExample::printError);
        }
    }

    private static void printError(ConstraintViolation<?> violation) {
        System.out.println(violation.getPropertyPath()
                + " " + violation.getMessage());
    }

    public static Validator createValidator() {
        Configuration<?> config = Validation.byDefaultProvider().configure();
        ValidatorFactory factory = config.buildValidatorFactory();
        Validator validator = factory.getValidator();
        factory.close();
        return validator;
    }
}

Output:

3 violations:
employees[1].phone must match "\d{3}-\d{3}-\d{4}"
employees[0].name may not be null
employees[0].phone must match "\d{3}-\d{3}-\d{4}"

As seen, the List elements (employees) were validated as expected.
https://www.logicbig.com/tutorials/java-ee-tutorial/bean-validation/collection-validation.html

Let's remove @Valid annotation in above example:
Output:

No violations.

https://www.logicbig.com/tutorials/java-ee-tutorial/bean-validation/collection-validation.html


@Valid is a JSR-303 annotation and JSR-303 applies to validation on JavaBeans. A java.util.List is not a JavaBean (according to the official description of a JavaBean), hence it cannot be validated directly using a JSR-303 compliant validator. This is supported by two observations.

Section 3.1.3 of the JSR-303 Specification says that:

In addition to supporting instance validation, validation of graphs of object is also supported. The result of a graph validation is returned as a unified set of constraint violations. Consider the situation where bean X contains a field of type Y. By annotating field Y with the @Valid annotation, the Validator will validate Y (and its properties) when X is validated. The exact type Z of the value contained in the field declared of type Y (subclass, implementation) is determined at runtime. The constraint definitions of Z are used. This ensures proper polymorphic behavior for associations marked @Valid.

Collection-valued, array-valued and generally Iterable fields and properties may also be decorated with the @Valid annotation. This causes the contents of the iterator to be validated. Any object implementing java.lang.Iterable is supported.

I have marked the important pieces of information in bold. This section implies that in order for a collection type to be validated, it must be encapsulated inside a bean (implied by Consider the situation where bean X contains a field of type Y); and further that collections cannot be validated directly (implied by Collection-valued, array-valued and generally Iterable fields and properties may also be decorated, with emphasis on fields and properties).

Actual JSR-303 implementations

I have a sample application that tests collection validation with both Hibernate Validator and Apache Beans Validator. If you run tests on this sample as mvn clean test -Phibernate (with Hibernate Validator) and mvn clean test -Papache (for Beans Validator), both refuse to validate collections directly, which seems to be in line with the specification. Since Hibernate Validator is the reference implementation for JSR-303, this sample is further proof that collections need to be encapsulated in a bean in order to be validated.

https://stackoverflow.com/questions/17207766/spring-mvc-valid-on-list-of-beans-in-rest-service

3. Validation, Data Binding, and Type Conversion

There are pros and cons for considering validation as business logic, and Spring offers a design for validation (and data binding) that does not exclude either one of them. Specifically, validation should not be tied to the web tier and should be easy to localize, and it should be possible to plug in any available validator. Considering these concerns, Spring has come up with a Validator interface that is both basic and eminently usable in every layer of an application.

Data binding is useful for letting user input be dynamically bound to the domain model of an application (or whatever objects you use to process user input). Spring provides the aptly named DataBinder to do exactly that. The Validator and theDataBinder make up the validation package, which is primarily used in but not limited to the MVC framework.

The BeanWrapper is a fundamental concept in the Spring Framework and is used in a lot of places. However, you probably do not need to use the BeanWrapper directly. Because this is reference documentation, however, we felt that some explanation might be in order. We explain the BeanWrapper in this chapter, since, if you are going to use it at all, you are most likely do so when trying to bind data to objects.

Spring’s DataBinder and the lower-level BeanWrapper both use PropertyEditorSupport implementations to parse and format property values. The PropertyEditor and PropertyEditorSupport interfaces are part of the JavaBeans specification and are also explained in this chapter. Spring 3 introduced a core.convert package that provides a general type conversion facility, as well as a higher-level “format” package for formatting UI field values. You can use these packages as simpler alternatives to PropertyEditorSupport implementations. They are also discussed in this chapter.

JSR-303/JSR-349 Bean Validation

As of version 4.0, Spring Frameworksupports Bean Validation 1.0 (JSR-303) and Bean Validation 1.1 (JSR-349) for setup support and adapting them to Spring’s Validator interface.

An application can choose to enable Bean Validation once globally, as described in Spring Validation, and use it exclusively for all validation needs.

An application can also register additional Spring Validator instances for each DataBinder instance, as described in Configuring a DataBinder. This may be useful for plugging in validation logic without the use of annotations.

3.1. Validation by Using Spring’s Validator Interface

Spring features a Validator interface that you can use to validate objects. The Validator interface works by using an Errorsobject so that, while validating, validators can report validation failures to the Errors object.

Consider the following example of a small data object:

public class Person {

    private String name;
    private int age;

    // the usual getters and setters...
}

The next example provides validation behavior for the Person class by implementing the following two methods of the org.springframework.validation.Validator interface:

  • supports(Class): Can this Validator validate instances of the supplied Class?

  • validate(Object, org.springframework.validation.Errors): Validates the given object and, in case of validation errors, registers those with the given Errors object.

Implementing a Validator is fairly straightforward, especially when you know of the ValidationUtils helper class that the Spring Framework also provides. The following example implements Validator for Person instances:

public class PersonValidator implements Validator {

    /**
     * This Validator validates *only* Person instances
     */
    public boolean supports(Class clazz) {
        return Person.class.equals(clazz);
    }

    public void validate(Object obj, Errors e) {
        ValidationUtils.rejectIfEmpty(e, "name", "name.empty");
        Person p = (Person) obj;
        if (p.getAge() < 0) {
            e.rejectValue("age", "negativevalue");
        } else if (p.getAge() > 110) {
            e.rejectValue("age", "too.darn.old");
        }
    }
}

The static rejectIfEmpty(..) method on the ValidationUtils class is used to reject the name property if it is null or the empty string. Have a look at the ValidationUtils javadoc to see what functionality it provides besides the example shown previously.

While it is certainly possible to implement a single Validator class to validate each of the nested objects in a rich object, it may be better to encapsulate the validation logic for each nested class of object in its own Validator implementation. A simple example of a “rich” object would be a Customer that is composed of two String properties (a first and a second name) and a complex Address object. Address objects may be used independently of Customer objects, so a distinct AddressValidator has been implemented. If you want your CustomerValidator to reuse the logic contained within the AddressValidator class without resorting to copy-and-paste, you can dependency-inject or instantiate an AddressValidator within your CustomerValidator, as the following example shows:

public class CustomerValidator implements Validator {

    private final Validator addressValidator;

    public CustomerValidator(Validator addressValidator) {
        if (addressValidator == null) {
            throw new IllegalArgumentException("The supplied [Validator] is " +
                "required and must not be null.");
        }
        if (!addressValidator.supports(Address.class)) {
            throw new IllegalArgumentException("The supplied [Validator] must " +
                "support the validation of [Address] instances.");
        }
        this.addressValidator = addressValidator;
    }

    /**
     * This Validator validates Customer instances, and any subclasses of Customer too
     */
    public boolean supports(Class clazz) {
        return Customer.class.isAssignableFrom(clazz);
    }

    public void validate(Object target, Errors errors) {
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "firstName", "field.required");
        ValidationUtils.rejectIfEmptyOrWhitespace(errors, "surname", "field.required");
        Customer customer = (Customer) target;
        try {
            errors.pushNestedPath("address");
            ValidationUtils.invokeValidator(this.addressValidator, customer.getAddress(), errors);
        } finally {
            errors.popNestedPath();
        }
    }
}

Validation errors are reported to the Errors object passed to the validator. In the case of Spring Web MVC, you can use the <spring:bind/> tag to inspect the error messages, but you can also inspect the Errors object yourself. More information about the methods it offers can be found in the javadoc.

3.2. Resolving Codes to Error Messages

We covered databinding and validation. This section covers outputting messages that correspond to validation errors. In the example shown in the preceding section, we rejected the name and age fields. If we want to output the error messages by using a MessageSource, we can do so using the error code we provide when rejecting the field ('name' and 'age' in this case). When you call (either directly, or indirectly, by using, for example, the ValidationUtils class) rejectValue or one of the other rejectmethods from the Errors interface, the underlying implementation not only registers the code you passed in but also registers a number of additional error codes. The MessageCodesResolver determines which error codes the Errors interface registers. By default, the DefaultMessageCodesResolver is used, which (for example) not only registers a message with the code you gave but also registers messages that include the field name you passed to the reject method. So, if you reject a field by usingrejectValue("age", "too.darn.old"), apart from the too.darn.old code, Spring also registers too.darn.old.age and too.darn.old.age.int (the first includes the field name and the second includes the type of the field). This is done as a convenience to aid developers when targeting error messages.

More information on the MessageCodesResolver and the default strategy can be found in the javadoc of MessageCodesResolverand DefaultMessageCodesResolver, respectively.

https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation






 

posted @ 2018-12-06 22:11  沧海一滴  阅读(4615)  评论(0编辑  收藏  举报