擼了一個 Feign 增強包 V2.0 升級版

前言

大概在兩年前我寫過一篇 擼了一個 Feign 增強包,當時準備是利用 SpringBoot + K8s 構建應用衙四,這個庫可以類似于 SpringCloud 那樣結合 SpringBoot 使用聲明式接口來達到服務間通訊的目的此洲。

但后期由于技術棧發(fā)生變化(改為 Go)港准,導致該項目只實現了基本需求后就擱置了鹃愤。

巧合的時最近內部有部分項目又計劃采用 SpringBoot + K8s 開發(fā)暴凑,于是便著手繼續(xù)維護凛篙;現已經內部迭代了幾個版本比較穩(wěn)定了黍匾,也增加了一些實用功能,在此分享給大家呛梆。

https://github.com/crossoverJie/feign-plus

首先是新增了一些 features:

  • 更加統(tǒng)一的 API锐涯。
  • 統(tǒng)一的請求、響應填物、異常日志記錄纹腌。
  • 自定義攔截器。
  • Metric 支持滞磺。
  • 異常傳遞壶笼。

示例

結合上面提到的一些特性做一些簡單介紹,統(tǒng)一的 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 來模擬調用:
[圖片上傳失敗...(image-fde8d-1651814139759)]

provider: 作為服務提供者提供了一系列接口供消費方調用坤候,并對外提供了一個 api 模塊。
[圖片上傳失敗...(image-f19e3c-1651814139759)]


demo:作為服務消費者依賴 provider-api 模塊企蹭,根據其中聲明的接口進行遠程調用白筹。
[圖片上傳失敗...(image-7fa75f-1651814139759)]
配置文件:

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 接口時從控制臺可以看到調用結果:
[圖片上傳失敗...(image-c4c0d6-1651814139759)]

日志記錄

從上圖中可以看出 feign-plus 會用 debug 記錄請求/響應結果智末,如果需要打印出來時需要將該包下的日志級別調整為 debug:

logging:
  level:
    top:
      crossoverjie: debug

由于內置了攔截器,也可以自己繼承 top.crossoverjie.feign.plus.log.DefaultLogInterceptor 來實現自己的日志攔截記錄徒河,或者其他業(yè)務邏輯系馆。

@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");
    }
}

監(jiān)控 metric

feign-plus 會自行記錄每個接口之間的調用耗時、異常等情況顽照。
[圖片上傳失敗...(image-ee3ae8-1651814139759)]
訪問 http://127.0.0.1:8181/actuator/prometheus 會看到相關埋點信息由蘑,通過 feign_call* 的 key 可以自行在 Grafana 配置相關面板,類似于下圖:
[圖片上傳失敗...(image-4571a6-1651814139759)]

異常傳遞

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 中實現該功能需要幾個步驟:

  1. 自定義一個通用異常裳擎。
  2. 服務提供方需要實現一個全局攔截器,當發(fā)生異常時統(tǒng)一對外響應數據思币。
  3. 服務消費方需要自定義一個異常解碼器的 bean句惯。

這里我在 provider 中自定義了一個 DemoException
[圖片上傳失敗...(image-85d5ee-1651814139759)]

通常這個類應該定義在公司內部的通用包中,這里為了演示方便支救。

接著定義了一個 HttpStatus 的類用于統(tǒng)一對外響應抢野。

@Data
@AllArgsConstructor
@NoArgsConstructor
public class HttpStatus {
    private String appName;
    private int code;
    private String message;
    private String debugStackTrace;
}

這個也應該放在通用包中。

然后在 provider 中定義全局異常處理:
[圖片上傳失敗...(image-69ee11-1651814139759)]

當出現異常時便會返回一個 http_code=500 的數據:
[圖片上傳失敗...(image-6005c5-1651814139759)]

到這一步又會出現一個引戰(zhàn)話題:HTTP 接口返回到底是全部返回 200 然后通過 code 來來判斷各墨,還是參考 http_code 進行返回?

這里不做過多討論指孤,具體可以參考耗子叔的文章:
“一把梭:REST API 全用 POST”

feign-plus 默認采用的 http_code !=200 才會認為發(fā)生了異常。

而這里的 http_status 也是參考了 Google 的 api 設計:
[圖片上傳失敗...(image-320f34-1651814139759)]
具體可以參考這個鏈接:
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());
        };
    }
}

通常這塊代碼也是放在基礎包中贬堵。


這樣當服務提供方拋出異常時恃轩,消費者便能成功拿到該異常:
[圖片上傳失敗...(image-8d5fcf-1651814139759)]

實現原理

實現原理其實也比較簡單,了解 rpc 原理的話應該會知道黎做,服務提供者返回的異常調用方是不可能接收到的叉跛,這和是否由一種語言實現也沒關系。

畢竟兩個進程之間的棧是完全不同的蒸殿,不在一臺服務器上筷厘,甚至都不在一個地區(qū)。

所以 provider 拋出異常后宏所,消費者只能拿到一串報文酥艳,我們只能根據這段報文解析出其中的異常信息,然后再重新創(chuàng)建一個內部自定義的異常(比如這里的 DemoException)爬骤,也就是我們自定義異常解析器所干的事情充石。

下圖就是這個異常傳遞的大致流程:
[圖片上傳失敗...(image-ad53fe-1651814139759)]

code message 模式

由于 feign-plus 默認是采用 http_code != 200 的方式來拋出異常的,所以采用 http_code=200, code message 的方式響應數據將不會傳遞異常霞玄,依然會任務是一次正常調用骤铃。

不過基于該模式傳遞異常也是可以實現的拉岁,但沒法做到統(tǒng)一,比如有些團隊習慣 code !=0 表示異常惰爬,甚至字段都不是 code喊暖;再或者異常信息有些是放在 message 或 msg 字段中。

每個團隊补鼻、個人習慣都不相同哄啄,所以沒法抽象出一個標準雅任,因此也就沒做相關適配风范。

這也印證了使用國際標準所帶來的好處。

限于篇幅沪么,如果有相關需求的朋友也可以在評論區(qū)溝通硼婿,實現上會比現在稍微復雜一點點????。

總結

項目源碼:
https://github.com/crossoverJie/feign-plus

基于2022年云原生這個背景禽车,當然更推薦大家使用 gRPC 來做服務間通信寇漫,這樣也不需要維護類似于這樣的庫了。

不過在一些調用第三方接口而對方也沒有提供 SDK 時殉摔,這個庫也有一定用武之地州胳,雖然使用原生 feign 也能達到相同目的,但使用該庫可以使得與 Spring 開發(fā)體驗一致逸月,同時內置了日志栓撞、metric 等功能,避免了重復開發(fā)碗硬。

你的點贊與分享是對我最大的支持

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末瓤湘,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子恩尾,更是在濱河造成了極大的恐慌弛说,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件翰意,死亡現場離奇詭異木人,居然都是意外死亡,警方通過查閱死者的電腦和手機冀偶,發(fā)現死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進店門虎囚,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人蔫磨,你說我怎么就攤上這事淘讥。” “怎么了堤如?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵蒲列,是天一觀的道長窒朋。 經常有香客問我,道長蝗岖,這世上最難降的妖魔是什么侥猩? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮抵赢,結果婚禮上欺劳,老公的妹妹穿的比我還像新娘。我一直安慰自己铅鲤,他們只是感情好划提,可當我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著邢享,像睡著了一般鹏往。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上骇塘,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天伊履,我揣著相機與錄音,去河邊找鬼款违。 笑死唐瀑,一個胖子當著我的面吹牛,可吹牛的內容都是我干的插爹。 我是一名探鬼主播哄辣,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼递惋!你這毒婦竟也來了柔滔?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤萍虽,失蹤者是張志新(化名)和其女友劉穎睛廊,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體杉编,經...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡超全,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了邓馒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘶朱。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖光酣,靈堂內的尸體忽然破棺而出疏遏,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布财异,位于F島的核電站倘零,受9級特大地震影響,放射性物質發(fā)生泄漏戳寸。R本人自食惡果不足惜呈驶,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望疫鹊。 院中可真熱鬧袖瞻,春花似錦、人聲如沸拆吆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽锈拨。三九已至砌庄,卻和暖如春羹唠,著一層夾襖步出監(jiān)牢的瞬間奕枢,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工佩微, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留缝彬,地道東北人。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓哺眯,卻偏偏與公主長得像谷浅,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子奶卓,可洞房花燭夜當晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內容