• 欢迎访问开心洋葱网站,在线教程,推荐使用最新版火狐浏览器和Chrome浏览器访问本网站,欢迎加入开心洋葱 QQ群
  • 为方便开心洋葱网用户,开心洋葱官网已经开启复制功能!
  • 欢迎访问开心洋葱网站,手机也能访问哦~欢迎加入开心洋葱多维思维学习平台 QQ群
  • 如果您觉得本站非常有看点,那么赶紧使用Ctrl+D 收藏开心洋葱吧~~~~~~~~~~~~~!
  • 由于近期流量激增,小站的ECS没能经的起亲们的访问,本站依然没有盈利,如果各位看如果觉着文字不错,还请看官给小站打个赏~~~~~~~~~~~~~!

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

JAVA相关 开心洋葱 3026次浏览 0个评论

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 的核心组件。

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

② 在 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。

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

 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 的核心组件了,对应的默认实现类如下。

SpringCloud 源码系列(6)—— 声明式服务调用 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 方法来创建动态代理。

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

三、构建 @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 有如下的一些实现类:

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

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 }

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

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。

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

四、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 上的占位符就被参数替换了,并且拼接了请求参数。

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

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() => http://demo-producer
4     // input.target 处理请求模板
5     input.target(url());
6   }
7   return input.request();
8 }

可以看到经过 HardCodedTarget 的 apply 方法之后,就拼接上了 url 前缀了。

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

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 解析响应结果,返回接口方法定义的返回类型。

SpringCloud 源码系列(6)—— 声明式服务调用 Feign

负载均衡获取Server的核心组件是 LoadBalancerClient,具体的源码分析可以参考 Ribbon 源码分析的两篇文章。LoadBalancerClient 负载均衡的原理可以看下面这张图。

SpringCloud 源码系列(6)—— 声明式服务调用 Feign


开心洋葱 , 版权所有丨如未注明 , 均为原创丨未经授权请勿修改 , 转载请注明SpringCloud 源码系列(6)—— 声明式服务调用 Feign
喜欢 (0)

您必须 登录 才能发表评论!

加载中……