SpringBoot源码解析之集成hibernate-validator验证框架

本文尝试探究SpringBoot为集成验证框架而做的一些架构设计与实现,以期在实际运用中能够更加自如,做到心里有底。

1. 概述

当前谈到Spring框架,基本都是SpringBoot起步,因此本文也就顺势从SpringBoot开始,探究SpringBoot是如何将参数校验的Validated机制无缝融入到自身的架构体系的。

本文主体部分大概分为三个小节,分别对应于MVC分层之下:

  1. Controller层的参数校验如何实现?
  2. Service层的参数校验如何实现?
  3. SpringBoot中的配置类校验如何实现?

2. 源码解析 - Controller层参数校验

关于Controller层,因为HTTP协议本身是允许多种Content-Type的,而SpringMVC也是进行相应的设计以支持全部的协议需求,这里笔者只以最为常用的REST约定里常用的content-type:application/json为例进行论述。该类Content-Type直接决定了下面用例中使用的注解 @RestController@RequestBody

2.1 用例

按照惯例,我们先来看看本小节源码解读涉及到测试用例。

@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是如何让这个注解生效的。

2.2 源码解读 - 初始化

首先让我们先来看看SpringBoot是如何进行验证框架的整合的。

跟随过往的经验SpringBoot源码解析之AutoConfiguration,我们找到了WebMvcAutoConfiguration 以及 ValidationAutoConfiguration

2.2.1. 配置类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如何为基类字段targetValidatorjavax.validation.Validator类型)赋值为org.hibernate.validator.internal.engine.ValidatorImpl 实例的(感兴趣的读者可以在基类SpringValidatorAdaptersetTargetValidator()处打上断点)。
SpringBoot源码解析之集成hibernate-validator验证框架_第1张图片

2.2.2. 配置类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的装配已经完成,让我们尝试总结一下:

  1. 配置类 ValidationAutoConfiguration向Spring容器中注入的LocalValidatorFactoryBean借助 Spring自身提供一系列扩展性接口(诸如InitializingBean)等完成了对于 JSR303契约接口javax.validation.Validator (本例中实现者为hibernate-validator-6.0.18.Final.jar中的ValidatorImpl)与 Spring自身的验证接口org.springframework.validation.Validator 的适配。这一点从其基类SpringValidatorAdapter的名称上就可见一二。
  2. 配置类WebMvcAutoConfiguration中通过配置RequestMappingHandlerAdapter(正是它负责处理Controller层注解@RequestMapping 修饰的方法请求调用)里的webBindingInitializer来介入到校验逻辑里来的。
  3. 本例中负责解析注解入参注解@RequestBody的,正是在RequestMappingHandlerAdapter类中默认会进行注册的RequestResponseBodyMethodProcessor,而我们所期待的Validate逻辑正是发生在RequestResponseBodyMethodProcessor类的resolveArgument()方法中,该方法将在每次请求的时候被回调。

ConfigurableWebBindingInitializer实例为每个请求都会重新创建的WebDataBinder实例注入Validator实现类,)

2.3 源码解读 - 运行时

下面让我们来看看校验的实际操作是怎么发生的。

正如上面所说, 所有的请求最终一定会经过负责响应 @GetMapping / @PostMapping 注解方法的 RequestMappingHandlerAdapter , 顺藤摸瓜我们找到如下堆栈:
SpringBoot源码解析之集成hibernate-validator验证框架_第2张图片
以上我们可以得出如下结论:

  1. SpringMVC中,传入参数的校验是在进行参数检索和转换之后进行的,例证就是 RequestResponseBodyMethodProcessor对于接口HandlerMethodArgumentResolver的实现,其实现的resolveArgument() 方法会在对传入参数进行检索转换之后,直接进行参数合规性校验。
  2. 合规性校验这一步是通过在 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; } } }

3. 源码解析 - Service层参数校验

解释了Controller层的校验之后,接下来让我们看看Service层是如何进行参数校验的?

首先要说明的是,不是很建议在Service层进行参数校验,推荐还是尽量在Controller完成所有的入参校验,避免校验逻辑被分散到过多的位置,当然如果Service层的方法被多方调用,为了契约的完整性,在Service层进行防御性编程也是一项非常好的编程实践。

3.1 用例

依然是一个简单的测试用例:


@Service
@Validated
public class RoleServiceImpl extends ServiceImpl<RoleMapper, Role> implements IRoleService {
	@Override
	public List<RoleVO> tree(@NotEmpty String id) {
		......
	}
}
3.2 源码解读 - 初始化

其实在上文对于 ValidationAutoConfiguration配置类的讲解中,我们就已经提到了SpringBoot应用在启动的时候会将名为 MethodValidationPostProcessor实例注入到Spring容器中。 关于此类,观察其继承链和内部的字段定义,大致可以做出如下推测:

  1. 通过实现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));
    	}
    
  2. 以上赋值成功的 advisor 字段将被应用到基类AbstractAdvisingBeanPostProcessor所实现的BeanPostProcessor接口的postProcessAfterInitialization方法中 —— 为满足条件的Bean(类型被 @Validated修饰)进行AOP并附加上本advisor ,用作之后执行的Validate逻辑介入(换而言之,如果类型没有被@Validated修饰,进入到执行阶段的时候不会触发这段Validate AOP逻辑)。
    SpringBoot源码解析之集成hibernate-validator验证框架_第3张图片

3.3 源码解读 - 运行时

讨论完启动阶段,接下来我们来看看执行阶段的校验是如何进行的?

以上测试用例中,请求达到时候,调用逻辑达到MethodValidationInterceptor时候相关的调用堆栈如下:
SpringBoot源码解析之集成hibernate-validator验证框架_第4张图片
以上堆栈图很清晰地表明了请求处理逻辑是如何达到验证环节的。感兴趣的读者可以在此打上断点,查看各个参数的值来感受下。

4. 源码解析 - Config类参数校验

关于这一部分的源码解读,笔者换一种方式来展现,也算是给有意阅读源码的同学一个思路。

4.1 用例

先看下本次的测试用例。

@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());
	}
}
4.2 源码解读

本次我们抛弃前面两个小节展示的先"初始化",再"运行时"的方式,直接执行上面的单元测试。

该单元测试毫无意外地将直接失败,然后我们可以获得如下堆栈:

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]

从以上错误堆栈中,我们可以找到如下关键类:

  1. ConfigurationPropertiesBindingPostProcessor
  2. ConfigurationPropertiesBinder
  3. ValidationBindHandler

依据以上堆栈图以及推测的关键类,我们可以找到如下执行堆栈图:SpringBoot源码解析之集成hibernate-validator验证框架_第5张图片
以上截图中:

  1. 唯一的Validator实现类为ConfigurationPropertiesJsr303Validator(PACKAGEorg.springframework.boot.context.properties之下)。
  2. 针对容器中的每个@ConfigurationProperties修饰的Bean,SB都将对应生成一个BindHandler 实例来做专职的绑定操作,这些操作基本都是在Binder类中完成的,所以SB这个思路就是把Binder当作一个绑定发生的熔炼炉,要被进行属性等绑定的对象和包含着绑定逻辑的BindHandler被并列传递到 Binder中。
  3. 这其中,如果发现当前被 @ConfigurationProperties修饰的Bean同时也被@Validated修饰,则在 BindHandler执行链条上加入一个ValidationBindHandler(是的,它也是BindHandler的一个实现类;这一块的源码参见ConfigurationPropertiesBinder类的getBindHandler()方法,在这个方法中你会发现SB将BindHandler像千层饼一样堆叠起来)。

5. 总结

5.1 应用
  1. Controller层,直接使用 @Valid 触发校验,实际的校验操作将由RequestResponseBodyMethodProcessor 来完成。应用起来就是直接在对应的方法参数上标注注解 @Valid@Validated 即可(适合于用自定义类型接收参数)。
  2. Service层,由 @Validated 来触发校验,实际操作由 MethodValidationInterceptor 来完成。应用起来类似于在 Service类上标记@Validated注解,然后使用 JSR303或者hibernate-validator 提供的注解进行自定义需求的满足。(当然这些操作同样适用于Controller,如果你想要在Controller层使用简单类型而非自定义类型接受参数,这将是你最好的选择)。
5.2 执行顺序

对于入参的类型转换,校验,以及相应的权限校验,执行顺序如下:

  1. 方法参数填充。
  2. 方法参数校验。
  3. 其它AOP鉴权,例如BladeX的 @PreAuth。

6. Links

  1. @Validated和@Valid区别:Spring validation验证框架对入参实体进行嵌套验证必须在相应属性(字段)加上@Valid而不是@Validated
  2. 高效使用hibernate-validator校验框架
  3. springboot使用hibernate validator校验

你可能感兴趣的:(SpringBoot,SpringMVC)