一般項(xiàng)目成員變量定義如下:
@ApiModelProperty("姓名")
@NotBlank("姓名不能為空")
@Length(max = 20, value = "姓名不能超過20")
可以”姓名“在三個(gè)地方出現(xiàn)過,而且,注釋冗長(zhǎng)
我想達(dá)到的效果是:
@ApiValidate(value = "姓名", max = 20, notBlank = true)
同時(shí),對(duì)原來的swagger和validation又不會(huì)產(chǎn)生影響。
這里牽扯到swagger、和hibernate validate。
代碼地址:https://gitee.com/wuliaozhiyuan/private/tree/master/api-validate%E5%90%88%E5%B9%B6
首先解決swagger 能夠掃描自定義注解的問題。
swagger原來的ApiModelProperty,看它是怎么做到的。
用idea點(diǎn)擊ApiModelProperty在源代碼出現(xiàn)的地方:
只有兩個(gè)地方:
1、ApiModelPropertyPropertyBuilder
2、SwaggerExpandedParameterBuilder
1、粗略地看一下代碼
1)ApiModelPropertyPropertyBuilder代碼,馬上就能感覺到這策略模式的感覺,多個(gè)子類實(shí)現(xiàn)父接口或父類的方法,然后外部for循環(huán)找到匹配的策略,調(diào)用。
很多地方的源碼都是這么做的,看多了馬上就能反應(yīng)過來。
2)這個(gè)類是Component注解修飾的,會(huì)存入spring容器。
很容易就想到,我只要同樣實(shí)現(xiàn)接口,同樣存入spring容器,外部for循環(huán)自然能使用到自定義的實(shí)現(xiàn)邏輯。
2、再用idea點(diǎn)擊,看哪些地方調(diào)用了這個(gè)代碼。
SchemaPluginsManager的這里調(diào)用了,for循環(huán)。
而且同樣是spring管理,spring的依賴注入的一些屬性。
public ModelProperty property(ModelPropertyContext context) {
for (ModelPropertyBuilderPlugin enricher : propertyEnrichers.getPluginsFor(context.getDocumentationType())) {
enricher.apply(context);
}
return context.getBuilder().build();
}
再idea debug看看,執(zhí)行過程,每個(gè)對(duì)象的參數(shù),基本就能搞定了。
如果要寫ApiOperator類似的注解,同樣的解決問題的方法。
再看hibernate validate。因?yàn)橹皩?shí)現(xiàn)過自定義hibernate validate注解,所以對(duì)源碼了解一些,主要問題是message的動(dòng)態(tài)化,根據(jù)參數(shù),動(dòng)態(tài)返回message。
同樣看類似的注解:notBlank
很容易看到應(yīng)該是ConstraintHelper的這行代碼,添加了注解和校驗(yàn)器。
而且,這個(gè)ConstraintHelper添加了大量的內(nèi)置注解和校驗(yàn)器,但是沒有發(fā)現(xiàn)可以添加自定義注解的地方,而且保存這些的是一個(gè)Collections.unmodifiableMap( tmpConstraints )修飾的。
putBuiltinConstraint( tmpConstraints, NotBlank.class, NotBlankValidator.class );
那再看,保存了,就得使用,看上層是怎么使用的,跟這個(gè)變量enabledBuiltinConstraints,發(fā)現(xiàn),如果內(nèi)置注解沒有,就會(huì)讀取Constraint標(biāo)識(shí)的校驗(yàn)器,自然就知道自定義注解應(yīng)該如何使用了。
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 ) );
}
自定義校驗(yàn)器注解很容易,網(wǎng)上都能搜索一大堆。
而動(dòng)態(tài)message,就比較少。
點(diǎn)擊message查看調(diào)用,發(fā)現(xiàn)看不到。
那么debug看,看debug校驗(yàn)失敗的報(bào)錯(cuò)棧,
直接看打印的錯(cuò)誤棧,會(huì)發(fā)現(xiàn)看不出來,所以應(yīng)該反應(yīng)出來,錯(cuò)誤被重置替換了。
那么通過校驗(yàn)器debug跟蹤。
發(fā)現(xiàn)錯(cuò)誤之后封裝返回了constraintValidatorContext對(duì)象,而這個(gè)對(duì)象最后add到violatedConstraintValidatorContexts集合中。
之后遍歷處理這個(gè)集合。
for ( ConstraintValidatorContextImpl constraintValidatorContext : violatedConstraintValidatorContexts ) {
for ( ConstraintViolationCreationContext constraintViolationCreationContext : constraintValidatorContext.getConstraintViolationCreationContexts() ) {
validationContext.addConstraintFailure(
valueContext, constraintViolationCreationContext, constraintValidatorContext.getConstraintDescriptor()
);
}
}
跟進(jìn)去,看實(shí)現(xiàn)類的實(shí)現(xiàn)
通過debug看到,messageTemplate 還是原來的{javax.validation.constraints.NotBlank.message},沒有被替換。
執(zhí)行換了interpolate方法之后,就被替換了。所以替換的邏輯就在interpolate里面,
這里吐槽一句,add開頭的方法里,執(zhí)行很多邏輯處理,數(shù)據(jù)替換,代碼可讀性不強(qiáng),因?yàn)槟悴稽c(diǎn)進(jìn)去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
)
);
}
之后發(fā)現(xiàn)主要就是validatorScopedContext.getMessageInterpolator().interpolate()方法,如果我能把MessageInterpolator替換掉,就能動(dòng)態(tài)message消息。
但是,我debug跟蹤的時(shí)候,發(fā)現(xiàn)很難定位到到底什么時(shí)候,替換的MessageInterpolator,應(yīng)該如何替換。
后來才發(fā)現(xiàn),spring boot啟動(dòng)的時(shí)候,會(huì)掉兩次這個(gè)代碼,
而且我在堆棧中看到afterPropertiesSet方法,那自然就是spring初始化bean調(diào)用的,
然后看到這個(gè)afterPropertiesSet方法所在的bean,LocalValidatorFactoryBean的引用地方,馬上發(fā)現(xiàn)ValidationAutoConfiguration,太熟悉了,所有的spring boot starter都有自動(dòng)化配置類,原來這里注入了LocalValidatorFactoryBean,那么自然,我復(fù)制一份,重新注入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、粗略看代碼,看每一步,大概發(fā)生了什么,保存了什么成員變量,這個(gè)成員變量是怎么使用的。通過idea輔助
3、打斷點(diǎn),看變量的變化。
4、google,查詢類似的問題,補(bǔ)充相關(guān)的知識(shí)。