swagger注解和validation注解合并

一般项目成员变量定义如下:

@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>[] validatedBy = (Class>[]) 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,查询类似的问题,补充相关的知识。




你可能感兴趣的:(swagger注解和validation注解合并)