使用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.
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 thisValidator
validate instances of the suppliedClass
? -
validate(Object, org.springframework.validation.Errors)
: Validates the given object and, in case of validation errors, registers those with the givenErrors
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 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 MessageCodesResolver
and DefaultMessageCodesResolver
, respectively.
https://docs.spring.io/spring/docs/current/spring-framework-reference/core.html#validation