前言
關(guān)鍵詞:Interface定義Controller;feign服務(wù)端;feign interface;Http RPC;cat-client;cat-server;catface;
概???要:catface
,使用類似FeignClient
的Interface作為客戶端發(fā)起Http請(qǐng)求,然后在服務(wù)端使用實(shí)現(xiàn)了這些Interface的類作為Controller
角色,將客戶端、服務(wù)端通過Interface耦合在一起,實(shí)現(xiàn)無感知調(diào)用的輕量級(jí)組件。其底層通訊協(xié)議依舊是Http,支持Http的所有特性:證書、負(fù)載均衡、熔斷、路由轉(zhuǎn)發(fā)、報(bào)文日志、swagger等;
如果使用過dubbo
就比較好理解,和dubbo
調(diào)用非常類似。只不過catface
只支持Http協(xié)議,并且目前只有spring項(xiàng)目可以使用。
catface
的靈感來于FeignClient
,想必其他同學(xué)在看見feignClient的Interface的時(shí)候,應(yīng)該都有一個(gè)想法:feign interface使用的注解,和Controller有很是多共用,那么能不能直接用使用這些Interface來定義Controller呢?
舉個(gè)例子
先粗略看一下feign的接口IDemoService
:
為了后續(xù)避免歧義,此處我們將類似于IDemoService的Interface,稱呼為feign-interface
public interface IDemoService {
@RequestMapping(value = "/server/demo41", method = RequestMethod.POST)
Demo demo1(@RequestBody Demo req);
@RequestMapping(value = "/server/demo43", method = RequestMethod.GET)
List<Demo> demo3(@ModelAttribute Demo req);
@RequestMapping(value = "/server/demo44", method = RequestMethod.POST)
ResponseEntity<Demo> demo4(@RequestParam("userName") String name, @RequestParam("userMark") String mark);
@RequestMapping(value = "/server/demo46/{uid}", method = RequestMethod.GET)
Void demo6(@PathVariable("uid") Long userId, @RequestParam("userName") String name);
@RequestMapping(value = "/server/demo47", method = RequestMethod.PUT)
Demo demo7(@RequestHeader("token") String token);
}
FeignClient客戶端,在spring項(xiàng)目中,只需要把IDemoService
作為一個(gè)普通的Service類使用,自動(dòng)注入到其他Bean中,就可以直接執(zhí)行IDemoService類中的方法,F(xiàn)eignClient便會(huì)自動(dòng)根據(jù)方法上的注解信息,發(fā)起Http請(qǐng)求;
FeignClient服務(wù)端:沒有嚴(yán)格意義上的FeignClient服務(wù)端!在spring項(xiàng)目中一般是Controller
提供的接口,返回相應(yīng)的報(bào)文;
如果是公司內(nèi)部系統(tǒng)相互調(diào)用,例如:clientA調(diào)用serverB:
在clientA中定義若干feign-interface作為客戶端,并定義接口方法的入?yún)?、響?yīng)類;
在serverB中定義Controller、以及對(duì)應(yīng)DTO、VO模型;
可以發(fā)現(xiàn):
:: serverB中的DTO、VO,其數(shù)據(jù)結(jié)構(gòu),和feign-interface方法上入?yún)?、響?yīng)類是一致的;
:: clientA與serverB,在代碼層面上沒有任何聯(lián)系。無論是feign-interface的方法入?yún)ⅰ㈨憫?yīng)類增減字段,都不會(huì)影響到DTO、VO模型;反之也是一樣;
如果存在feign-interface可以直接定義Controller這個(gè)功能,那么上述一般流程,可以修改成如下模式:
新增一個(gè)接口層:serverB-facede,表示由serverB模塊對(duì)外提供服務(wù);
其中:
:: serverB-facede中包含:feign-interface、方法的入?yún)?、響?yīng)類;
:: clientA依賴serverB-facede模塊,在clientA中依舊可以使用feign-interface客戶端;
:: serverB同樣依賴serverB-facede模塊,在serverB中實(shí)現(xiàn)feign-interface接口,將實(shí)現(xiàn)類注冊(cè)成Controller;
如此以來,clientA和serverB,便通過serverB-facede耦合在一起??瓷先ゾ拖袷窃赾lientA模塊中,自動(dòng)注入了一個(gè)feign-interface類,執(zhí)行其中的方法,就可以得到serverB模塊中實(shí)現(xiàn)類返回的數(shù)據(jù)!Http調(diào)用對(duì)于消費(fèi)者、提供者,都是無感知的;
clientA 調(diào)用 serverB
服務(wù)消費(fèi)者 ===================================> 服務(wù)提供者
│ │
│ 注入 │
│ │
└────────────── feign-interface │
│ │
└───────────────────────┤
│
實(shí)現(xiàn) │
│
feign-interface 實(shí)現(xiàn)類
如果了解spring掃描Bean的原理,在服務(wù)端根據(jù)feign-interface上的注解,手動(dòng)注冊(cè)Controller不難;而且在熟悉動(dòng)態(tài)代理的情況下,自己根據(jù)feign-interface寫一個(gè)http RPC也不是不可能;
于是在技術(shù)實(shí)現(xiàn)都不難的情況下,catface
就出現(xiàn)辣~:
項(xiàng)目地址:https://github.com/bugCats/cat-client-all
catface
基于spring框架開發(fā)的 (不會(huì)吧不會(huì)吧,在如今spring大有一統(tǒng)江湖的趨勢(shì),還會(huì)有新項(xiàng)目沒有使用spring全家桶的吧?),通過Interface、以及其方法的輸入返回模型,耦合客戶端與服務(wù)端,實(shí)現(xiàn)無感知Http調(diào)用。
對(duì)于開發(fā)者而言,眼前沒有客戶端、服務(wù)端,只有Interface調(diào)用者和Interface的實(shí)現(xiàn)類:
-
精細(xì)化到每個(gè)接口的輸入輸出報(bào)文記錄方案;
比如某些API是核心流程,需要記錄詳細(xì)的輸入輸出報(bào)文,以便如果后續(xù)出現(xiàn)問題,可以查閱日志內(nèi)容;而有些API不是那么重要,但調(diào)用又非常頻繁,此時(shí)我們不希望打印它們,以免浪費(fèi)系統(tǒng)性能、浪費(fèi)存儲(chǔ)空間;對(duì)于一般的API,平時(shí)的時(shí)候就記錄一下調(diào)用記錄,如果發(fā)生Http異常、或返回了業(yè)務(wù)異常,就記錄詳細(xì)的輸入輸出報(bào)文; -
精細(xì)化到每個(gè)接口的Http等待時(shí)間:
對(duì)于中臺(tái)而言,調(diào)用后臺(tái)接口,不可能無限等待后臺(tái)接口響應(yīng);和日志一樣的邏輯,有的API等待時(shí)間可以長(zhǎng)點(diǎn),有的又不行; -
自動(dòng)加、拆響應(yīng)包裝器類:
當(dāng)服務(wù)端的所有響應(yīng),為統(tǒng)一的一個(gè)數(shù)據(jù)模型、具體的業(yè)務(wù)數(shù)據(jù),是該模型通過泛型確認(rèn)
的屬性時(shí),例如:HttpEntity<User>、ResponesDTO<User>,稱這種似于HttpEntity、ResponesDTO的數(shù)據(jù)結(jié)構(gòu)為響應(yīng)包裝器類
;public class ResponesDTO<T> { private String code; // code為1表示成功,非1表示失敗 private String message; //異常原因說明 private T data; //業(yè)務(wù)數(shù)據(jù) }
當(dāng)客戶端獲取到響應(yīng)對(duì)象ResponesDTO<User>之后:
1. 需要先判斷對(duì)象是否為null;
2. 再判斷code是否為1,不為1需要進(jìn)入異常流程;
3. 最后才能獲取到業(yè)務(wù)數(shù)據(jù)User;catface
支持自動(dòng)拆響應(yīng)包裝器類。當(dāng)啟用后,客戶端feign-interface的方法返回?cái)?shù)據(jù)類型:
:: 可以是ResponesDTO<User>,保持原流程不變;
:: 直接為User,而服務(wù)端無需做任何修改。當(dāng)feign-interface的方法成功執(zhí)行完并返回了User對(duì)象,表示Http調(diào)用成功,并且響應(yīng)的code一定是1;如果調(diào)用失敗、或者code不為1,會(huì)自動(dòng)進(jìn)入預(yù)先設(shè)置好的異常流程;加響應(yīng)包裝器類,針對(duì)服務(wù)端而言,是拆包裝器類的逆操作。當(dāng)服務(wù)端啟用之后,feign-interface和與之對(duì)應(yīng)的Controller方法響應(yīng)的數(shù)據(jù)類型,可以直接是User,
catface
會(huì)自動(dòng)在最外層加上ResponesDTO;另外,對(duì)于使用繼承實(shí)現(xiàn)公共屬性code、message和業(yè)務(wù)數(shù)據(jù)并列的情況,也同樣適用于此功能;
-
自定義標(biāo)記功能:
在feign-interface上添加自定義標(biāo)記、還可以為單個(gè)API接口添加標(biāo)記。結(jié)合攔截器、springEL動(dòng)態(tài)解析入?yún)?,便可以靈活實(shí)現(xiàn)各種各樣的業(yè)務(wù)邏輯; -
在服務(wù)端,通過繼承實(shí)現(xiàn)API接口升級(jí):
在Java中,繼承本身就可以增強(qiáng)父類功能。此特性也適用于catface
,通過繼承特性對(duì)服務(wù)端API接口升級(jí)增強(qiáng),而客戶端無需任何修改; -
通過feign-interface生成的Controller,依舊支持swagger;
-
其他比較一般的功能:攔截器、修改http Jar包、負(fù)載均衡、熔斷、異常重試、Mock測(cè)試等等;
cat-client 模塊
此模塊可以單獨(dú)使用,使用方式和feignclient
非常類似;以feign-interface為模板動(dòng)態(tài)生成Http調(diào)用實(shí)現(xiàn)類,在應(yīng)用層自動(dòng)注入feign-interface對(duì)象,執(zhí)行feign-interface中的方法,即可發(fā)起http請(qǐng)求,并最終將Http響應(yīng)結(jié)果,轉(zhuǎn)換成方法返回對(duì)象數(shù)據(jù)類型,返回給應(yīng)用層;
@EnableCatClient
這個(gè)注解表示啟用CatClient客戶端。該注解依賴于spring容器
,置于任何spring bean之上,例如:springboot項(xiàng)目啟動(dòng)類上、或者任意包含@Component
注解的類上;
- value: 掃描的包路徑,處于這個(gè)目錄中的feign-interface都會(huì)被注冊(cè)成客戶端。默認(rèn)值是被標(biāo)記類的同級(jí)目錄;
-
classes: 指定客戶端class進(jìn)行注冊(cè),優(yōu)先度高于包路徑。class值分為2大類:
:: 普通的class: 當(dāng)類上包含@CatClient注解時(shí),將該class注冊(cè)成客戶端;
:: CatClientProvider的子類: 解析該子類中包含@CatClient注解的方法,將方法返回對(duì)象的class注冊(cè)成客戶端; - configuration: 生成客戶端的一些配置項(xiàng)和默認(rèn)值;
@CatClient
該注解用于定義某個(gè)feign-interface為客戶端,包含:客戶端別名、遠(yuǎn)程服務(wù)端host、攔截器、異?;卣{(diào)、http等待時(shí)間、默認(rèn)的API日志記錄方案;定義CatClient客戶端有2種方式:
方式1: 在feign-interface上添加@CatClient
;系統(tǒng)啟動(dòng)時(shí),會(huì)根據(jù)@EnableCatClient的配置,自動(dòng)掃描并注冊(cè)成客戶端:
@CatClient(host = "${userService.remoteApi}", connect = 3000, logs = RequestLogs.All2)
public interface IUserService {
ResponseEntity<PageInfo<UserInfo>> userPage(@RequestHeader("token") String token, @ModelAttribute UserPageVi vi);
UserInfo userInfo(@PathVariable("uid") String uid, @RequestParam("type") String type);
ResponseEntity<Void> userSave(@RequestBody UserSaveVi vi);
}
方式2: 通過CatClientProvider的子類,集中批量定義。將包含@CatClient
注解的方法其返回對(duì)象的class注冊(cè)成客戶端;
public interface RemoteProvider extends CatClientProvider {
@CatClient(host = "${userService.remoteApi}", connect = 3000, socket = 3000)
IUserService userService(); //實(shí)際上將IUserService注冊(cè)成客戶端
@CatClient(host = "${orderService.remoteApi}")
IOrderService orderService(); //將IOrderService注冊(cè)成客戶端
}
-
value: 客戶端組件的別名,默認(rèn)首字母小寫。最終客戶端會(huì)注冊(cè)到spring容器中,可以通過
@Autowired
實(shí)現(xiàn)自動(dòng)注入; -
host: http請(qǐng)求的主機(jī)地址??梢允?IP+端口,也可以是域名、cloud服務(wù)名:
:: 字面量: https://www.bugcat.cc
:: 配置文件值: ${xxx.xxx}
:: 服務(wù)名,配合注冊(cè)中心: http://myserver-name/ctx - interceptor: http請(qǐng)求攔截器;用來修改入?yún)ⅰ⒄?qǐng)求url、修改參數(shù)簽名、添加token等處理;默認(rèn)值受CatClientConfiguration控制;
- factory: 負(fù)責(zé)創(chuàng)建發(fā)送Http請(qǐng)求相關(guān)對(duì)象的工廠類;一般如果僅需修改入?yún)ⅰ⒒蛘咛砑雍灻?,可以使用攔截器修改。如果有比較大的Http流程調(diào)整,才考慮修改CatClientFactory,例如:添加負(fù)載均衡;默認(rèn)值受CatClientConfiguration控制;
-
fallback: 異常處理類;當(dāng)接口發(fā)生http異常(40x、50x),執(zhí)行的回調(diào)方法。如果在回調(diào)方法中,繼續(xù)拋出異常,或者關(guān)閉回調(diào)模式,則會(huì)執(zhí)行CatResultProcessor#onHttpError進(jìn)行最終兜底處理;其值可以取以下類型:
:: Object.class: 嘗試使用feign-interface默認(rèn)方法,如果feign-interface沒有默認(rèn)實(shí)現(xiàn),再執(zhí)行兜底方法;
:: Void.class: 關(guān)閉回調(diào)模式,直接執(zhí)行兜底方法;
:: 其他class值: 必須實(shí)現(xiàn)該feign-interface。當(dāng)發(fā)生異常之后,執(zhí)行實(shí)現(xiàn)類的對(duì)應(yīng)方法; - socket: http讀值超時(shí)毫秒,-1 代表不限制;默認(rèn)值受CatClientConfiguration控制;
- connect: http鏈接超時(shí)毫秒,-1 代表不限制;默認(rèn)值受CatClientConfiguration控制;
- logsMod: 記錄日志方案;默認(rèn)值受CatClientConfiguration控制;
- tags: 分組標(biāo)記,給客戶端添加自定義標(biāo)記;詳細(xì)使用見CatNote;
@CatMethod
定義客戶端feign-interface中的方法,為API接口;
-
value: 具體的url;
:: 字面量: /qq/972245132
:: 配置文件值: ${xxx.xxx}
:: uri類型參數(shù): /qq/{pathVariable} - method: Http請(qǐng)求方式,默認(rèn)使用POST發(fā)送表單;
- notes: 自定義參數(shù)、或標(biāo)記;當(dāng)方法上還存在@CatNotes注解時(shí),會(huì)忽略這個(gè)屬性值!詳細(xì)使用見CatNotes;
- socket: http讀值超時(shí)毫秒;-1 不限;0 同當(dāng)前feign-interface配置;其他數(shù)值,超時(shí)的毫秒數(shù);
- connect: http鏈接超時(shí)毫秒;-1 不限;0 同當(dāng)前feign-interface配置;其他數(shù)值,超時(shí)的毫秒數(shù);
- logsMod: 日志記錄方案;Def 同當(dāng)前feign-interface配置;
對(duì)于常用的GET、POST請(qǐng)求方式,還有@CatGet
、@CatPost
2個(gè)便捷組合注解;
@CatNote
自定義標(biāo)記注解;有2大類使用場(chǎng)景:
- 為feign-interface、方法、參數(shù),添加標(biāo)記;
- @CatNote(key=“name”, value=“bugcat”): 字面量;創(chuàng)建了一個(gè)標(biāo)記,標(biāo)記名=name,標(biāo)記值=bugcat;
- @CatNote(“bugcat”): 字面量;省略key屬性,最終key與value值相同,即標(biāo)記名=標(biāo)記值=bugcat;
- @CatNote(key=“host”, value=“${orderhost}”): 取配置文件值;創(chuàng)建了一個(gè)標(biāo)記,標(biāo)記名=host,標(biāo)記值從配置中獲取orderhost對(duì)應(yīng)的值;
- @CatNote(key=“userId”, value=“#{req.userId}”): 取方法入?yún)⒅担皇褂胹pringEL表達(dá)式,從方法入?yún)?duì)象中獲取標(biāo)記值。此種方法,必須要為入?yún)⑷e名;
- 為方法入?yún)⑷e名;一般配合
@RequestBody
使用,可以實(shí)現(xiàn) #{參數(shù)別名.屬性} 動(dòng)態(tài)獲取入?yún)⒌膶傩灾?;可為入?yún)⑷e名注解有:- @ModelAttribute(“paramName”): 為GET、POST表單對(duì)象取別名;
- @PathVariable(“paramName”): PathVariable類型參數(shù)別名;
- @RequestParam(“paramName”): 鍵值對(duì)參數(shù)別名;
- @RequestHeader(“paramName”): 請(qǐng)求頭參數(shù)別名;
-
@CatNote(“paramName”): 通用類型參數(shù)別名,一般結(jié)合
@RequestBody
使用;
@CatResponesWrapper
為當(dāng)前feign-interface,配置加、拆響應(yīng)包裝器類;由于這個(gè)注解是標(biāo)記在feign-interface接口上,因此如果feign-interface是作為客戶端,那么@CatResponesWrapper
便是啟用拆響應(yīng)包裝器類功能;如果是作為服務(wù)端Controller,則是加響應(yīng)包裝器類;
- value: Http響應(yīng)包裝器類處理類;
CatClientConfiguration
生成客戶端的一些配置項(xiàng)和默認(rèn)值。配合@EnableCatClient使用,可用于修改@CatClient
、@CatResponesWrapper
注解的實(shí)際默認(rèn)值;
例如:@CatClient#socket的默認(rèn)值為1000。如果需要統(tǒng)一修改成3000,而不想在每個(gè)feign-interface客戶端上修改socket=3000,可以重寫CatClientConfiguration#getSocket方法,使其返回3000即可;
- getSocket: http讀值超時(shí),默認(rèn)1000毫秒;對(duì)應(yīng)@CatClient#socket;
- getConnect: http鏈接超時(shí),默認(rèn)1000毫秒;對(duì)應(yīng)@CatClient#connect;
- getLogsMod: API接口輸入輸出報(bào)文記錄方案,默認(rèn)是當(dāng)發(fā)生Http異常時(shí)記錄;對(duì)應(yīng)@CatClient#logsMod;
- getWrapper: 拆包裝器類處理類,對(duì)應(yīng)@CatResponesWrapper#value;
- getMethodInterceptor: http請(qǐng)求攔截器,對(duì)應(yīng)@CatClient#interceptor;
- getClientFactory: 創(chuàng)建客戶端發(fā)送Http請(qǐng)求相關(guān)對(duì)象的工廠類,對(duì)應(yīng)@CatClient#factory;
-
getCatHttp: Http請(qǐng)求發(fā)送工具類,默認(rèn)使用
RestTemplate
; -
getPayloadResolver: Http請(qǐng)求輸入輸出對(duì)象,序列化與反序列化處理類;默認(rèn)使用
Jackson
框架; -
getLoggerProcessor: 打印Http日志類;默認(rèn)使用
logback
框架。如果需要修改日志打印格式,可以實(shí)現(xiàn)CatLoggerProcessor
接口;
CatClientProvider
批量注冊(cè)客戶端類;
public interface RemoteProvider extends CatClientProvider {
@CatClient(host = "${userService.remoteApi}", connect = 3000, socket = 3000)
IUserService userService(); //實(shí)際上將IUserService注冊(cè)成客戶端
@CatClient(host = "${orderService.remoteApi}")
IOrderService orderService(); //將IOrderService注冊(cè)成客戶端
}
采用此方法定義客戶端,將@CatClient置于CatClientProvider子類的方法之上,使得@CatClient注解與feign-interface類在物理上分隔開,避免注解污染feign-interface,以便可以多次復(fù)用feign-interface;
配合@EnableCatClient#classes使用,可以集中處理,按需加載使用。特別適用于多模塊、多客戶端feign-interface的場(chǎng)景;
例如:在serverB-facede模塊中,與非常多的feign-interface,其中若干個(gè)feign-interface實(shí)現(xiàn)一個(gè)完整的業(yè)務(wù)流程。
為了避免多個(gè)消費(fèi)端,需要多次手動(dòng)注冊(cè)多個(gè)feign-interface客戶端,可以在serverB-facede模塊中創(chuàng)建一個(gè)CatClientProvider子類,將相關(guān)feign-interface在其子類中預(yù)先定義好。
消費(fèi)端在@EnableCatClient#classes中指定該子類,即可實(shí)現(xiàn)批量注冊(cè)feign-interface客戶端;
CatClientFactory
創(chuàng)建客戶端發(fā)送Http請(qǐng)求相關(guān)對(duì)象的工廠類。CatClientConfiguration
中的參數(shù)適用于全局的默認(rèn)配置。如果存在部分feign-interface客戶端,需要特別的個(gè)性化配置,就需要使用自定義CatClientFactory
,返回個(gè)性化的配置項(xiàng);可以實(shí)現(xiàn)CatClientFactory
接口,或者繼承SimpleClientFactory
,再修改@CatClient#factory參數(shù)值;
@CatClient(host = “${orderhost}”, factory = TokenFactory.class)
- getCatHttp: 自定義Http發(fā)送類;
- getPayloadResolver: 自定義入?yún)㈨憫?yīng)序列化與反序列類;
- getLoggerProcessor: 自定義日志格式打印類;
- getResultHandler: 自定義http響應(yīng)處理類;
- newSendHandler: 自定義http發(fā)送流程類;返回對(duì)象必須是多例!
CatSendInterceptor
Http發(fā)送請(qǐng)求流程中的攔截器;可以重寫CatClientConfiguration#getMethodInterceptor方法修改全局默認(rèn)的攔截器;也可以通過@CatClient#interceptor為指定的feign-interface修改;
全局?jǐn)r截器,和自定義攔截器只能生效一個(gè)!(支持多個(gè)攔截器下下下個(gè)版本再加)
攔截器有4個(gè)切入點(diǎn),對(duì)應(yīng)CatSendProcessor的四個(gè)方法:
- executeConfigurationResolver: 處理http配置項(xiàng)前后;對(duì)應(yīng)CatSendProcessor#doConfigurationResolver方法,處理Http請(qǐng)求相關(guān)配置:host、url、讀取超時(shí)、請(qǐng)求方式、請(qǐng)求頭參數(shù)、解析自定義標(biāo)記;
-
executeVariableResolver: 處理入?yún)?shù)據(jù)前后;對(duì)應(yīng)CatSendProcessor#doVariableResolver、CatSendProcessor#postVariableResolver2個(gè)方法:如果在調(diào)用遠(yuǎn)程API,需要額外處理參數(shù)、或添加簽名等,可以在此處添加;
:: doVariableResolver,入?yún)⑥D(zhuǎn)字符串、或表單對(duì)象;
:: postVariableResolver,入?yún)⑥D(zhuǎn)換之后處理,這是給子類提供重寫的方法; - executeHttpSend: 發(fā)送Http請(qǐng)求前后;對(duì)應(yīng)CatSendProcessor#postHttpSend方法;如果啟用重連,那么該方法會(huì)執(zhí)行多次!
- postComplete: 在成功、異常回調(diào)方法之后、拆響應(yīng)包裝器之前執(zhí)行;此處可以再次對(duì)響應(yīng)對(duì)象進(jìn)行修改;對(duì)于異常流程,可以繼續(xù)拋出異常;
調(diào)用流程示意:
CatMethodAopInterceptor#intercept
│
CatClientContextHolder#executeConfigurationResolver
│
CatSendProcessor#doConfigurationResolver <------- CatSendInterceptor#executeConfigurationResolver
│
CatClientContextHolder#executeVariableResolver
│
CatSendProcessor#doVariableResolver <----┬---- CatSendInterceptor#executeVariableResolver
│ :
CatSendProcessor#postVariableResolver <--┘
│
CatClientContextHolder#executeRequest
│
CatSendProcessor#postHttpSend <------- CatSendInterceptor#executeHttpSend
│
[[CatResultProcessor#onHttpError]]
│
CatResultProcessor#resultToBean
│
CatClientContextHolder#postComplete <------- CatSendInterceptor#postComplete
│
CatResultProcessor#onFinally
│
return <───────────────┘
CatHttp
Http發(fā)送請(qǐng)求工具類,默認(rèn)實(shí)現(xiàn)類CatRestHttp
,底層使用RestTemplate
發(fā)送請(qǐng)求。優(yōu)先是從spring容器中獲取RestTemplate,如果spring容器中沒有,才會(huì)自動(dòng)創(chuàng)建。
因此,如果spring容器中的RestTemplate配置了負(fù)載均衡,那么對(duì)應(yīng)的CatRestHttp同樣也有負(fù)載均衡特性!
如果需要修改成其他Http框架,可以如下操作:首先需要實(shí)現(xiàn)CatHttp
接口;再將實(shí)現(xiàn)類編織到客戶端Http發(fā)送流程中:
方案1:將CatHttp實(shí)現(xiàn)類對(duì)象,注冊(cè)到spring容器中;適用于全局;
方案2:重寫CatClientConfiguration#getCatHttp方法,使其返回指定CatHttp對(duì)象;適用于全局;
方案3:對(duì)于特定的API接口,可以利用CatClientFactory。在定義feign-interface客戶端時(shí),修改@CatClient#factory值為指定CatClientFactory子類,再重寫CatClientFactory#getCatHttp方法,返回自定義CatHttp實(shí)現(xiàn)類對(duì)象;
CatPayloadResolver
針對(duì)于使用POST方式發(fā)送IO流情況,將輸入輸出對(duì)象,序列化與反序列化的處理類。如果是GET、POST發(fā)送表單數(shù)據(jù),應(yīng)該在CatHttp層進(jìn)行統(tǒng)一的uri編碼
。
默認(rèn)使用Jackson
框架,catface
中還內(nèi)置了Fastjson
框架處理類。如果需要使用其他框架、或者使用xml,可以自行實(shí)現(xiàn)CatPayloadResolver
接口,實(shí)現(xiàn)類編織到Http發(fā)送流程中方式,和自定義CatHttp一致;
CatObjectResolver
將feign-interface方法上的復(fù)雜數(shù)據(jù)類型入?yún)ⅲD(zhuǎn)成表單對(duì)象;
catface
不建議使用POST、GET發(fā)送太過于復(fù)雜的表單對(duì)象,推薦使用POST + Json這種一般方式。
雖然內(nèi)置了入?yún)?shù)據(jù)轉(zhuǎn)表單對(duì)象的處理類,但是對(duì)于怪異的場(chǎng)景如果出現(xiàn)不支持情況,就需要自行編寫轉(zhuǎn)換類。實(shí)現(xiàn)CatObjectResolver
接口,再執(zhí)行CatSendProcessor#setObjectResolverSupplier方法手動(dòng)賦值;CatSendProcessor對(duì)象可以在攔截器中獲取得到;
CatLoggerProcessor
調(diào)用API接口的日志記錄處理類??梢宰孕锌刂迫罩敬蛴〖?jí)別,以及日志格式;
CatResultProcessor
Http響應(yīng)處理類:
- onHttpError: 當(dāng)發(fā)生Http異常后默認(rèn)執(zhí)行流程,最終進(jìn)行兜底的異常處理;
- canRetry: 異常是否需要重連,如果開啟重連,并且發(fā)送Http異常,判斷是否需要重新請(qǐng)求。一般結(jié)合熔斷器或者注冊(cè)中心使用,可以重新選擇一個(gè)健康的服務(wù)端實(shí)例;
-
resultToBean: 響應(yīng)報(bào)文轉(zhuǎn)結(jié)果對(duì)象,默認(rèn)使用
Jackson
框架。如果是通過xml傳輸信息,需要自行實(shí)現(xiàn)CatPayloadResolver接口; - onFinally: 自動(dòng)拆響應(yīng)包裝器類,結(jié)合響應(yīng)包裝器類使用,可以使API接口方法,直接返回業(yè)務(wù)對(duì)象;
只能通過自定義CatClientFactory#getResultHandler修改,為單例;
CatSendProcessor
Http發(fā)送請(qǐng)求核心處理類;該對(duì)象可以通過CatClientFactory#newSendHandler自動(dòng)創(chuàng)建,也支持手動(dòng)創(chuàng)建后,作為feign-interface方法的入?yún)魅搿?/p>
CatSendProcessor
類雖然提供了擴(kuò)展的入口,但一般情況下無需修改,如果對(duì)Http請(qǐng)求整體流程有比較大的修改,才考慮覆蓋重寫。例如:搭配注冊(cè)中心、負(fù)載均衡器使用、或者換成Socket協(xié)議等。(負(fù)載均衡也可以使用RestTemplate實(shí)現(xiàn))
若僅僅是修改入?yún)ⅰ⑻砑觮oken、簽名,可以直接使用更輕量級(jí)的攔截器實(shí)現(xiàn)。
-
doConfigurationResolver: 初始化http相關(guān)配置??尚薷捻?xiàng)包括:
:: Http連接讀取超時(shí):
:: Http請(qǐng)求方式: 雖然在@CatClient中聲明是使用POST發(fā)送Json字符串,但是此處也可以修改成POST發(fā)送表單方式;
:: 遠(yuǎn)程服務(wù)端host、API調(diào)用的url: 可以使用@PathVariable、取環(huán)境配置參數(shù)${xxx}等方式動(dòng)態(tài)給url賦值,也可以在此方法中修改host、url;
:: 自定義的標(biāo)記數(shù)據(jù): 根據(jù)@CatNote標(biāo)記,獲取到環(huán)境配置參數(shù)、動(dòng)態(tài)取入?yún)⒌膶傩灾?,可以?duì)Host、url、入?yún)?、?qǐng)求方式等,執(zhí)行更自由修改;例如,集成負(fù)載均衡、自定義路由規(guī)則; - doVariableResolver: 請(qǐng)求入?yún)⒌哪J(rèn)處理。如果是POST、GET表單方式,將方法入?yún)⑥D(zhuǎn)成表單對(duì)象;如果POST發(fā)送IO流,則將入?yún)?duì)象序列化成字符串;此方法可以將入?yún)⒛P停薷某杀韱螌?duì)象;轉(zhuǎn)成Json字符串;添加公共參數(shù);添加Token;計(jì)算簽名;記錄請(qǐng)求信息;
- doVariableResolver: 處理入?yún)⒑筇幚?。默認(rèn)是個(gè)空方法,提供給子類重寫。
- postHttpSend: 全部數(shù)據(jù)準(zhǔn)備充分之后,發(fā)起Http請(qǐng)求;如果需要換成其他協(xié)議,如Socket,重寫此方法,將最終響應(yīng)結(jié)果存儲(chǔ)到CatClientContextHolder#setResponseObject中;
AbstractResponesWrapper
響應(yīng)包裝器類處理類;
- getWrapperClass: 獲取響應(yīng)包裝器類的class;用于判斷API接口方法的響應(yīng)對(duì)象,是包裝器類、還是直接業(yè)務(wù)數(shù)據(jù)類;
-
getWrapperType: 將業(yè)務(wù)數(shù)據(jù)類型Type,組裝到響應(yīng)包裝器類中,返回標(biāo)準(zhǔn)響應(yīng)Type的引用;例如,包裝器類是ResponseDTO<T>,業(yè)務(wù)數(shù)據(jù)類型為User,API接口返回的原始數(shù)據(jù)類型應(yīng)該為ResponseDTO<User>。對(duì)于使用拆包裝器類的API接口方法,需要將方法的響應(yīng)類(即業(yè)務(wù)數(shù)據(jù)類型),組裝到包裝器類中。
public <T> CatTypeReference getWrapperType(Type type){ //type 為業(yè)務(wù)數(shù)據(jù)的類型,可以是User、List<Order>、Long、String[]等 //ResponseDTO是響應(yīng)包裝器類,type最終會(huì)替換掉T的位置,最終結(jié)果是ResponseDTO<Type> //注意后面的一對(duì)花括號(hào)不能少! return new CatTypeReference<ResponseDTO<T>>(type){}; }
-
checkValid: 校驗(yàn)返回的業(yè)務(wù)數(shù)據(jù)是否正確。需要注意異常分為兩大類:
:: Http異常 由http請(qǐng)求造成的異常,例如:403 404 500 503,讀取超時(shí)等,此類異常可以重新連接,或者換遠(yuǎn)程服務(wù)實(shí)例調(diào)用;
:: 業(yè)務(wù)異常 此類為服務(wù)端接收到請(qǐng)求,并且通過業(yè)務(wù)邏輯判斷,得出不處理該請(qǐng)求,并將消息成功返回給Http調(diào)用者。例如:調(diào)用取消訂單,服務(wù)端判斷該訂單已經(jīng)使用,不可以取消。API接口調(diào)用成功,但是響應(yīng)為邏輯處理失敗。
checkValid方法主要是針對(duì)于業(yè)務(wù)異常場(chǎng)景,當(dāng)發(fā)生業(yè)務(wù)異常時(shí),程序該如何處理?可以選擇繼續(xù)拋出,由最頂層的@ControllerAdvice統(tǒng)一處理異常;也可以自行解析業(yè)務(wù)異常編碼,再修改返回默認(rèn)業(yè)務(wù)對(duì)象; - getValue: 從響應(yīng)包裝器類中獲取業(yè)務(wù)數(shù)據(jù);
- createEntryOnSuccess: 服務(wù)器端加響應(yīng)包裝器類,當(dāng)成功執(zhí)行;
- createEntryOnException: 服務(wù)器端加響應(yīng)包裝器類,當(dāng)異常執(zhí)行;
無論是CatResultProcessor#onHttpError繼續(xù)拋出、還是AbstractResponesWrapper#checkValid校驗(yàn)失敗拋出,都會(huì)造成應(yīng)用層調(diào)用feign-interface方法發(fā)生異常。
但是在定義feign-interface方法時(shí),方法可以不顯示拋出異常,因此在調(diào)用時(shí),應(yīng)當(dāng)清楚feign-interface方法會(huì)隱式拋出異常,需要注意如果發(fā)生異常該如何處理。
當(dāng)應(yīng)用層執(zhí)行feign-interface方法后,希望無論是成功還是失敗,都要有結(jié)果返回,然后應(yīng)用層再根據(jù)執(zhí)行結(jié)果,自行處理異常,那么不應(yīng)該使用自動(dòng)拆包裝器類!
僅當(dāng)應(yīng)用層執(zhí)行方法后,對(duì)于異常流程沒有嚴(yán)格要求時(shí),才會(huì)建議使用!
CatClientBuilders
靜態(tài)方法創(chuàng)建CatClient客戶端。可以在非spring環(huán)境中使用,也可以在運(yùn)行過程中手動(dòng)創(chuàng)建,或者單元測(cè)試時(shí)期使用;
CatHttpRetryConfigurer
當(dāng)發(fā)生Http異常時(shí),重新連接策略:
- enable: 是否開啟重連;默認(rèn)false;
- retries: 重連次數(shù),不包含第一次調(diào)用!默認(rèn)2,實(shí)際上最多會(huì)調(diào)用3次;
-
status: 重連的狀態(tài)碼:多個(gè)用逗號(hào)隔開;可以為500,501,401或400-410,500-519,419或
*
或any,默認(rèn)500-520; -
method: 需要重連的請(qǐng)求方式,多個(gè)用逗號(hào)隔開;可以為post,get 或
*
或any,默認(rèn)any; -
exception: 需要重連的異常、或其子類;多個(gè)用逗號(hào)隔開;可以為java.io.IOException 或
*
或any,默認(rèn)空; - tags: 需要重連的客戶端分組,在@CatClient#tags中配置;多個(gè)用逗號(hào)隔開,默認(rèn)空;
-
note: 需要重連的API方法標(biāo)記;多個(gè)用逗號(hào)隔開;會(huì)匹配@CatMethod#notes中的值;當(dāng)配置的note值,在@CatNote#value中存在時(shí),觸發(fā)重連;
例如:note=bugcat匹配@CatNote(key=“name”, value=“bugcat”)、@CatNote(“bugcat”),不會(huì)匹配@CatNote(key=“bugcat”, value=“972245132”) -
noteMatch: 需要重連的API方法標(biāo)記鍵值對(duì);在配置文件中,使用單引號(hào)包裹的Json字符串,默認(rèn)值
'{}'
;當(dāng)noteMatch設(shè)置的鍵值對(duì),在@CatMethod#notes的鍵值對(duì)中完全匹配時(shí),觸發(fā)重連:
note-match=‘{“name”:“bugcat”,“age”:“17”}’,會(huì)匹配notes={@CatNote(key=“name”, value=“bugcat”), @CatNote(key=“age”, value=“17”)};
如果@CatNote采用springEL表達(dá)式形式,可以實(shí)現(xiàn)運(yùn)行時(shí),根據(jù)入?yún)Q定是否需要重連!
例如:當(dāng)設(shè)置note=save,其中@CatMethod(notes = @CatNote(“#{req.methodName}”)),或者note-match=‘{“method”:“save”}’、對(duì)應(yīng)@CatMethod(notes = @CatNote(key=“method”, value=“#{req.methodName}”))時(shí),如果請(qǐng)求入?yún)eq的methodName值為save,會(huì)觸發(fā)重連,其他則不會(huì);
其他說明
1. 方法入?yún)⒆⒔?/h5>
@ModelAttribute、@RequestBody、@RequestParam在同一個(gè)方法中,只能三選一,可以和@RequestHeader、@PathVariable共存;
- @ModelAttribute: 用于標(biāo)記復(fù)雜對(duì)象,只能存在一個(gè);表示使用表單方式發(fā)送參數(shù);
- @RequestBody: 用于標(biāo)記復(fù)雜對(duì)象,只能存在一個(gè);表示使用POST IO流方式發(fā)送參數(shù);可以使用@CatNote為參數(shù)取別名;
- @RequestParam: 用于標(biāo)記基礎(chǔ)數(shù)據(jù)類型、字符串、日期對(duì)象,可以有多組;表示使用表單方式發(fā)送參數(shù);
- @RequestHeader: 表示請(qǐng)求頭參數(shù),可以有多組;
- @PathVariable: 表示uri參數(shù),可以有多組;
默認(rèn)情況下@ModelAttribute、@RequestBody代表把對(duì)應(yīng)的對(duì)象轉(zhuǎn)換成表單、或者字符串。但是具體數(shù)據(jù)格式,需要參考攔截器、CatSendProcessor子類中的自定義邏輯;
2. 方法響應(yīng)類
- 如果響應(yīng)是Object類型,那么會(huì)返回Http的原始響應(yīng),無論是否啟用了拆響應(yīng)包裝器類;
- 如果響應(yīng)是Date類型,默認(rèn)日期格式為
yyyy-mm-dd HH:mi:ss.SSS
,可以使用@JsonFormat#pattern
、JSONField#format
修改格式;
3. 示例
/**
* 定義一個(gè)客戶端;
* 遠(yuǎn)程服務(wù)器地址為:${core-server.remoteApi},需要從環(huán)境變量中獲??;
* 該客戶端定義了一個(gè)攔截器:TokenInterceptor;
* 單獨(dú)配置了http鏈接、讀取超時(shí):3000ms;
* 其他配置為默認(rèn)值,參考CatClientConfiguration;
* 并且該客戶端,配置了自動(dòng)拆包裝器ResponseEntityWrapper,實(shí)際API接口返回?cái)?shù)據(jù)類型為ResponseEntity<T>;
* 如果方法的返回類型不是ResponseEntity(除Object類型以外),一律推定需要使用自動(dòng)拆包裝器!
* */
@CatResponesWrapper(ResponseEntityWrapper.class)
@CatClient(host = "${core-server.remoteApi}", interceptor = TokenInterceptor.class, connect = 3000, socket = 3000)
public interface TokenRemote {
/**
* CatSendProcessor手動(dòng)創(chuàng)建并且作為方法入?yún)魅耄? * 定義了2個(gè)標(biāo)記:username、pwd,其標(biāo)記值從環(huán)境配置中獲取demo.username、demo.pwd對(duì)應(yīng)的參數(shù)值;
* 方法有默認(rèn)實(shí)現(xiàn),當(dāng)發(fā)生Http異常后,會(huì)自動(dòng)執(zhí)行,并將結(jié)果作為Http請(qǐng)求的結(jié)果返回;
* 雖然添加了自動(dòng)拆響應(yīng)包裝器類,但是該方法返回?cái)?shù)據(jù)類型仍然是ResponseEntity,
* 所以依舊按正常流程解析,將原始響應(yīng),轉(zhuǎn)成ResponseEntity<String>對(duì)象后,再返回;
* */
@CatMethod(value = "/cat/getToken", method = RequestMethod.POST, notes = {@CatNote(key = "username", value = "${demo.username}"), @CatNote(key = "pwd", value = "${demo.pwd}")})
default ResponseEntity<String> getToken(CatSendProcessor sender) {
return ResponseEntity.fail("-1", "當(dāng)前網(wǎng)絡(luò)異常!");
}
/**
* 定義了1個(gè)標(biāo)記,標(biāo)記的key和value都是'needToken'這個(gè)字符串;
* 方法返回?cái)?shù)據(jù)類型String,和配置的包裝器類型不一致,因此推定需要自動(dòng)拆包裝器,實(shí)際返回?cái)?shù)據(jù)類型應(yīng)該為ResponseEntity<String>;
* 先將原始數(shù)據(jù)轉(zhuǎn)換ResponseEntity<String>,再獲取泛型屬性對(duì)應(yīng)值返回;
* */
@CatMethod(value = "/cat/sendDemo", method = RequestMethod.POST, notes = @CatNote("needToken"))
String sendDemo1(@RequestBody Demo demo);
/**
* 將token參數(shù),作為請(qǐng)求頭參數(shù)傳輸;
* 該方法返回類型為Object,為內(nèi)定的特定數(shù)據(jù)類型,直接返回最原始的響應(yīng)字符串;
* */
@CatMethod(value = "/cat/sendDemo", method = RequestMethod.POST)
Object sendDemo2(@RequestBody Demo demo, @RequestHeader("token") String token);
/**
* 動(dòng)態(tài)url,具體訪問地址,由方法入?yún)rl確定;
* */
@CatMethod(value = "{sendurl}", method = RequestMethod.POST)
default ResponseEntity<Void> sendDemo3(@PathVariable("sendurl") String url, @RequestHeader("token") String token, @RequestBody String req) {
return ResponseEntity.fail("-1", "默認(rèn)異常!");
}
/**
* 給入?yún)rderInfo取了別名:'order';
* 自定義了一個(gè)標(biāo)記,標(biāo)記的key='routeId',其value為入?yún)rderInfo的oid屬性值;
* 實(shí)際返回?cái)?shù)據(jù)類型應(yīng)該是ResponseEntity<Void>;
* 如果返回的是基礎(chǔ)數(shù)據(jù)類型,對(duì)應(yīng)的ResponseEntity<基礎(chǔ)數(shù)據(jù)類型包裝類>;
* */
@CatMethod(value = "/order/edit", notes = @CatNote(key = "routeId", value = "#{order.oid}"), method = RequestMethod.POST)
void sendDemo4(@CatNote("order") @RequestBody OrderInfo orderInfo);
}
/**
* 攔截器
* */
@Component
public class TokenInterceptor implements CatSendInterceptor {
/**
* 使用攔截器修改參數(shù)
* */
@Override
public void executeVariableResolver(CatClientContextHolder context, Intercepting intercepting) throws Exception {
CatSendProcessor sendHandler = context.getSendHandler();
sendHandler.setTracerId(String.valueOf(System.currentTimeMillis())); //設(shè)置日志id,可以通過日志id查詢本次請(qǐng)求所有內(nèi)容。如果不指定,自動(dòng)使用uuid
JSONObject notes = sendHandler.getNotes(); //所有的自定義標(biāo)記都存放在這里
CatHttpPoint httpPoint = sendHandler.getHttpPoint();
String need = notes.getString("needToken");//使用note標(biāo)記是否需要添加簽名
if( CatToosUtil.isNotBlank(need)){
String token = TokenInfo.getToken();
httpPoint.getHeaderMap().put("token", token);//將token存入到請(qǐng)求頭中
System.out.println(token);
}
intercepting.executeInternal(); // 執(zhí)行默認(rèn)參數(shù)處理
}
/**
* token管理
* */
private static class TokenInfo {
private static TokenInfo info = new TokenInfo();
public static String getToken(){
return info.getToken(System.currentTimeMillis());
}
private TokenRemote tokenRemote = CatClientUtil.getBean(TokenRemote.class);
private long keepTime;
private String value;
private String getToken(long now){
if( now > keepTime ){
TokenSend sender = new TokenSend(); // 獲取token的時(shí)候,顯示使用指定CatSendProcessor實(shí)例
ResponseEntity<String> bean = tokenRemote.getToken(sender);
keepTime = System.currentTimeMillis() + 3600;
value = bean.getData();
return value;
} else {
return value;
}
}
}
/**
* 獲取token的時(shí)候單獨(dú)處理器;
* 一般情況使用攔截器即可,此處演示作用,使用繼承CatSendProcessor形式修改參數(shù)
* */
private static class TokenSend extends CatSendProcessor {
@Override
public void postVariableResolver(CatClientContextHolder context){
String pwd = notes.getString("pwd"); //notes 已經(jīng)在postConfigurationResolver方法中解析完畢
String username = notes.getString("username");
MultiValueMap<String, Object> keyValueParam = this.getHttpPoint().getKeyValueParam();
keyValueParam.add("username", username);
keyValueParam.add("pwd", pwd);
//注意feign-interface中的getToken方法,
//原getToken方法沒有“有效的”入?yún)ⅲ菍?shí)際發(fā)送Http請(qǐng)求的時(shí)候,卻有2組請(qǐng)求參數(shù)!
//此特性可以非常靈活調(diào)整feign-interface的入?yún)?shù)量、請(qǐng)求方式等
}
}
}
/**
* http響應(yīng)包裝器類處理。包裝器類為:ResponseEntity<T>;
* 如果在客戶端,則為拆包裝器;
* 如果在服務(wù)端,則為加包裝器;
*
* @see AbstractResponesWrapper
* @author bugcat
* */
public class ResponseEntityWrapper extends AbstractResponesWrapper<ResponseEntity>{
/**
* 返回包裝器類class
* */
@Override
public Class<ResponseEntity> getWrapperClass() {
return ResponseEntity.class;
}
/**
* 組裝包裝器類中的實(shí)際泛型
* */
@Override
public <T> CatTypeReference getWrapperType(Type type){
return new CatTypeReference<ResponseEntity<T>>(type){};
}
/**
* 拆包裝器,并且自動(dòng)校驗(yàn)業(yè)務(wù)是否成功?
* 本示例直接繼續(xù)拋出異常;
* */
@Override
public void checkValid(ResponseEntity wrapper) throws Exception {
if( ResponseEntity.succ.equals(wrapper.getErrCode())){
//正常
} else {
//業(yè)務(wù)異常記錄日志
CatClientContextHolder contextHolder = CatClientContextHolder.getContextHolder(); //CatClientContextHolder可以獲取到本次http請(qǐng)求相關(guān)上下文對(duì)象,里面包含請(qǐng)求相關(guān)的各種參數(shù)
CatClientLogger lastCatLog = contextHolder.getSendHandler().getHttpPoint().getLastCatLog();
lastCatLog.setErrorMessge("[" + wrapper.getErrCode() + "]" + wrapper.getErrMsg());
//業(yè)務(wù)異常,可以直接繼續(xù)拋出,在公共的異常處理類中,統(tǒng)一處理
throw new RuntimeException(lastCatLog.getErrorMessge());
}
}
/**
* 拆包裝器,獲取包裝器類中的業(yè)務(wù)對(duì)象
* */
@Override
public Object getValue(ResponseEntity wrapper) {
return wrapper.getData();
}
/**
* 服務(wù)端成功之后加包裝器類
* */
@Override
public ResponseEntity createEntryOnSuccess(Object value, Class methodReturnClass) {
return ResponseEntity.ok(value);
}
/**
* 服務(wù)端當(dāng)發(fā)生異常時(shí)加包裝器
* */
@Override
public ResponseEntity createEntryOnException(Throwable throwable, Class methodReturnClass) {
throwable.printStackTrace();
return ResponseEntity.fail("-1", throwable.getMessage() == null ? "NullPointerException" : throwable.getMessage());
}
}
cat-server 模塊
此模塊也可以單獨(dú)使用,但是更多是搭配feign-interface類型的客戶端使用;具體的業(yè)務(wù)類實(shí)現(xiàn)feign-interface,業(yè)務(wù)類的方法便可以通過Http模式調(diào)用;
cat-server
為了支持自動(dòng)加響應(yīng)包裝器類,以及整合自家的cat-client
客戶端,因此做了看似很復(fù)雜的邏輯,但是如果看到文檔最后catface
部分,就會(huì)發(fā)現(xiàn)很合理了。
1. feign-interface -----------------------┐
↑ :
│ : 3. asm增強(qiáng)interface
│ :
│ ↓
│ 4. Enhancer-Interface
│ ↑
│ │
│ │
│ │ 5. 使用cglib
│ │
2. CatServer <═════════════════════╗ │
║ │
║ │
6. cglib-controller <══════════ 7. http調(diào)用
- feign-interface: 包含@PostMapping、@GetMapping、@RequestBody、@RequestParam等注解的Interface接口;
-
CatServer: 具體的業(yè)務(wù)類,實(shí)現(xiàn)了feign-interface,其類上加有
@CatServer
注解; - 使用asm對(duì)feign-interface增強(qiáng)處理:如果配置了加響應(yīng)包裝器類功能,修改interface的方法返回對(duì)象,統(tǒng)一為響應(yīng)包裝器類;如果使用了catface模式,將方法入?yún)⒘斜磙D(zhuǎn)成虛擬入?yún)?duì)象;并將feign-interface的、方法級(jí)、入?yún)⒓?jí)注解,轉(zhuǎn)移到新Interface上;
- Enhancer-Interface: 增強(qiáng)后的Interface,與原feign-interface沒有任何結(jié)構(gòu)上的關(guān)系;
- 使用cglib對(duì)Enhancer-Interface動(dòng)態(tài)代理,生成Controller角色的類,并注冊(cè)到spring容器;
- cglib-controller: 動(dòng)態(tài)代理生成的Controller對(duì)象,持有CatServer實(shí)現(xiàn)類的引用(對(duì)象適配模式);
- http訪問cglib-controller對(duì)象方法,cglib-controller預(yù)處理入?yún)⒅螅賵?zhí)行CatServer對(duì)應(yīng)的方法;
Enhancer-Interface、cglib-controller 是自動(dòng)成的類;Http請(qǐng)求指向cglib-controller,cglib-controller做預(yù)處理之后,再執(zhí)行CatServer業(yè)務(wù)類的方法,看上去就好像Http請(qǐng)求是直接調(diào)用到CatServer業(yè)務(wù)類。待業(yè)務(wù)處理完畢之后,cglib-controller再判斷是否需要添加響應(yīng)包裝器類,最終返回結(jié)果;
@EnableCatServer
這個(gè)注解表示啟用CatServer服務(wù)端。該注解依賴于spring容器
,置于任何spring bean之上,例如:springboot項(xiàng)目啟動(dòng)類上、或者任意包含@Component
注解的類上;
- value: 掃描的包路徑,處于這個(gè)目錄中的feign-interface都會(huì)被注冊(cè)成服務(wù)端。默認(rèn)值是被標(biāo)記類的同級(jí)目錄;
- classes: 指定服務(wù)端class進(jìn)行注冊(cè),優(yōu)先度高于包路徑;
- configuration: 生成服務(wù)端的一些配置項(xiàng)和默認(rèn)值;
@CatServer
該注解用于定義某個(gè)feign-interface為服務(wù)端接口。包含:服務(wù)端別名、攔截器
、自定義標(biāo)記、響應(yīng)處理類;
//feign-interface,@CatMethod也可以換成@RequestMapping
@Api(tags = "Catface - 用戶操作api")
@CatResponesWrapper(ResponseEntityWrapper.class)
public interface UserService {
@ApiOperation("分頁查詢用戶")
@CatMethod(value = "/user/userPage")
ResponseEntity<PageInfo<UserInfo>> userPage(@ModelAttribute("vi") UserPageVi vi);
@ApiOperation("根據(jù)用戶id查詢用戶信息")
@CatMethod(value = "/user/get/{uid}", method = RequestMethod.GET, notes = @CatNote("user"))
UserInfo userInfo(@PathVariable("uid") String uid);
@ApiOperation("編輯用戶")
@CatMethod(value = "/user/save", method = RequestMethod.POST, notes = @CatNote(key = "name", value = "#{vi.name}"))
ResponseEntity<Void> userSave(@RequestBody @CatNote("vi") UserSaveVi vi) throws Exception;
@ApiOperation("設(shè)置用戶狀態(tài)")
@CatMethod(value = "/user/status", method = RequestMethod.GET)
void status(@RequestParam("uid") String userId, @RequestParam("status") String status);
}
//服務(wù)端具體實(shí)現(xiàn)類
//此處可以添加事務(wù)注解,或其他AOP注解
@CatServer(interceptors = {UserInterceptor2.class, CatServerInterceptor.class, UserInterceptor.class, CatServerInterceptor.GroupOff.class}) //自定義攔截器 + 全局?jǐn)r截器,無攔截器組
public class UserServiceImpl implements UserService{
@Override
public ResponseEntity<PageInfo<UserInfo>> userPage(UserPageVi vi) {
//具體實(shí)現(xiàn)
return ResponseEntity.ok(page);
}
@Override
public UserInfo userInfo(String uid) {
//具體實(shí)現(xiàn)
return info;
}
@Override
public ResponseEntity<Void> userSave(UserSaveVi vi) {
//具體實(shí)現(xiàn)
return ResponseEntity.ok(null);
}
@Override
public void status(String userId, String status) {
System.out.println("userSave >>> userId=" + userId + ", status=" + status);
}
}
- value: 服務(wù)端組件的別名,默認(rèn)首字母小寫;
- resultHandler: 結(jié)果處理類,如果配置了加響應(yīng)包裝器類,在此處添加;默認(rèn)值受CatServerConfiguration控制;
- tags: 自定義標(biāo)記;用于攔截器組匹配;
-
interceptors: 攔截器;cglib-controller調(diào)用CatServer過程中的攔截器鏈。其值有4種類型:
- 為空: 表示啟用攔截器組,和全局默認(rèn)攔截器;默認(rèn)攔截器會(huì)被CatServerConfiguration#getServerInterceptor的返回對(duì)象替換;
- CatServerInterceptor.GroupOff.class: 關(guān)閉攔截器組;
- CatServerInterceptor.NoOp.class: 關(guān)閉自定義攔截器和全局默認(rèn)攔截器;
- 其他值: 表示啟用自定義攔截器;
? ? ? 攔截器規(guī)則:
? ? ? 1. 攔截器組
,是在運(yùn)行中動(dòng)態(tài)匹配,除非在interceptors中配置了CatServerInterceptor.GroupOff.class
,否則總是生效;
? ? ? 2. interceptors值如果為空、或者沒有自定義攔截器
類型,則全局默認(rèn)攔截器生效,可以使用CatServerInterceptor.NoOp.class
關(guān)閉這一功能;若存在任一一個(gè)自定義攔截器
類型,則會(huì)忽略全局默認(rèn)攔截器;
? ? ? 3. 多個(gè)自定義攔截器,按配置的先后順序執(zhí)行;如果需要執(zhí)行全局默認(rèn)攔截器,可以使用CatServerInterceptor.class
占位;
? ? ? :: @CatServer(): 啟用攔截器組,和全局默認(rèn)攔截器;
? ? ? :: @CatServer(interceptors = {A.class, CatServerInterceptor.class}): 啟用攔截器組、A攔截器、和全局默認(rèn)攔截器。此處CatServerInterceptor.class表示全局?jǐn)r截器的占位符,故A攔截器先于全局執(zhí)行;
? ? ? :: @CatServer(interceptors = {A.class}): 啟用攔截器組,和自定義攔截器;
? ? ? :: @CatServer(interceptors = {UserInterceptor.GroupOff.class}): 關(guān)閉攔截器組;僅全局默認(rèn)攔截器有效;
? ? ? :: @CatServer(interceptors = {UserInterceptor.NoOp.class, A.class}): 僅攔截器組有效,關(guān)閉全局?jǐn)r截器和自定義攔截器;
? ? ? :: @CatServer(interceptors = {CatServerInterceptor.class, A.class, UserInterceptor.GroupOff.class}): 關(guān)閉攔截器組;全局?jǐn)r截器、A攔截器有效;
? ? ? :: @CatServer(interceptors = {UserInterceptor.NoOp.class, UserInterceptor.GroupOff.class, A.class, B.class}): 關(guān)閉所有攔截器;
@CatBefore
配置入?yún)⑻幚砥黝?;在?zhí)行業(yè)務(wù)類方法之前執(zhí)行,用于驗(yàn)證、修改、打印方法入?yún)ⅲ?/p>
CatServerConfiguration
生成服務(wù)端的一些配置項(xiàng)和默認(rèn)值。配合@EnableCatServer使用,可用于修改@CatServer
、@CatResponesWrapper
注解的實(shí)際默認(rèn)值;
- getWrapper: 加包裝器類處理類,對(duì)應(yīng)@CatResponesWrapper#value;
- getResultHandler: 默認(rèn)的結(jié)果處理類;
- getServerInterceptor: 全局的默認(rèn)攔截器;用于替換CatServer#interceptors中CatServerInterceptor.class位置;
- getInterceptorGroup: 攔截器組;在運(yùn)行過程中動(dòng)態(tài)匹配;
CatParameterResolver
參入預(yù)處理類,通過@CatBefore配置。在執(zhí)行業(yè)務(wù)類方法之前執(zhí)行,用于驗(yàn)證、修改、打印方法入?yún)⒌龋?/p>
CatResultHandler
feign-interface實(shí)現(xiàn)類的返回值處理類。配合響應(yīng)包裝器使用,可以將返回對(duì)象、異常轉(zhuǎn)換成統(tǒng)一風(fēng)格的響應(yīng);
CatServerInterceptor
在cglib-controller調(diào)用CatServer過程中的自定義攔截器;可以用于驗(yàn)證調(diào)用權(quán)限、必要緩存注入、記錄輸入輸出入?yún)⑷罩镜龋?/p>
- CatServerInterceptor.NoOp.class: 特殊枚舉類,關(guān)閉自定義攔截器和全局默認(rèn)攔截器;
- CatServerInterceptor.GroupOff.class: 特殊枚舉類,關(guān)閉攔截器組;
-
preHandle: 某個(gè)攔截器可以被多個(gè)服務(wù)端引用,在執(zhí)行攔截器內(nèi)容之前執(zhí)行,用于判斷是否滿足前置要求:
:: false 表示不滿足,不執(zhí)行攔截器內(nèi)容;
:: true 表示滿足,需要執(zhí)行攔截器;
:: 拋出異常,默認(rèn)情況下表示立刻結(jié)束攔截器鏈,不執(zhí)行CatServer的對(duì)應(yīng)方法;以上2種布爾返回值,仍然會(huì)執(zhí)行CatServer方法! - postHandle: 攔截器內(nèi)容;
CatInterceptorGroup
攔截器組,在運(yùn)行過程中動(dòng)態(tài)匹配,優(yōu)先于自定義攔截器執(zhí)行;如果服務(wù)端沒有配置CatServerInterceptor.GroupOff.class,則總是執(zhí)行;一般用于記錄日志、驗(yàn)證權(quán)限使用;
- matcher: 匹配方法;在運(yùn)行過程中,根據(jù)入?yún)⒑驼{(diào)用的上下文進(jìn)行匹配校驗(yàn);
- getInterceptorFactory: 當(dāng)匹配方法返回true后執(zhí)行,返回滿足該分組要求的攔截器集合;
- getOrder: 執(zhí)行順序,越小越先執(zhí)行;
其他說明
cglib-controller在調(diào)用CatServer業(yè)務(wù)類過程中,以及CatServer業(yè)務(wù)類內(nèi)部發(fā)生的異常,均可以通過CatResultHandler#onError統(tǒng)一處理。
但是對(duì)于Http請(qǐng)求cglib-controller過程中的異常(403、404、500、入?yún)Ⅱ?yàn)證不通過)等,只能通過@ControllerAdvice
處理!
CatServer組件中內(nèi)置了CatControllerAssist
異常處理類,可以通過cat-server.controller-assist.enable=false
關(guān)閉;
如果feign-interface的實(shí)現(xiàn)類,被其他類繼承了,并且該子類上也存在@CatServer
注解,那么Http請(qǐng)求會(huì)指向子類的方法!
cat-face 模塊
把cat-client和cat-server結(jié)合使用,好像就可以實(shí)現(xiàn)最開始提出的「客戶端、服務(wù)端用過Interface耦合實(shí)現(xiàn)無感知調(diào)用」?答案是,也不完全是!是「如是」.jpg
客戶端與服務(wù)端共享feign-interface、入?yún)⒑头祷貙?duì)象的數(shù)據(jù)類型。其中客戶端發(fā)起Http請(qǐng)求的url,是通過feign-interface方法上的@CatClient注解獲取,服務(wù)端注冊(cè)Controller的url,也是通過@CatClient注解獲取,也就是說@CatClient#value()無論返回什么值,客戶端總能找到對(duì)應(yīng)的服務(wù)端!同樣的,Http請(qǐng)求方式也是如此。
既然如此,何不固定請(qǐng)求方式為POST、url通過feign-interface的特征值自動(dòng)生成,那豈不是可以省下@CatClient注解不用寫了?
例如url生成規(guī)則:自定義命名空間 + feign-interface組件別名 + 方法名;
至于feign-interface方法入?yún)ⅲD(zhuǎn)Http請(qǐng)求報(bào)文這部分比較麻煩。POST請(qǐng)求只能傳輸form表單對(duì)象、或IO流,考慮到方法入?yún)⒌膹?fù)雜性,因此有2種轉(zhuǎn)換方案:
-
在客戶端,將方法的每個(gè)入?yún)ⅲ冀y(tǒng)一轉(zhuǎn)成字符串,然后使用“入?yún)⒚?= 字符串” 組成form表單對(duì)象。在服務(wù)端生成cglib-controller時(shí),Http入?yún)⑷渴褂米址邮?,然后再逐個(gè)轉(zhuǎn)成實(shí)際數(shù)據(jù)類型,并驗(yàn)證入?yún)⒂行裕?/p>
-
在客戶端,將方法的入?yún)?,組合成一個(gè)“入?yún)⒚? 入?yún)?duì)象” 的Map,再將Map序列化成一個(gè)Json字符串;在服務(wù)端生成cglib-controller時(shí),將原入?yún)⒘斜恚D(zhuǎn)成一個(gè)虛擬的入?yún)?duì)象,入?yún)?duì)象的屬性,就是原入?yún)⒚?;這樣Http請(qǐng)求的Json,可以直接轉(zhuǎn)成虛擬入?yún)?duì)象,并自動(dòng)執(zhí)行入?yún)Ⅱ?yàn)證框架;
但是由于Interface在編譯成class字節(jié)碼之后,參數(shù)名會(huì)被擦除(可以使用@CatNote為參數(shù)取別名),實(shí)際上的參數(shù)名應(yīng)該是:arg0、arg1、…、argX;故:
? ? ? 方案1示意:
UserInfo param8(@ApiParam("參數(shù)map") Map<String, Object> map,
@ApiParam("參數(shù)vi1") @Valid UserPageVi vi1,
@ApiParam("參數(shù)vi2") UserPageVi vi2,
@ApiParam("參數(shù)status") @NotNull(message = "status 不能為空") @CatNote("status") Boolean status,
@ApiParam("參數(shù)vi3") @Valid ResponseEntity<PageInfo<UserPageVi>> vi3);
/**
* 最后Http請(qǐng)求格式為
* url:/feign-interface別名/param8
* query:
* arg0="{\"mapKey\":\"mapValue\"}"
* arg1="{\"name\":\"vi1's name\"}"
* arg2="{\"label\":\"vi2's label\"}"
* arg3="false"
* arg4={\"errCode\":\"1", \"data\":"{\"total\": \"12\", \"list\":"[{\"qname\":\"vi3\"}]"}"}"
* */
? ? ? 方案2示例:
UserInfo param8(@ApiParam("參數(shù)map") Map<String, Object> map,
@ApiParam("參數(shù)vi1") @Valid UserPageVi vi1,
@ApiParam("參數(shù)vi2") UserPageVi vi2,
@ApiParam("參數(shù)status") @NotNull(message = "status 不能為空") @CatNote("status") Boolean status,
@ApiParam("參數(shù)vi3") @Valid ResponseEntity<PageInfo<UserPageVi>> vi3);
/**
* 最后服務(wù)端cglib-controller
* url:/feign-interface別名/param8
* class Virtual {
* private Map<String, Object> arg0;
* @Valid private UserPageVi vi1;
* private UserPageVi vi2;
* private Boolean status;
* @Valid private ResponseEntity<PageInfo<UserPageVi>> arg4;
* }
* UserInfo param8(@Valid @RequestBody Virtual virtual);
* */
第1種方案,實(shí)現(xiàn)起來比較容易。缺點(diǎn)是:記錄入?yún)⑷罩緯r(shí),入?yún)⑷渴亲址诖蛴〉臅r(shí)候會(huì)出現(xiàn)引號(hào)轉(zhuǎn)義;服務(wù)端Controller的入?yún)⒍际亲址?,swagger生成的接口文檔沒有詳細(xì)的字段說明,不夠友好;
第2種方案,實(shí)現(xiàn)起來比較麻煩。不存在方案1的缺點(diǎn),但是由于在服務(wù)端生成一個(gè)虛擬的入?yún)?duì)象,因此在feign-interface中不能出現(xiàn)方法重載!
catface中主要使用方案2
轉(zhuǎn)換參數(shù);
@Catface
標(biāo)記feign-interface為catface模式;表示將feign-interface中的所有方法都注冊(cè)成客戶端API,方法上、方法入?yún)⑸峡梢詻]有任何注解!
在客戶端,方法上的入?yún)⒘斜?,?huì)先轉(zhuǎn)換成Map,Map鍵為arg0
~argX
按順序自動(dòng)生成,值為入?yún)?duì)象;然后再轉(zhuǎn)換成Json字符串,POST + Json方式發(fā)起Http請(qǐng)求。請(qǐng)求的url為:配置的命名空間 + feign-interface別名 + 方法名,因此,這需要feign-interface中的方法名不能相同,即不能存在重載方法!
在服務(wù)端,會(huì)為每個(gè)方法自動(dòng)生成一個(gè)虛擬入?yún)?duì)象,方法入?yún)?huì)轉(zhuǎn)換成虛擬入?yún)?duì)象的屬性;這樣Http入?yún)son字符串,可以直接轉(zhuǎn)換成方法入?yún)?duì)應(yīng)的數(shù)據(jù)類型;
- value: feign-interface別名;默認(rèn)是首字母小寫。
- namespace: 命名空間;統(tǒng)一的url前綴,默認(rèn)空;
最終生成的url為:[/命名空間]/feign-interface別名/方法名
@CatNotes
在catface模式下,為feign-interface方法添加自定義標(biāo)記;
- value: 自定義標(biāo)記;
-
scope: 自定義標(biāo)記適用范圍:
:: All: 適用于客戶端、服務(wù)端;
:: Cilent: 標(biāo)記僅在作為客戶端使用時(shí)生效;
:: Server: 標(biāo)記僅在作為服務(wù)端使用時(shí)生效;
最后feign-interface可以簡(jiǎn)化成如下形式:
//@Api、@ApiOperation、@ApiParam是swagger框架的注解,如果沒有這方面需求,可以刪除;
//@NotBlank、@NotNull、@Valid、@Validated是springMVC驗(yàn)證框架注解;
@Api(tags = "Catface - 精簡(jiǎn)模式")
@Catface
@CatResponesWrapper(ResponseEntityWrapper.class)
public interface FaceDemoService{
UserInfo queryById(@NotBlank(message = "userId不能為空") String userId);
@ApiOperation("api - param2")
ResponseEntity<UserInfo> enable(String userId, Integer status);
@CatNotes(value = {@CatNote(key = "uname", value = "#{user.name}")}, scope = CatNotes.Scope.Cilent)
@CatNotes(value = {@CatNote(key = "uid", value = "#{user.id}")}, scope = CatNotes.Scope.Server)
UserPageVi query(@CatNote("user") UserInfo vi);
PageInfo<UserPageVi> queryByBean(String userId, UserInfo vi, @CatNote("isStatus") Boolean status);
int param8(@ApiParam("參數(shù)map") Map<String, Object> map,
@ApiParam("參數(shù)vi1") @Valid UserPageVi vi1,
@ApiParam("參數(shù)vi2") UserPageVi vi2,
@ApiParam("參數(shù)status") @NotNull(message = "status 不能為空") @CatNote("status") Boolean status,
@ApiParam("參數(shù)vi3") @Valid ResponseEntity<PageInfo<UserPageVi>> vi3);
default void dosomething(@ApiParam("參數(shù)map") Map<String, Object> map,
@ApiParam("參數(shù)vi1") @Validated UserPageVi vi1,
@ApiParam("參數(shù)date") Date date,
@ApiParam("參數(shù)status") Integer status,
@ApiParam("參數(shù)decimal") BigDecimal decimal,
@ApiParam("參數(shù)vi3") @Valid ResponseEntity<PageInfo<UserPageVi>> vi3) {
CatClientContextHolder holder = CatClientContextHolder.getContextHolder();
Throwable exception = holder.getException();
System.out.println("異常:" + exception.getMessage());
return null;
}
}
其他說明
除了feign-interface中方法不能重載,還要注意一點(diǎn)的是:如果在生產(chǎn)環(huán)境上迭代升級(jí)feign-interface,假設(shè)將FaceDemoService#dosomething方法入?yún)⒂性鰷p,無論是先更新客戶端、還是先更新服務(wù)端,都會(huì)造成該API接口參數(shù)接收會(huì)錯(cuò)位!
一般這種情況,可以事先給入?yún)⑷e名,這樣在接收入?yún)r(shí),會(huì)根據(jù)參數(shù)名匹配,而不是參數(shù)順序;或者采用面向?qū)ο箝_發(fā),保持方法入?yún)⑸现挥幸粋€(gè)入?yún)?duì)象,增減參數(shù)數(shù)量,轉(zhuǎn)換成增減對(duì)象屬性多少的問題。文章來源:http://www.zghlxwxcb.cn/news/detail-697520.html
文章來源地址http://www.zghlxwxcb.cn/news/detail-697520.html
到了這里,關(guān)于catface,使用Interface定義Controller,實(shí)現(xiàn)基于Http協(xié)議的RPC調(diào)用的文章就介紹完了。如果您還想了解更多內(nèi)容,請(qǐng)?jiān)谟疑辖撬阉鱐OY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!