springboot情操陶冶-web配置(七)
引數校驗通常是OpenApi必做的操作,其會對不合法的輸入做統一的校驗以防止惡意的請求。本文則對引數校驗這方面作下簡單的分析
spring.factories
讀者應該對此檔案加以深刻的印象,很多springboot整合第三方外掛的方式均是從此配置檔案去讀取的,本文關注下檢驗方面的東西。在相應的檔案搜尋validation關鍵字,最終定位至ValidationAutoConfiguration類,筆者這就針對此類作主要的分析
ValidationAutoConfiguration
優先看下其頭上的註解
@Configuration @ConditionalOnClass(ExecutableValidator.class) @ConditionalOnResource(resources = "classpath:META-INF/services/javax.validation.spi.ValidationProvider") @Import(PrimaryDefaultValidatorPostProcessor.class)
使此類成功被註冊的條件有兩個,第一是當前環境下存在ExecutableValidator類,第二是當前類環境存在META-INF/services/javax.validation.spi.ValidationProvider檔案。
通過檢視maven依賴得知,其實springboot在引入starter-web板塊便引入了hibernate-validator包,此包便滿足了上述的兩個要求。
筆者發現其也引入了PrimaryDefaultValidatorPostProcessor類,主要是判斷當前的bean工廠是否已經包含了LocalValidatorFactoryBean和Validator
@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; }
通過查閱程式碼得知,使用註解式的校驗方式是通過新增@Validated註解來實現的,但是其作用於引數上還是類上是有不同的操作邏輯的。筆者將之區分開,方便後續查閱。先附上@Validated註解原始碼
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Validated {
/**
* Specify one or more validation groups to apply to the validation step
* kicked off by this annotation.
* <p>JSR-303 defines validation groups as custom annotations which an application declares
* for the sole purpose of using them as type-safe group arguments, as implemented in
* {@link org.springframework.validation.beanvalidation.SpringValidatorAdapter}.
* <p>Other {@link org.springframework.validation.SmartValidator} implementations may
* support class arguments in other ways as well.
*/
Class<?>[] value() default {};
}
類級別的校驗
即@Validated作用於類上,其相關的處理邏輯便是由MethodValidationPostProcessor來實現的,筆者稍微看下關鍵原始碼方法afterPropertiesSet()
@Override
public void afterPropertiesSet() {
// 查詢對應的類以及祖先類上是否含有@Validated註解
Pointcut pointcut = new AnnotationMatchingPointcut(this.validatedAnnotationType, true);
// 建立MethodValidationInterceptor處理類來處理具體的邏輯
this.advisor = new DefaultPointcutAdvisor(pointcut, createMethodValidationAdvice(this.validator));
}
上述的配置表明只要某個類上使用了@Validated註解,其相應的方法就會被校驗相關的引數。筆者緊接著看下MethodValidationInterceptor#invoke()方法
@Override
@SuppressWarnings("unchecked")
public Object invoke(MethodInvocation invocation) throws Throwable {
// 讀取相應方法上的@Validated的value屬性,為空也是沒問題的
Class<?>[] groups = determineValidationGroups(invocation);
// Standard Bean Validation 1.1 API
ExecutableValidator execVal = this.validator.forExecutables();
Method methodToValidate = invocation.getMethod();
Set<ConstraintViolation<Object>> result;
try {
// ①校驗引數
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
catch (IllegalArgumentException ex) {
// ②校驗對應的橋接方法(相容jdk1.5+後的泛型用法)的引數
methodToValidate = BridgeMethodResolver.findBridgedMethod(
ClassUtils.getMostSpecificMethod(invocation.getMethod(), invocation.getThis().getClass()));
result = execVal.validateParameters(
invocation.getThis(), methodToValidate, invocation.getArguments(), groups);
}
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
// ③校驗對應的返回值
Object returnValue = invocation.proceed();
result = execVal.validateReturnValue(invocation.getThis(), methodToValidate, returnValue, groups);
if (!result.isEmpty()) {
throw new ConstraintViolationException(result);
}
return returnValue;
}
只要類上使用了@Validated註解,則其下的所有方法都會被校驗。
檢驗規則如下:引數和返回值都會被校驗,只要某一個沒有通過,則會丟擲ConstraintViolationException異常以示警告。
具體的引數校驗屬於hibernate-validator的範疇了,感興趣的讀者可自行分析~
引數級別的校驗
即@Validated註解作用於方法的引數上,其有關的校驗則是被springmvc的引數校驗器處理的。筆者在ModelAttributeMethodProcessor#resolveArgument()方法中查詢到了相應的蛛絲馬跡,列出關鍵的程式碼
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
....
Object attribute = null;
BindingResult bindingResult = null;
if (mavContainer.containsAttribute(name)) {
attribute = mavContainer.getModel().get(name);
}
else {
// Create attribute instance
try {
attribute = createAttribute(name, parameter, binderFactory, webRequest);
}
catch (BindException ex) {
.....
}
}
if (bindingResult == null) {
WebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
if (binder.getTarget() != null) {
if (!mavContainer.isBindingDisabled(name)) {
bindRequestParameters(binder, webRequest);
}
// 就是這裡
validateIfApplicable(binder, parameter);
if (binder.getBindingResult().hasErrors() && isBindExceptionRequired(binder, parameter)) {
throw new BindException(binder.getBindingResult());
}
}
// Value type adaptation, also covering java.util.Optional
if (!parameter.getParameterType().isInstance(attribute)) {
attribute = binder.convertIfNecessary(binder.getTarget(), parameter.getParameterType(), parameter);
}
bindingResult = binder.getBindingResult();
}
....
return attribute;
}
我們繼續看下其下的validateIfApplicable()方法
protected void validateIfApplicable(WebDataBinder binder, MethodParameter parameter) {
// 對引數上含有@Validated註解的進行校驗器解析
Annotation[] annotations = parameter.getParameterAnnotations();
for (Annotation ann : annotations) {
Validated validatedAnn = AnnotationUtils.getAnnotation(ann, Validated.class);
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});
binder.validate(validationHints);
break;
}
}
}
上述的程式碼已經很簡明概要了,筆者就不展開了。當然如果使用者想要在出現異常的時候進行友好的返回,建議參考springboot情操陶冶-web配置(五)的異常機制文章便可迎刃而解
小結
引數的校驗一般都是結合spring-context板塊內的@Validated註解搭配hibernate的校驗器便完成了相應的檢測功能。邏輯還是很簡單的,希望對大家有所幫助