一般项目成员变量定义如下:
@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 List> getDefaultValidatorDescriptors(Class annotationType) {
//safe cause all CV for a given annotation A are CV
final List> builtInValidators = (List>) enabledBuiltinConstraints
.get( annotationType );
if ( builtInValidators != null ) {
return builtInValidators;
}
Class extends ConstraintValidator>[] validatedBy = (Class extends ConstraintValidator>[]) 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 messageParameters,
Map 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,查询类似的问题,补充相关的知识。