如果使用spring-boot-stater-validation,默认会集成hibernate-validator 6.x版本,如果想使用hibernate-validtor 7.x而java EE 在2018年命名空间全面迁移,由javax--> jakarta 则validation的api也由 javax-validation 迁移到了jakarta.validation,导致了失效。
可以看最新版本hibernate-validator 7.x的集成环境
7.0 series - Hibernate Validator
降低版本方式解决
直接使用spring-boot-stater-validation,默认hibernate-validator 6.x版本和
javax.validation-api 的版本 或者显示的使用下面maven的版本
jakarta.validation-api 2.x版本(也可以使用javax.validation-api因为2.x版本做了横向迁移)
如下图。spring-boot 新增独立依赖spring-boot-starter-parent或者 spring-boot-dependencies 的版本所以无需添加版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
如果不使用spring-boot-starter如下图
<dependency>
<groupId>jakarta.validation</groupId>
<artifactId>jakarta.validation-api</artifactId>
<version>2.0.2</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.3.Final</version>
</dependency>
自定义初始化spring的Validator适配器
如果就想使用hibernate-validator7.x版本,可以使用自定义初始化逻辑,下面我们看看源码,可以先参考之前的源码解读javax.validation.constraints 及spring的整合,hibernate-validator的实现 - 简书 (jianshu.com)
前面的源码可以看到 spring和validator的集成通过一个适配器,首先通过参数解析器然后通过 dataBinder来调用校验,spring自己定义了org.springframework.validation.validator作为适配器接口,由SpringValidatorAdapter同时实现spring的适配器和被适配的javax.validation.Validator(新版本变成了jakarta.validation.Validator)那么我们进入源码也能看到spring的适配器还是实现的旧的javax.validation.Validator
可以看到全部使用的旧的命名空间,那么我们可不可以自己初始化这个适配器呢。继续看看源码。我们找到了最下面的一个子类,这个子类被spring注入了适配器接口 org.springframework.validation.validator
public class OptionalValidatorFactoryBean extends LocalValidatorFactoryBean {
@Override
public void afterPropertiesSet() {
try {
super.afterPropertiesSet();
}
catch (ValidationException ex) {
LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
}
}
}
WebMvcConfigurationSupport 类进行注入适配器作为springmvc的controller参数校验
@Bean
public Validator mvcValidator() {
// 这里是一个模版方法,可以由子类覆盖,但是下面又做了类命名限定检查,只能使用旧版本api,所以这个方法覆盖并不能实现我们想要的
Validator validator = getValidator();
if (validator == null) {
// 这里看到指向的还是旧版本的命名空间
if (ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
Class<?> clazz;
try {
String className = "org.springframework.validation.beanvalidation.OptionalValidatorFactoryBean";
clazz = ClassUtils.forName(className, WebMvcConfigurationSupport.class.getClassLoader());
}
catch (ClassNotFoundException | LinkageError ex) {
throw new BeanInitializationException("Failed to resolve default validator class", ex);
}
validator = (Validator) BeanUtils.instantiateClass(clazz);
}
else {
// 如果没有找到旧的命名空间就返回空适配器的实现,这个适配器并没有实现javax.validation.Validator来做适配
validator = new NoOpValidator();
}
}
return validator;
}
我们可以在上面代码来自己初始化适配器,然后看到SpringValidatorAdapter的代码全部是引用的旧命名空间,new的话一定会NoClassDefFoundError的,spring为了支持多语言实际初始化的是OptionalValidatorFactoryBean,那么我们可以直将LocalValidatorFactoryBean和SpringValidatorAdapter和OptionalValidatorFactoryBean的代码复制出来,然后将所有命名空间替换即可.
要复制的类及功能如下
1.LocaleContextMessageInterpolator 实现MessageInterpolator接口对校验不通过的提示信息的实现
- SpringValidatorAdaptor 实现spring的适配器接口,及真正做校验的jakarta.validation.Validator接口,适配器的具体实现,做校验工作
- LocalValidatorFactoryBean 作为SpringValidatorAdaptor 适配器的子类用来整合spring的上下文,对当前适配器bean的管理及提示信息解析
- OptionalValidatorFactoryBean 适配器的真正实例化的子类
- SpringConstraintValidatorFactory 校验器工厂,例如@NotNull注解对应一个专门的非空判断的校验器,我们也可以自定义校验注解,实现的就是这个工厂实例化出来的。具体可以看代码ConstraintValidator<A extends Annotation, T>
我们将上面所有的类复制一份出来修改命名空间然后注入即可
@Override
@Bean
public Validator mvcValidator() {
return new CustomOptionalValidatorFactoryBean();
}
启动之后还是不好用,我们通过debug初始化Validator的过程,发现报错
public class CustomOptionalValidatorFactoryBean extends CustomLocalValidatorFactoryBean {
@Override
public void afterPropertiesSet() {
try {
super.afterPropertiesSet();
}
catch (ValidationException ex) {
// 这里报错打印日志,发现
ex.printStackTrace();
LogFactory.getLog(getClass()).debug("Failed to set up a Bean Validation provider", ex);
}
}
}
但是启动程序还是不好用我们debug发现还是没有注入Validator,这里debug方式就不写了,我们直接在上面代码初始化时发现有报错
jakarta.validation.ValidationException: HV000183: Unable to initialize 'jakarta.el.ExpressionFactory'. Check that you have the EL dependencies on the classpath, or use ParameterMessageInterpolator instead
at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.buildExpressionFactory(ResourceBundleMessageInterpolator.java:211)
at org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator.<init>(ResourceBundleMessageInterpolator.java:97)
at org.hibernate.validator.internal.engine.AbstractConfigurationImpl.getDefaultMessageInterpolator(AbstractConfigurationImpl.java:574)
at io.g.erha.test.CustomLocalValidatorFactoryBean.afterPropertiesSet(CustomLocalValidatorFactoryBean.java:265)
发现是jakarta.el 包没发现,突然想起来最新的整合还需要依赖新得jakarta.el版本,新得版本el包抽出来了我们添加maven配置
<dependency>
<groupId>org.glassfish</groupId>
<artifactId>jakarta.el</artifactId>
<version>4.0.2</version>
</dependency>
然后重新编译启动 。看见可以了
以上套路最好不要使用,就当是提供了另一种角度看spring和validator的整合逻辑,首先hibernate-validator是javaEE的javax.validation标准的实现,那么javaEE在2018年更改了命名空间后hibernate-validtor随后推出了7.x版本支持,但是spring-boot-starter一直使用旧的版本,我们可以从hibernate-validator的发布信息看到基本做了很少的改动,修复了少量bug,如果您就是遇到了7.x版本修复的bug的使用场景可以考虑使用上述方式提升版本,而通过上述的代码复制我们也知道了spring通过适配器方式整合validator结合springmvc进行参数校验,并提供了Spring上下文支持的javax.validation(jakarta.validation)的自定义校验器的工厂实现,因为我们默认注入的Validator都是spring的适配器,那么这个spring上下文的支持就是我们在自定义实现了ConstraintValidator<A extends Annotation, T> 接口通过自定义校验注解校验时,虽然这个校验器子类是每次实例化但是spring也会帮你@Autowired 和@Resource注入
最后我们看看spring官方也解释了为什么不集成新的命名空间版本,及未来会在今年的spring-boot 3.x 和 spring 6.x版本全面支持! 大家尽量等等喽
支持 Jakarta EE 9(jakarta.* 命名空间中的注释和接口) ·问题 #25354 ·弹簧项目/弹簧框架 (github.com)