現(xiàn) Feign 接口請(qǐng)求參數(shù)前置校驗(yàn))
后端API設(shè)計(jì)【免費(fèi)下載鏈接】feignFeign makes writing java http clients easier項(xiàng)目地址https://gitcode.com/gh_mirrors/fe/feign點(diǎn)擊查看免費(fèi)下載Feign 的feign-validation-jakarta模塊基于 Jakarta Bean Validationjakarta.validation為 Feign 構(gòu)建的接口提供請(qǐng)求發(fā)出之前的參數(shù)校驗(yàn)?zāi)芰λ詣?dòng)校驗(yàn)方法 body 參數(shù)以及任何標(biāo)注了Valid的方法參數(shù)一旦發(fā)現(xiàn)約束違規(guī)Constraint Violation就直接拋出jakarta.validation.ConstraintViolationExceptionHTTP 請(qǐng)求根本不會(huì)發(fā)送出去。讀完本文你將掌握該模塊的 Maven 引入方式、兩種配置方法默認(rèn) Validator 工廠與顯式 Validator 校驗(yàn)分組、底層MethodInterceptor的執(zhí)行時(shí)機(jī)與校驗(yàn)邏輯實(shí)現(xiàn)以及如何用單元測(cè)試驗(yàn)證校驗(yàn)失敗不發(fā)請(qǐng)求這一關(guān)鍵行為。一、模塊定位把校驗(yàn)前移到 HTTP 請(qǐng)求之前很多 HTTP 客戶端在請(qǐng)求發(fā)出后才由服務(wù)端校驗(yàn)參數(shù)或者依賴調(diào)用方自己手工校驗(yàn)容易遺漏且散落各處。feign-validation-jakarta提供的是聲明式、集中式的前置校驗(yàn)對(duì) Feign 接口方法的body 參數(shù)即 Contract 解析出的bodyIndex所指向的參數(shù)始終執(zhí)行校驗(yàn)對(duì)方法上其他標(biāo)注了Valid的參數(shù)例如攜帶復(fù)雜對(duì)象的 header、query 參數(shù)執(zhí)行校驗(yàn)校驗(yàn)失敗時(shí)拋出jakarta.validation.ConstraintViolationException且請(qǐng)求從未被派發(fā)。當(dāng)前模塊被標(biāo)記為Experimental原因是它依賴的底層擴(kuò)展點(diǎn)feign.interceptor.MethodInterceptorAPI 仍在演進(jìn)中見 validation-jakarta/README.md 與 MethodInterceptor.java 中的Experimental標(biāo)注。對(duì)于仍在使用傳統(tǒng)javax.validation命名空間的場(chǎng)景請(qǐng)使用同倉(cāng)庫(kù)的兄弟模塊feign-validation其用法與本文完全平行僅命名空間不同。二、引入依賴在pom.xml中加入以下依賴dependency groupIdio.github.openfeign/groupId artifactIdfeign-validation-jakarta/artifactId version${feign.version}/version /dependency從該模塊自身的 validation-jakarta/pom.xml 可以看出幾個(gè)關(guān)鍵事實(shí)模塊坐標(biāo)feign-validation-jakarta父 POM 為feign-parent當(dāng)前倉(cāng)庫(kù)版本為 13.16-SNAPSHOT編譯目標(biāo)為Java 17main.java.version與maven.compiler.source/target均為 17核心依賴只有feign-core與jakarta.validation-api模塊本身不捆綁具體 Validator 實(shí)現(xiàn)測(cè)試階段使用hibernate-validatorJakarta 版本作為校驗(yàn)實(shí)現(xiàn)、expressly提供 EL 表達(dá)式支持、mockwebserver模擬 HTTP 服務(wù)端。因此在實(shí)際業(yè)務(wù)中你還需要在 classpath 中提供 Jakarta Bean Validation 的實(shí)現(xiàn)如 Hibernate Validator 6.2否則Validation.buildDefaultValidatorFactory()無法創(chuàng)建出可用的Validator。三、快速上手默認(rèn) Validator 工廠最小化配置只需一行.methodInterceptor(...)Api api Feign.builder() .methodInterceptor(BeanValidationMethodInterceptor.usingDefaultFactory()) .target(Api.class, https://example.com);usingDefaultFactory()是一個(gè)靜態(tài)工廠方法其底層實(shí)現(xiàn)見 BeanValidationMethodInterceptor.java等價(jià)于new BeanValidationMethodInterceptor( Validation.buildDefaultValidatorFactory().getValidator());即使用ValidationAPI 構(gòu)建默認(rèn)的校驗(yàn)器工廠并取得Validator實(shí)例。四、進(jìn)階用法顯式 Validator 與校驗(yàn)分組當(dāng)默認(rèn)工廠無法滿足需求例如需要自定義MessageInterpolator、TraversableResolver、約束校驗(yàn)器工廠或使用校驗(yàn)分組時(shí)可以直接構(gòu)造攔截器并傳入分組類型Validator validator Validation.buildDefaultValidatorFactory().getValidator(); Feign.builder() .methodInterceptor(new BeanValidationMethodInterceptor(validator, Create.class)) .target(Api.class, https://example.com);構(gòu)造函數(shù)簽名見源碼 L55-L58為public BeanValidationMethodInterceptor(Validator validator, Class?... groups)validator校驗(yàn)器實(shí)例完全由調(diào)用方掌控groups可變參數(shù)指定 Bean Validation 分組如Create.class、Update.class校驗(yàn)時(shí)僅執(zhí)行這些分組下的約束groups為null時(shí)會(huì)被安全轉(zhuǎn)換為空數(shù)組L57此時(shí)等價(jià)于默認(rèn)分組校驗(yàn)。這樣你就可以在同一個(gè) DTO 上定義創(chuàng)建時(shí)必填、更新時(shí)可空等分組差異化約束并讓 Feign 客戶端按調(diào)用場(chǎng)景選擇分組。五、底層原理MethodInterceptor 在請(qǐng)求流水線中的位置BeanValidationMethodInterceptor實(shí)現(xiàn)了 Feign 的feign.interceptor.MethodInterceptor接口見 MethodInterceptor.java。該接口是環(huán)繞式around-style攔截器文檔明確說明其執(zhí)行時(shí)機(jī)每次方法調(diào)用執(zhí)行一次位于 Contract 將方法參數(shù)解析為RequestTemplate之后、RequestInterceptor修改模板之前。也就是說攔截器能拿到解析完成、尚未發(fā)送的RequestTemplateheaders、query params、body 字節(jié)Contract 解析出的MethodMetadata原始方法參數(shù)數(shù)組arguments。攔截器通過Chain鏈?zhǔn)轿欣^續(xù)執(zhí)行下游請(qǐng)求攔截器 → HTTP → 響應(yīng)攔截器 → 解碼器并可通過Invocation.response()在鏈路完成后拿到Response。其核心約定MethodInterceptor.java短路直接返回某個(gè)值而不調(diào)用Chain.next(...)可完全跳過 HTTP 交換異常傳播拋出的任何Throwable會(huì)在重試處理之后直接呈現(xiàn)在 Feign 接口方法的調(diào)用方andThen將多個(gè)攔截器組合成鏈apply把攔截器應(yīng)用到現(xiàn)有鏈上。BeanValidationMethodInterceptor.intercept正是利用短路語(yǔ)義實(shí)現(xiàn)前置校驗(yàn)Override public Object intercept(Invocation invocation, Chain chain) throws Throwable { SetConstraintViolationObject violations collectViolations(invocation); if (!violations.isEmpty()) { throw new ConstraintViolationException(violations); } return chain.next(invocation); }一旦發(fā)現(xiàn)違規(guī)立即拋出ConstraintViolationException并阻斷鏈的執(zhí)行從而保證 HTTP 請(qǐng)求永遠(yuǎn)不會(huì)發(fā)出。這一定位在MethodInterceptor的 Javadoc 中被明確為僅靠RequestInterceptor/ResponseInterceptor不夠、需要同時(shí)訪問類型化參數(shù)對(duì)象與已解析請(qǐng)求時(shí)的擴(kuò)展點(diǎn)。從調(diào)用鏈上看methodInterceptor注冊(cè)入口位于 BaseBuilder.javaExperimental同步執(zhí)行路徑由SynchronousMethodHandler持有ListMethodInterceptor異步路徑由AsynchronousMethodHandler持有見 SynchronousMethodHandler.java 與 AsynchronousMethodHandler.java。異步鏈路中多個(gè)攔截器通過reduce(MethodInterceptor::andThen)組合后apply(endOfChain)形成完整鏈AsynchronousMethodHandler.java。六、校驗(yàn)范圍與跳過規(guī)則源碼級(jí)解析collectViolations方法是整個(gè)模塊的核心邏輯BeanValidationMethodInterceptor.java其處理流程如下private SetConstraintViolationObject collectViolations(Invocation invocation) { Object[] arguments invocation.arguments(); if (arguments null || arguments.length 0) { return Collections.emptySet(); // 無參方法直接通過 } SetConstraintViolationObject violations new LinkedHashSet(); Object body invocation.body(); if (body ! null) { violations.addAll(validator.validate(body, groups)); // ① body 始終校驗(yàn) } Integer bodyIndex invocation.methodMetadata().bodyIndex(); Annotation[][] parameterAnnotations parameterAnnotationsCache.computeIfAbsent( invocation.method(), Method::getParameterAnnotations); // ② 注解緩存 for (int i 0; i arguments.length; i) { if (arguments[i] null) { continue; // ③ null 參數(shù)跳過 } if (bodyIndex ! null bodyIndex i) { continue; // ④ 跳過 body 本身避免重復(fù)校驗(yàn) } if (hasValidAnnotation(parameterAnnotations[i])) { // ⑤ 僅校驗(yàn) Valid 參數(shù) violations.addAll(validator.validate(arguments[i], groups)); } } return violations; }逐條解讀規(guī)則無參方法直通arguments為null或長(zhǎng)度為 0 時(shí)返回空集合不產(chǎn)生任何校驗(yàn)開銷body 參數(shù)總是校驗(yàn)通過Invocation.body()獲取內(nèi)部按methodMetadata.bodyIndex()定位非空即執(zhí)行validator.validate(body, groups)。注意 body 為null時(shí)跳過校驗(yàn)——如果希望body 不能為空需要由 DTO 上的NotNull之類約束承擔(dān)或自行在調(diào)用方處理注解緩存方法參數(shù)的注解數(shù)組通過ConcurrentHashMapMethod, Annotation[][]按反射Method緩存避免每次調(diào)用都重復(fù)反射獲取注解兼顧了校驗(yàn)功能的并發(fā)安全與性能Valid 參數(shù)遍歷所有參數(shù)凡標(biāo)注jakarta.validation.Valid且非 body 位置、非 null 的一律執(zhí)行validate。這意味著復(fù)雜對(duì)象形式的 header 參數(shù)、query 參數(shù)對(duì)象都可以聲明式校驗(yàn)去重body 位置通過bodyIndex識(shí)別并跳過確保 body 只被校驗(yàn)一次。需要特別指出的是校驗(yàn)觸發(fā)以參數(shù)上有Valid注解為前提BeanValidationMethodInterceptor不會(huì)對(duì)接口方法本身做方法級(jí)校驗(yàn)method validation這與在服務(wù)端使用Validated做方法參數(shù)校驗(yàn)的場(chǎng)景不同屬于客戶端出站請(qǐng)求的前置把關(guān)。七、行為驗(yàn)證測(cè)試用例是如何證明請(qǐng)求未發(fā)出的倉(cāng)庫(kù)測(cè)試 BeanValidationMethodInterceptorTest.java 用 MockWebServer 精確驗(yàn)證了上述語(yǔ)義測(cè)試模型定義如下static class Payload { NotNull public String name; NotBlank public String description; // ... } interface Api { RequestLine(POST /things) String create(Payload body); RequestLine(GET /things/{id}) String fetch(Param(id) String id, Valid HeaderInfo info); RequestLine(GET /things) String list(); }四個(gè)測(cè)試用例分別覆蓋validBodyReachesServerbody 合法時(shí)請(qǐng)求正常發(fā)出server.getRequestCount() 1返回服務(wù)端響應(yīng)invalidBodyThrowsBeforeRequest構(gòu)造namenull、description的 Payload斷言拋出ConstraintViolationException、getConstraintViolations()大小為 2同時(shí)命中NotNull與NotBlank兩條約束且server.getRequestCount()為 0 ——沒有任何請(qǐng)求到達(dá)服務(wù)端invalidNonBodyValidParameterThrowsBeforeRequest非 body 的Valid HeaderInfotenant為空白字符串違規(guī)時(shí)同樣在請(qǐng)求前拋出異常證明Valid參數(shù)校驗(yàn)確實(shí)生效noArgsMethodPassesThrough無參方法list()正常通過并返回響應(yīng)驗(yàn)證無參直通規(guī)則。這一組測(cè)試同時(shí)驗(yàn)證了校驗(yàn)失敗不發(fā)請(qǐng)求與校驗(yàn)成功正常發(fā)請(qǐng)求兩條路徑是理解該模塊行為的最佳參考。測(cè)試還展示了如何在測(cè)試環(huán)境快速搭建 Feign 客戶端使用okhttp3.mockwebserver作為目標(biāo)地址。八、注意事項(xiàng)與適用邊界必須存在 Validator 實(shí)現(xiàn)模塊只依賴jakarta.validation-api運(yùn)行期需要如 Hibernate ValidatorJakarta 變體這樣的實(shí)現(xiàn)使用Email、Size等需要 EL 的約束時(shí)還需提供 EL 表達(dá)式實(shí)現(xiàn)倉(cāng)庫(kù)測(cè)試即用expressly。ExperimentalAPIMethodInterceptor與攔截器本身均標(biāo)注Experimental底層 API 穩(wěn)定前可能發(fā)生變化生產(chǎn)環(huán)境引入前需評(píng)估升級(jí)成本。null 與空參語(yǔ)義null 參數(shù)一律跳過無參方法零開銷直通body 為 null 時(shí)不做必填判斷。異常類型校驗(yàn)失敗拋出的jakarta.validation.ConstraintViolationException會(huì)在重試處理之后直接傳播給 Feign 方法調(diào)用方可在客戶端統(tǒng)一捕獲并轉(zhuǎn)為 4xx 或業(yè)務(wù)錯(cuò)誤無需任何網(wǎng)絡(luò)往返。與javax版本的選擇JDK/Jakarta 體系下使用本模塊jakarta.validation遺留的javax.validation命名空間請(qǐng)參考 validation/README.md 中的feign-validation模塊兩者 API 與行為一一對(duì)應(yīng)。九、相關(guān)資源模塊文檔validation-jakarta/README.md模塊構(gòu)建配置validation-jakarta/pom.xml核心實(shí)現(xiàn)BeanValidationMethodInterceptor.java單元測(cè)試BeanValidationMethodInterceptorTest.java底層擴(kuò)展點(diǎn)MethodInterceptor.java、Invocation.java攔截器注冊(cè)與鏈路BaseBuilder.java、SynchronousMethodHandler.java、AsynchronousMethodHandler.java贊分享后端API設(shè)計(jì)【免費(fèi)下載鏈接】feignFeign makes writing java http clients easier項(xiàng)目地址https://gitcode.com/gh_mirrors/fe/feign點(diǎn)擊查看免費(fèi)下載相關(guān)推薦Feign Validation在請(qǐng)求發(fā)出前用 Bean Validation 校驗(yàn) Feign 接口參數(shù)的實(shí)戰(zhàn)指南Feign Validation在請(qǐng)求發(fā)出前用 Bean Validation 校驗(yàn) Feign 接口參數(shù)的實(shí)戰(zhàn)指南 導(dǎo)讀 本文圍繞 Feign 官方子模塊后端API設(shè)計(jì)3步在Mac上無縫運(yùn)行Windows應(yīng)用Whisky終極兼容方案3步在Mac上無縫運(yùn)行Windows應(yīng)用Whisky終極兼容方案 想在蘋果電腦上運(yùn)行Windows專屬軟件卻不想安裝虛擬機(jī)Whisky為你提供了革命性的解決后端API設(shè)計(jì)上一篇如何高效使用開源工具M(jìn)OOTDX通達(dá)信數(shù)據(jù)接口深度實(shí)戰(zhàn)指南下一篇a-picture-is-worth-a-1000-words項(xiàng)目可訪問性評(píng)估工具自動(dòng)化與手動(dòng)測(cè)試創(chuàng)作聲明:本文部分內(nèi)容由AI輔助生成(AIGC),僅供參考