前言
大概在兩年前我寫過一篇 擼了一個 Feign 增強包,當時準備是利用 SpringBoot + K8s
構建應用,這個庫可以類似於 SpringCloud
那樣結合 SpringBoot
使用宣告式介面來達到服務間通訊的目的。
但後期由於技術棧發生變化(改為 Go),導致該專案只實現了基本需求後就擱置了。
巧合的時最近內部有部分專案又計劃採用 SpringBoot + K8s
開發,於是便著手繼續維護;現已經內部迭代了幾個版本比較穩定了,也增加了一些實用功能,在此分享給大家。
https://github.com/crossoverJie/feign-plus
首先是新增了一些 features
:
- 更加統一的 API。
- 統一的請求、響應、異常日誌記錄。
- 自定義攔截器。
- Metric 支援。
- 異常傳遞。
示例
結合上面提到的一些特性做一些簡單介紹,統一的 API 主要是在使用層面:
在上一個版本中宣告介面如下:
@FeignPlusClient(name = "github", url = "${github.url}")
public interface Github {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<GitHubRes> contributors(@Param("owner") String owner, @Param("repo") String repo);
}
其中的 @RequestLine
等註解都是使用 feign 包所提供的。
這次更新後改為如下方式:
@RequestMapping("/v1/demo")
@FeignPlusClient(name = "demo", url = "${feign.demo.url}", port = "${feign.demo.port}")
public interface DemoApi {
@GetMapping("/id")
String sayHello(@RequestParam(value = "id") Long id);
@GetMapping("/id/{id}")
String id(@PathVariable(value = "id") Long id);
@PostMapping("/create")
Order create(@RequestBody OrderCreateReq req);
@GetMapping("/query")
Order query(@SpringQueryMap OrderQueryDTO dto);
}
熟悉的味道,基本都是 Spring
自帶的註解,這樣在使用上學習成本更低,同時與專案中原本的介面寫法保持一致。
@SpringQueryMap(top.crossoverjie.feign.plus.contract.SpringQueryMap) 是由 feign-plus 提供,其實就是從 SpringCloud 中 copy 過來的。
我這裡寫了兩個 demo 來模擬呼叫:
provider
: 作為服務提供者提供了一系列介面供消費方呼叫,並對外提供了一個 api 模組。
demo
:作為服務消費者依賴 provider-api
模組,根據其中宣告的介面進行遠端呼叫。
配置檔案:
server:
port: 8181
feign:
demo:
url : http://127.0.0.1
port: 8080
logging:
level:
top:
crossoverjie: debug
management:
endpoints:
web:
base-path: /actuator
exposure:
include: '*'
metrics:
distribution:
percentiles:
all: 0.5,0.75,0.95,0.99
export:
prometheus:
enabled: true
step: 1m
spring:
application:
name: demo
當我們訪問 http://127.0.0.1:8181/hello/2
介面時從控制檯可以看到呼叫結果:
日誌記錄
從上圖中可以看出 feign-plus
會用 debug 記錄請求/響應結果,如果需要列印出來時需要將該包下的日誌級別調整為 debug:
logging:
level:
top:
crossoverjie: debug
由於內建了攔截器,也可以自己繼承 top.crossoverjie.feign.plus.log.DefaultLogInterceptor
來實現自己的日誌攔截記錄,或者其他業務邏輯。
@Component
@Slf4j
public class CustomFeignInterceptor extends DefaultLogInterceptor {
@Override
public void request(String target, String url, String body) {
super.request(target, url, body);
log.info("request");
}
@Override
public void exception(String target, String url, FeignException feignException) {
super.exception(target, url, feignException);
}
@Override
public void response(String target, String url, Object response) {
super.response(target, url, response);
log.info("response");
}
}
監控 metric
feign-plus
會自行記錄每個介面之間的呼叫耗時、異常等情況。
訪問 http://127.0.0.1:8181/actuator/prometheus
會看到相關埋點資訊,通過 feign_call*
的 key 可以自行在 Grafana
配置相關皮膚,類似於下圖:
異常傳遞
rpc
(遠端呼叫)要使用起來真的類似於本地呼叫,異常傳遞必不可少。
// provider
public Order query(OrderQueryDTO dto) {
log.info("dto = {}", dto);
if (dto.getId().equals("1")) {
throw new DemoException("provider test exception");
}
return new Order(dto.getId());
}
// consumer
try {
demoApi.query(new OrderQueryDTO(id, "zhangsan"));
} catch (DemoException e) {
log.error("feignCall:{}, sourceApp:[{}], sourceStackTrace:{}", e.getMessage(), e.getAppName(), e.getDebugStackTrace(), e);
}
比如 provider
中丟擲了一個自定義的異常,在 consumer
中可以通過 try/catch
捕獲到該異常。
為了在 feign-plus 中實現該功能需要幾個步驟:
- 自定義一個通用異常。
- 服務提供方需要實現一個全域性攔截器,當發生異常時統一對外響應資料。
- 服務消費方需要自定義一個異常解碼器的 bean。
這裡我在 provider
中自定義了一個 DemoException
:
通常這個類應該定義在公司內部的通用包中,這裡為了演示方便。
接著定義了一個 HttpStatus
的類用於統一對外響應。
@Data
@AllArgsConstructor
@NoArgsConstructor
public class HttpStatus {
private String appName;
private int code;
private String message;
private String debugStackTrace;
}
這個也應該放在通用包中。
然後在 provider
中定義全域性異常處理:
當出現異常時便會返回一個 http_code=500 的資料:
到這一步又會出現一個引戰話題:HTTP 介面返回到底是全部返回 200 然後通過 code 來來判斷,還是參考 http_code 進行返回?
這裡不做過多討論,具體可以參考耗子叔的文章:
“一把梭:REST API 全用 POST”
feign-plus
預設採用的 http_code !=200 才會認為發生了異常。
而這裡的 http_status 也是參考了 Google 的 api 設計:
具體可以參考這個連結:
https://cloud.google.com/apis/design/errors#propagating_errors
然後定義一個異常解析器:
@Configuration
public class FeignExceptionConfig {
@Bean
public FeignErrorDecoder feignExceptionDecoder() {
return (methodName, response, e) -> {
HttpStatus status = JSONUtil.toBean(response, HttpStatus.class);
return new DemoException(status.getAppName(), status.getCode(), status.getMessage(), status.getDebugStackTrace());
};
}
}
通常這塊程式碼也是放在基礎包中。
這樣當服務提供方丟擲異常時,消費者便能成功拿到該異常:
實現原理
實現原理其實也比較簡單,瞭解 rpc
原理的話應該會知道,服務提供者返回的異常呼叫方是不可能接收到的,這和是否由一種語言實現也沒關係。
畢竟兩個程式之間的棧是完全不同的,不在一臺伺服器上,甚至都不在一個地區。
所以 provider
丟擲異常後,消費者只能拿到一串報文,我們只能根據這段報文解析出其中的異常資訊,然後再重新建立一個內部自定義的異常(比如這裡的 DemoException
),也就是我們自定義異常解析器所幹的事情。
下圖就是這個異常傳遞的大致流程:
code message 模式
由於 feign-plus 預設是採用 http_code != 200
的方式來丟擲異常的,所以採用 http_code=200, code message
的方式響應資料將不會傳遞異常,依然會任務是一次正常呼叫。
不過基於該模式傳遞異常也是可以實現的,但沒法做到統一,比如有些團隊習慣 code !=0
表示異常,甚至欄位都不是 code;再或者異常資訊有些是放在 message 或 msg 欄位中。
每個團隊、個人習慣都不相同,所以沒法抽象出一個標準,因此也就沒做相關適配。
這也印證了使用國際標準所帶來的好處。
限於篇幅,如果有相關需求的朋友也可以在評論區溝通,實現上會比現在稍微複雜一點點??。
總結
專案原始碼:
https://github.com/crossoverJie/feign-plus
基於2022年雲原生這個背景,當然更推薦大家使用 gRPC
來做服務間通訊,這樣也不需要維護類似於這樣的庫了。
不過在一些呼叫第三方介面而對方也沒有提供 SDK 時,這個庫也有一定用武之地,雖然使用原生 feign 也能達到相同目的,但使用該庫可以使得與 Spring
開發體驗一致,同時內建了日誌、metric
等功能,避免了重複開發。
你的點贊與分享是對我最大的支援