一般項目成員變量定義如下:
@ApiModelProperty("姓名")
@NotBlank("姓名不能為空")
@Length(max = 20, value = "姓名不能超過20")
可以”姓名“在三個地方出現過,而且,注釋冗長
我想達到的效果是:
@ApiValidate(value = "姓名", max = 20, notBlank = true)
同時,對原來的swagger和validation又不會產生影響。
這里牽扯到swagger、和hibernate validate。
代碼地址:https://gitee.com/wuliaozhiyuan/private/tree/master/api-validate%E5%90%88%E5%B9%B6
首先解決swagger 能夠掃描自定義注解的問題。
swagger原來的ApiModelProperty,看它是怎么做到的。
用idea點擊ApiModelProperty在源代碼出現的地方:
只有兩個地方:
1、ApiModelPropertyPropertyBuilder
2、SwaggerExpandedParameterBuilder
1、粗略地看一下代碼
1)ApiModelPropertyPropertyBuilder代碼,馬上就能感覺到這策略模式的感覺,多個子類實現父接口或父類的方法,然后外部for循環找到匹配的策略,調用。
很多地方的源碼都是這么做的,看多了馬上就能反應過來。
2)這個類是Component注解修飾的,會存入spring容器。
很容易就想到,我只要同樣實現接口,同樣存入spring容器,外部for循環自然能使用到自定義的實現邏輯。
2、再用idea點擊,看哪些地方調用了這個代碼。
SchemaPluginsManager的這里調用了,for循環。
而且同樣是spring管理,spring的依賴注入的一些屬性。
public ModelProperty property(ModelPropertyContext context) {
for (ModelPropertyBuilderPlugin enricher : propertyEnrichers.getPluginsFor(context.getDocumentationType())) {
enricher.apply(context);
}
return context.getBuilder().build();
}
再idea debug看看,執行過程,每個對象的參數,基本就能搞定了。
如果要寫ApiOperator類似的注解,同樣的解決問題的方法。
再看hibernate validate。因為之前實現過自定義hibernate validate注解,所以對源碼了解一些,主要問題是message的動態化,根據參數,動態返回message。
同樣看類似的注解:notBlank
很容易看到應該是ConstraintHelper的這行代碼,添加了注解和校驗器。
而且,這個ConstraintHelper添加了大量的內置注解和校驗器,但是沒有發現可以添加自定義注解的地方,而且保存這些的是一個Collections.unmodifiableMap( tmpConstraints )修飾的。
putBuiltinConstraint( tmpConstraints, NotBlank.class, NotBlankValidator.class );
那再看,保存了,就得使用,看上層是怎么使用的,跟這個變量enabledBuiltinConstraints,發現,如果內置注解沒有,就會讀取Constraint標識的校驗器,自然就知道自定義注解應該如何使用了。
private <A extends Annotation> List<ConstraintValidatorDescriptor<A>> getDefaultValidatorDescriptors(Class<A> annotationType) {
//safe cause all CV for a given annotation A are CV<A, ?>
final List<ConstraintValidatorDescriptor<A>> builtInValidators = (List<ConstraintValidatorDescriptor<A>>) enabledBuiltinConstraints
.get( annotationType );
if ( builtInValidators != null ) {
return builtInValidators;
}
Class<? extends ConstraintValidator<A, ?>>[] validatedBy = (Class<? extends ConstraintValidator<A, ?>>[]) annotationType
.getAnnotation( Constraint.class )
.validatedBy();
return Stream.of( validatedBy )
.map( c -> ConstraintValidatorDescriptor.forClass( c, annotationType ) )
.collect( Collectors.collectingAndThen( Collectors.toList(), CollectionHelper::toImmutableList ) );
}
自定義校驗器注解很容易,網上都能搜索一大堆。
而動態message,就比較少。
點擊message查看調用,發現看不到。
那么debug看,看debug校驗失敗的報錯棧,
直接看打印的錯誤棧,會發現看不出來,所以應該反應出來,錯誤被重置替換了。
那么通過校驗器debug跟蹤。
發現錯誤之后封裝返回了constraintValidatorContext對象,而這個對象最后add到violatedConstraintValidatorContexts集合中。
之后遍歷處理這個集合。
for ( ConstraintValidatorContextImpl constraintValidatorContext : violatedConstraintValidatorContexts ) {
for ( ConstraintViolationCreationContext constraintViolationCreationContext : constraintValidatorContext.getConstraintViolationCreationContexts() ) {
validationContext.addConstraintFailure(
valueContext, constraintViolationCreationContext, constraintValidatorContext.getConstraintDescriptor()
);
}
}
跟進去,看實現類的實現
通過debug看到,messageTemplate 還是原來的{javax.validation.constraints.NotBlank.message},沒有被替換。
執行換了interpolate方法之后,就被替換了。所以替換的邏輯就在interpolate里面,
這里吐槽一句,add開頭的方法里,執行很多邏輯處理,數據替換,代碼可讀性不強,因為你不點進去add方法,根本知道做了什么事情。
public void addConstraintFailure(
ValueContext<?, ?> valueContext,
ConstraintViolationCreationContext constraintViolationCreationContext,
ConstraintDescriptor<?> descriptor
) {
String messageTemplate = constraintViolationCreationContext.getMessage();
String interpolatedMessage = interpolate(
messageTemplate,
valueContext.getCurrentValidatedValue(),
descriptor,
constraintViolationCreationContext.getPath(),
constraintViolationCreationContext.getMessageParameters(),
constraintViolationCreationContext.getExpressionVariables()
);
// at this point we make a copy of the path to avoid side effects
Path path = PathImpl.createCopy( constraintViolationCreationContext.getPath() );
getInitializedFailingConstraintViolations().add(
createConstraintViolation(
messageTemplate,
interpolatedMessage,
path,
descriptor,
valueContext,
constraintViolationCreationContext
)
);
}
之后發現主要就是validatorScopedContext.getMessageInterpolator().interpolate()方法,如果我能把MessageInterpolator替換掉,就能動態message消息。
但是,我debug跟蹤的時候,發現很難定位到到底什么時候,替換的MessageInterpolator,應該如何替換。
后來才發現,spring boot啟動的時候,會掉兩次這個代碼,
而且我在堆棧中看到afterPropertiesSet方法,那自然就是spring初始化bean調用的,
然后看到這個afterPropertiesSet方法所在的bean,LocalValidatorFactoryBean的引用地方,馬上發現ValidationAutoConfiguration,太熟悉了,所有的spring boot starter都有自動化配置類,原來這里注入了LocalValidatorFactoryBean,那么自然,我復制一份,重新注入LocalValidatorFactoryBean,然后替換MessageInterpolatorFactory就完了。
private String interpolate(
String messageTemplate,
Object validatedValue,
ConstraintDescriptor<?> descriptor,
Path path,
Map<String, Object> messageParameters,
Map<String, Object> expressionVariables) {
MessageInterpolatorContext context = new MessageInterpolatorContext(
descriptor,
validatedValue,
getRootBeanClass(),
path,
messageParameters,
expressionVariables
);
try {
return validatorScopedContext.getMessageInterpolator().interpolate(
messageTemplate,
context
);
}
catch (ValidationException ve) {
throw ve;
}
catch (Exception e) {
throw LOG.getExceptionOccurredDuringMessageInterpolationException( e );
}
}
···
通過源碼解決問題的方式:
1、查看同類的問題,源碼是怎樣解決的。
2、粗略看代碼,看每一步,大概發生了什么,保存了什么成員變量,這個成員變量是怎么使用的。通過idea輔助
3、打斷點,看變量的變化。
4、google,查詢類似的問題,補充相關的知識。