swagger注解和validation注解合并

一般項(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í)。




?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評(píng)論 6 543
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,520評(píng)論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,362評(píng)論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我,道長(zhǎng),這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評(píng)論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,541評(píng)論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,896評(píng)論 1 328
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評(píng)論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,062評(píng)論 0 290
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,608評(píng)論 1 336
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 41,356評(píng)論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,555評(píng)論 1 374
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評(píng)論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,769評(píng)論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評(píng)論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評(píng)論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 52,289評(píng)論 3 400
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,516評(píng)論 2 379

推薦閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 134,826評(píng)論 18 139
  • Java常用注解 Controller常用注解 @Controller 注解在類上。定義了一個(gè)控制器類,并檢測(cè)該方...
    四核阿宅閱讀 2,355評(píng)論 0 1
  • Spring Boot 參考指南 介紹 轉(zhuǎn)載自:https://www.gitbook.com/book/qbgb...
    毛宇鵬閱讀 46,922評(píng)論 6 342
  • 前言: 本篇文章不是API參考文檔,所以不會(huì)將用到的所有內(nèi)容詳細(xì)列出來。本文的目的主要是告訴讀者關(guān)于Java的 B...
    比軒閱讀 5,553評(píng)論 0 4
  • 1.@SpringBootApplication : Spring Boot應(yīng)用標(biāo)注在某個(gè)類上說明這個(gè)類是Spri...
    灰色的秩序閱讀 2,429評(píng)論 0 1