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閱讀 229,117評論 6 537
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內容