使用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

SpringMVC参数校验_java

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

 

SpringMVC参数校验_List_02

*每个注解都有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注解开启对参数的校验

支持分组校验,声明在入参上.

不支持分组校验,声明在入参上.

 *在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;
}

}

 

演示:

SpringMVC参数校验_spring_03

 

 结果:

密码不能为空!
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();
}

}

 

只有组相同的属性才会被进行校验(默认全匹配),

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

}

 

演示:

SpringMVC参数校验_java_04

 


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 the​​DataBinder​​​ 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 ​​Errors​​​object 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 ​​reject​​​methods 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 using​​rejectValue("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 ​​MessageCodesResolver​​​and ​​DefaultMessageCodesResolver​​, respectively.

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