1. 程式人生 > >SpringCloud 原始碼系列(6)—— 宣告式服務呼叫 Feign

SpringCloud 原始碼系列(6)—— 宣告式服務呼叫 Feign

SpringCloud 原始碼系列(1)—— 註冊中心 Eureka(上)

SpringCloud 原始碼系列(2)—— 註冊中心 Eureka(中)

SpringCloud 原始碼系列(3)—— 註冊中心 Eureka(下)

SpringCloud 原始碼系列(4)—— 負載均衡 Ribbon(上)

SpringCloud 原始碼系列(5)—— 負載均衡 Ribbon(下)

SpringCloud 原始碼系列(6)—— 宣告式服務呼叫 Feign

 

一、Feign 基礎入門

1、Feign 概述

在使用 Spring Cloud 開發微服務應用時,各個服務提供者都是以HTTP介面的形式對外提供服務,因此在服務消費者呼叫服務提供者時,底層通過 HTTP Client 的方式訪問。我們可以使用JDK原生的 URLConnection、Apache的HTTP Client、OkHttp、Spring 的 RestTemplate 去實現服務間的呼叫。但是最方便、最優雅的方式是通過 Spring Cloud OpenFeign 進行服務間的呼叫。

Feign 是一個宣告式的 Web Service 客戶端,它的目的就是讓Web Service呼叫更加簡單。Spring Cloud 對 Feign 進行了增強,使 Feign 支援 Spring MVC 的註解,並整合了 Ribbon、Hystrix 等。Feign還提供了HTTP請求的模板,通過編寫簡單的介面和註解,就可以定義好HTTP請求的引數、格式、地址等資訊。Feign 會完全代理HTTP的請求,在使用過程中我們只需要依賴注入Bean,然後呼叫對應的方法傳遞引數即可。Feign 的首要目標是將 Java HTTP 客戶端的書寫過程變得簡單。

Feign 的一些主要特性如下:

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

GitHub地址:

  • OpenFeign 地址:https://github.com/OpenFeign/feign
  • SpringCloud OpenFeign 地址:https://github.com/spring-cloud/spring-cloud-openfeign

2、DEMO示例

還是使用前面研究 Eureka 和 Ribbon 時的 demo-producer、demo-consumer 服務來做測試。

① 首先,需要引入 openfeign 的依賴

1 <dependency>
2     <groupId>org.springframework.cloud</groupId>
3     <artifactId>spring-cloud-starter-openfeign</artifactId>
4 </dependency>

spring-cloud-starter-openfeign 會幫我們引入如下依賴,包含了 OpenFeign 的核心元件。

② 在 demo-consumer 服務中,增加一個 Feign 客戶端介面,來呼叫 demo-producer 的介面。

 1 @FeignClient(value = "demo-producer")
 2 public interface ProducerFeignClient {
 3 
 4     @GetMapping("/v1/user/{id}")
 5     ResponseEntity<User> getUserById(@PathVariable Long id, @RequestParam(required = false) String name);
 6 
 7     @PostMapping("/v1/user")
 8     ResponseEntity<User> createUser(@RequestBody User user);
 9 
10 }

③ 在啟動類加上 @EnableFeignClients 註解。

1 @EnableFeignClients
2 @SpringBootApplication
3 public class ConsumerApplication {
4     //....       
5 }

④ 在介面中注入 ProducerFeignClient 就可以使用 Feign 客戶端介面來呼叫遠端服務了。

 1 @RestController
 2 public class FeignController {
 3     private final Logger logger = LoggerFactory.getLogger(getClass());
 4 
 5     @Autowired
 6     private ProducerFeignClient producerFeignClient;
 7 
 8     @GetMapping("/v1/user/query")
 9     public ResponseEntity<User> queryUser() {
10         ResponseEntity<User> result = producerFeignClient.getUserById(1L, "tom");
11         User user = result.getBody();
12         logger.info("query user: {}", user);
13         return ResponseEntity.ok(user);
14     }
15 
16     @GetMapping("/v1/user/create")
17     public ResponseEntity<User> createUser() {
18         ResponseEntity<User> result = producerFeignClient.createUser(new User(10L, "Jerry", 20));
19         User user = result.getBody();
20         logger.info("create user: {}", user);
21         return ResponseEntity.ok(user);
22     }
23 }

⑤ 在 demo-producer 服務增加 UserController 介面供消費者呼叫

 1 @RestController
 2 public class UserController {
 3     private final Logger logger = LoggerFactory.getLogger(getClass());
 4 
 5     @PostMapping("/v1/user/{id}")
 6     public ResponseEntity<User> queryUser(@PathVariable Long id, @RequestParam String name) {
 7         logger.info("query params: id :{}, name:{}", id, name);
 8         return ResponseEntity.ok(new User(id, name, 10));
 9     }
10 
11     @PostMapping("/v1/user/{id}")
12     public ResponseEntity<User> createUser(@RequestBody User user) {
13         logger.info("create params: {}", user);
14         return ResponseEntity.ok(user);
15     }
16 }

⑥ 測試

先把把註冊中心啟起來,然後 demo-producer 啟兩個例項,再啟動 demo-consumer,呼叫 demo-consumer 的介面測試,會發現,ProducerFeignClient 的呼叫會輪詢到 demo-consumer 的兩個例項上。

通過簡單的測試可以發現,Feign 使得 Java HTTP 客戶端的書寫過程變得非常簡單,就像開發介面一樣。另外,Feign底層一定整合了 Ribbon,@FeignClient 指定了服務名稱,請求最終一定是通過 Ribbon 的 ILoadBalancer 元件進行負載均衡的。

3、FeignClient 註解

通過前面的DEMO可以發現,使用 Feign 最核心的應該就是 @EnableFeignClients 和 @FeignClient 這兩個註解,@FeignClient 加在客戶端介面類上,@EnableFeignClients 加在啟動類上,就是用來掃描加了 @FeignClient 介面的類。我們研究原始碼就從這兩個入口開始。

要知道介面是不能直接注入和呼叫的,那麼一定是 @EnableFeignClients 掃描到 @FeignClient 註解的介面後,基於這個介面生成了動態代理物件,並注入到 Spring IOC 容器中,才可以被注入使用。最終呢,一定會通過 Ribbon 負載均衡獲取一個 Server,然後重構 URI,再發起最終的HTTP呼叫。

① @EnableFeignClients 註解

首先看 @EnableFeignClients 的類註釋,註釋就已經說明了,這個註解就是用來掃描 @FeignClient 註解的介面的,那麼核心的邏輯應該就是在 @Import 匯入的類 FeignClientsRegistrar 中的。

EnableFeignClients 的主要屬性有如下:

  • value、basePackages: 配置掃描 @FeignClient 的包路徑
  • clients:直接指定掃描的 @FeignClient 介面
  • defaultConfiguration:配置 Feign 客戶端全域性預設配置類,從註釋可以得知,預設的全域性配置類是 FeignClientsConfiguration
 1 package org.springframework.cloud.openfeign;
 2 
 3 /**
 4  * Scans for interfaces that declare they are feign clients (via
 5  * {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>).
 6  * Configures component scanning directives for use with
 7  * {@link org.springframework.context.annotation.Configuration}
 8  * <code>@Configuration</code> classes.
 9  */
10 @Retention(RetentionPolicy.RUNTIME)
11 @Target(ElementType.TYPE)
12 @Documented
13 @Import(FeignClientsRegistrar.class)
14 public @interface EnableFeignClients {
15 
16     // 指定掃描 @FeignClient 包所在目錄
17     String[] value() default {};
18 
19     // 指定掃描 @FeignClient 包所在目錄
20     String[] basePackages() default {};
21 
22     // 指定標記介面來掃描包
23     Class<?>[] basePackageClasses() default {};
24 
25     // Feign 客戶端全域性預設配置類
26     /**
27      * A custom <code>@Configuration</code> for all feign clients. Can contain override
28      * <code>@Bean</code> definition for the pieces that make up the client, for instance
29      * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
30      *
31      * @see FeignClientsConfiguration for the defaults
32      * @return list of default configurations
33      */
34     Class<?>[] defaultConfiguration() default {};
35 
36     // 直接指定 @FeignClient 註解的類,這時就會禁用類路徑掃描
37     Class<?>[] clients() default {};
38 }

② @FeignClient 註解

首先看 FeignClient 的類註釋,註釋說明 @FeignClient 註解就是宣告一個 REST 客戶端介面,而且會建立一個可以注入的元件,應該就是動態代理的bean。而且如果Ribbon可用,然後就可以用Ribbon做負載均衡,這個負載均衡可以用 @RibbonClient 定製配置類,名稱一樣就行。

FeignClient 註解被 @Target(ElementType.TYPE) 修飾,表示 FeignClient 註解的作用目標在介面上。@Retention(RetentionPolicy.RUNTIME) 註解表明該註解會在 Class 位元組碼檔案中存在,在執行時可以通過反射獲取到。

@FeignClient 註解用於建立宣告式 API 介面,該介面是 RESTful 風格的。Feign 被設計成插拔式的,可以注入其他元件和 Feign 一起使用。最典型的是如果 Ribbon 可用,Feign 會和Ribbon 相結合進行負載均衡。

FeignClient 主要有如下屬性:

  • name:指定 FeignClient 的名稱,如果專案使用了 Ribbon,name 屬性會作為微服務的名稱,用於服務發現。
  • url:url 一般用於除錯,可以手動指定 @FeignClient 呼叫的地址。
  • decode404:當發生404錯誤時,如果該欄位為true,會呼叫 decoder 進行解碼,否則丟擲 FeignException。
  • configuration:FeignClient 配置類,可以自定義Feign的Encoder、Decoder、LogLevel、Contracto
  • fallback:定義容錯的處理類,當呼叫遠端介面失敗或超時時,會呼叫對應介面的容錯邏輯,fallback 指定的類必須實現 @FeignClient 標記的介面。
  • fallbackFactory:工廠類,用於生成 fallback 類例項,通過這個屬性我們可以實現每個介面通用的容錯邏輯,減少重複的程式碼。
  • path:定義當前 FeignClient 的統一字首。
 1 package org.springframework.cloud.openfeign;
 2 
 3 /**
 4  * Annotation for interfaces declaring that a REST client with that interface should be
 5  * created (e.g. for autowiring into another component). If ribbon is available it will be
 6  * used to load balance the backend requests, and the load balancer can be configured
 7  * using a <code>@RibbonClient</code> with the same name (i.e. value) as the feign client.
 8  */
 9 @Target(ElementType.TYPE)
10 @Retention(RetentionPolicy.RUNTIME)
11 @Documented
12 @Inherited
13 public @interface FeignClient {
14 
15     // 指定服務名稱
16     @AliasFor("name")
17     String value() default "";
18 
19     // 指定服務名稱,已過期
20     @Deprecated
21     String serviceId() default "";
22 
23     // FeignClient 介面生成的動態代理的bean名稱
24     String contextId() default "";
25 
26     // 指定服務名稱
27     @AliasFor("value")
28     String name() default "";
29 
30     // @Qualifier 標記
31     String qualifier() default "";
32 
33     // 如果不使用Ribbon負載均衡,就需要使用url返回一個絕對地址
34     String url() default "";
35 
36     // 404 預設丟擲 FeignExceptions 異常,設定為true則替換為404異常
37     boolean decode404() default false;
38 
39     // Feign客戶端配置類,可以定製 Decoder、Encoder、Contract
40     /**
41      * A custom configuration class for the feign client. Can contain override
42      * <code>@Bean</code> definition for the pieces that make up the client, for instance
43      * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
44      *
45      * @see FeignClientsConfiguration for the defaults
46      * @return list of configurations for feign client
47      */
48     Class<?>[] configuration() default {};
49 
50     // FeignClient 介面的回撥類,必須實現客戶端介面,並註冊為一個bean物件。
51     // 求失敗或降級時就會進入回撥方法中
52     /**
53      * Fallback class for the specified Feign client interface. The fallback class must
54      * implement the interface annotated by this annotation and be a valid spring bean.
55      * @return fallback class for the specified Feign client interface
56      */
57     Class<?> fallback() default void.class;
58 
59     // 回撥類建立工廠
60     Class<?> fallbackFactory() default void.class;
61 
62     // URL字首
63     String path() default "";
64 
65     // 定義為 primary bean
66     boolean primary() default true;
67 }

4、FeignClient 核心元件

從上面已經得知,FeignClient 的預設配置類為 FeignClientsConfiguration,這個類在 spring-cloud-openfeign-core 的 jar 包下,並且每個 FeignClient 都可以定義各自的配置類。

開啟這個類,可以發現這個類注入了很多 Feign 相關的配置 Bean,包括 Retryer、FeignLoggerFactory、Decoder、Encoder、Contract 等,這些類在沒有 Bean 被注入的情況下,會自動注入預設配置的 Bean。

 1 package org.springframework.cloud.openfeign;
 2 
 3 @Configuration(proxyBeanMethods = false)
 4 public class FeignClientsConfiguration {
 5     @Autowired
 6     private ObjectFactory<HttpMessageConverters> messageConverters;
 7     @Autowired(required = false)
 8     private List<AnnotatedParameterProcessor> parameterProcessors = new ArrayList<>();
 9     @Autowired(required = false)
10     private List<FeignFormatterRegistrar> feignFormatterRegistrars = new ArrayList<>();
11     @Autowired(required = false)
12     private Logger logger;
13 
14     @Bean
15     @ConditionalOnMissingBean
16     public Decoder feignDecoder() {
17         return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
18     }
19 
20     @Bean
21     @ConditionalOnMissingBean
22     @ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
23     public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
24         return springEncoder(formWriterProvider);
25     }
26 
27     @Bean
28     @ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
29     @ConditionalOnMissingBean
30     public Encoder feignEncoderPageable(
31             ObjectProvider<AbstractFormWriter> formWriterProvider) {
32         //...
33         return encoder;
34     }
35 
36     @Bean
37     @ConditionalOnMissingBean
38     public Contract feignContract(ConversionService feignConversionService) {
39         return new SpringMvcContract(this.parameterProcessors, feignConversionService);
40     }
41 
42     @Bean
43     @ConditionalOnMissingBean
44     public Retryer feignRetryer() {
45         return Retryer.NEVER_RETRY;
46     }
47 
48     @Bean
49     @Scope("prototype")
50     @ConditionalOnMissingBean
51     public Feign.Builder feignBuilder(Retryer retryer) {
52         return Feign.builder().retryer(retryer);
53     }
54 
55     @Bean
56     @ConditionalOnMissingBean(FeignLoggerFactory.class)
57     public FeignLoggerFactory feignLoggerFactory() {
58         return new DefaultFeignLoggerFactory(this.logger);
59     }
60     
61     @Configuration(proxyBeanMethods = false)
62     @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
63     protected static class HystrixFeignConfiguration {
64         @Bean
65         @Scope("prototype")
66         @ConditionalOnMissingBean
67         @ConditionalOnProperty(name = "feign.hystrix.enabled")
68         public Feign.Builder feignHystrixBuilder() {
69             return HystrixFeign.builder();
70         }
71 
72     }
73 
74     //...
75 }
View Code

這些其實就是 Feign 的核心元件了,對應的預設實現類如下。

如果想自定義這些配置,可增加一個配置類,然後配置到 @FeignClient 的 configuration 上。

① 先定義一個配置類

1 public class ProducerFeignConfiguration {
2 
3     @Bean
4     public Retryer feignRetryer() {
5         return new Retryer.Default();
6     }
7 }

② 配置到 @FeignClient 中

1 @FeignClient(value = "demo-producer", configuration = ProducerFeignConfiguration.class)
2 public interface ProducerFeignClient {
3 
4     //...
5 }

5、Feign 屬性檔案配置

① 全域性配置

前面已經瞭解到,@EnableFeignClients 的 defaultConfiguration 可以配置全域性的預設配置bean物件。也可以使用 application.yml 檔案來配置。

1 feign:
2   client:
3     config:
4       # 預設全域性配置
5       default:
6         connectTimeout: 1000
7         readTimeout: 1000
8         loggerLevel: basic

② 指定客戶端配置

@FeignClient 的 configuration 可以配置客戶端特定的配置類,也可以使用 application.yml 配置。

 1 feign:
 2   client:
 3     config:
 4       # 指定客戶端名稱
 5       demo-producer:
 6         # 連線超時時間
 7         connectTimeout: 5000
 8         # 讀取超時時間
 9         readTimeout: 5000
10         # Feign日誌級別
11         loggerLevel: full
12         # Feign的錯誤解碼器
13         errorDecoder: com.example.simpleErrorDecoder
14         # 配置攔截器
15         requestInterceptors:
16           - com.example.FooRequestInterceptor
17           - com.example.BarRequestInterceptor
18         # 404是否解碼
19         decode404: false
20         #Feign的編碼器
21         encoder: com.example.simpleEncoder
22         #Feign的解碼器
23         decoder: com.example.simpleDecoder
24         #Feign的Contract配置
25         contract: com.example.simpleContract

注意,如果通過Java程式碼的方式配置過 Feign,然後又通過屬性檔案的方式配置 Feign,屬性檔案中Feign的配置會覆蓋Java程式碼的配置。但是可以配置 feign.client.default-to-properties=false 來改變Feign配置生效的優先順序。

③ 開啟壓縮配置

Spring Cloud Feign支援對請求和響應進行GZIP壓縮,以提高通訊效率。

 1 feign:
 2   compression:
 3     request:
 4       # 配置請求GZIP壓縮
 5       enabled: true
 6       # 配置壓縮支援的 MIME TYPE
 7       mime-types: text/xml,application/xml,application/json
 8       # 配置壓縮資料大小的下限
 9       min-request-size: 2048
10     response:
11       # 配置響應GZIP壓縮
12       enabled: true

6、FeignClient 開啟日誌

Feign 為每一個 FeignClient 都提供了一-個 feign.Logger 例項,可以在配置中開啟日誌。但是生產環境一般不要開啟日誌,因為介面呼叫可能會產生大量日誌,一般在開發環境除錯開啟即可。

① 通過配置檔案開啟日誌

首先設定客戶端的 loggerLevel,然後配置 logging.level 日誌級別為 debug。

 1 feign:
 2   client:
 3     config:
 4       demo-producer:
 5         # Feign日誌級別
 6         loggerLevel: full
 7 
 8 logging:
 9   level:
10     # 設定日誌輸出級別
11     com.lyyzoo.sunny.register.feign: debug

之後呼叫 FeignClient 就可以看到介面呼叫日誌了:

 1 2020-12-30 15:33:02.459 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] ---> GET http://demo-producer/v1/user/1?name=tom HTTP/1.1
 2 2020-12-30 15:33:02.459 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] ---> END HTTP (0-byte body)
 3 2020-12-30 15:33:02.462 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] <--- HTTP/1.1 200 (3ms)
 4 2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] connection: keep-alive
 5 2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] content-type: application/json
 6 2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] date: Wed, 30 Dec 2020 07:33:02 GMT
 7 2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] keep-alive: timeout=60
 8 2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] transfer-encoding: chunked
 9 2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] 
10 2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] {"id":1,"name":"tom","age":10}
11 2020-12-30 15:33:02.463 DEBUG 2720 --- [nio-8020-exec-6] c.l.s.r.feign.ProducerFeignClient        : [ProducerFeignClient#getUserById] <--- END HTTP (30-byte body)
12 2020-12-30 15:33:02.463  INFO 2720 --- [nio-8020-exec-6] c.l.s.r.controller.FeignController       : query user: User{id=1, name='tom', age=10}

② 通過Java程式碼開啟日誌

首先還是需要設定日誌輸出級別:

1 logging:
2   level:
3     # 設定日誌輸出級別
4     com.lyyzoo.sunny.register.feign: debug

然後配置一個 feign.Logger.Level 物件:

1 @Bean
2 public feign.Logger.Level loggerLevel() {
3     return Logger.Level.FULL;
4 }

③ Logger.Level

Logger.Level 的具體級別如下:

 1 public enum Level {
 2     // 不列印任何日誌
 3     NONE,
 4     // 只打印請求的方法和URL,以及響應狀態碼和執行時間
 5     BASIC,
 6     // 在BASIC的基礎上,列印請求頭和響應頭資訊
 7     HEADERS,
 8     // 記錄所有請求與相應的明細,包含請求頭、請求體、元資料
 9     FULL
10 }

二、掃描 @FeignClient 註解介面

Feign 是一個偽 Java HTTP 客戶端,Feign 不做任何的請求處理,它只是簡化API呼叫的開發,開發人員只需定義客戶端介面,按照 springmvc 的風格開發宣告式介面。然後在使用過程中我們只需要依賴注入Bean,然後呼叫對應的方法傳遞引數即可。

這裡就有個問題,我們開發的是一個介面,然後使用 @FeignClient 註解標註,那又是如何能夠注入這個介面的Bean物件的呢?其實很容易就能想到,一定是生成了介面的動態代理並注入到Spring容器中了,才能依賴注入這個客戶端介面。這節就來看看 feign 是如何生成動態代理物件的。

1、FeignClient 動態註冊元件 FeignClientsRegistrar

再看下 @EnableFeignClients  註解,它使用 @Import 匯入了 FeignClientsRegistrar,FeignClient 註冊者。從名字就可以看出,FeignClientsRegistrar 就是完成 FeignClient 註冊的核心元件。

1 @Retention(RetentionPolicy.RUNTIME)
2 @Target(ElementType.TYPE)
3 @Documented
4 // FeignClient 註冊處理類
5 @Import(FeignClientsRegistrar.class)
6 public @interface EnableFeignClients {
7     //...
8 }

FeignClientsRegistrar 實現了 ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware 三個介面。

ResourceLoaderAware 是為了注入資源載入器 ResourceLoader,EnvironmentAware 是為了注入當前環境元件 Environment,ImportBeanDefinitionRegistrar 是 Spring 動態註冊 bean 的介面。

 1 class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
 2 
 3     // patterned after Spring Integration IntegrationComponentScanRegistrar
 4     // and RibbonClientsConfigurationRegistgrar
 5 
 6     // 資源載入器
 7     private ResourceLoader resourceLoader;
 8     // 當前環境元件
 9     private Environment environment;
10     
11     //....
12 }

ImportBeanDefinitionRegistrar 主要包含一個介面 registerBeanDefinitions,就是用來動態註冊 BeanDefinition 的。平時我們一般就使用 @Service、@Component、@Bean 等註解向 Spring 容器註冊物件,我們也可以實現 ImportBeanDefinitionRegistrar 介面來動態註冊 BeanDefinition。

所有實現了 ImportBeanDefinitionRegistrar  介面的類的都會被 ConfigurationClassPostProcessor 處理,ConfigurationClassPostProcessor 實現了 BeanFactoryPostProcessor 介面,所以 ImportBeanDefinitionRegistrar 中動態註冊的bean是優先於依賴它的bean初始化的,也能被aop、validator等機制處理。ImportBeanDefinitionRegistrar 實現類寫好之後,還要使用 @Import 註解匯入實現類。

 1 public interface ImportBeanDefinitionRegistrar {
 2 
 3     /**
 4      * Register bean definitions as necessary based on the given annotation metadata of
 5      * the importing {@code @Configuration} class.
 6      * <p>Note that {@link BeanDefinitionRegistryPostProcessor} types may <em>not</em> be
 7      * registered here, due to lifecycle constraints related to {@code @Configuration}
 8      * class processing.
 9      * <p>The default implementation is empty.
10      * @param importingClassMetadata annotation metadata of the importing class
11      * @param registry current bean definition registry
12      */
13     default void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
14     }
15 

BeanDefinition 又是什麼呢?從註釋可以瞭解到,BeanDefinition 就是用來描述 bean 例項的,BeanDefinition 包含了例項的屬性值、建構函式引數等。其實就是通過這個 BeanDefinition 來獲取例項物件。

 1 /**
 2  * A BeanDefinition describes a bean instance, which has property values,
 3  * constructor argument values, and further information supplied by
 4  * concrete implementations.
 5  *
 6  * <p>This is just a minimal interface: The main intention is to allow a
 7  * {@link BeanFactoryPostProcessor} to introspect and modify property values
 8  * and other bean metadata.
 9  */
10 public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { 
11 }

FeignClientsRegistrar 實現的 registerBeanDefinitions 方法中,主要有兩步:

  • 註冊FeignClient預設配置物件,就是根據 @EnableFeignClients 的 defaultConfiguration 配置類注入預設配置,這個一般就是全域性配置。
  • 之後就是掃描 @FeignClient 註解的介面,封裝成 BeanDefinition,然後用 BeanDefinitionRegistry 來註冊。

因此,FeignClientsRegistrar 就是掃描 @FeignClient 註解的介面,並註冊 FeignClient 的核心元件。

1 // 根據註解元資料註冊bean定義
2 @Override
3 public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
4     // 註冊 FeignClient 預設配置類,根據 @EnableFeignClients 的 defaultConfiguration 注入預設配置
5     registerDefaultConfiguration(metadata, registry);
6     // 掃描 FeignClient 介面,註冊 FeignClient
7     registerFeignClients(metadata, registry);
8 }

2、掃描 @FeignClient 註解介面

接著看 registerFeignClients 方法,這個方法主要就是完成掃描 @FeignClient 註解的介面並完成 FeignClient 註冊的工作。

主要的流程如下:

  • 首先得到一個類路徑掃描器 ClassPathScanningCandidateComponentProvider,就是用這個元件來掃描包路徑獲取到 @FeignClient 註解的介面。
  • 如果 @EnableFeignClients 沒有配置 clients 屬性,掃描的包路徑就是 @EnableFeignClients 配置的 value、basePackages、basePackageClasses 配置的包路徑。並且根據註解過濾器來篩選有 @FeignClient 註解的介面。
  • 如果 @EnableFeignClients 配置了 clients 屬性,就只掃描 clients 配置的介面類。
  • 之後就遍歷掃描包路徑,獲取到 @FeignClient 註解的介面。可以看到 @FeignClient 註解的型別必須是一個介面,否則斷言會丟擲異常。
  • 最後兩步就是註冊配置類和註冊 FeignClient了,配置類就是 @FeignClient 的 configuration 屬性配置的客戶端配置類,這個配置類將覆蓋 @EnableFeignClients 配置的全域性配置類。
 1 **
 2  * 註冊 FeignClient
 3  *
 4  * @param metadata @EnableFeignClients 註解的元資料
 5  * @param registry BeanDefinition 註冊器
 6  */
 7 public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
 8     // ClassPath 掃描器
 9     ClassPathScanningCandidateComponentProvider scanner = getScanner();
10     scanner.setResourceLoader(this.resourceLoader);
11 
12     Set<String> basePackages;
13 
14     // @EnableFeignClients 註解的屬性
15     Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
16     // 註解型別過濾器,過濾 @FeignClient 註解的介面
17     AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
18     final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
19 
20     // 如果 @EnableFeignClients 沒有配置 clients,就取 value、basePackages、basePackageClasses 基礎包
21     if (clients == null || clients.length == 0) {
22         // @FeignClient 註解過濾器
23         scanner.addIncludeFilter(annotationTypeFilter);
24         basePackages = getBasePackages(metadata);
25     }
26     // 如果 @EnableFeignClients 中配置了 clients
27     else {
28         final Set<String> clientClasses = new HashSet<>();
29         basePackages = new HashSet<>();
30         for (Class<?> clazz : clients) {
31             // 基礎包取配置的 client 類所在的包
32             basePackages.add(ClassUtils.getPackageName(clazz));
33             // 根據名稱過濾
34             clientClasses.add(clazz.getCanonicalName());
35         }
36         // 類過濾器
37         AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
38             @Override
39             protected boolean match(ClassMetadata metadata) {
40                 String cleaned = metadata.getClassName().replaceAll("\\$", ".");
41                 // 根據名稱過濾
42                 return clientClasses.contains(cleaned);
43             }
44         };
45         // 必須類名在 clientClasses 中且類上有 @FeignClient 註解
46         scanner.addIncludeFilter(new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
47     }
48 
49     // 掃描基礎包
50     for (String basePackage : basePackages) {
51         Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
52         for (BeanDefinition candidateComponent : candidateComponents) {
53             if (candidateComponent instanceof AnnotatedBeanDefinition) {
54                 // verify annotated class is an interface
55                 AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
56                 AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
57                 // @FeignClient 註解的型別必須是一個介面
58                 Assert.isTrue(annotationMetadata.isInterface(),
59                         "@FeignClient can only be specified on an interface");
60 
61                 // @FeignClient 註解的屬性
62                 Map<String, Object> attributes = annotationMetadata
63                         .getAnnotationAttributes(FeignClient.class.getCanonicalName());
64                 // Feign 客戶端名稱,就是服務名
65                 String name = getClientName(attributes);
66                 // 註解客戶端配置類
67                 registerClientConfiguration(registry, name, attributes.get("configuration"));
68                 // 註冊 FeignClient
69                 registerFeignClient(registry, annotationMetadata, attributes);
70             }
71         }
72     }
73 }

看下 getBasePackages 方法,可以看出,要掃描的包路徑包含 @EnableFeignClients 配置的 value、basePackages、basePackageClasses 類所在的包,這裡是取的多個配置的並集。

還有個需要注意的是,從最後一步可以看出,如果配置了 value、basePackages、basePackageClasses 時,就不會掃描 @EnableFeignClients 所在的包路徑了,如果要掃描,需配置到 value 等屬性中。

 1 protected Set<String> getBasePackages(AnnotationMetadata importingClassMetadata) {
 2     Map<String, Object> attributes = importingClassMetadata
 3             .getAnnotationAttributes(EnableFeignClients.class.getCanonicalName());
 4 
 5     Set<String> basePackages = new HashSet<>();
 6     // 先取 value
 7     for (String pkg : (String[]) attributes.get("value")) {
 8         if (StringUtils.hasText(pkg)) {
 9             basePackages.add(pkg);
10         }
11     }
12     // 再取 basePackages
13     for (String pkg : (String[]) attributes.get("basePackages")) {
14         if (StringUtils.hasText(pkg)) {
15             basePackages.add(pkg);
16         }
17     }
18     // 再從 basePackageClasses 的 Class 獲取包
19     for (Class<?> clazz : (Class[]) attributes.get("basePackageClasses")) {
20         basePackages.add(ClassUtils.getPackageName(clazz));
21     }
22 
23     // 只有當沒有配置 value、basePackages、basePackageClasses 時,才會掃描 @EnableFeignClients 所在的包路徑
24     if (basePackages.isEmpty()) {
25         basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));
26     }
27     return basePackages;
28 }

3、@FeignClient 介面構造 BeanDefinition 並註冊

registerFeignClients 中掃描了包路徑下的 @FeignCient 註解的介面,然後呼叫了 registerFeignClient 註冊 FeignClient 介面的 BeanDefinition。

主要的流程如下:

  • 首先建立了 BeanDefinitionBuilder,要構建的型別是 FeignClientFactoryBean,從名字可以看出就是建立 FeignClient 代理物件的工廠類。FeignClientFactoryBean 就是生成 FeignClient 介面動態代理的核心元件。
  • 接著就是將 @FeignClient 註解的屬性設定到 definition 中,它這裡還設定了回撥類 fallback 和回撥工廠 fallbackFactory,但是有沒有用呢?這個後面再分析。
  • 然後是 bean 的名稱,預設為 服務名稱 + "FeignClient",例如 "demo-consumerFeignClient";如果設定了 qualifier 屬性,名稱就是 qualifier 設定的值。
  • 之後用 BeanDefinitionBuilder 獲取 BeanDefinition,並設定了物件型別為 FeignClient 介面的全限定名。
  • 最後,將 BeanDefinition 等資訊封裝到 BeanDefinitionHolder,然後呼叫 BeanDefinitionReaderUtils.registerBeanDefinition 將 BeanDefinition 註冊到Spring IoC 容器中。
 1 private void registerFeignClient(BeanDefinitionRegistry registry,
 2         AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
 3     String className = annotationMetadata.getClassName();
 4     // FeignClientFactoryBean 就是用來生成 FeignClient 介面代理類的核心元件
 5     BeanDefinitionBuilder definition = BeanDefinitionBuilder
 6             .genericBeanDefinition(FeignClientFactoryBean.class);
 7     validate(attributes);
 8     // 從 @FeignClient 中得到的屬性,並設定到 BeanDefinitionBuilder
 9     definition.addPropertyValue("url", getUrl(attributes));
10     definition.addPropertyValue("path", getPath(attributes));
11     String name = getName(attributes);
12     definition.addPropertyValue("name", name);
13     String contextId = getContextId(attributes);
14     definition.addPropertyValue("contextId", contextId);
15     definition.addPropertyValue("type", className);
16     definition.addPropertyValue("decode404", attributes.get("decode404"));
17     definition.addPropertyValue("fallback", attributes.get("fallback"));
18     definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
19     definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
20 
21     // bean 的別名,demo-consumerFeignClient
22     String alias = contextId + "FeignClient";
23     AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
24     // bean 的型別,就是 FeignClient 介面
25     beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
26 
27     // has a default, won't be null
28     boolean primary = (Boolean) attributes.get("primary");
29     beanDefinition.setPrimary(primary);
30 
31     // 自定義的別名標識
32     String qualifier = getQualifier(attributes);
33     if (StringUtils.hasText(qualifier)) {
34         alias = qualifier;
35     }
36 
37     // 將資訊都封裝到 BeanDefinitionHolder
38     BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
39     // 註冊bean
40     BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
41 }

4、一張圖總結 @FeignClient 介面掃描流程

下面用一張圖來總結下 @FeignClient 介面是如何被掃描並註冊到容器中的。

  • 首先我們在程式碼中開發了 FeignClient 客戶端呼叫介面,並用 @FeignClient 註解,注意 @FeignClient 只能加到介面上面。
  • 之後我們需要在啟動類或配置類中加一個 @EnableFeignClients 註解來啟用 FeignClien。@EnableFeignClients 其實就是匯入了 FeignClient 註冊器 FeignClientsRegistrar。
  • FeignClientsRegistrar 實現了 ImportBeanDefinitionRegistrar 介面,在 registerBeanDefinitions 實現中,主要有兩步:
    • 註冊全域性配置配置類,就是 @EnableFeignClients 中指定的 defaultConfiguration
    • 接著就是掃描註冊 FeignClient
  • 註冊客戶端時,先用 ClassPathScanningCandidateComponentProvider 掃描器掃描出配置的包下的 @FeignClient 註解的介面
  • 掃描到 @FeignClient 介面後,先註冊客戶端特定的配置,就是 @FeignClient 配置的 configuration。
  • 接著註冊客戶端:
    • 先構建一個 BeanDefinitionBuilder,要建立的 BeanDefinition 型別是 FeignClientFactoryBean。
    • 然後就是將 @FeignClient 中的配置設定到 BeanDefinitionBuilder,其實就是設定給 FeignClientFactoryBean。
    • 之後解析出 FeignClient 的別名,預設是 服務名+“FeignClient”。
    • 再用 BeanDefinitionBuilder 構建出 BeanDefinition,並將相關資訊封裝到 BeanDefinitionHolder 中。
    • 最後使用 BeanDefinitionReaderUtils 完成 BeanDefinition 的註冊。
    • 將 BeanDefinition 注入容器後,就會呼叫 FeignClientFactoryBean 的 getObject 方法來建立動態代理。

三、構建 @FeignClient 介面動態代理

1、構造 FeignClient 的動態代理元件 FeignClientFactoryBean

FeignClientFactoryBean 這個元件就是生成 FeignClient 介面動態代理的元件。

FeignClientFactoryBean 實現了 FactoryBean 介面,當一個Bean實現了 FactoryBean 介面後,Spring 會先例項化這個工廠,然後呼叫 getObject() 建立真正的Bean。

1 class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
2 
3 }

FeignClientFactoryBean 實現了 getObject 方法,它又呼叫了 getTarget 方法,getTarget 最後就建立了 FeignClient 介面的動態代理物件。

建立動態代理物件的主要流程如下:

  • 首先獲取了 Feign 上下文 FeignContext,FeignContext 跟 Ribbon 中 SpringClientFactory 是類似的,可以獲取到每個服務的上下文。因為每個服務都有自己的配置、Encoder、Decoder 元件等,所以可以從 FeignContext 中獲取到當前服務的元件。
  • 然後從 FeignContext 中得到了 Feign.Builder,這個 Feign.Builder 就是最終用來建立動態代理物件的構造器。
  • @FeignClient 如果沒有配置 url,就會通過服務名稱構造帶服務名的url地址,跟 RestTemplate 類似,最終肯定就是走負載均衡的請求;如果配置了 url,就是直接呼叫這個地址。
  • 都會從 FeignContext 中獲取一個 Client,如果配置了 url,就是獲取 client 裡的代理物件,並設定到 builder 中;否則就直接將 Client 設定到 builder。也就是說根據 url 判斷是否使用負載均衡的 Client。
  • 最終都會呼叫 Targeter 的 target 方法來構造動態代理物件,target 傳入的引數包括當前的 FeignClientFactoryBean 物件、Feign.Builder、FeignContext,以及封裝的 HardCodedTarget 物件。
 1 // 獲取 FeignClient 代理物件的入口
 2 @Override
 3 public Object getObject() throws Exception {
 4     return getTarget();
 5 }
 6 
 7 /**
 8  * 建立一個 FeignClient 介面的代理物件,T 就是 @FeignClient 註解的介面型別
 9  *
10  * @param <T> the target type of the Feign client
11  * @return a {@link Feign} client created with the specified data and the context information
12  */
13 <T> T getTarget() {
14     // Feign 上下文
15     FeignContext context = applicationContext.getBean(FeignContext.class);
16     // Feign 構造器
17     Feign.Builder builder = feign(context);
18 
19     // 如果沒有直接配置 url,就走負載均衡請求
20     if (!StringUtils.hasText(url)) {
21         if (!name.startsWith("http")) {
22             url = "http://" + name;
23         }
24         else {
25             url = name;
26         }
27         // 帶服務名的地址 => http://demo-consumer
28         url += cleanPath();
29         // 返回的型別肯定是具備負載均衡能力的;HardCodedTarget => 硬編碼的 Target
30         return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
31     }
32 
33     // 如果配置了 url,就直接請求 url 地址
34     if (StringUtils.hasText(url) && !url.startsWith("http")) {
35         url = "http://" + url;
36     }
37     String url = this.url + cleanPath();
38     // Client => Feign 發起 HTTP 呼叫的核心元件
39     Client client = getOptional(context, Client.class);
40     if (client != null) {
41         if (client instanceof LoadBalancerFeignClient) {
42             // 得到的是代理物件,就是原生的 Client.Default
43             client = ((LoadBalancerFeignClient) client).getDelegate();
44         }
45         if (client instanceof FeignBlockingLoadBalancerClient) {
46             // 得到的是代理物件,就是原生的 Client.Default
47             client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
48         }
49         builder.client(client);
50     }
51     Targeter targeter = get(context, Targeter.class);
52     // targeter 建立動態代理物件
53     return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
54 }
 1 protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
 2     // 獲取 Client
 3     Client client = getOptional(context, Client.class);
 4     if (client != null) {
 5         builder.client(client);
 6         // Targeter => HystrixTargeter
 7         Targeter targeter = get(context, Targeter.class);
 8         // targeter 建立動態代理物件
 9         return targeter.target(this, builder, context, target);
10     }
11 
12     throw new IllegalStateException(
13             "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?");
14 }

2、Feign 動態代理構造器 Feign.Builder

feign() 方法返回了 Feign.Builder,它也是從 FeignContext 中獲取的,這個方法最重要的是設定了 Logger、Encoder、Decoder、Contract,並讀取配置檔案中 feign.client.* 相關的配置。FeignClientsConfiguration 中配置了這幾個介面的預設實現類,我們也可以自定義這幾個實現類。

 1 protected Feign.Builder feign(FeignContext context) {
 2     FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
 3     Logger logger = loggerFactory.create(type);
 4 
 5     // 我們可以定製 Logger、Encoder、Decoder、Contract
 6     Feign.Builder builder = get(context, Feign.Builder.class)
 7             // required values
 8             .logger(logger)
 9             .encoder(get(context, Encoder.class))
10             .decoder(get(context, Decoder.class))
11             .contract(get(context, Contract.class));
12     // @formatter:on
13 
14     // 讀取配置檔案中 feign.client.* 的配置來配置 Feign
15     configureFeign(context, builder);
16 
17     return builder;
18 }

Feign.Builder 的預設實現是什麼呢?從 FeignClientsConfiguration 中可以知道,預設情況下就是 Feign.Builder,如果啟用了 feign.hystrix.enabled,那預設實現就是 HystrixFeign.Builder。

那 Feign.Builder 和 HystrixFeign.Build 有什麼區別呢?對比下不難發現,主要區別就是建立動態代理的實現類 InvocationHandler 是不同的,在啟用 hystrix 的情況下,會涉及到熔斷、降級等,HystrixFeign.Build 也會設定 @FeignClient 配置的 fallback、fallbackFactory 降級配置類。這塊等後面分析 hystrix 原始碼時再來看。現在只需要知道,feign 沒有啟用 hystrix,@FeignClient 配置的 fallback、fallbackFactory 降級回撥是不生效的。

 1 public class FeignClientsConfiguration {
 2 
 3     @Bean
 4     @ConditionalOnMissingBean
 5     public Retryer feignRetryer() {
 6         // 從不重試
 7         return Retryer.NEVER_RETRY;
 8     }
 9 
10     @Bean
11     @Scope("prototype")
12     @ConditionalOnMissingBean
13     public Feign.Builder feignBuilder(Retryer retryer) {
14         // 預設為 Feign.Builder
15         return Feign.builder().retryer(retryer);
16     }
17 
18     @Configuration(proxyBeanMethods = false)
19     @ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
20     protected static class HystrixFeignConfiguration {
21 
22         // 引入了 hystrix 並且,feign.hystrix.enabled = true
23         @Bean
24         @Scope("prototype")
25         @ConditionalOnMissingBean
26         @ConditionalOnProperty(name = "feign.hystrix.enabled")
27         public Feign.Builder feignHystrixBuilder() {
28             // feign 啟用 hystrix 後,Feign.Builder 就是 HystrixFeign.Builder
29             return HystrixFeign.builder();
30         }
31     }
32 }

configureFeign 就是配置 Feign.Builder 的,從這個方法可以瞭解到,feign 配置生效的優先順序。

Feign 有三塊配置,一個是可以通過 Configuration 的方式配置,然後設定到 @FeignClient 的 configuration 引數;然後是全域性的 feign.client.default 預設配置,以及服務特定的配置 feign.client.<clientName>。

從 configureFeign 方法可以看出,預設情況下,優先順序最低的是程式碼配置,其次是預設配置,最高優先順序的是服務特定的配置。

如果想使程式碼配置優先順序高於檔案中的配置,可以設定 feign.client.defalut-to-properties=false 來改變 Feign 配置生效的優先順序。

 1 protected void configureFeign(FeignContext context, Feign.Builder builder) {
 2     // 配置檔案中 feign.client.* 客戶端配置
 3     FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
 4 
 5     FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
 6     setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
 7 
 8     if (properties != null && inheritParentContext) {
 9         // defaultToProperties:優先使用配置檔案中的配置
10         if (properties.isDefaultToProperties()) {
11             // 最低優先順序:使用程式碼中的 Configuration 配置
12             configureUsingConfiguration(context, builder);
13             // 次優先順序:使用 feign.client.default 預設配置
14             configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
15             // 高優先順序:使用 feign.client.<clientName> 定義的配置
16             configureUsingProperties(properties.getConfig().get(contextId), builder);
17         }
18         // 優先使用Java程式碼的配置
19         else {
20             configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
21             configureUsingProperties(properties.getConfig().get(contextId), builder);
22             configureUsingConfiguration(context, builder);
23         }
24     }
25     else {
26         configureUsingConfiguration(context, builder);
27     }
28 }

3、Feign 網路呼叫元件 Client

Client 是 feign-core 中的元件,它只有一個介面 execute,這個介面就是呼叫 Request 的 url,然後將返回介面封裝到 Response 中。

 1 public interface Client {
 2 
 3   /**
 4    * Executes a request against its {@link Request#url() url} and returns a response.
 5    *
 6    * @param request safe to replay.
 7    * @param options options to apply to this request.
 8    * @return connected response, {@link Response.Body} is absent or unread.
 9    * @throws IOException on a network error connecting to {@link Request#url()}.
10    */
11   Response execute(Request request, Options options) throws IOException;
12 }

Client 有如下的一些實現類:

Client 的自動化配置類是 FeignRibbonClientAutoConfiguration,FeignRibbonClientAutoConfiguration 匯入了 HttpClient、OkHttp 以及預設的 Feign 負載均衡配置類。

 1 @ConditionalOnClass({ ILoadBalancer.class, Feign.class })
 2 @ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled", matchIfMissing = true)
 3 @Configuration(proxyBeanMethods = false)
 4 @AutoConfigureBefore(FeignAutoConfiguration.class)
 5 @EnableConfigurationProperties({ FeignHttpClientProperties.class })
 6 @Import({ HttpClientFeignLoadBalancedConfiguration.class,
 7         OkHttpFeignLoadBalancedConfiguration.class,
 8         DefaultFeignLoadBalancedConfiguration.class })
 9 public class FeignRibbonClientAutoConfiguration {
10 }

① 啟用 apache httpclient

從 HttpClientFeignLoadBalancedConfiguration 的配置可以看出,要啟用 apache httpclient,需設定 feign.httpclient.enabled=true(預設為 true),並且需要加入了 feign-httpclient 的依賴(ApacheHttpClient)

啟用 apache httpclient 後,LoadBalancerFeignClient 的代理物件就是 feign-httpclient 中的 ApacheHttpClient。

 1 @Configuration(proxyBeanMethods = false)
 2 @ConditionalOnClass(ApacheHttpClient.class)
 3 @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
 4 @Import(HttpClientFeignConfiguration.class)
 5 class HttpClientFeignLoadBalancedConfiguration {
 6 
 7     @Bean
 8     @ConditionalOnMissingBean(Client.class)
 9     public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
10             SpringClientFactory clientFactory, HttpClient httpClient) {
11         ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
12         return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
13     }
14 
15 }

② 啟用 okhttp

從 OkHttpFeignLoadBalancedConfiguration  的配置可以看出,要啟用 okhttp,需設定 feign.okhttp.enabled=true,且需要引入 feign-okhttp 的依賴(OkHttpClient)。

啟用 okhttp 後,LoadBalancerFeignClient 的代理物件就是 feign-okhttp 的 OkHttpClient。

 1 @Configuration(proxyBeanMethods = false)
 2 @ConditionalOnClass(OkHttpClient.class)
 3 @ConditionalOnProperty("feign.okhttp.enabled")
 4 @Import(OkHttpFeignConfiguration.class)
 5 class OkHttpFeignLoadBalancedConfiguration {
 6 
 7     @Bean
 8     @ConditionalOnMissingBean(Client.class)
 9     public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
10             SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
11         OkHttpClient delegate = new OkHttpClient(okHttpClient);
12         return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
13     }
14 
15 }

③ 預設配置

沒有引入 feign-httpclient 或者 feign-okhttp,就會走預設的 DefaultFeignLoadBalancedConfiguration。而預設的代理物件 Client.Default 其實就是使用 HttpURLConnection 發起 HTTP 呼叫。

 1 @Configuration(proxyBeanMethods = false)
 2 class DefaultFeignLoadBalancedConfiguration {
 3 
 4     @Bean
 5     @ConditionalOnMissingBean
 6     public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
 7             SpringClientFactory clientFactory) {
 8         return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
 9                 clientFactory);
10     }
11 
12 }

可以看出,三個配置類建立的 Client 物件都是 LoadBalancerFeignClient,也就是支援負載均衡的請求。只是代理類不同,也就是最終發起 HTTP 呼叫的元件是不同的,預設配置下的代理類是 Client.Default,底層就是 HttpURLConnection。

這塊其實跟分析 Ribbon 原始碼時,RestTemplate 的負載均衡是類似的。

4、動態代理目標器 Targeter

Targeter 介面只有一個介面方法,就是通過 target 方法獲取動態代理物件。Targeter 有 DefaultTargeter、HystrixTargeter 兩個實現類,

1 interface Targeter {
2 
3     <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
4             FeignContext context, Target.HardCodedTarget<T> target);
5 }

在 FeignAutoConfiguration 配置類中可看到,只要引入了 HystrixFeign,Targeter 的預設實現就是 HystrixTargeter。

HystrixTargeter 一看就是用來整合 feign 和 hystrix 的,使 feign 呼叫可以實現熔斷、限流、降級。

 1 public class FeignAutoConfiguration {
 2 
 3     @Configuration(proxyBeanMethods = false)
 4     @ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
 5     protected static class HystrixFeignTargeterConfiguration {
 6 
 7         @Bean
 8         @ConditionalOnMissingBean
 9         public Targeter feignTargeter() {
10             return new HystrixTargeter();
11         }
12 
13     }
14 
15     @Configuration(proxyBeanMethods = false)
16     @ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
17     protected static class DefaultFeignTargeterConfiguration {
18 
19         @Bean
20         @ConditionalOnMissingBean
21         public Targeter feignTargeter() {
22             return new DefaultTargeter();
23         }
24 
25     }
26 
27 }

可以看到 HystrixTargeter 和 DefaultTargeter 的區別就在於 HystrixTargeter  會向 Feign.Builder 設定降級回撥處理類,這樣 feign 呼叫觸發熔斷、降級時,就可以進入回撥類處理。

它們本質上最終來說都是呼叫 Feign.Builder 的 target 方法建立動態代理物件。

 1 class HystrixTargeter implements Targeter {
 2 
 3     @Override
 4     public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
 5                         FeignContext context, Target.HardCodedTarget<T> target) {
 6         if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
 7             // 非 HystrixFeign.Builder 型別,就直接呼叫 target 方法
 8             return feign.target(target);
 9         }
10         // Feign 啟用了 hystrix 後,就會向 HystrixFeign.Builder 設定回撥類或回撥工廠
11         feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
12         String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName() : factory.getContextId();
13         SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
14         if (setterFactory != null) {
15             builder.setterFactory(setterFactory);
16         }
17         Class<?> fallback = factory.getFallback();
18         // 設定回撥類
19         if (fallback != void.class) {
20             return targetWithFallback(name, context, target, builder, fallback);
21         }
22         // 設定回撥工廠類
23         Class<?> fallbackFactory = factory.getFallbackFactory();
24         if (fallbackFactory != void.class) {
25             return targetWithFallbackFactory(name, context, target, builder, fallbackFactory);
26         }
27 
28         return feign.target(target);
29     }
30 
31 }
1 class DefaultTargeter implements Targeter {
2 
3     @Override
4     public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
5             FeignContext context, Target.HardCodedTarget<T> target) {
6         return feign.target(target);
7     }
8 }

5、Feign.Builder 建立動態代理

前面已經分析出,Feign.Builder 的預設實現就是 Feign.Builder,HystrixTargeter 中呼叫了 Feign.Builder 的 target 方法來建立動態代理。

  • target 方法中首先呼叫 build() 方法構建出 Feign,然後呼叫 Feign 的 newInstance 建立動態代理物件。
  • build() 方法中首先讀取配置的 Client、Retryer、Logger、Contract、Encoder、Decoder 等物件。
  • 然後獲取了 InvocationHandlerFactory,預設就是 InvocationHandlerFactory.Default,這是 feign 提供的一個工廠類來建立代理物件 InvocationHandler。
  • 接著建立了介面方法處理器工廠 SynchronousMethodHandler.Factor,它就是用來將介面方法封裝成一個方法執行器 MethodHandler,預設實現類是 SynchronousMethodHandler。
  • 還建立了 springmvc 註解處理器 ParseHandlersByName,可想而知,這就是用來處理介面中的 springmvc 註解的,將 REST 介面解析生成 MethodHandler。
  • 最後建立了 Feign 物件,實現類是 ReflectiveFeign,之後就是使用 ReflectiveFeign 來建立動態代理物件了。
 1 public <T> T target(Target<T> target) {
 2   return build().newInstance(target);
 3 }
 4 
 5 // 構建 Feign
 6 public Feign build() {
 7     // Feign Http呼叫客戶端,預設為 Client.Default
 8     Client client = Capability.enrich(this.client, capabilities);
 9     // 重試器,預設是重不重試
10     Retryer retryer = Capability.enrich(this.retryer, capabilities);
11     // Feign 請求攔截器,可以對 Feign 請求模板RequestTemplate做一些定製化處理
12     List<RequestInterceptor> requestInterceptors = this.requestInterceptors.stream()
13       .map(ri -> Capability.enrich(ri, capabilities))
14       .collect(Collectors.toList());
15     // 日誌元件,預設為 Slf4jLogger      
16     Logger logger = Capability.enrich(this.logger, capabilities);
17     // 介面協議元件,預設為 SpringMvcContract
18     Contract contract = Capability.enrich(this.contract, capabilities);
19     // 配置類
20     Options options = Capability.enrich(this.options, capabilities);
21     // 編碼器
22     Encoder encoder = Capability.enrich(this.encoder, capabilities);
23     // 解碼器
24     Decoder decoder = Capability.enrich(this.decoder, capabilities);
25     // 建立 InvocationHandler 的工廠類
26     InvocationHandlerFactory invocationHandlerFactory =
27       Capability.enrich(this.invocationHandlerFactory, capabilities);
28     QueryMapEncoder queryMapEncoder = Capability.enrich(this.queryMapEncoder, capabilities);
29     // 介面方法處理器工廠
30     SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
31       new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
32           logLevel, decode404, closeAfterDecode, propagationPolicy, forceDecoding);
33     // 解析 springmvc 註解          
34     ParseHandlersByName handlersByName =
35       new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
36           errorDecoder, synchronousMethodHandlerFactory);
37     // ReflectiveFeign          
38     return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
39 }

InvocationHandlerFactory 包含一個 create 介面方法,預設實現是 InvocationHandlerFactory.Default,返回的 InvocationHandler 型別是 ReflectiveFeign.FeignInvocationHandler。

 1 package feign;
 2 
 3 public interface InvocationHandlerFactory {
 4 
 5   // 建立動態代理
 6   InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch);
 7 
 8   // 方法處理器
 9   interface MethodHandler {
10 
11     Object invoke(Object[] argv) throws Throwable;
12   }
13 
14   static final class Default implements InvocationHandlerFactory {
15 
16     @Override
17     public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
18       return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
19     }
20   }
21 }

接著看 ReflectiveFeign 的 newInstance() 方法:

  • newInstance 的引數 target 就是前面封裝的 Target.HardCodedTarget,它封裝了客戶端的型別、url 等屬性。
  • 首先是使用 ParseHandlersByName 將 FeignClient 介面中的介面轉換成 MethodHandler,實際型別就是 SynchronousMethodHandler,這個細節就不在看了。
  • 然後用 InvocationHandlerFactory 建立 InvocationHandler 代理物件,也就是 ReflectiveFeign.FeignInvocationHandler,呼叫動態代理物件的方法,最終都會進入到這個執行處理器裡面。
  • 最後,終於看到建立動態代理的地方了,使用 Proxy 建立了 FeignClient 的動態代理物件,這個動態代理的型別就是 @FeignClient 註解的介面的型別。最後被注入到 IoC 容器後,就可以在程式碼中注入自己編寫的 FeignClient 客戶端元件了。

最終就是通過 Proxy 建立一個實現了 FeignClient 介面的動態代理,然後所有介面方法的呼叫都會被 FeignInvocationHandler 攔截處理。

 1 public <T> T newInstance(Target<T> target) {
 2     // 使用 ParseHandlersByName 將 FeignClient 介面中的介面轉換成 MethodHandler,springmvc 註解由 Contract 元件處理
 3     // MethodHandler => SynchronousMethodHandler
 4     Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
 5     Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
 6     List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
 7 
 8     // 轉換成 Method - MethodHandler 對映
 9