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地址:

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 = "//" + name;
23         }
24         else {
25             url = name;
26         }
27         // 帶服務名的地址 => //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 = "//" + 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     for (Method method : target.type().getMethods()) {
10       if (method.getDeclaringClass() == Object.class) {
11         continue;
12       } else if (Util.isDefault(method)) {
13         DefaultMethodHandler handler = new DefaultMethodHandler(method);
14         defaultMethodHandlers.add(handler);
15         methodToHandler.put(method, handler);
16       } else {
17         methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));
18       }
19     }
20     // 用 SynchronousMethodHandler.Factory 創建 SynchronousMethodHandler
21     InvocationHandler handler = factory.create(target, methodToHandler);
22     // 用 Proxy 創建動態代理,動態代理對象就是 SynchronousMethodHandler
23     T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
24         new Class<?>[] {target.type()}, handler);
25 
26     for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
27       defaultMethodHandler.bindTo(proxy);
28     }
29     return proxy;
30 }

6、一張圖總結 FeignClient 生成動態代理的流程

下面用一張圖來總結下生成 FeignClient 動態代理的流程:

  • 首先 @EnableFeignClients 導入的註冊器 FeignClientsRegistrar 會掃描 @FeignClient 註解的介面,並生成 FeingClientFactoryBean 的 BeanDefinition 註冊到容器中。最後會調用 FeingClientFactoryBean 的 getObject 方法來獲取介面的動態代理對象。
  • 進入  FeingClientFactoryBean 的 getObject 方法,首先獲取了 FeignContext,它其實就是每個客戶端的容器,類似於一個 Map 結構,快取了客戶端與容器間的關係,後續大部分組件都是從 FeignContext 中獲取。
  • 從 FeignContext 中獲取 Feign 構造器 Feign.Builder,並配置 Feign.Builder,配置來源有多個地方,優先順序最高的是 application.yml 中的配置生效;也可以配置 feign.client.default-to-properties=false 設置Java程式碼配置為高優先順序。
  • 接下來就要根據 @FeignClient 是否配置了 url 決定是否走負載均衡的請求,其實就是設置的 Client 不一樣:
    • 如果配置了 url,表示一個具體的地址,就使用將 LoadBalancerFeignClient 的 delegate 作為 Client 設置給 Feign.Builder。
    • 如果沒有配置 url,表示通過服務名請求,就將 LoadBalancerFeignClient 作為 Client 設置給 Feign.Builder。
  • 再從 FeignContext 中獲取 Targeter,調用它的 target 方法來獲取動態代理。
  • 在 target 方法中,先調用 Feign.Builder 的 build() 方法構建了 ReflectiveFeign:
    • 先是獲取代理對象工廠 InvocationHandlerFactory,用於創建 InvocationHandler
    • 然後用各個組件,構造了方法處理器工廠 SynchronousMethodHandler.Factory,接著創建了方法解析器 ParseHandlersByName
    • 最後基於 InvocationHandlerFactory 和 ParseHandlersByName 構造了 ReflectiveFeign
  • 最後調用  ReflectiveFeign 的 newInstance 方法反射創建介面的動態代理:
    • 先用方法解析器 ParseHandlersByName 解析介面,將介面解析成 SynchronousMethodHandler
    • 接著使用 InvocationHandlerFactory 創建了代理對象 InvocationHandler(ReflectiveFeign.FeignInvocationHandler)
    • 最終用 Proxy 創建動態代理對象,對象的類型就是介面的類型,代理對象就是 ReflectiveFeign.FeignInvocationHandler。

四、FeignClient 結合Ribbon進行負載均衡請求

上一節已經分析出,最終在 Feign.Builder 的 build 方法構建了 ReflectiveFeign,然後利用 ReflectiveFeign 的 newInstance 方法創建了動態代理。這個動態代理的代理對象是 ReflectiveFeign.FeignInvocationHandler。最終來說肯定就會利用 Client 進行負載均衡的請求。這節就來看看 Feign 如果利用動態代理髮起HTTP請求的。

1、FeignClient 動態代理請求

使用 FeignClient 介面時,注入的其實是動態代理對象,調用介面方法時就會進入執行器 ReflectiveFeign.FeignInvocationHandler,從 FeignInvocationHandler 的 invoke 方法可以看出,就是根據 method 獲取要執行的方法處理器 MethodHandler,然後執行方法。MethodHandler 的實際類型就是 SynchronousMethodHandler。

 1 static class FeignInvocationHandler implements InvocationHandler {
 2     private final Target target;
 3     private final Map<Method, MethodHandler> dispatch;
 4 
 5     FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {
 6       this.target = checkNotNull(target, "target");
 7       this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);
 8     }
 9 
10     @Override
11     public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
12         //...
13         // 根據 method 獲取 MethodHandler,然後執行方法
14         return dispatch.get(method).invoke(args);
15     }
16 }

接著看 SynchronousMethodHandler 的 invoke 方法,核心邏輯就兩步:

  • 先根據請求參數構建請求模板 RequestTemplate,就是處理 URI 模板、參數,比如替換掉 uri 中的佔位符、拼接參數等。
  • 然後調用了 executeAndDecode 執行請求,並將相應結果解碼返回。
 1 public Object invoke(Object[] argv) throws Throwable {
 2     // 構建請求模板,例如有 url 參數,請求參數之類的
 3     RequestTemplate template = buildTemplateFromArgs.create(argv);
 4     Options options = findOptions(argv);
 5     Retryer retryer = this.retryer.clone();
 6     while (true) {
 7       try {
 8         // 執行並解碼
 9         return executeAndDecode(template, options);
10       } catch (RetryableException e) {
11         // 重試,默認是從不重試
12         try {
13           retryer.continueOrPropagate(e);
14         } catch (RetryableException th) {
15           Throwable cause = th.getCause();
16           if (propagationPolicy == UNWRAP && cause != null) {
17             throw cause;
18           } else {
19             throw th;
20           }
21         }
22         if (logLevel != Logger.Level.NONE) {
23           logger.logRetry(metadata.configKey(), logLevel);
24         }
25         continue;
26       }
27     }
28 }

可以看到,經過處理後,URI 上的佔位符就被參數替換了,並且拼接了請求參數。

2、執行請求 executeAndDecode

接著看 executeAndDecode,主要有三步:

  • 先調用 targetRequest 方法,主要就是遍歷 RequestInterceptor 對請求模板 RequestTemplate 訂製化,然後調用 HardCodedTarget 的 target 方法將 RequestTemplate 轉換成 Request 請求對象,Request 封裝了請求地址、請求頭、body 等資訊。
  • 然後使用客戶端 client 來執行請求,就是 LoadBalancerFeignClient,這裡就進入了負載均衡請求了。
  • 最後用解碼器 decoder 來解析響應結果,將結果轉換成介面的返回類型。
 1 Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
 2     // 處理RequestTemplate,得到請求對象 Request
 3     Request request = targetRequest(template);
 4 
 5     Response response;
 6     try {
 7       // 調用 client 執行請求,client => LoadBalancerFeignClient
 8       response = client.execute(request, options);
 9       // 構建響應 Response
10       response = response.toBuilder()
11           .request(request)
12           .requestTemplate(template)
13           .build();
14     } catch (IOException e) {
15       //...
16     }
17 
18     if (decoder != null) {
19       // 使用解碼器解碼,將返回數據轉換成介面的返回類型
20       return decoder.decode(response, metadata.returnType());
21     }  
22 
23     //....
24 }
25 // 應用攔截器處理 RequestTemplate,最後使用 target 從 RequestTemplate 中得到 Request 
26 Request targetRequest(RequestTemplate template) {
27     for (RequestInterceptor interceptor : requestInterceptors) {
28       interceptor.apply(template);
29     }
30     // target => HardCodedTarget
31     return target.apply(template);
32 }

HardCodedTarget 是硬編碼寫死的,我們沒有辦法訂製化,看下它的 apply 方法,主要就是處理 RequestTemplate 模板的地址,生成完成的請求地址。最後返回 Request 請求對象。

1 public Request apply(RequestTemplate input) {
2   if (input.url().indexOf("http") != 0) {
3     // url() => //demo-producer
4     // input.target 處理請求模板
5     input.target(url());
6   }
7   return input.request();
8 }

可以看到經過 HardCodedTarget 的 apply 方法之後,就拼接上了 url 前綴了。

3、LoadBalancerFeignClient 負載均衡

LoadBalancerFeignClient 是 Feign 實現負載均衡核心的組件,是 Feign 網路請求組件 Client 的默認實現,LoadBalancerFeignClient 最後是使用 FeignLoadBalancer 來進行負載均衡的請求。

看 LoadBalancerFeignClient 的 execute 方法,從這裡到後面執行負載均衡請求,其實跟分析 Ribbon 源碼中 RestTemplate 的負載均衡請求都是類似的了。

  • 可以看到也是先將請求封裝到 ClientRequest,實現類是 FeignLoadBalancer.RibbonRequest。注意 RibbonRequest 第一個參數 Client 就是設置的 LoadBalancerFeignClient 的代理對象,啟用 apache httpclient 時,就是 ApacheHttpClient。
  • 然後獲取客戶端配置,也就是說 Ribbon 的客戶端配置對 Feign 通用生效。
  • 最後獲取了負載均衡器 FeignLoadBalancer,然後執行負載均衡請求。
 1 public Response execute(Request request, Request.Options options) throws IOException {
 2     try {
 3         URI asUri = URI.create(request.url());
 4         // 客戶端名稱:demo-producer
 5         String clientName = asUri.getHost();
 6         URI uriWithoutHost = cleanUrl(request.url(), clientName);
 7         // 封裝 ClientRequest => FeignLoadBalancer.RibbonRequest
 8         FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
 9                 this.delegate, request, uriWithoutHost);
10         // 客戶端負載均衡配置 ribbon.demo-producer.*
11         IClientConfig requestConfig = getClientConfig(options, clientName);
12         // lbClient => 負載均衡器 FeignLoadBalancer,執行負載均衡請求
13         return lbClient(clientName)
14                 .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
15     }
16     catch (ClientException e) {
17         //...
18     }
19 }
20 
21 private FeignLoadBalancer lbClient(String clientName) {
22     return this.lbClientFactory.create(clientName);
23 }

進入 executeWithLoadBalancer 方法,這就跟 Ribbon 源碼中分析的是一樣的了,最終就驗證了 Feign 基於 Ribbon 來做負載均衡請求。

 1 public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
 2     // 負載均衡器執行命令
 3     LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig);
 4 
 5     try {
 6         return command.submit(
 7             new ServerOperation<T>() {
 8                 @Override
 9                 public Observable<T> call(Server server) {
10                     // 用Server的資訊重構URI地址
11                     URI finalUri = reconstructURIWithServer(server, request.getUri());
12                     S requestForServer = (S) request.replaceUri(finalUri);
13                     try {
14                         // 實際調用 LoadBalancerFeignClient 的 execute 方法
15                         return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
16                     } 
17                     catch (Exception e) {
18                         return Observable.error(e);
19                     }
20                 }
21             })
22             .toBlocking()
23             .single();
24     } catch (Exception e) {
25         //....
26     }
27 }

重構URI後,實際是調用 FeignLoadBalancer 的 execute 方法來執行最終的HTTP調用的。看下 FeignLoadBalancer 的 execute 方法,最終來說,就是使用代理的HTTP客戶端來執行請求。

默認情況下,就是 Client.Default,用 HttpURLConnection 執行HTTP請求;啟用了 httpclient 後,就是 ApacheHttpClient;啟用了 okhttp,就是 OkHttpClient。

這裡有一點需要注意的是,FeignClient 雖然可以配置超時時間,但進入 FeignLoadBalancer 的 execute 方法後,可以看到會用 Ribbon 的超時時間覆蓋 Feign 配置的超時時間,最終以 Ribbon 的超時時間為準。

 1 public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride) throws IOException {
 2     Request.Options options;
 3     if (configOverride != null) {
 4         // 用 Ribbon 的超時時間覆蓋了feign配置的超時時間
 5         RibbonProperties override = RibbonProperties.from(configOverride);
 6         options = new Request.Options(override.connectTimeout(this.connectTimeout),
 7                 override.readTimeout(this.readTimeout));
 8     }
 9     else {
10         options = new Request.Options(this.connectTimeout, this.readTimeout);
11     }
12     // request.client() HTTP客戶端對象
13     Response response = request.client().execute(request.toRequest(), options);
14     return new RibbonResponse(request.getUri(), response);
15 }

4、一張圖總結 Feign 負載均衡請求

關於Ribbon的源碼分析請看前面 Ribbon 相關的兩篇文章,Ribbon 如何從 eureka 註冊中心獲取 Server 就不再分析了。

下面這張圖總結了 Feign 負載均衡請求的流程:

  • 首先服務啟動的時候會掃描解析 @FeignClient 註解的介面,並生成代理類注入到容器中。我們注入 @FeignClient 介面時其實就是注入的這個代理類。
  • 調用介面方法時,會被代理對象攔截,進入 ReflectiveFeign.FeignInvocationHandler 的 invoke 方法執行請求。
  • FeignInvocationHandler 會根據調用的介面方法獲取已經構建好的方法處理器 SynchronousMethodHandler,然後調用它的 invoke 方法執行請求。
  • 在 SynchronousMethodHandler 的 invoke 方法中,會先根據請求參數構建請求模板 RequestTemplate,這個時候會處理參數中的佔位符、拼接請求參數、處理body中的參數等等。
  • 然後將 RequestTemplate 轉成 Request,在轉換的過程中:
    • 先是用 RequestInterceptor 處理請求模板,因此我們可以自定義攔截器來訂製化 RequestTemplate。
    • 之後用 Target(HardCodedTarget)處理請求地址,拼接上服務名前綴。
    • 最後調用 RequestTemplate 的 request 方法獲取到 Request 對象。
  • 得到 Request 後,就調用 LoadBalancerFeignClient 的 execute 方法來執行請求並得到請求結果 Response:
    • 先構造 ClientRequest,並獲取到負載均衡器 FeignLoadBalancer,然後就執行負載均衡請求。
    • 負載均衡請求最終進入到 AbstractLoadBalancerAwareClient,executeWithLoadBalancer 方法中,會先構建一個 LoadBalancerCommand,然後提交一個 ServerOperation。
    • LoadBalancerCommand 會通過 LoadBalancerContext 根據服務名獲取一個 Server。
    • 在 ServerOperation 中根據 Server 的資訊重構URI,將服務名替換為具體的IP地址,之後就可以發起真正的HTTP調用了。
    • HTTP調用時,底層使用的組件默認是 HttpURLConnection;啟用了okhttp,就是 okhttp 的 OkHttpClient;啟用了 httpclient,就是 apache 的 HttpClient。
    • 最紅用 HTTP 客戶端組件執行請求,得到響應結果 Response。
  • 得到 Response 後,就使用解碼器 Decoder 解析響應結果,返回介面方法定義的返回類型。

負載均衡獲取Server的核心組件是 LoadBalancerClient,具體的源碼分析可以參考 Ribbon 源碼分析的兩篇文章。LoadBalancerClient 負載均衡的原理可以看下面這張圖。