Spring 整合 Java Bean Validation
Spring 整合 Java Bean Validation
官方文档
Validation by Using Spring’s Validator Interface
参考博客
深入了解数据校验:Java Bean Validation 2.0(JSR380) - YourBatman - 博客园
详述Spring对数据校验支持的核心API:SmartValidator - YourBatman - 博客园
Spring方法级别数据校验:@Validated + MethodValidationPostProcessor - YourBatman - 博客园
@Validated和@Valid的区别?校验级联属性(内部类) - YourBatman - 博客园
让Controller支持对平铺参数执行@Valid数据校验 - YourBatman - 博客园
从深处去掌握数据校验@Valid的作用(级联校验) - YourBatman - 博客园
Bean Validation完结篇:你必须关注的边边角角(约束级联、自定义约束、自定义校验器、国际化失败消息...) - YourBatman - 博客园
解决多字段联合逻辑校验问题【享学Spring MVC】 - YourBatman - 博客园
Spring 简单集成 Bean Validation
org.springframework.validation包
接口结构

Validator
应用程序特定对象的验证器,注意区别于 javax.validation.Validator
,这是 Spring 自己另起炉灶搞的一套。这个接口完全脱离了任何基础设施或上下文
,也就是说,它没有耦合到只验证 Web 层、数据访问层或任何层中的对象。它支持应用于程序内的任何层
SmartValidator
继承 Validator,增加校验分组:hints
SpringValidatorAdapter
实现 SmartValidator。PS:此类同时实现了 javax.validation.Validator 和 org.springframework.validation.Validator
这个实现类 Class 是非常重要的,它是 javax.validation.Validator
到 Spring 的 org.springframework.validation.Validator
的适配,通过它就可以对接到 JSR 的校验器来完成校验工作了,实际上可以理解为一个包装器,将 javax.validation.Validator 包一层,外面是 org.springframework.validation.Validator。实际上所有的 Spring 接口的校验方法,最终都委托给了 org.springframework.validation.Validator
CustomValidatorBean
继承 SpringValidatorAdapter。也就是实现了 org.springframework.validation.Validator
此类同时又显式地实现了 javax.validation.Validator,实际上用的还是 SpringValidatorAdapter 中的实现。
这个类地功能是,可以配置 ValidatorFactory 验证器工厂、MessageInterpolator 插值器和 traversableResolver 遍历解析器,在 afterPropertiesSet 方法中将已经配置好的这个 Validator 设置进去
大部分的时候用于直接进行校验,而不在 AOP 中进行使用
LocalValidatorFactoryBean
Spring 上下文中 javax.validation 的中心配置类
其实作用跟 CustomValidatorBean 一样,都是为了自定义配置,提供 javax.validation.Validator,并设置到 targetValidator(从 SpringValidatorAdapter 中继承)中,类的结构也类似,都是在 afterPropertiesSet 方法中进行配置的自定义,最终校验工作都是委托给 javax.validation.Validator
不同的是,CustomValidatorBean 的 validator 校验器是从上下文拿的,这里是从工厂拿的
它和 CustomValidatorBean
平级,都是继承自 SpringValidatorAdapter
,但是它提供的能力更加的强大,比如 Spring
处理校验这块最重要的处理器 MethodValidationPostProcessor
就是依赖于它来给提供验证器。
备注:虽然命名后缀是
FactoryBean
,但它并不是org.springframework.beans.factory.FactoryBean
这个接口的子类。
其实这是断句问题,正确断句方式是:LocalValidatorFactory
Bean~
因为父类实现了 javax.validation.Validator
,而 LocalValidatorFactoryBean 本身也是配置 javax.validation.Validator
的,因此,LocalValidatorFactoryBean 就是我们在《Java Bean Validation + Hibernate Validator》中说的全局单例 javax.validation.Validator
实例。
大部分的时候用于在 AOP 或者 PostProcessor 中进行使用
实践
若你想使用 org.springframework.validation.SmartValidator
来完成对 Bean 的校验,那就手动定义一个这样的 Bean,然后自行调用 API 校验完成校验(还是不够方便),写个例子来看看
先引入相关依赖:以下依赖为 Spring 以外的依赖
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.8.2</version>
<scope>test</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.24</version>
<scope>provided</scope>
</dependency>
<!--日志相关-->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<version>2.17.2</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.logging.log4j/log4j-api -->
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-api</artifactId>
<version>2.17.2</version>
</dependency>
<!-- BeanValidation 2.0-->
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
<!-- 引入 Expression Language 3.0,实现-->
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.29</version>
<!-- 如果是web项目,则跟servlet-api一样,tomcat已经提供了,所以scope 是provided-->
<!--<scope>provided</scope> 打包时最用此scope-->
</dependency>
为了在错误信息中能够打印完整的方法参数名,我们需要在编译的时候加上编译参数 -parameters
,当我们使用 Maven 的时候,添加以下配置
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<compilerArgs>
<arg>-parameters</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>
Spring 配置类
@Configuration
@ComponentScan(basePackages = {"xyz.xiashuo"})
@PropertySource({"classpath:extra.properties"})
public class SpringConfig {
@Bean(name = "customValidatorBean")
public CustomValidatorBean getCustomValidatorBean() {
return new CustomValidatorBean();
}
}
待校验的 bean
@Data
@Component
public class User {
@NotNull(message = "姓名不能为空",groups = Simple.class)
@NotBlank(message = "姓名不能为空",groups = Simple.class)
private String name;
@NotNull(message = "性别不能为空",groups = Simple.class)
@NotBlank(message = "性别不能为空",groups = Simple.class)
private String gender;
@Min(value = 0,message = "年龄不能小于0",groups = Simple.class)
@Max(value = 100,message = "年龄不能大于100",groups = Simple.class)
@NotNull(message = "年龄不能为空",groups = Simple.class)
private Integer age;
@NotNull(message = "生日不能为空",groups = Advanced.class)
@PastOrPresent(message = "生日不能为将来的时间",groups = Advanced.class)
private Date birthDate;
@NotNull(message = "工作职位不能为空",groups = Advanced.class)
@NotBlank(message = "工作职位不能为空",groups = Advanced.class)
private String occupation;
}
两个分组接口
public interface Simple {
}
public interface Advanced {
}
测试类
@SpringJUnitConfig(classes = SpringConfig.class)
public class UserTest {
@Autowired
private User user;
// 直接获取Validator对象
@Autowired
private SmartValidator smartValidator;
@Test
public void test() {
//这里可以设置一下 user 的各种属性
Errors simpleErrors = new DirectFieldBindingResult(user, "user");
// ValidationUtils 非常方便
ValidationUtils.invokeValidator(smartValidator, user, simpleErrors, Simple.class);
System.out.println("Simple Validation");
System.out.println(simpleErrors);
System.out.println("================================================");
Errors advancedErrors = new DirectFieldBindingResult(user, "user");
ValidationUtils.invokeValidator(smartValidator, user, advancedErrors, Advanced.class);
System.out.println("Advanced Validation");
System.out.println(advancedErrors);
}
}
输出日志
Simple Validation
org.springframework.validation.DirectFieldBindingResult: 8 errors
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
Field error in object 'user' on field 'name': rejected value [null]; codes [NotNull.user.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [姓名不能为空]
Field error in object 'user' on field 'name': rejected value [null]; codes [NotNull.user.name,NotNull.name,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [姓名不能为空]
Field error in object 'user' on field 'name': rejected value [null]; codes [NotBlank.user.name,NotBlank.name,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.name,name]; arguments []; default message [name]]; default message [姓名不能为空]
Field error in object 'user' on field 'gender': rejected value [null]; codes [NotNull.user.gender,NotNull.gender,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.gender,gender]; arguments []; default message [gender]]; default message [性别不能为空]
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
Field error in object 'user' on field 'gender': rejected value [null]; codes [NotNull.user.gender,NotNull.gender,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.gender,gender]; arguments []; default message [gender]]; default message [性别不能为空]
Field error in object 'user' on field 'gender': rejected value [null]; codes [NotBlank.user.gender,NotBlank.gender,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.gender,gender]; arguments []; default message [gender]]; default message [性别不能为空]
================================================
Advanced Validation
org.springframework.validation.DirectFieldBindingResult: 5 errors
Field error in object 'user' on field 'birthDate': rejected value [null]; codes [NotNull.user.birthDate,NotNull.birthDate,NotNull.java.util.Date,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.birthDate,birthDate]; arguments []; default message [birthDate]]; default message [生日不能为空]
Field error in object 'user' on field 'birthDate': rejected value [null]; codes [NotNull.user.birthDate,NotNull.birthDate,NotNull.java.util.Date,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.birthDate,birthDate]; arguments []; default message [birthDate]]; default message [生日不能为空]
Field error in object 'user' on field 'occupation': rejected value [null]; codes [NotNull.user.occupation,NotNull.occupation,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.occupation,occupation]; arguments []; default message [occupation]]; default message [工作职位不能为空]
Field error in object 'user' on field 'occupation': rejected value [null]; codes [NotNull.user.occupation,NotNull.occupation,NotNull.java.lang.String,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.occupation,occupation]; arguments []; default message [occupation]]; default message [工作职位不能为空]
Field error in object 'user' on field 'occupation': rejected value [null]; codes [NotBlank.user.occupation,NotBlank.occupation,NotBlank.java.lang.String,NotBlank]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.occupation,occupation]; arguments []; default message [occupation]]; default message [工作职位不能为空]
org.springframework.validation.beanvalidation
BeanValidationPostProcessor - 校验 bean 字段/属性
一个普通的 BeanPostProcessor
。传入 javax.validation.Validator
或者 javax.validation.ValidatorFactory
对象,然后在 postProcessBeforeInitialization
方法中,根据校验注解,调用 javax.validation.Validator
原生的 API(跟 org.springframework.validation.Validator
一毛钱关系都没有)对准备初始化的 bean 的属性\字段进行校验,从而决定允不允许它初始化完成。若校验不通过,在违反约束的情况下就会抛出异常,阻止容器的正常启动。
注意:默认的
BeanValidationPostProcessor
不支持分组校验,但是我们可以继承BeanValidationPostProcessor
,然后添加上分组。
`BeanValidationPostProcessor 默认没有被装配进容器的,需要手动注册到 IOC 容器中。
总的来说,这个其实用的少,因为大部分的时候,我们放到容器中的 bean 的属性都是处于默认状态的
实践
在 SpringConfig 中注册 BeanValidationPostProcessor
@Bean(name = "beanValidationPostProcessor")
public BeanValidationPostProcessor getBeanValidationPostProcessor(Validator customValidatorBean) {
BeanValidationPostProcessor beanValidationPostProcessor = new BeanValidationPostProcessor();
beanValidationPostProcessor.setValidator(customValidatorBean);
return beanValidationPostProcessor;
}
添加测试 bean
@Data
@Component
public class Student {
@NotNull(message = "姓名不能为空")
@NotBlank(message = "姓名不能为空")
private String name;
@NotNull(message = "性别不能为空")
@NotBlank(message = "性别不能为空")
private String gender;
@Min(value = 0,message = "年龄不能小于0")
@Max(value = 100,message = "年龄不能大于100")
@NotNull(message = "年龄不能为空")
private Integer age;
@NotNull(message = "生日不能为空")
@PastOrPresent(message = "生日不能为将来的时间")
private Date birthDate;
@Min(value = 0,message = "得分不能小于0")
@Max(value = 100,message = "得分不能大于100")
@NotNull(message = "得分不能为空")
private Integer score;
}
测试类
@SpringJUnitConfig(classes = SpringConfig.class)
class StudentTest {
@Autowired
private Student student;
@Test
public void testStudent() throws Exception {
System.out.println(student.toString());
}
}
输出日志
Caused by: org.springframework.beans.factory.BeanInitializationException: Bean state is invalid: age - 年龄不能为空; name - 姓名不能为空; name - 姓名不能为空; score - 得分不能为空; age - 年龄不能为空; gender - 性别不能为空; gender - 性别不能为空; birthDate - 生日不能为空; score - 得分不能为空; gender - 性别不能为空; birthDate - 生日不能为空; name - 姓名不能为空
MethodValidationPostProcessor - 校验方法参数/返回值 重点使用
在 JavaSE 中,每个方法要校验参数,都要手动调用一遍校验方法方法,侵入性太强了,写起来也非常不方便,在 Spring 中,我们可以使用 AOP,将参数校验代码放到切面中,在进入方法前执行,这就是 MethodValidationPostProcessor。
实际上 MethodValidationPostProcessor 的作用是添加通知 MethodValidationInterceptor,方法校验的实际执行地是通知中。在 MethodValidationInterceptor 的校验逻辑中,若入参校验失败了,直接就报错了,方法体是不会执行的,返回值校验也是不会执行的。
需要配合@Validated 注解一起使用。
MethodValidationPostProcessor 支持分组,分组需要在@Validated 注解中标注,@Validated 也只有这一个参数
MethodValidationPostProcessor
默认没有被装配进容器的,需要手动注册到 IOC 容器中。
使用
@Validated
去校验方法Method
,不管从使用上还是原理上,都是非常简单和简约的,建议大家在企业应用中多多使用。
关于分组
-
校验注解可以属于多个分组
-
如果校验注解没有带分组,那么校验注解默认属于 Default 分组,如果指定的分组中没有显示包含 Default 分组(Default.class),则不会自动添加 Default 分组
-
@Validated 可以指定多个分组
-
@Validated 如果没有指定分组,则默认校验 Default 分组,如果指定的分组中没有显示包含 Default 分组(Default.class),则不会自动添加 Default 分组
@Validated 和@Valid 的异同点和关联
-
@Valid
只是用于级联和递归校验 -
@Validated
注解只是为了建立切面在切面中进行参数或者方法值的校验,同时方便地配置和使用Bean Validation
校验注解中本身就有的分组功能。 -
@Validated
只能用在类、方法和参数上,而@Valid
可用于方法、字段、构造器和参数上 -
在
Controller
中校验方法参数时,使用@Valid 和@Validated 并无特殊差异(若不需要分组校验的话),如果要使用分组,那还是使用@Validated
实践
注册 MethodValidationPostProcessor。
@Bean(name = "methodValidationPostProcessor")
public MethodValidationPostProcessor getMethodValidationPostProcessor(Validator validation) {
MethodValidationPostProcessor methodValidationPostProcessor = new MethodValidationPostProcessor();
methodValidationPostProcessor.setValidator(validation);
return methodValidationPostProcessor;
}
添加接口,然后将注解都写在接口上,写在接口上的原因,我们在 JavaBeanValidation+HibernateValidator
中讨论过。当参数为自定义对象比如 User、Student 的时候,在参数前面添加@Valid 注解即可实现级联校验。这里就不演示了。
public interface UserService {
@NotNull @Valid User addUser(@NotNull @NotBlank(groups = Simple.class) String name, @NotBlank(groups = Simple.class) @NotEmpty String occupation, @NotNull @Min(value = 1, groups = Simple.class) @Max(100) Integer age);
}
实现类,添加@Validated 注解,先不指定分组,PS:@Validated
注解它写在实现类/接口上均可
@Service
@Validated
public class UserServiceImpl implements UserService {
@Override
public User addUser(String name, String occupation, Integer age) {
User user = new User();
//user.setName(name);
//user.setOccupation(occupation);
//user.setAge(age);
return user;
}
}
测试类
@SpringJUnitConfig(classes = SpringConfig.class)
class UserFunctionTest {
@Autowired
private UserService userService;
@Test
public void testStudent() throws Exception {
User user = userService.addUser("", "", 0);
System.out.println(user.toString());
}
}
日志,在默认分组的校验注解中,只有第二个参数 occupation 不符合@NotEmpty 约束,所以报错
javax.validation.ConstraintViolationException: addUser.occupation: 不能为空
填上之后
User user = userService.addUser("", "1", 0);
日志正常,虽然 User 前面有 @Valid
级联校验,但是因为 User 中的字段注解要么是 Simple 分组要么是 Advanced 分组,都不是默认分组,所以这里都不会校验
User(name=null, gender=null, age=null, birthDate=null, occupation=null)
在@Validated 中指定分组
@Service
@Validated(Simple.class)
public class UserServiceImpl implements UserService {
@Override
public User addUser(String name, String occupation, Integer age) {
User user = new User();
//user.setName(name);
//user.setOccupation(occupation);
//user.setAge(age);
return user;
}
}
测试
@SpringJUnitConfig(classes = SpringConfig.class)
class UserFunctionTest {
@Autowired
private UserService userService;
@Test
public void testStudent() throws Exception {
User user = userService.addUser("", "", 0);
System.out.println(user.toString());
}
}
输出日志,Simple 分组的三个约束均不满足
javax.validation.ConstraintViolationException: addUser.age: 最小不能小于1, addUser.name: 不能为空, addUser.occupation: 不能为空
添加参数
User user = userService.addUser("xiashuo", "worker", 12);
再次测试,User 对象中,仅 Simple 分组的字段报错
javax.validation.ConstraintViolationException: addUser.<return value>.age: 年龄不能为空, addUser.<return value>.gender: 性别不能为空, addUser.<return value>.gender: 性别不能为空, addUser.<return value>.name: 姓名不能为空, addUser.<return value>.name: 姓名不能为空, addUser.<return value>.gender: 性别不能为空, addUser.<return value>.age: 年龄不能为空, addUser.<return value>.name: 姓名不能为空
@Validated 类中的方法相互调用
接口
public interface CommonService {
@NotNull(groups = Advanced.class) @Valid User getUser(@NotNull(groups = Advanced.class) @NotBlank(groups = Advanced.class) String userName, @NotNull(groups = Advanced.class) @Min(value = 1,groups = Advanced.class) @Max(100) Integer userAge);
void printUserInfo(@NotNull(groups = Advanced.class) @Valid User user);
}
实现类,在 getUser 方法中,调用 printUserInfo 方法
@Service
@Validated(Advanced.class)
public class CommonServiceImpl implements CommonService{
@Autowired
ApplicationContext applicationContext;
@Override
public User getUser(String userName, Integer userAge) {
User user = new User();
CommonService commonService = (CommonService) AopContext.currentProxy();
commonService.printUserInfo(user);
return user;
}
@Override
public void printUserInfo(User user) {
System.out.println(user.toString());
}
}
测试类
@SpringJUnitConfig(classes = SpringConfig.class)
class CommonServiceImplTest {
@Autowired
private CommonService commonService;
@Test
public void testStudent() throws Exception {
commonService.getUser("xiashuo", 2);
}
}
这其实是一个 AOP 问题,参考《Spring5-AOP》中 被代理类的内部调用被代理类的另一个方法,会走代理类吗?
小节,很容易想到的方案是 @EnableAspectJAutoProxy(exposeProxy = true)
配合在方法内部通过 AopContext.currentProxy()
获取代理对象,再调用另一个方法
但是实际 AopContext.currentProxy()
报错,
java.lang.IllegalStateException: Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true' to make it available, and ensure that AopContext.currentProxy() is invoked in the same thread as the AOP invocation context.
根据提示,在 SpringConfig
上添加 @EnableAspectJAutoProxy(exposeProxy = true)
,依然报错。
为什么会报错,如何解决这个报错?
参考博客:
SpringBoot2.x—方法参数/返回值的注解版校验(解决方法内事务不生效)
@EnableAspectJAutoProxy(exposeProxy = true)
设置无效的根本原因是:
@EnableAspectJAutoProxy 主要配置 AnnotationAwareAspectJAutoProxyCreator(AbstractAutoProxyCreator 的子类)的各种属性,这是系统默认的处理 Advisor 从而创建代理类的代理创造器,而@Validated 注解修饰的类的代理的创建,不是走的 AnnotationAwareAspectJAutoProxyCreator,而是这个 MethodValidationPostProcessor 自己处理 Advisor 自己来生产代理(继承 AbstractBeanFactoryAwareAdvisingPostProcessor),所以 @EnableAspectJAutoProxy(exposeProxy = true)
的配置无法作用到@Validated 注解修饰的类的代理对象,AopContext.currentProxy()
会报错,为了不报错,我们需要手动设置代理生产者的 exposeProxy 属性,也就是 MethodValidationPostProcessor 的 exposeProxy 属性
这个 MethodValidationPostProcessor 的角色有点像一个代理生产者了,跟 AbstractAutoProxyCreator 类似。
MethodValidationPostProcessor 并不是一个简单的 PostProcessor,它继承了 AbstractBeanFactoryAwareAdvisingPostProcessor,由 AbstractBeanFactoryAwareAdvisingPostProcessor 的父类 AbstractAdvisingBeanPostProcessor 对生产出来的 advisor 进行进一步的处理,AbstractAdvisingBeanPostProcessor 还继承了 ProxyProcessorSupport,所以可以设置其 exposeProxy 属性。
具体细节看《Spring5-AOP 源码相关》Advisor 如何生效 - AbstractAutoProxyCreator
解决方案:
无需注解 @EnableAspectJAutoProxy(exposeProxy = true)
,只需要我们手动设置注解对应的 beanpostprocessor 的 exposeProxy 属性即可。创建类 MyBeanFactoryPostProcessor
@Component
public class MyBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
@Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
BeanDefinition methodValidationPostProcessor = beanFactory.getBeanDefinition("methodValidationPostProcessor");
methodValidationPostProcessor.getPropertyValues().add("exposeProxy", true);
}
}
然后即可正常使用 AopContext.currentProxy()
,输出日志
javax.validation.ConstraintViolationException: printUserInfo.user.birthDate: 生日不能为空, printUserInfo.user.occupation: 工作职位不能为空, printUserInfo.user.occupation: 工作职位不能为空, printUserInfo.user.birthDate: 生日不能为空, printUserInfo.user.occupation: 工作职位不能为空
全局异常处理
方法参数/返回值的校验信息,如果可以通过结合 Spring 全局统一的异常处理进行抛出,就能让整个工程都能尽显完美之势。(错误消息可以从异常 ConstraintViolationException
的 getConstraintViolations()
方法里获得的)
请看《SpringMVC- 第七篇:控制器方法异常处理.md》
Spring 自定义 message
请看《Spring-MessageSource 相关解析.md》
Spring MVC 集成 Java Bean Validation - 重点
Controller
提供的使用 @Valid
便捷校验 JavaBean
的原理,和Spring 方法级别的校验支持的原理是有很大差异的。
常见例子 - @PostMapping + @RequestBody @Valid/@Validated 校验 Java Bean
Java Bean,或者叫 POJO
@Data
@Component
public class Student {
@NotNull(message = "姓名不能为空")
@NotBlank(message = "姓名不能为空")
private String name;
@NotNull(message = "性别不能为空")
@NotBlank(message = "性别不能为空")
private String gender;
@Min(value = 0,message = "年龄不能小于0")
@Max(value = 100,message = "年龄不能大于100")
@NotNull(message = "年龄不能为空")
private Integer age;
@NotNull(message = "生日不能为空")
@PastOrPresent(message = "生日不能为将来的时间")
private Date birthDate;
@Min(value = 0,message = "得分不能小于0")
@Max(value = 100,message = "得分不能大于100")
@NotNull(message = "得分不能为空")
private Integer score;
}
控制器方法,若方法入参不写 BindingResult result 这个参数,请求得到的直接是 400 错误,因为若有校验失败的服务端会抛出异常 org.springframework.web.bind.MethodArgumentNotValidException
。若写了,那就调用者自己处理。
/**
* 若方法入参不写BindingResult result这个参数,请求得到的直接是400错误,因为若有校验失败的服务端会抛出异常org.springframework.web.bind.MethodArgumentNotValidException。
* 若写了,那就调用者自己处理
*
* @param student
* @param result
* @return
*/
@PostMapping("/postStudent")
public String postStudent(@RequestBody @Valid Student student, BindingResult result) {
System.out.println("------------------------");
System.out.println(student.toString());
System.out.println(result);
System.out.println("------------------------");
//设置视图名称
return "index";
}
测试类
@Test
void testPostStudent() throws Exception {
this.mockMvc.perform(post("/postStudent")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"xiashuo\",\"gender\":\"男\"}")
)
.andExpect(status().isOk());
}
日志
------------------------
Student(name=xiashuo, gender=男, age=null, birthDate=null, score=null)
org.springframework.validation.BeanPropertyBindingResult: 6 errors
Field error in object 'student' on field 'birthDate': rejected value [null]; codes [NotNull.student.birthDate,NotNull.birthDate,NotNull.java.util.Date,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.birthDate,birthDate]; arguments []; default message [birthDate]]; default message [生日不能为空]
Field error in object 'student' on field 'score': rejected value [null]; codes [NotNull.student.score,NotNull.score,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.score,score]; arguments []; default message [score]]; default message [得分不能为空]
Field error in object 'student' on field 'birthDate': rejected value [null]; codes [NotNull.student.birthDate,NotNull.birthDate,NotNull.java.util.Date,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.birthDate,birthDate]; arguments []; default message [birthDate]]; default message [生日不能为空]
Field error in object 'student' on field 'age': rejected value [null]; codes [NotNull.student.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
Field error in object 'student' on field 'score': rejected value [null]; codes [NotNull.student.score,NotNull.score,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.score,score]; arguments []; default message [score]]; default message [得分不能为空]
Field error in object 'student' on field 'age': rejected value [null]; codes [NotNull.student.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [student.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
------------------------
一般我们推荐使用 @Validated
,还可以指定分组,反正 @Validated
和 @Valid
本身的功能没有差别,只要是 Valid
开头的注解就行
@Data
@Component
public class User {
@NotNull(message = "姓名不能为空",groups = Simple.class)
@NotBlank(message = "姓名不能为空",groups = Simple.class)
private String name;
@NotNull(message = "性别不能为空",groups = Simple.class)
@NotBlank(message = "性别不能为空",groups = Simple.class)
private String gender;
@Min(value = 0,message = "年龄不能小于0",groups = Simple.class)
@Max(value = 100,message = "年龄不能大于100",groups = Simple.class)
@NotNull(message = "年龄不能为空",groups = Simple.class)
private Integer age;
@NotNull(message = "生日不能为空",groups = Advanced.class)
@PastOrPresent(message = "生日不能为将来的时间",groups = Advanced.class)
private Date birthDate;
@NotNull(message = "工作职位不能为空",groups = Advanced.class)
@NotBlank(message = "工作职位不能为空",groups = Advanced.class)
private String occupation;
}
控制器
@PostMapping("/postUser")
public String postUser(@RequestBody @Validated(Simple.class) User user, BindingResult result) {
System.out.println("------------------------");
System.out.println(user.toString());
System.out.println(result);
System.out.println("------------------------");
//设置视图名称
return "index";
}
测试
@Test
void testPostUser() throws Exception {
this.mockMvc.perform(post("/postUser")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"xiashuo\",\"gender\":\"男\"}")
)
.andExpect(status().isOk());
}
输出日志,只输出了 age
一个字段的校验信息,因为只有它这一个 Simple
分组的字段不通过校验。
------------------------
User(name=xiashuo, gender=男, age=null, birthDate=null, occupation=null)
org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
------------------------
常见例子的分析
Controller
提供的使用 @Valid
便捷校验 JavaBean
主要是通过 WebDataBinder 中的 validate 方法(OptionalValidatorFactoryBean 类型)实现,和 Spring 方法级别的校验 (MethodValidationPostProcessor) 的原理是有很大差异的(可类比 Spring MVC
拦截器和 Spring AOP
的差异区别)。
处理入参的处理器:HandlerMethodArgumentResolver
,处理 @RequestBody
最终使用的实现类是:RequestResponseBodyMethodProcessor
,Spring
借助此处理器完成一系列的消息转换器、数据绑定、数据校验等工作。RequestResponseBodyMethodProcessor
中,不需要 MethodValidationPostProcessor
的帮助,在解析参数的时候,会自动进行参数校验和参数绑定,在校验之前,会检查参数前是否有 Valid
开头的注解,也就是说,只要注解是 Valid
开头的就会校验,参数标注了@Validated 注解或者@Valid 注解都会有效。你自定义注解,名称只要 Valid 开头都成。而且只要有一个就够。
这是使用 @RequestBody
结合 @Valid
完成数据校验的基本原理。其实当 Spring MVC
在处理 @RequestPart
注解入参数据时,也会执行绑定、校验的相关逻辑。对应处理器是 RequestPartMethodArgumentResolver
,原理大体上和这相似,它主要处理 Multipart
相关。
如果想要自定义 WebDataBinder
中的 Valdator,请看 自定义全局校验器Validator
小节。
非@RequestBody 注解也可以进行 Java Bean 校验
请注意:并不一样要求是请求 Body 体哦,比如 get 请求的入参若用 JavaBean 接收的话,依旧能启用校验。原因跟上一小节中提到的大同小异,因为不需要 MethodValidationPostProcessor
的帮助,在解析参数的并将其注入到 Java Bean 类型的形参中的时候,会自动进行参数校验和参数绑定,
@GetMapping("/getUser")
public String getUser(@Validated(Simple.class) User user, BindingResult result) {
System.out.println("------------------------");
System.out.println(user.toString());
System.out.println(result.toString());
System.out.println("------------------------");
//设置视图名称
return "index";
}
测试类
@Test
void testGetUser() throws Exception {
this.mockMvc.perform(get("/getUser")
.param("name","xiashuo")
.param("gender","男")
)
.andDo(print());
}
输出日志
------------------------
User(name=xiashuo, gender=男, age=null, birthDate=null, occupation=null)
org.springframework.validation.BeanPropertyBindingResult: 2 errors
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
Field error in object 'user' on field 'age': rejected value [null]; codes [NotNull.user.age,NotNull.age,NotNull.java.lang.Integer,NotNull]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [user.age,age]; arguments []; default message [age]]; default message [年龄不能为空]
------------------------
跟 @PostMapping + @RequestBody @Valid 校验Java Bean
不同的是,controller 方法中不添加 , BindingResult result
参数 controller 也不会中止,只是会报错 BindException.class
,这个时候,可以手动添加全局异常处理。
@ExceptionHandler(value = {BindException.class})
public String handleMethodArgumentNotValid2(BindException ex ) {
StringBuilder stringBuilder = new StringBuilder();
BindingResult bindingResult = ex.getBindingResult();
for (FieldError error : bindingResult.getFieldErrors()) {
String field = error.getField();
Object value = error.getRejectedValue();
String msg = error.getDefaultMessage();
String message = String.format("错误字段:%s,错误值:%s,原因:%s;", field, value, msg);
stringBuilder.append(message).append("\r\n");
}
return stringBuilder.toString();
}
Controller 方法非 JavaBean 类型的参数的校验
大部分的场景下,我们需要校验 Controller 中零散的参数,而不是 Java Bean,这种情况使用@Valid 并不能覆盖。
方案一 MethodValidationPostProcessor + 全局异常处理 - 常用
注册 springframework.validation.Validator
和 org.springframework.validation.beanvalidation.MethodValidationPostProcessor
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
</bean>
<bean id="methodValidationPostProcessor" class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
<property name="validator" ref="validator"/>
</bean>
在控制器上添加 @Validated
注解
@Controller
@Validated({Simple.class,Advanced.class})
public class ValidatorController {
@GetMapping("/getInfo")
public String getInfo(@NotBlank(message = "姓名不能为空", groups = Simple.class) String name, @NotBlank(message = "性别不能为空", groups = Simple.class) String gender, @NotNull(groups = Advanced.class) @Min(value = 1, groups = Advanced.class) Integer age) {
System.out.println("------------------------");
System.out.println(name);
System.out.println(gender);
System.out.println(age);
System.out.println("------------------------");
//设置视图名称
return "index";
}
}
添加全局异常处理,处理的是 ConstraintViolationException
类
@ControllerAdvice
@ResponseBody
public class MethodArgumentNotValidExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public String handleMethodArgumentNotValid(ConstraintViolationException ex) {
StringBuilder stringBuilder = new StringBuilder();
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
for (ConstraintViolation<?> constraintViolation : constraintViolations) {
String propertyPath = constraintViolation.getPropertyPath().toString();
Object invalidValue = constraintViolation.getInvalidValue();
String infp = constraintViolation.getMessage();
String message = String.format("错误字段:%s,错误值:%s,原因:%s;", propertyPath, invalidValue, infp);
stringBuilder.append(message).append("\r\n");
}
return stringBuilder.toString();
}
}
然后开始测试
@Test
void testGetUser() throws Exception {
this.mockMvc.perform(get("/getInfo")
.param("name","")
.param("gender","")
)
.andDo(print());
}
输出日志
MockHttpServletRequest:
HTTP Method = GET
Request URI = /getInfo
Parameters = {name=[], gender=[]}
Headers = []
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = xyz.xiashuo.springmvcbeanvalidation.controller.ValidatorController
Method = xyz.xiashuo.springmvcbeanvalidation.controller.ValidatorController#getInfo(String, String, Integer)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = javax.validation.ConstraintViolationException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/html;charset=UTF-8", Content-Length:"234"]
Content type = text/html;charset=UTF-8
Body = 错误字段:getInfo.age,错误值:null,原因:must not be null;
错误字段:getInfo.name,错误值:,原因:姓名不能为空;
错误字段:getInfo.gender,错误值:,原因:性别不能为空;
Forwarded URL = null
Redirected URL = null
Cookies = []
可谓非常方便,优雅
这种方案一样有一个非常值得注意但是很多人都会忽略的地方:因为我们希望能够代理 Controller
这个 Bean,所以仅仅只在父容器中配置 MethodValidationPostProcessor
是无效的,必须在子容器(web 容器)的配置文件中再配置一个 MethodValidationPostProcessor
,请务必注意
有小伙伴问我了,为什么它的项目里只配置了一个
MethodValidationPostProcessor
也生效了呢? 我的回答是:检查一下你是否是用的 SpringBoot。可是我用的是原生的 SpringMVC,也只配置了一次
MethodValidationPostProcessor
就生效了。
其实关于配置一个还是多个 MethodValidationPostProcessor
的 case,其实是个 Bean 覆盖有很大关系的,这方面内容可参考:【小家Spring】聊聊Spring的bean覆盖(存在同名name/id问题),介绍Spring名称生成策略接口BeanNameGenerator
方案二 自定义 HandlerInterceptor - 太复杂,不常用
设计思路:Controller
拦截器 + @Validated
注解 + 自定义校验器
添加拦截器
// 注意:此处只支持@RequesrMapping方式~~~~
@Component
public class ValidationInterceptor implements HandlerInterceptor, InitializingBean {
@Autowired
@Qualifier("validator")
private LocalValidatorFactoryBean validatorFactoryBean;
@Autowired
private RequestMappingHandlerAdapter adapter;
private List<HandlerMethodArgumentResolver> argumentResolvers;
@Override
public void afterPropertiesSet() throws Exception {
argumentResolvers = adapter.getArgumentResolvers();
}
// 缓存
private final Map<MethodParameter, HandlerMethodArgumentResolver> argumentResolverCache = new ConcurrentHashMap<>(256);
private final Map<Class<?>, Set<Method>> initBinderCache = new ConcurrentHashMap<>(64);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 只处理HandlerMethod方式
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod) handler;
Validated valid = method.getMethodAnnotation(Validated.class); //
if (valid != null) {
// 根据工厂,拿到一个校验器
ValidatorImpl validatorImpl = (ValidatorImpl) validatorFactoryBean.getValidator();
// 拿到该方法所有的参数们~~~ org.springframework.core.MethodParameter
MethodParameter[] parameters = method.getMethodParameters();
Object[] parameterValues = new Object[parameters.length];
//遍历所有的入参:给每个参数做赋值和数据绑定
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
// 找到适合解析这个参数的处理器~
HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
Assert.notNull(resolver, "Unknown parameter type [" + parameter.getParameterType().getName() + "]");
ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
WebDataBinderFactory webDataBinderFactory = getDataBinderFactory(method);
// 调用此方法有个前提,就是控制器方法的必须配置注解表示参数的名称,比如 @PathVariable("paramName") 或者 @RequestParam("name"),一般在不影响功能的前提下,使用 @RequestParam("name") 最合适
// 否则会报错 Request processing failed; nested exception is java.lang.IllegalArgumentException: Name for argument of type [java.lang.String] not specified, and parameter name information not found in class file either.
Object value = resolver.resolveArgument(parameter, mavContainer, new ServletWebRequest(request, response), webDataBinderFactory);
parameterValues[i] = value; // 赋值
}
// 对入参进行统一校验
Set<ConstraintViolation<Object>> violations = validatorImpl.validateParameters(method.getBean(), method.getMethod(), parameterValues, valid.value());
// 若存在错误消息,此处也做抛出异常处理 javax.validation.ConstraintViolationException
if (!violations.isEmpty()) {
System.err.println("方法入参校验失败~~~~~~~");
throw new ConstraintViolationException(violations);
}
}
}
return true;
}
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) {
Class<?> handlerType = handlerMethod.getBeanType();
Set<Method> methods = this.initBinderCache.get(handlerType);
if (methods == null) {
// 支持到@InitBinder注解
methods = MethodIntrospector.selectMethods(handlerType, RequestMappingHandlerAdapter.INIT_BINDER_METHODS);
this.initBinderCache.put(handlerType, methods);
}
List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
for (Method method : methods) {
Object bean = handlerMethod.getBean();
initBinderMethods.add(new InvocableHandlerMethod(bean, method));
}
return new ServletRequestDataBinderFactory(initBinderMethods, adapter.getWebBindingInitializer());
}
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) {
if (methodArgumentResolver.supportsParameter(parameter)) {
result = methodArgumentResolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
自定义拦截器
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="/validationByInterceptor/**"/>
<!-- <mvc:exclude-mapping path="/index/*"/>-->
<bean class="xyz.xiashuo.springmvcbeanvalidation.interceptor.ValidationInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
控制器方法
@Controller
@RequestMapping("/validationByInterceptor")
public class ValidatorByInterceptorController {
@Validated
@GetMapping("/getInfo")
public String getInfo(@NotBlank(message = "姓名不能为空") @RequestParam("name") String name, @NotBlank(message = "性别不能为空") @RequestParam("gender") String gender, @NotNull @Min(value = 1) @RequestParam("age") Integer age) {
System.out.println("------------------------");
System.out.println(name);
System.out.println(gender);
System.out.println(age);
System.out.println("------------------------");
//设置视图名称
return "index";
}
}
因为拦截器中使用了 resolver.resolveArgument
,用此方法有个前提,就是控制器方法的必须配置注解表示参数的名称,比如 @PathVariable("paramName") 或者 @RequestParam("name"),一般在不影响功能的前提下,使用 @RequestParam("name") 最合适。否则会报错:
Request processing failed; nested exception is java.lang.IllegalArgumentException: Name for argument of type [java.lang.String] not specified, and parameter name information not found in class file either.
添加全局异常处理,处理的是 ConstraintViolationException
类
@ControllerAdvice
@ResponseBody
public class MethodArgumentNotValidExceptionHandler {
@ExceptionHandler(ConstraintViolationException.class)
public String handleMethodArgumentNotValid(ConstraintViolationException ex) {
StringBuilder stringBuilder = new StringBuilder();
Set<ConstraintViolation<?>> constraintViolations = ex.getConstraintViolations();
for (ConstraintViolation<?> constraintViolation : constraintViolations) {
String propertyPath = constraintViolation.getPropertyPath().toString();
Object invalidValue = constraintViolation.getInvalidValue();
String infp = constraintViolation.getMessage();
String message = String.format("错误字段:%s,错误值:%s,原因:%s;", propertyPath, invalidValue, infp);
stringBuilder.append(message).append("\r\n");
}
return stringBuilder.toString();
}
}
测试类
@SpringJUnitWebConfig(locations = "classpath:springMVC.xml")
class ValidatorByInterceptorControllerTest {
MockMvc mockMvc;
@BeforeEach
void setup(WebApplicationContext wac) {
this.mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
void testGetUser() throws Exception {
this.mockMvc.perform(get("/validationByInterceptor/getInfo")
.param("name","")
.param("gender","")
.param("age", String.valueOf(10))
)
.andDo(print());
}
}
输出日志
---------------- 方法入参校验失败 ----------------
MockHttpServletRequest:
HTTP Method = GET
Request URI = /validationByInterceptor/getInfo
Parameters = {name=[], gender=[], age=[10]}
Headers = []
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = xyz.xiashuo.springmvcbeanvalidation.controller.ValidatorByInterceptorController
Method = xyz.xiashuo.springmvcbeanvalidation.controller.ValidatorByInterceptorController#getInfo(String, String, Integer)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = javax.validation.ConstraintViolationException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/html;charset=UTF-8", Content-Length:"156"]
Content type = text/html;charset=UTF-8
Body = 错误字段:getInfo.gender,错误值:,原因:性别不能为空;
错误字段:getInfo.name,错误值:,原因:姓名不能为空;
Forwarded URL = null
Redirected URL = null
Cookies = []
Controller 方法校验失败后的全局异常处理示例
对于不同的参数解析方式,Spring 做参数校验时会抛出不同的异常,而且这些异常没有继承关系,通过异常获取校验结果的方式也各不相同(好坑爹~)。
-
对象参数接收请求体,即
@RequestBody
,抛出 MethodArgumentNotValidException -
请求参数绑定到对象参数上:BindException
-
散装的(非 JavaBean)普通参数:ConstraintViolationException
-
必填参数缺失:ServletRequestBindingException
这里我们以用的最多的 @RequestBody
校验为例,当校验失败时,Spring
会抛出 MethodArgumentNotValidException
异常,该异常会持有校验结果对象 BindingResult
,从而获得校验失败信息。
全局异常处理器
@ControllerAdvice
@ResponseBody
public class MethodArgumentNotValidExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder stringBuilder = new StringBuilder();
for (FieldError error : bindingResult.getFieldErrors()) {
String field = error.getField();
Object value = error.getRejectedValue();
String msg = error.getDefaultMessage();
String message = String.format("错误字段:%s,错误值:%s,原因:%s;", field, value, msg);
stringBuilder.append(message).append("\r\n");
}
return stringBuilder.toString();
}
}
控制器方法
@PostMapping("/postUserThrowException")
public String postUserThrowException(@RequestBody @Validated(Simple.class) User user) {
System.out.println("------------------------");
System.out.println(user.toString());
System.out.println("------------------------");
//设置视图名称
return "index";
}
测试类方法
@Test
void testPostUserThrowException() throws Exception {
this.mockMvc.perform(post("/postUserThrowException")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"xiashuo\",\"gender\":\"男\"}")
)
.andDo(print());
}
控制器的异常由 MethodArgumentNotValidExceptionHandler
的 handleMethodArgumentNotValid
方法处理,并返回,输出日志
MockHttpServletRequest:
HTTP Method = POST
Request URI = /postUserThrowException
Parameters = {}
Headers = [Content-Type:"application/json", Content-Length:"33"]
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = xyz.xiashuo.springmvcbeanvalidation.controller.HelloController
Method = xyz.xiashuo.springmvcbeanvalidation.controller.HelloController#postUserThrowException(User)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = org.springframework.web.bind.MethodArgumentNotValidException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/html;charset=UTF-8", Content-Length:"144"]
Content type = text/html;charset=UTF-8
Body = 错误字段:age,错误值:null,原因:年龄不能为空;
错误字段:age,错误值:null,原因:年龄不能为空;
Forwarded URL = null
Redirected URL = null
Cookies = []
处理所有 Controller 参数校验异常的集大成者
@ExceptionHandler({ConstraintViolationException.class, MethodArgumentNotValidException.class, ServletRequestBindingException.class, BindException.class})
@ResponseBody
public ResultBean<?> handleValidationException(Exception e) {
String msg = "";
if (e instanceof MethodArgumentNotValidException) {
MethodArgumentNotValidException t = (MethodArgumentNotValidException) e;
msg = t.getBindingResultgetDefaultMessage).collect(Collectors.joining(",");
} else if (e instanceof BindException) {
BindException t = (BindException) e;
msg = t.getBindingResultgetDefaultMessage).collect(Collectors.joining(",");
} else if (e instanceof ConstraintViolationException) {
ConstraintViolationException t = (ConstraintViolationException) e;
msg = t.getConstraintViolationsgetMessage).collect(Collectors.joining(",");
} else if (e instanceof MissingServletRequestParameterException) {
MissingServletRequestParameterException t = (MissingServletRequestParameterException) e;
msg = t.getParameterName() + " 不能为空";
} else if (e instanceof MissingPathVariableException) {
MissingPathVariableException t = (MissingPathVariableException) e;
msg = t.getVariableName() + " 不能为空";
} else {
msg = "必填参数缺失";
}
log.warn("参数校验不通过,msg: {}", msg);
return ResultBean.fail(msg);
}
SpringMVC 中自定义 message 信息
背景知识,请看《Spring-MessageSource 相关解析.md》
在 JavaBeanValidation+HibernateValidator
中使用的是 Hibernate Validation
内置的对国际化的支持,由于大部分情况下我们都是在 Spring
环境下使用数据校验,因此有必要讲讲 Spring 加持情况下的国际化做法。我们知道 Spring MVC
是有专门做国际化的模块的,因此国际化这个动作当然也是可以交给 Spring
自己来做的
其实跟使用的是 Hibernate Validation
内置的对国际化的支持的时候的使用方法差别不大,主要的差别在于如何加载国际化的资源文件
Controller
提供的使用 @Valid
校验 JavaBean
主要是通过 WebDataBinder 中的 validate 方法实现,系统内部有一个自动生成的 Validator,类型是 OptionalValidatorFactoryBean,此 Validator 在 WebMvcConfigurationSupport 的 mvcValidator 方法中生成;
我们使用 MethodValidationPostProcessor 的时候,一般是自己自定义一个 Validator:org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<bean id="methodValidationPostProcessor" class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
<property name="validator" ref="validator"/>
</bean>
我们需要设置做的事情其实很简单,自定义一个 Validator 并设置 validationMessageSource 属性,然后替换 WebDataBinder 中使用的 validator即可。
-
自定义个一个
org.springframework.validation.beanvalidation.LocalValidatorFactoryBean
-
设置其 validationMessageSource 属性
-
将其替换到 WebDataBinder 的 validator 中:通过
@InitBinder
修饰的方法自定义 WebDataBinder
实践
JavaBean
@Data
@Component
public class UserWithCustomizeMessage {
@NotNull(message = "{xyz.xiashuo.pojo.User.name.NotNull}")
@NotBlank(message = "{xyz.xiashuo.pojo.User.name.NotBlank}")
private String name;
@NotNull(message = "{xyz.xiashuo.pojo.User.gender.NotNull}")
@NotBlank(message = "{xyz.xiashuo.pojo.User.gender.NotBlank}")
private String gender;
@NotNull(message = "{xyz.xiashuo.pojo.User.occupation.NotNull}")
@NotBlank(message = "{xyz.xiashuo.pojo.User.occupation.NotBlank}")
private String occupation;
@NotNull(message = "{xyz.xiashuo.pojo.User.age.NotNull}")
@Min(value = 18, message = "{xyz.xiashuo.pojo.User.age.Min}")
private int age;
}
控制器
@Controller
@RequestMapping("/CustomizeMessage")
public class CustomizeMessageController {
@Autowired
@Qualifier("validator")
private LocalValidatorFactoryBean localValidatorFactoryBean;
@InitBinder
public void InitBinder(WebDataBinder binder) {
binder.setValidator(localValidatorFactoryBean);
}
@PostMapping("/getInfo")
public String getInfo(@RequestBody @Valid UserWithCustomizeMessage user) {
System.out.println("------------------------");
System.out.println(user.toString());
System.out.println("------------------------");
//设置视图名称
return "index";
}
}
全局异常处理
@ControllerAdvice
@ResponseBody
public class MethodArgumentNotValidExceptionHandler {
@ExceptionHandler(MethodArgumentNotValidException.class)
public String handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder stringBuilder = new StringBuilder();
for (FieldError error : bindingResult.getFieldErrors()) {
String field = error.getField();
Object value = error.getRejectedValue();
String msg = error.getDefaultMessage();
String message = String.format("错误字段:%s,错误值:%s,原因:%s;", field, value, msg);
stringBuilder.append(message).append("\r\n");
}
return stringBuilder.toString();
}
}
Validator 的配置
<bean id="validator" class="org.springframework.validation.beanvalidation.LocalValidatorFactoryBean">
<property name="providerClass" value="org.hibernate.validator.HibernateValidator"/>
<property name="validationMessageSource" ref="messageSource"/>
</bean>
<bean id="methodValidationPostProcessor" class="org.springframework.validation.beanvalidation.MethodValidationPostProcessor">
<property name="validator" ref="validator"/>
</bean>
<bean id="messageSource" class="org.springframework.context.support.ReloadableResourceBundleMessageSource">
<property name="basename" value="classpath:validationMessage"/>
<property name="defaultEncoding" value="utf-8"/>
<property name="useCodeAsDefaultMessage" value="false"/>
<property name="cacheSeconds" value="60"/>
</bean>
同时在 resources
目录下添加文件 validationMessage.properties
// 此处可以使用占位符{value}读取注解对应属性上的值
xyz.xiashuo.pojo.User.name.NotNull = [自定义消息]姓名不能为空
xyz.xiashuo.pojo.User.name.NotBlank = [自定义消息]姓名不能为空
xyz.xiashuo.pojo.User.gender.NotNull = [自定义消息]性别不能为空
xyz.xiashuo.pojo.User.gender.NotBlank = [自定义消息]性别不能为空
xyz.xiashuo.pojo.User.occupation.NotNull = [自定义消息]职位不能为空
xyz.xiashuo.pojo.User.occupation.NotBlank = [自定义消息]职位不能为空
xyz.xiashuo.pojo.User.age.NotNull = [自定义消息]年龄不能为空
xyz.xiashuo.pojo.User.age.Min = [自定义消息]年龄最小值不能小于{value}
测试方法
@Test
void getInfo() throws Exception {
this.mockMvc.perform(post("/CustomizeMessage/getInfo")
.contentType(MediaType.APPLICATION_JSON)
.content("{\"name\":\"\",\"gender\":\"\"}")
)
.andDo(print());
}
输出日志
MockHttpServletRequest:
HTTP Method = POST
Request URI = /CustomizeMessage/getInfo
Parameters = {}
Headers = [Content-Type:"application/json", Content-Length:"23"]
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = xyz.xiashuo.springmvcbeanvalidation.controller.CustomizeMessageController
Method = xyz.xiashuo.springmvcbeanvalidation.controller.CustomizeMessageController#getInfo(UserWithCustomizeMessage)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = org.springframework.web.bind.MethodArgumentNotValidException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 200
Error message = null
Headers = [Content-Type:"text/html;charset=UTF-8", Content-Length:"559"]
Content type = text/html;charset=UTF-8
Body = 错误字段:occupation,错误值:null,原因:[自定义消息]职位不能为空;
错误字段:age,错误值:0,原因:[自定义消息]年龄最小值不能小于18;
错误字段:occupation,错误值:null,原因:[自定义消息]职位不能为空;
错误字段:name,错误值:,原因:[自定义消息]姓名不能为空;
错误字段:gender,错误值:,原因:[自定义消息]性别不能为空;
错误字段:occupation,错误值:null,原因:[自定义消息]职位不能为空;
Forwarded URL = null
Redirected URL = null
Cookies = []
自定义全局校验器 Validator,即 WebDataBinder 中使用的那个 Validator
只要引入 hibernate-validator
,系统内部有一个自动生成的 Validator,类型是 OptionalValidatorFactoryBean,
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.0.17.Final</version>
</dependency>
在使用 XML 配置 SpringMVC 的时候,在 AnnotationDrivenBeanDefinitionParser
的静态代码块中,根据 classpath 中是否存在 javax.validation.Validator
来判断是否创建 OptionalValidatorFactoryBean
,参考《SpringMVC- 第九篇:基于 XML 配置 SpringMVC》。
在使用注解配置 SpringMVC 的时候,在 WebMvcConfigurationSupport 的 mvcValidator 方法中,根据 classpath 中是否存在 javax.validation.Validator
来判断是否创建 OptionalValidatorFactoryBean
,参考《SpringMVC- 第十篇:基于注解配置 SpringMVC》
此全局 Validator 可通过 @Autowired
注解直接引用。如果想要全局替换,继承 WebMvcConfigurerAdapter 重写 getValidator 即可。
@Configuration
@EnableWebMvc
public class WebMvcConfig extends WebMvcConfigurerAdapter {
...
@Override
public Validator getValidator() {
// return "global" validator
return new LocalValidatorFactoryBean();
}
...
}
当然,如果你不想全局控制,只想在当前 Controller 中进行个性化设置,你还可以使用 @InitBinder
来设置自定义 WebDataBinder,比如你可以使用自定校验器实现各种私有的、比较复杂的逻辑判断。
例子请看 SpringMVC中自定义message信息