OpenFeign:聲明式 RESTful 客戶端
類似於 RestTemplate ,OpenFeign 是對 JDK 的 HttpURLConnection(以及第三方庫 HttpClient 和 OkHttp)的包裝和簡化,並且還自動整合了 Ribbon 。
#1. 什么是 OpenFeign
Feign 早先由 Netflix 公司提供並開源,在它的 8.18.0
之后,Nefflix 將其捐贈給 Spring Cloud 社區,並更名為 OpenFeign 。OpenFeign 的第一個版本就是 9.0.0
。
OpenFeign 會完全代理 HTTP 的請求,在使用過程中我們只需要依賴注入 Bean,然后調用對應的方法傳遞參數即可。這對程序員而言屏蔽了 HTTP 的請求響應過程,讓代碼更趨近於『調用』的形式。
#2. Feign 的入門案例
#2.1 啟動 Nacos 注冊中心
啟動你本地(或服務器)上的 Nacos Server ,確保其正在運行。
#2.2 創建被調用服務
注意
在調用和被調關系中,被調方是不需要 OpenFeign 的,主調方才需要。
創建一個 Spring Boot Maven 項目作為被調方,命名為 b-service(或其他),確保:
-
對外暴露出一個 URL ,即 ,對外提供一個功能。未來,我們的 a-service 會向這個 URL 發出 HTTP 請求,觸發 b-service 的這個功能的執行,並從 b-service 這里獲得 HTTP 響應。
-
b-service 能啟動、運行,並能連上 Nacos Server ,即,在 Nacos Server 上能看到 b-service 。
#2.3 創建主調服務
創建一個 Spring Boot Maven 項目作為主調方(調用發起方、HTTP 請求發起方),命名為 a-service(或其他)。
- 在 Spring Initializer 中引入依賴:在 Initializer 的搜索框內搜索並選擇 Spring Web 、 Nacos Service Discovery 和 OpenFeign 。
注意
這里自動引入的 OpenFeign 的 maven 依賴為:
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
-
為項目添加配置 application.yaml :
server :port: 8080 spring: application: name: a-service cloud: nacos: discovery: server-addr: 127.0.0.1:8848 username: nacos password: nacos namespace: public # group: hemiao
Copied! -
最后創建一個啟動類 AserviceApplication:
@SpringBootApplication@EnableDiscoveryClient @EnableFeignClients(basePackages = "...") // 看這里,看這里,看這里 public class AserviceApplication { public static void main(String[] args) { SpringApplication.run(AserviceApplication.class, args); } }
Copied!
我們可以看到啟動類增加了一個新的注解: @EnableFeignClients ,如果我們要使用 OpenFeign ,必須要在啟動類加入這個注解,以開啟 OpenFeign 。
這樣,我們的 Feign 就已經集成完成了,那么如何通過 Feign 去調用之前我們寫的 HTTP 接口呢?
和 MyBatis 類似:
首先創建一個接口 BServiceClient(名字任意),並且通過注解配置要調用的服務地址:
@FeignClient(value = "b-service") // 這里要和 b-service 在 nacos-server 上登記的名字相呼應 public interface BServiceClient { @GetMapping("/") public String index(); @PostMapping("/login1") public String login1(@RequestParam("username") String username, @RequestParam("uesrname") String password); @PostMapping("/login2") public String login2(@SpringQueryMap LoginToken token); @PostMapping("/login3") public String login3(@RequestBody LoginToken token); }
@FeignClient 注解的 name 屬性的值是被調方(也就是服務的提供者)在 Nacos 注冊中心上所注冊的名字,通常也就是被調方(服務提供者)的 spring.application.name 。
注意
一個服務只能被一個類綁定,不能讓多個類綁定同一個遠程服務,否則,會在啟動項目是出現 “已綁定” 異常。
然后在 OpenFeign 里面通過單元測試來查看效果。
@Test public void test() { try { log.debug("{}", bService.index()); } catch (Exception e) { e.printStackTrace(); } }
說明
OpenFeign 的能力包括但不僅包括這個。
#3. FeignClient 拋出異常
當調用方 b-service 正常返回時,b-service(的 Spring MVC)的返回就是正常的 HTTP 200 響應,而在 a-service 這邊,Openfeign 會幫我們做數據(從 HTTP 響應體中的)提取、轉換操作,並從 FeignClient 中返回。
當被調方 b-service 返回的是非 200 的響應(比如,500、429 等)時,在 a-service 這邊,Openfeign 則會在 FeignClient 方法中拋出一個異常(一個 RuntimeException 的子類)。
#4. OpenFeign 的配置
#4.1 超時和超時重試
OpenFeign 本身也具備重試能力,在早期的 Spring Cloud 中,OpenFeign 默認使用的是 feign.Retryer.Default#Default ,重試 5 次。但 OpenFeign 整合了 Ribbon ,而 Ribbon 也有重試的能力,此時,就可能會導致行為的混亂。(總重試次數 = OpenFeign 重試次數 x Ribbon 的重試次數,這是一個笛卡爾積。)
后來 Spring Cloud 意識到了此問題,因此做了改進(issues 467 (opens new window)),將 OpenFeign 的默認重試改為 feign.Retryer#NEVER_RETRY ,即,默認關閉 。
簡單來說,OpenFeign 對外表現出的超時和重試的行為,實際上是它所用到的 Ribbon 的超時和超時重試行為。我們在項目中進行的配置,也都是配置 Ribbon 的超時和超時重試。
# 全局配置 ribbon: readTimeout: 1000 # 請求處理的超時時間 MaxAutoRetries: 5 # 最大重試次數 MaxAutoRetriesNextServer: 1 # 切換實例的重試次數 # 是否開啟對所有請求進行超時重試。一般不會開啟這個功能。默認值是 false ,表示僅對 get 請求進行超時重試 # okToRetryOnAllOperations: true
整個 OpenFeign(實際上是 Ribbon)的最大重試次數為:
(1 + MaxAutoRetries) x (1 + MaxAutoRetriesNextServer)
這里需要注意的是『重試』次數是不包含『本身那一次』的。
故意加大被調服務的返回響應時長,你會看到主調服務中打印類似如下消息:
feign.RetryableException: Read timed out executing GET http://SERVICE-PRODUCER/demo?username=tom&password=123
at feign.FeignException.errorExecuting(FeignException.java:249)
at feign.SynchronousMethodHandler.executeAndDecode(SynchronousMethodHandler.java:129)
at feign.SynchronousMethodHandler.invoke(SynchronousMethodHandler.java:89)
...
另外,在被調服務方,你會發現上述配置會導致被調服務收到 12 次請求:
請求次數 = (1 + 5) x (1 + 1)
你也可以指定對某個特定服務的超時和超時重試:
# 針對自己向 b-service 發出請求超時的設置 b-service: ribbon: readTimeout: 3000 MaxAutoRetries: 2 MaxAutoRetriesNextServer: 0
#4.2 替換底層 HTTP 實現(了解)
類似 RestTemplate,本質上是 OpenFeign 的底層會用到 JDK 的HttpURLConnection 發出 HTTP 請求。另外,如果有需要,你也可以換成第三方庫 HttpClient 或 OkHttp 。
將 OpenFeign 的底層 HTTP 客戶端替換成 HTTPClient 需要 2 步:
-
引入依賴:
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
Copied! -
在配置文件中啟用它:
feign: httpclient: enabled: true # 激活 httpclient 的使用
將 OpenFeign 的底層 HTTP 客戶端替換成 OkHttp 需要 2 步:
-
引入依賴:
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-okhttp</artifactId> </dependency>
Copied! -
在配置文件中啟用它:
feign: okhttp: enabled: true # 激活 okhttp 的使用
#4.3 日志配置(了解)
SpringCloudFeign 為每一個 FeignClient 都提供了一個 feign.Logger 實例。可以根據 logging.level.<FeignClient> 參數配置格式來開啟 Feign 客戶端的 DEBUG 日志,其中 <FeignClient> 部分為 Feign 客戶端定義接口的完整路徑。如:
logging: level: com.woniu.outlet.client: DEBUG
然后再在配置類(比如主程序入口類)中加入 Looger.Level 的 Bean:
@Bean public Logger.Level feignLoggerLevel() { return Logger.Level.FULL; }
級別 | 說明 |
---|---|
NONE | 不輸出任何日志 |
BASIC | 只輸出 Http 方法名稱、請求 URL、返回狀態碼和執行時間 |
HEADERS | 輸出 Http 方法名稱、請求 URL、返回狀態碼和執行時間 和 Header 信息 |
FULL | 記錄 Request 和 Response 的 Header,Body 和一些請求元數據 |
#5. OpenFeign 的底層原理概述
雖然在使用 OpenFeign 時,我們( 程序員 )定義的是接口,但是 OpenFeign 框架會通過 JDK 動態代理生成 @FeignClient 接口的代理對象。邏輯相當於:
@Autowired XxxServiceClient client = Proxy.newProxyInstance(invocationHandler);
在這里,出現了一個 InvocationHandler 對象,結合 JDK 動態代理的知識,我們知道,當你調用 client 的某個方法時,實際上觸發的就是這個 InvocationHandler 對象的 invoke 方法。InvocationHandler 對象邏輯相當於:
public class SimpleInvocationHandler implements InvocationHandler { Map<Method, MethodHandler> methodToHandler = new LinkedHashMap(); public SimpleInvocationHandler(Map<Method, MethodHandler> methodToHandler) { this.methodToHandler = methodToHandler; } @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { MethodHandler handler = methodToHandler.get(method); return handler.invoke(); } }
在 InvocationHandler 中最核心的在於它有一個 Map ,這個 map 以 InvocationHandler 所代理的那個 FeignClient 中所聲明的方法的 Method 對象為 key ,值是一個一個的 MethodHandler 對象。
假設有一個 @FeignClient 為如下形式:
@FeignClient("a-service") public interface AService { @RequestMapping("/hello") public String hello(); @RequestMapping("/world") public String world(); }
那么,AService 有一個代理對象 InvocationHandler ,它里面的 Map 邏輯上形如:
key | value |
---|---|
helloMethod | helloMethodHandler |
worldMethod | worldMethodHandler |
那么,當你調用 bService.hello();
方法時,實際上是 InvocationHandler 對象的 invoke 方法被執行,而 InvocationHandler 對象會從它的 Map 中以 hello 方法的 Method 對象為 key 找到對應的一個 MethodHandler 對象,然后調用 MethodHandler 對象的 invoke 方法。
調用關系和流程形如:
bService.hello()
└──> invocationHandler.invoke()
└──> methodHandler.invoke()
├──> 第一件事 ...
└──> 第二件事 ...
MethodHandler 的 invoke() 方法核心就是干了 2 件事情:
-
傳給 Ribbon 目標服務的服務名,找它 “要” 一個該服務的實例的具體的地址;
-
根據 Ribbon 返回的具體地址,發出 HTTP 請求,並等待、解析響應。
6. OpenFeign 的攔截器機制
OpenFeign 有一個攔截器機制,對於它的作用 OpenFeign 的官方是這樣描述的:
Zero or more may be configured for purposes such as adding headers to all requests.
你可以自定義類繼承 RequestInterceptor ,當然,你也可以使用 lambda 表達式結合 @Bean 進行簡化:
@Bean public RequestInterceptor requestInterceptor() { return requestTemplate -> { requestTemplate.header("x-jwt-token", "..."); }; }
下面代碼是將當前請求的所有請求頭添加到 openfeign 將要發出的請求中:
@Bean public RequestInterceptor requestInterceptor() { return requestTemplate -> { ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); HttpServletRequest request = attributes.getRequest(); Enumeration<String> headerNames = request.getHeaderNames(); if (headerNames == null) return; while (headerNames.hasMoreElements()) { String name = headerNames.nextElement(); String values = request.getHeader(name); requestTemplate.header(name, values); } } }