本文尝试探究SpringBoot为集成验证框架而做的一些架构设计与实现,以期在实际运用中能够更加自如,做到心里有底。
当前谈到Spring框架,基本都是SpringBoot起步,因此本文也就顺势从SpringBoot开始,探究SpringBoot是如何将参数校验的Validated机制无缝融入到自身的架构体系的。
本文主体部分大概分为三个小节,分别对应于MVC分层之下:
关于Controller层,因为HTTP协议本身是允许多种Content-Type
的,而SpringMVC也是进行相应的设计以支持全部的协议需求,这里笔者只以最为常用的REST约定里常用的content-type:application/json
为例进行论述。该类Content-Type
直接决定了下面用例中使用的注解 @RestController
和 @RequestBody
。
按照惯例,我们先来看看本小节源码解读涉及到测试用例。
@RestController
@RequestMapping(AppConstant.APPLICATION_USER_NAME)
public class UserController {
/**
* 新增或修改
*/
@PostMapping("/submit")
public R submit(@Valid @RequestBody User user) {
CacheUtil.clear(USER_CACHE);
return R.status(userService.submit(user));
}
......
}
@Data
@TableName("xxx_user")
@EqualsAndHashCode(callSuper = true)
public class User {
private static final long serialVersionUID = 1L;
/**
* 用户编号
*/
@NotNull
@NotEmpty
private String code;
/**
* 账号
*/
@NotNull
@NotEmpty
private String account;
@Valid // 嵌套验证必须用@Valid
@NotNull(message = "children不能为空")
@Size(min = 1, message = "children至少要有一个元素")
private List<User> children;
......
}
以上代码很简单,一眼就能看出是一个基本的Controller层方法和对应的方法参数类型User
的定义,Controller层上唯一能看出与验证框架有关联的应该就是方法参数上注解的那个@Valid
了,下面让我们从源码底层看看SpringBoot是如何让这个注解生效的。
首先让我们先来看看SpringBoot是如何进行验证框架的整合的。
跟随过往的经验SpringBoot源码解析之AutoConfiguration,我们找到了WebMvcAutoConfiguration
以及 ValidationAutoConfiguration
。
ValidationAutoConfiguration
遵循先易后难,我们将目光先集中到 ValidationAutoConfiguration
类上。
@Configuration
// 为了强化区别, 这里笔者将类名进行了补全
@ConditionalOnClass(javax.validation.executable.ExecutableValidator.class)
// resources指示的文件可以在 hibernate-validator-6.0.19.Final.jar 中找到
@ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider"),通过这个文件hibernate-validator借助Java SPI思想来注册自身的Validate实现
// 通过继承 ImportBeanDefinitionRegistrar 接口来实现 org.springframework.validation.Validator接口实现类实例的Primary。
@Import(PrimaryDefaultValidatorPostProcessor.class)
public class ValidationAutoConfiguration {
@Bean
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
@ConditionalOnMissingBean(Validator.class)
public static LocalValidatorFactoryBean defaultValidator() {
LocalValidatorFactoryBean factoryBean = new LocalValidatorFactoryBean();
MessageInterpolatorFactory interpolatorFactory = new MessageInterpolatorFactory();
factoryBean.setMessageInterpolator(interpolatorFactory.getObject());
return factoryBean;
}
@Bean
@ConditionalOnMissingBean
public static MethodValidationPostProcessor methodValidationPostProcessor(Environment environment,
@Lazy Validator validator) {
MethodValidationPostProcessor processor = new MethodValidationPostProcessor();
boolean proxyTargetClass = environment.getProperty("spring.aop.proxy-target-class", Boolean.class, true);
processor.setProxyTargetClass(proxyTargetClass);
processor.setValidator(validator);
return processor;
}
}
以上定义中,首先我们来看看返回 LocalValidatorFactoryBean
类型的 defaultValidator()
,通过断点调试(下图)我们可以看到,在该实例构建之初其内部的字段大部分还没有被装配到位,真正的装配的操作还是依赖于 LocalValidatorFactoryBean
类自身所实现的Spring接口InitializingBean
来实现的,感兴趣的读者可以自行查阅LocalValidatorFactoryBean.afterPropertiesSet()
,在其中可以清晰地看到LocalValidatorFactoryBean
如何为基类字段targetValidator
(javax.validation.Validator
类型)赋值为org.hibernate.validator.internal.engine.ValidatorImpl
实例的(感兴趣的读者可以在基类SpringValidatorAdapter
的setTargetValidator()
处打上断点)。
WebMvcAutoConfiguration
接下来再让我们看看配置类WebMvcAutoConfiguration
,主要关注如下几个方法:
// =================== 本类属于WebMvcAutoConfiguration的内部类
// 基类DelegatingWebMvcConfiguration直接继承自WebMvcConfigurationSupport, 也就是WebMvcConfigurationSupport为本类的祖先类
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
@Bean
@Override
public Validator mvcValidator() {
if (!ClassUtils.isPresent("javax.validation.Validator", getClass().getClassLoader())) {
return super.mvcValidator();
}
// 本次测试用例下将走这条分支
// 最终生成的实例为 org.springframework.boot.autoconfigure.validation.ValidatorAdapter
// 上面这个实例的名字都不用猜正是使用了Adapter模式
// 而Adapter底层的实例正是我们前文提到的LocalValidatorFactoryBean实例。双重适配最为致命啊.....
return ValidatorAdapter.get(getApplicationContext(), getValidator());
}
@Override
protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
try {
return this.beanFactory.getBean(ConfigurableWebBindingInitializer.class);
}
catch (NoSuchBeanDefinitionException ex) {
// 本次测试用例下将走这条分支
// 而这个逻辑中恰恰将回调上面覆写的 mvcValidator() 方法
return super.getConfigurableWebBindingInitializer();
}
}
// !!!!!!! 注意: 为了避免骗字数的嫌疑, 这里将祖先类 WebMvcConfigurationSupport 的方法拷贝在这里
/**
* Return the {@link ConfigurableWebBindingInitializer} to use for
* initializing all {@link WebDataBinder} instances.
* 这里我们特意保留了注释. 注意所创建的ConfigurableWebBindingInitializer 实例, 其实现的接口方法initBinder()会在每次请求到来的时候都被调用, 为每个DataBinder进行初始化操作, 感兴趣的读者可以自行断点调试.
* 上述的initBinder()方法中进行的一系列初始化操作就包括为WebDataBinder实例配置Validator实现
*/
protected ConfigurableWebBindingInitializer getConfigurableWebBindingInitializer() {
ConfigurableWebBindingInitializer initializer = new ConfigurableWebBindingInitializer();
initializer.setConversionService(mvcConversionService());
// 本例中, mvcValidator()被子类覆写了, 正如上方已经论述的
initializer.setValidator(mvcValidator());
MessageCodesResolver messageCodesResolver = getMessageCodesResolver();
if (messageCodesResolver != null) {
initializer.setMessageCodesResolver(messageCodesResolver);
}
return initializer;
}
/**
* 这里我们依然特意保留了注释, 这个 RequestMappingHandlerAdapter 范例将负责处理所有通过注解声明的Controller方法, 例如 @GetMapping / @PostMapping 等等. 而我们在这里正好将我们上面配置完毕的ConfigurableWebBindingInitializer实例注入到其内部,为每个请求进行参数校验提供保证。
* Returns a {@link RequestMappingHandlerAdapter} for processing requests
* through annotated controller methods.
*/
@Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter() {
RequestMappingHandlerAdapter adapter = createRequestMappingHandlerAdapter();
adapter.setContentNegotiationManager(mvcContentNegotiationManager());
adapter.setMessageConverters(getMessageConverters());
// 这个getConfigurableWebBindingInitializer()正是上面已经论述过的
adapter.setWebBindingInitializer(getConfigurableWebBindingInitializer());
// 有自定义需要的时候可以考虑覆写如下方法getArgumentResolvers(), getReturnValueHandlers(), getMessageConverters()等.
adapter.setCustomArgumentResolvers(getArgumentResolvers());
adapter.setCustomReturnValueHandlers(getReturnValueHandlers());
if (jackson2Present) {
adapter.setRequestBodyAdvice(Collections.singletonList(new JsonViewRequestBodyAdvice()));
adapter.setResponseBodyAdvice(Collections.singletonList(new JsonViewResponseBodyAdvice()));
}
AsyncSupportConfigurer configurer = new AsyncSupportConfigurer();
configureAsyncSupport(configurer);
if (configurer.getTaskExecutor() != null) {
adapter.setTaskExecutor(configurer.getTaskExecutor());
}
if (configurer.getTimeout() != null) {
adapter.setAsyncRequestTimeout(configurer.getTimeout());
}
adapter.setCallableInterceptors(configurer.getCallableInterceptors());
adapter.setDeferredResultInterceptors(configurer.getDeferredResultInterceptors());
return adapter;
}
}
至此Validator的装配已经完成,让我们尝试总结一下:
ValidationAutoConfiguration
向Spring容器中注入的LocalValidatorFactoryBean
借助 Spring自身提供一系列扩展性接口(诸如InitializingBean
)等完成了对于 JSR303契约接口javax.validation.Validator
(本例中实现者为hibernate-validator-6.0.18.Final.jar中的ValidatorImpl
)与 Spring自身的验证接口org.springframework.validation.Validator
的适配。这一点从其基类SpringValidatorAdapter
的名称上就可见一二。WebMvcAutoConfiguration
中通过配置RequestMappingHandlerAdapter
(正是它负责处理Controller层注解@RequestMapping
修饰的方法请求调用)里的webBindingInitializer
来介入到校验逻辑里来的。@RequestBody
的,正是在RequestMappingHandlerAdapter
类中默认会进行注册的RequestResponseBodyMethodProcessor
,而我们所期待的Validate逻辑正是发生在RequestResponseBodyMethodProcessor
类的resolveArgument()
方法中,该方法将在每次请求的时候被回调。(ConfigurableWebBindingInitializer
实例为每个请求都会重新创建的WebDataBinder
实例注入Validator
实现类,)
下面让我们来看看校验的实际操作是怎么发生的。
正如上面所说, 所有的请求最终一定会经过负责响应 @GetMapping / @PostMapping 注解方法的 RequestMappingHandlerAdapter , 顺藤摸瓜我们找到如下堆栈:
以上我们可以得出如下结论:
resolveArgument()
方法中回调基类的 validateIfApplicable(binder, parameter)
来完成的。而且校验失败将直接触发 MethodArgumentNotValidException
异常(前提是Controller层方法参数中没有Errors类型的入参)。 /**
* Validate the binding target if applicable.
* The default implementation checks for {@code @javax.validation.Valid},
* Spring's {@link org.springframework.validation.annotation.Validated},
* and custom annotations whose name starts with "Valid".
* @param binder the DataBinder to be used
* @param parameter the method parameter descriptor
* @since 4.1.5
* @see #isBindExceptionRequired
*/
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
// 注解为Spring的Validated,或者任何以 Valid 开头的都会触发校验.
if (validatedAnn != null || ann.annotationType().getSimpleName().startsWith("Valid")) {
Object hints = (validatedAnn != null ? validatedAnn.value() : AnnotationUtils.getValue(ann));
Object[] validationHints = (hints instanceof Object[] ? (Object[]) hints : new Object[] {hints});
// 进行校验逻辑环节, 在这里是 hibernate-validator 的校验实现。
// 对于校验结果返回值的处理, 参见 RequestResponseBodyMethodProcessor 中对于本方法回调之后的处理.
binder.validate(validationHints);
break;
}
}
}
解释了Controller层的校验之后,接下来让我们看看Service层是如何进行参数校验的?
首先要说明的是,不是很建议在Service层进行参数校验,推荐还是尽量在Controller完成所有的入参校验,避免校验逻辑被分散到过多的位置,当然如果Service层的方法被多方调用,为了契约的完整性,在Service层进行防御性编程也是一项非常好的编程实践。
依然是一个简单的测试用例:
@Service
@Validated
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IRoleService {
@Override
public List<RoleVO> tree(@NotEmpty String id) {
......
}
}
其实在上文对于 ValidationAutoConfiguration
配置类的讲解中,我们就已经提到了SpringBoot应用在启动的时候会将名为 MethodValidationPostProcessor
实例注入到Spring容器中。 关于此类,观察其继承链和内部的字段定义,大致可以做出如下推测:
通过实现InitializingBean
接口,该类会在内部构建一个 Pointcut为 匹配 @Validated
注解, Advice 为 MethodValidationInterceptor
实例的 Advisor实例并赋值给基类的 advisor 字段。
// MethodValidationPostProcessor对于 InitializingBean 接口的实现
@Override
public void afterPropertiesSet() {
// 这里的AnnotationMatchingPointcut看名称就能猜到大致是对那些标注了特定注解的类或者方法进行AOP, 比如这里就是将对标注了@Validated的类里的所有方法进行AOP操作
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
// 为基类AbstractAdvisingBeanPostProcessor的字段赋值
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
以上赋值成功的 advisor 字段将被应用到基类AbstractAdvisingBeanPostProcessor
所实现的BeanPostProcessor
接口的postProcessAfterInitialization
方法中 —— 为满足条件的Bean(类型被 @Validated
修饰)进行AOP并附加上本advisor ,用作之后执行的Validate逻辑介入(换而言之,如果类型没有被@Validated
修饰,进入到执行阶段的时候不会触发这段Validate AOP逻辑)。
讨论完启动阶段,接下来我们来看看执行阶段的校验是如何进行的?
以上测试用例中,请求达到时候,调用逻辑达到MethodValidationInterceptor
时候相关的调用堆栈如下:
以上堆栈图很清晰地表明了请求处理逻辑是如何达到验证环节的。感兴趣的读者可以在此打上断点,查看各个参数的值来感受下。
关于这一部分的源码解读,笔者换一种方式来展现,也算是给有意阅读源码的同学一个思路。
先看下本次的测试用例。
@ConfigurationProperties(prefix = "server")
@Validated // 必须
public class FuLiZheProperties {
@Range(max = 8080, min = 100)
private int port;
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
}
// ======= 单元测试
@RunWith(SpringRunner.class)
@SpringBootTest(classes = { FuLiZheConfiguration.class })
public class ConfigValidateTests {
@Autowired
private FuLiZheProperties fulizheProperties;
@Test
public void test() {
Console.log(fulizheProperties.getPort());
}
}
本次我们抛弃前面两个小节展示的先"初始化",再"运行时"的方式,直接执行上面的单元测试。
该单元测试毫无意外地将直接失败,然后我们可以获得如下堆栈:
Caused by: org.springframework.boot.context.properties.bind.validation.BindValidationException: Binding validation errors on server
- Field error in object 'server' on field 'port': rejected value [-1]; codes [Range.server.port,Range.port,Range.int,Range]; arguments [org.springframework.context.support.DefaultMessageSourceResolvable: codes [server.port,port]; arguments []; default message [port],8080,100]; default message [需要在100和8080之间]; origin "server.port" from property source "Inlined Test Properties"
at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.validateAndPush(ValidationBindHandler.java:137) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.validate(ValidationBindHandler.java:110) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
//
at org.springframework.boot.context.properties.bind.validation.ValidationBindHandler.onFinish(ValidationBindHandler.java:101) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.context.properties.bind.Binder.handleBindResult(Binder.java:340) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:321) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:308) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:238) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.context.properties.bind.Binder.bind(Binder.java:225) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.context.properties.ConfigurationPropertiesBinder.bind(ConfigurationPropertiesBinder.java:89) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.bind(ConfigurationPropertiesBindingPostProcessor.java:107) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
//
at org.springframework.boot.context.properties.ConfigurationPropertiesBindingPostProcessor.postProcessBeforeInitialization(ConfigurationPropertiesBindingPostProcessor.java:96) ~[spring-boot-2.2.6.RELEASE.jar:2.2.6.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:416) ~[spring-beans-5.2.5.RELEASE.jar:5.2.5.RELEASE]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1788) ~[spring-beans-5.2.5.RELEASE.jar:5.2.5.RELEASE]
从以上错误堆栈中,我们可以找到如下关键类:
依据以上堆栈图以及推测的关键类,我们可以找到如下执行堆栈图:
以上截图中:
Validator
实现类为ConfigurationPropertiesJsr303Validator
(PACKAGEorg.springframework.boot.context.properties
之下)。@ConfigurationProperties
修饰的Bean,SB都将对应生成一个BindHandler
实例来做专职的绑定操作,这些操作基本都是在Binder
类中完成的,所以SB这个思路就是把Binder
当作一个绑定发生的熔炼炉,要被进行属性等绑定的对象和包含着绑定逻辑的BindHandler
被并列传递到 Binder中。@ConfigurationProperties
修饰的Bean同时也被@Validated修饰,则在 BindHandler
执行链条上加入一个ValidationBindHandler
(是的,它也是BindHandler
的一个实现类;这一块的源码参见ConfigurationPropertiesBinder
类的getBindHandler()
方法,在这个方法中你会发现SB将BindHandler
像千层饼一样堆叠起来)。@Valid
触发校验,实际的校验操作将由RequestResponseBodyMethodProcessor
来完成。应用起来就是直接在对应的方法参数上标注注解 @Valid
或@Validated
即可(适合于用自定义类型接收参数)。@Validated
来触发校验,实际操作由 MethodValidationInterceptor
来完成。应用起来类似于在 Service类上标记@Validated
注解,然后使用 JSR303或者hibernate-validator 提供的注解进行自定义需求的满足。(当然这些操作同样适用于Controller,如果你想要在Controller层使用简单类型而非自定义类型接受参数,这将是你最好的选择)。对于入参的类型转换,校验,以及相应的权限校验,执行顺序如下: