又解鎖了一種OpenFeign的使用方式!
Hello 大家好,這里是Anyin。
在OpenFeign
在某些場景下的一些處理和使用方法,而今天Anyin再次解鎖了OpenFeign
的又一個使用場景,只能說真香。
在我們?nèi)粘i_發(fā)中,相信大家都會接觸過對接第三方系統(tǒng)。對接第三方系統(tǒng)最煩人的工作可能就是剛開始對接的時候關(guān)于認(rèn)證、加密、驗簽、JSON正反序列化等一系列的操作了。
我們知道OpenFeign
它其實是一個http的客戶端,主要的應(yīng)用場景就是在微服務(wù)體系內(nèi)進(jìn)行微服務(wù)之間的相互調(diào)用;那么它是不是也可以實現(xiàn)第三方調(diào)用? ?
很明顯是可以的!??!
需求分析
在驗證我們的觀點:OpenFeign
可以實現(xiàn)第三方系統(tǒng)的調(diào)用之前,我們先找一個公開的第三方系統(tǒng)協(xié)議進(jìn)行一波簡單的需求分析吧。
這里我們使用中電聯(lián)(中國電力企業(yè)聯(lián)合標(biāo)準(zhǔn))的協(xié)議文檔為例。這里附上下載地址,有需要的同學(xué)可以自取。
以下為協(xié)議文檔對于密鑰的要求。

通過查看協(xié)議文檔,我們知道整個對接過程會設(shè)計到以下幾個需求:
調(diào)用方式統(tǒng)一使用POST方式
傳輸格式使用JSON
傳輸過程業(yè)務(wù)數(shù)據(jù)需要進(jìn)行加密
傳輸過程整包數(shù)據(jù)需要生成簽名,因為服務(wù)端會進(jìn)行驗簽,保證數(shù)據(jù)沒有被篡改
在進(jìn)行第三方調(diào)用的時候需要像調(diào)用其他本地的
Service
一樣絲滑(行為一致)
業(yè)務(wù)實現(xiàn)
為了通過OpenFeign
實現(xiàn)以上需求,我們首先定義一個配置類,用于自定義客戶端的配置類。
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CECOperatorProperties.class)
public class CECFeignClientConfig implements RequestInterceptor {
? ?@Autowired
? ?private CECOperatorProperties properties;
? ?@Override
? ?public void apply(RequestTemplate requestTemplate) {} ? ?
}
實現(xiàn)
RequestInterceptor
接口,這里是為了在進(jìn)行認(rèn)證拿到access_token
之后,可以通過攔截器在header頭放入對應(yīng)的token信息注入
CECOperatorProperties
屬性,對于加解密、驗簽等操作需要的一些秘鑰信息,從配置中心獲取后,注入該屬性類中@Configuration(proxyBeanMethods = false)
配置該類配置類,并且不會在RootApplicationContext當(dāng)中注冊,只會在使用的時候才會進(jìn)行相關(guān)配置。
這里注意哈,在這個類配置的@Bean
實例,只有在當(dāng)前的FeignClient
實例的ApplicaitonContext當(dāng)中可以訪問到,其他地方訪問不到。具體可以看
接著,我們需要2個基本的數(shù)據(jù)傳輸對象:Request
和 Response
@Data
public class CECRequest<T> {
? ?@JsonProperty("OperatorID")
? ?private String operatorID;
? ?@JsonProperty("Data")
? ?private T data;
? ?@JsonProperty("TimeStamp")
? ?private String timeStamp;
? ?@JsonProperty("Seq")
? ?private String seq;
? ?@JsonProperty("Sig")
? ?private String sig;
}
@Data
public class CECResponse<T> {
? ?private Integer Ret;
? ?private T Data;
? ?private String Msg;
}
這里使用@JsonProperty
的原因是協(xié)議文檔字段的首字母都是大寫的,而我們一般的Java字段都是駝峰,為了在進(jìn)行JSON轉(zhuǎn)換的時候避免無法正常轉(zhuǎn)換。
然后,我們開始自定義編解碼器。這里不得不推薦下
這個類庫,是真的強大,因為涉及到的加解密和簽名生成,都是現(xiàn)成的。真香?。?!編碼器
@Slf4j
public class CECEncoder extends SpringEncoder {
? ?private final CECOperatorProperties properties;
? ?private final HMac mac;
? ?private final AES aes;
? ?public CECEncoder(ObjectFactory<HttpMessageConverters> messageConverters,
? ? ? ? ? ? ? ? ? ? ?CECOperatorProperties properties) {
? ? ? ?super(messageConverters);
? ? ? ?this.properties = properties;
? ? ? ?this.mac = new HMac(HmacAlgorithm.HmacMD5,
? ? ? ? ? ? ? ?properties.getSigSecret().getBytes(StandardCharsets.UTF_8));
? ? ? ?this.aes = new AES(Mode.CBC, Padding.PKCS5Padding,
? ? ? ? ? ? ? ?properties.getDataSecret().getBytes(),
? ? ? ? ? ? ? ?properties.getDataIv().getBytes());
? ?}
? ?@Override
? ?public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
? ? ? ?// 數(shù)據(jù)加密
? ? ? ?String data = this.getEncrypt(requestBody);
? ? ? ?CECRequest<String> req = new CECRequest<>();
? ? ? ?req.setData(data);
? ? ? ?req.setSeq("0001");
? ? ? ?req.setTimeStamp(DateUtil.formatDate(DateUtil.now(), DateEnum.YYYYMMDDHHMMSS));
? ? ? ?req.setOperatorID(properties.getOperatorID());
? ? ? ?// 簽名計算
? ? ? ?String sig = this.getSig(req);
? ? ? ?req.setSig(sig.toUpperCase());
? ? ? ?super.encode(req, CECRequest.class.getGenericSuperclass(), request);
? ?}
? ?private String getEncrypt(Object requestBody){
? ? ? ?String json = JsonUtil.toJson(requestBody);
? ? ? ?return Base64.encode(aes.encrypt(json.getBytes()));
? ?}
? ?private String getSig(CECRequest<String> req){
? ? ? ?String str = req.getOperatorID() + req.getData() + req.getTimeStamp() + req.getSeq();
? ? ? ?return mac.digestHex(str);
? ?}
}
可以看到,我們的編碼器其實是繼承了SpringEncoder
,因為在最終編碼之后,還是需要轉(zhuǎn)換為JSON發(fā)送給服務(wù)端,所以在繼承SpringEncoder
之后,構(gòu)造器還需要注入ObjectFactory<HttpMessageConverters>
的實例。另外,在構(gòu)造器我們也初始化了HMac
和AES
兩個實例,一個為了生成簽名,一個為了加密業(yè)務(wù)數(shù)據(jù)。
在encode
方法,我們把傳遞進(jìn)來的requestBody
包裝了下,先對其進(jìn)行加密,然后放在CECRequest
實例的data字段內(nèi),并且生成對應(yīng)的簽名,最終請求服務(wù)端的時候是一個CECRequest
實例的JSON化的結(jié)果。
可能有人會疑惑,為什么這里的requestBody
就直接是業(yè)務(wù)數(shù)據(jù)了,而不是CECRequest<T>
實例? 想想我們的第5點需求:在進(jìn)行第三方調(diào)用的時候需要像調(diào)用其他本地的Service
一樣絲滑(行為一致)。為了實現(xiàn)這個需求,我們不會把非業(yè)務(wù)的參數(shù)暴露給業(yè)務(wù)調(diào)用放,而是在編解碼的過程中進(jìn)行處理。
解碼器
@Slf4j
public class CECDecoder extends SpringDecoder {
? ?private final AES aes;
? ?public CECDecoder(ObjectFactory<HttpMessageConverters> messageConverters,
? ? ? ? ? ? ? ? ? ? ?CECOperatorProperties properties) {
? ? ? ?super(messageConverters);
? ? ? ?this.aes = new AES(Mode.CBC, Padding.PKCS5Padding,
? ? ? ? ? ? ? ?properties.getDataSecret().getBytes(),
? ? ? ? ? ? ? ?properties.getDataIv().getBytes());
? ?}
? ?@Override
? ?public Object decode(Response response, Type type) throws IOException, FeignException {
? ? ? ?CECResponse<String> resp = this.getCECResponse(response);
? ? ? ?// TODO 應(yīng)該做對應(yīng)的異常判斷然后拋出異常
? ? ? ?String json = this.aes.decryptStr(resp.getData());
? ? ? ?Response newResp = response.toBuilder().body(json, StandardCharsets.UTF_8).build();
? ? ? ?return super.decode(newResp, type);
? ?}
? ?private CECResponse<String> getCECResponse(Response response) throws IOException{
? ? ? ?try (InputStream inputStream = response.body().asInputStream()) {
? ? ? ? ? ?String json = StreamUtils.copyToString(inputStream, StandardCharsets.UTF_8);
? ? ? ? ? ?TypeReference<CECResponse<String>> reference = new TypeReference<CECResponse<String>>() {};
? ? ? ? ? ?return JSONUtil.toBean(json, reference.getType(), true);
? ? ? ?}
? ?}
}
解碼器會比較簡單,只需要進(jìn)行數(shù)據(jù)的解密即可。所以我們從Response
中拿到對應(yīng)的JSON字符串,然后通過反序列化拿到CECResponse
實例,接著做對應(yīng)的異常判斷(這里我的代碼暫時未實現(xiàn)),然后再做數(shù)據(jù)的解碼,拿到真正的業(yè)務(wù)數(shù)據(jù)的JSON字符串,最后通過OpenFeign
提供的toBuilder
方法重新構(gòu)造一個新的Response
實例交給SpringDecoder
進(jìn)行下一步的處理。
下一步,我們把編解碼器注冊到配置類中。完整的配置類信息如下
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(CECOperatorProperties.class)
public class CECFeignClientConfig implements RequestInterceptor {
? ?@Autowired
? ?private CECOperatorProperties properties;
? ?@Autowired
? ?private ObjectFactory<HttpMessageConverters> messageConverters;
? ?@Bean
? ?Logger.Level feignLoggerLevel() {
? ? ? ?return Logger.Level.FULL;
? ?}
? ?@Bean
? ?public Encoder encoder(){
? ? ? ?return new CECEncoder(messageConverters, properties);
? ?}
? ?@Bean
? ?public Decoder decoder(){
? ? ? ?return new CECDecoder(messageConverters, properties);
? ?}
? ?@Override
? ?public void apply(RequestTemplate requestTemplate) {
? ? ? ?// TODO 添加Token
? ?}
}
完整的配置類會注入從RootApplicationContext中拿到的ObjectFactory<HttpMessageConverters>
實例,另外再多配置了一個日志實例Logger.Level
,用于在debug的時候打印請求的具體日志。
最后,我們來測試下我們的程序是否正常。簡單測試用例如下:
@Slf4j
public class CECTest extends BaseTest{
? ?@Autowired
? ?private CECTokenService tokenService;
? ?@Autowired
? ?private CECStationService stationService;
? ?@Autowired
? ?private CECOperatorProperties properties;
? ?@Test
? ?public void test(){
? ? ? ?QueryTokenReq req = new QueryTokenReq();
? ? ? ?req.setOperatorID(properties.getOperatorID());
? ? ? ?req.setOperatorSecret(properties.getOperatorSecret());
? ? ? ?QueryTokenResp resp = tokenService.queryToken(req);
? ? ? ?log.info("resp: {}", JsonUtil.toJson(resp));
? ?}
}
看到吧,是不是和調(diào)用本地的Service
一樣絲滑? 只需要構(gòu)造對應(yīng)的入?yún)?,即可返回對?yīng)的出參,無需關(guān)心加密、簽名等煩人的操作。相關(guān)日志如下:

最后
對于使用OpenFeign
最后 Hutool