微服務實戰SpringCloud之Spring Cloud Feign替代HTTP Client

誰主沉浮oo7發表於2020-09-07

簡介

在專案中我們有時候需要呼叫第三方的API,微服務架構中這種情況則更是無法避免——各個微服務之間通訊。比如一般的專案中,有時候我們會使用 HTTP Client 傳送 HTTP 請求來進行呼叫,而在微服務架構,Spring Cloud 全家桶中,Spring Cloud Feign 則是更常見的選擇。那麼,我如何只使用 Spring Cloud Feign 而不引入整個 Spring Cloud 呢?

什麼是Feign?

Feign是一個宣告式的Web Service客戶端,它的目的就是讓Web Service呼叫更加簡單。Feign提供了HTTP請求的模板,通過編寫簡單的介面和插入註解,就可以定義好HTTP請求的引數、格式、地址等資訊。

而Feign則會完全代理HTTP請求,我們只需要像呼叫方法一樣呼叫它就可以完成服務請求及相關處理。Feign整合了Ribbon和Hystrix,可以讓我們不再需要顯式地使用這兩個元件。

總起來說,Feign具有如下特性:

  • 可插拔的註解支援,包括Feign註解和JAX-RS註解;
  • 支援可插拔的HTTP編碼器和解碼器;
  • 支援Hystrix和它的Fallback;
  • 支援Ribbon的負載均衡;
  • 支援HTTP請求和響應的壓縮。

這看起來有點像我們springmvc模式的Controller層的RequestMapping對映。這種模式是我們非常喜歡的。Feign是用@FeignClient來對映服務的。

首先找一個AIP

免費的API特別多,github上也有免費API地址彙總的repo,但這些都太正式了。有趣的事物總是會相互吸引的,無意間我發現了這麼一個網站,“渣男:說話的藝術”(lovelive.tools) ,每次請求都可以獲取一句甜言蜜語(渣男語錄),特別良心的是,作者提供了API列表,給作者點贊!

如何呼叫第三方服務?

首先,我們先快速構建一個 Spring Boot 的 web 專案,這裡我就省略了。然後在pom中新增feign的相關依賴

		<dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <version>2.1.3.RELEASE</version>
        </dependency>

然後,在啟動類新增響應的註解 @EnableFeignClients:

@SpringBootApplication
@EnableFeignClients
public class SpringbootMiddlewareFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootMiddlewareFeignApplication.class, args);
    }
}

接著,我們便可以配置我們的 Client 了,我們先建立一個介面類,比如叫BadGuyFeignClient ,並宣告為 FeignClient:

@FeignClient
public interface BadGuyFeignClient {

}

@FeignClient有以下幾個較常用屬性:

屬性名 預設值 作用 備註
value 空字串 呼叫服務名稱,和name屬性相同,如果專案使用了 Ribbon,name屬性會作為微服務的名稱,用於服務發現;
serviceId 空字串 服務id,作用和name屬性相同 已過期
name 空字串 呼叫服務名稱,和value屬性相同,如果專案使用了 Ribbon,name屬性會作為微服務的名稱,用於服務發現;
url 空字串 url一般用於除錯,可以手動指定@FeignClient呼叫的地址
decode404 false 配置響應狀態碼為404時是否應該丟擲FeignExceptions
configuration Feign配置類,可以自定義 Feign的 Encoder、Decoder、LogLevel、Contract; 參考FeignClientsConfiguration
fallback void.class 定義容錯的處理類,當呼叫遠端介面失敗或超時時,會呼叫對應介面的容錯邏輯,fallback指定的類必須實現@FeignClient標記的介面 底層依賴hystrix,啟動類要加上@EnableHystrix
fallbackFactory void.class 工廠類,用於生成fallback類示例,通過這個屬性我們可以實現每個介面通用的容錯邏輯,減少重複的程式碼
path 空字串 自動給所有方法的requestMapping前加上字首,類似與controller類上的requestMapping

然後,我們便可以配置對應的屬性,這裡我們只是用來實現類似於 HTTP Client 的功能,所以只是簡單配置了url和path這些屬性:

@FeignClient(name = "badGuy", url = "${bab.guy.url}", path = "api")
public interface BadGuyFeignClient {

    /**
     * 隨機獲取一句甜言蜜語
     *
     * @return
     */
    @GetMapping("SweetNothings")
    String getSweetNothings();

    /**
     * 獲取 count 條甜言蜜語
     *
     * @param count 獲取甜言蜜語條數
     * @return Json 格式的結果
     */
    @GetMapping("SweetNothings/{count}/Serialization/Json")
    ReturnResult<List<String>> getSweetNothingsJsonByCount(@PathVariable("count") Integer count);

}

宣告為FeignClient之後,我們便可以在程式碼中使用@Resource或者@Autowire進行注入使用了:

@Component
public class BadServiceImpl implements BadGuyService {

    @Autowired
    private BadGuyFeignClient badGuyFeignClient;

    @Override
    public List<String> getQuotations(Integer count) {
        if (count == null || count <= 0) {
            String singleQuotation = badGuyFeignClient.getSweetNothings();
            return new ArrayList<String>() {{
                add(singleQuotation);
            }};
        }
        return badGuyFeignClient.getSweetNothingsJsonByCount(count).getReturnObj();
    }
}

然後Controller中是這麼寫的:

@RestController
@Log4j2
@RequestMapping("/api/badGuy")
public class BadGuyController {

    @Resource
    private BadGuyService badGuyService;

    @GetMapping({"quotations", "quotations/{count}"})
    public PlainResult<List<String>> getBadGuyQuotations(
            @PathVariable(value = "count", required = false) Integer count
    ) {
        PlainResult<List<String>> result = new PlainResult<>();
        try {
            List<String> resultStrings = badGuyService.getQuotations(count);
            result.setData(resultStrings);
        } catch (Exception e) {
            log.error("Failed to get bad guy quotations.", e);
            result.setErrorMessage("error");
        }
        return result;
    }

}

啟動專案之後,我們可以訪問

http://localhost:8080/api/badGuy/quotations

或者

http://localhost:8080/api/badGuy/quotations/10

後面跟數字,即可得到對應條目數的結果

{
    "success":true,
    "code":0,
    "message":"successful",
    "data":[
        "我從未擁有過你一秒鐘,心裡卻失去過你千萬次。",
        "現在幾點了?是我們幸福的起點。",
        "我不很快樂,因為你不很愛我。但所謂不很快樂者,並不等於不快樂,正如不很愛我不等於不愛我一樣。",
        "我看你挺笨的吹口哨都不會,要不要我嘴對嘴教你。",
        "我玩了六年英雄聯盟,後來才發現你才是我的英雄。",
        "這裡一切都醜的,風、雨、太陽,都醜,人也醜,我也醜得很。只有你是青天一樣可羨。",
        "他對她說,他依然愛她,和過去一樣,至死不渝",
        "你猜我喜歡什麼制服” “被你制服”",
        "人說紅顏薄命,你做我的紅顏,我願為你薄命。",
        "有時候我想比你晚出生一百年,你的一生被拍成一部電影,而我一生只做一件事:獨自坐在房間,面對牆上的熒光屏,用我的一生把你的一生看完。"
    ]
}

Feign client 如何設定請求頭資訊?

在呼叫http介面時,一般都需要在請求頭裡面新增相應鑑權引數,類似於ak、sk之類的金鑰或者某個動態引數等,那麼在使用Feign client呼叫時,該如何新增請求頭資訊呢?

準備介面

首先,準備一個第三方服務介面,我這裡直接在rap上定義了一個查詢使用者資訊介面,呼叫該介面必須在請求頭傳一個token引數和請求體中傳userId,postman請求示例如下圖:

如何新增請求有呢,這裡提供兩種方式。

在請求方法上新增請求頭引數

示例程式碼如下:

@FeignClient(name = "apiClient", url = "${test.url}", path = "api")
public interface BasicApiClient {
    /**
     * 查詢使用者資訊介面-第一種方式
     *
     * @param userId
     * @param token
     * @return
     */
    @GetMapping(value = "/queryUser")
    PlainResult<User> queryUser(@RequestParam("userId") String userId,
                                @RequestHeader(name = "token") String token);

}

注意queryUser方法中多了一個@RequestHeader引數token,這個就相當於往請求頭新增了一個token引數,這種情況適用於該token是一個在業務中經常動態變化的引數,需要在介面呼叫方動態獲取。

利用@FeignClient的configuration屬性

新建一個配置類如下

public class ClientConfiguration {

    @Value("${test.token}")
    private String token;

    @Bean
    public RequestInterceptor headerInterceptor() {
        return new RequestInterceptor() {

            @Override
            public void apply(RequestTemplate template) {
                List<String> authorizationList = Lists.newArrayList(token);
                List<String> contentTypeList = Lists.newArrayList("application/x-www-form-urlencoded;charset=utf-8");
                Map<String, Collection<String>> headers = ImmutableMap.of("token", authorizationList, "Content-Type", contentTypeList);
                template.headers(headers);
            }
        };
    }
}

修改請求類如下:

@FeignClient(name = "apiClientTwo", url = "${test.url}", path = "api", configuration = ClientConfiguration.class)
public interface CommonApiClient {

    /**
     * 查詢使用者資訊介面-第二種方式
     *
     * @param userId
     * @return
     */
    @GetMapping(value = "/queryUser")
    PlainResult<User> queryUser(@RequestParam("userId") String userId);
}

注意:這裡使用了@FeignClient的configuration屬性,並在該配置類中往請求頭新增了token入參,這種方式適用於往請求頭中存放的引數是固定的,類似於ak、sk或者用於授權的應用ID金鑰之類的。

完整專案原始碼請參考:springboot-middleware-feign

FeignClient與HttpClient的區別是什麼?

HttpClient與之同樣實現的還有Okhttp、Httpurlconnection、RestTemplate等等,其 URL 引數是以程式設計方式構造的,資料被髮送到其他服務。在更復雜的情況下,我們將不得不RestTemplate深入到更低階別的 API提供的甚至是 API的細節。

FeignClient則更像是在基於 REST 的服務呼叫上提供更高階別的抽象,在客戶端編寫宣告式REST 服務介面,並使用這些介面來編寫客戶端程式。開發人員不用擔心這個介面的實現。這將在執行時由 Spring 動態配置。通過這種宣告性的方法,開發人員不需要深入瞭解由 HTTP 提供的 HTTP 級別API的細節的RestTemplate

總的來講,FeignClient更具抽象性,也更簡單、靈活。

總結

本文簡單介紹瞭如何使用Spring Cloud Feign元件來替代HttpClient來實現簡單呼叫第三方服務的方法,除了整合Feign元件,我們也可以在專案中加入Ribbon用於服務發現,加入Hystrix用於服務熔斷等等,這樣就會完整地構建出一個基本服務了。

參考

  1. https://juejin.im/post/5daf10836fb9a04e054da1b5
  2. https://blog.csdn.net/weixin_38809962/article/details/80354878
  3. https://www.jianshu.com/p/8bca50cb11d8

結語

歡迎關注微信公眾號『碼仔zonE』,專注於分享Java、雲端計算相關內容,包括SpringBoot、SpringCloud、微服務、Docker、Kubernetes、Python等領域相關技術乾貨,期待與您相遇!

相關文章