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 <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,查询类似的问题,补充相关的知识。




©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容