概述

Feign 是由 Netflix 开源的声明式的 HTTP 客户端,目前已经捐献给 OpenFeign社区。

原生Feign

直接使用main方法来测试一把:

  1. import feign.Feign;
  2. import feign.Param;
  3. import feign.RequestLine;
  4. /**
  5. * @author weimaomao
  6. * @createTime 2021-01-31日
  7. * @Description TODO
  8. */
  9. public class TestNativeFeign {
  10. public static void main(String[] args) {
  11. // 创建 SayHelloAPI 对象
  12. SayHelloAPI sayHelloAPI = Feign.builder().target(SayHelloAPI.class,
  13. "http://localhost:8081");
  14. String product = sayHelloAPI.sayNiHao("hahaha");
  15. System.out.println(product);
  16. }
  17. interface SayHelloAPI {
  18. @RequestLine("GET /sayNiHao/{name}")
  19. String sayNiHao(@Param("name") String name);
  20. }
  21. }

我们发现,这样的写法需要把Feign 定义的 @RequestLine@Param 注解写到接口上!这样写很复杂。

SpringCloud集成feign

于是乎,Spring Cloud OpenFeign组件将 Feign 集成到 Spring Cloud 体系中,实现服务的声明式 HTTP 调用。相比使用 RestTemplate 实现服务的调用,Feign 简化了代码的编写,提高了代码的可读性,大大提升了开发的效率。

Spring Cloud OpenFeign 除了支持 Feign 自带的注解之外,额外提供了对 JAX-RS 注解、SpringMVC 注解的支持。特别是对 SpringMVC 注解的支持,简直是神来之笔,让我们不用学习 Feign 自带的注解,而直接使用超级熟悉的 SpringMVC 注解。

同时,Spring Cloud OpenFeign 进一步将 Feign 和 Ribbon 整合,提供了负载均衡的功能。另外,Feign 自身已经完成和 Hystrix 整合,提供了服务容错的功能。

如此,我们基于注解,极其简单的实现服务的调用,并且具有负载均衡、服务容错的功能。

环境搭建

1、整体架构:

项目根据eureka注册中心进行搭建。主要分为4个子项目:

3.1 细节分析 - 图1

注册中心:eureka-server

服务提供者:eureka-client

服务公共api:eureka-client-api

服务消费者:feign-client

2、搭建父工程

pom.xml文件

  1. <modelVersion>4.0.0</modelVersion>
  2. <packaging>pom</packaging>
  3. <parent>
  4. <groupId>org.springframework.boot</groupId>
  5. <artifactId>spring-boot-starter-parent</artifactId>
  6. <version>2.0.3.RELEASE</version>
  7. <relativePath/> <!-- lookup parent from repository -->
  8. </parent>
  9. <groupId>org.example</groupId>
  10. <artifactId>feign-demo</artifactId>
  11. <version>1.0-SNAPSHOT</version>
  12. <properties>
  13. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  14. </properties>
  15. <dependencyManagement>
  16. <dependencies>
  17. <dependency>
  18. <groupId>org.springframework.cloud</groupId>
  19. <artifactId>spring-cloud-dependencies</artifactId>
  20. <version>Finchley.RELEASE</version>
  21. <type>pom</type>
  22. <scope>import</scope>
  23. </dependency>
  24. </dependencies>
  25. </dependencyManagement>
  26. <modules>
  27. <module>feign-client</module>
  28. <module>eureka-server</module>
  29. <module>eureka-client</module>
  30. <module>eureka-client-api</module>
  31. </modules>

3、注册中心:eureka-server

pom.xml文件

  1. <parent>
  2. <artifactId>feign-demo</artifactId>
  3. <groupId>org.example</groupId>
  4. <version>1.0-SNAPSHOT</version>
  5. </parent>
  6. <modelVersion>4.0.0</modelVersion>
  7. <artifactId>eureka-server</artifactId>
  8. <dependencies>
  9. <dependency>
  10. <groupId>org.springframework.cloud</groupId>
  11. <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
  12. </dependency>
  13. </dependencies>

application.yml

  1. server:
  2. port: 8761
  3. spring:
  4. application:
  5. name: eureka-server
  6. eureka:
  7. client:
  8. # 表示是否将自己注册到Eureka Server,默认为true。
  9. register-with-eureka: false
  10. # 表示是否从Eureka Server获取注册信息,默认为true。
  11. fetch-registry: false
  12. # 设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka ;多个地址可使用,分隔
  13. service-url:
  14. defaultZone: http://localhost:${server.port}/eureka/

启动类

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

4、api提供者:eureka-client-api

pom.xml

    <parent>
        <artifactId>feign-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>eureka-client-api</artifactId>


    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

    </dependencies>

api接口

@FeignClient(value = "eureka-client")
public interface GreetingFeignApi {

    @GetMapping("/sayNiHao/{name}")
    String sayNiHao(@PathVariable("name") String name);

}

5、服务提供者:eureka-client

pom.xml

<parent>
    <artifactId>feign-demo</artifactId>
    <groupId>org.example</groupId>
    <version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>eureka-client</artifactId>


<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.example</groupId>
        <artifactId>eureka-client-api</artifactId>
        <version>${project.version}</version>
    </dependency>
</dependencies>

application.yml

spring:
  application:
    name: eureka-client # Spring 应用名

server:
  port: 8082  # 随机服务器端口。默认为 8080

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

启动类

/**
 * 注意:此处如果不需要fegin作为客户端的对外请求就不需要加入@EnableFeignClients
 */
@SpringBootApplication
@EnableEurekaClient
public class EurekaClientApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaClientApplication.class,args);
    }
}

controller

@RestController
public class GreetingFeginClient implements GreetingFeignApi {

    @Value("${server.port}")
    private int port;

    /**
     * 注意,此处必须加上@PathVariable等mvc的注解
     * @param name
     * @return
     */
    @Override
    public String sayNiHao(@PathVariable("name") String name) {
        System.out.println(port+"接收到了一次请求调用");
        return "hello, " + name;
    }
}

6、服务消费者:feign-client

pom.xml

    <parent>
        <artifactId>feign-demo</artifactId>
        <groupId>org.example</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <groupId>org.example</groupId>
    <artifactId>feign-client</artifactId>
    <version>1.0-SNAPSHOT</version>

    <name>feign-client</name>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>

        <dependency>
            <groupId>org.example</groupId>
            <artifactId>eureka-client-api</artifactId>
            <version>${project.version}</version>
        </dependency>

    </dependencies>

application.yml

spring:
  application:
    name: feign-client # Spring 应用名

server:
  port: 8083  # 随机服务器端口。默认为 8080

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8761/eureka/

启动类

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

服务调用

@RestController
public class FeignClientController {

    @Autowired
    private GreetingFeignApi greetingFeignApi;

    @GetMapping("feign/sayNiHao/{name}")
    public void sayNiHao(@PathVariable("name")String name){
        System.out.println("我是feign,去调用别的服务啦!");
        greetingFeignApi.sayNiHao(name);
    }

}

7、测试

a、先启动注册中心:eureka-server:8761

b、再启动两个服务提供者eureka-client:8081、8082

c、启动服务消费者:fegin-client:8083

d、访问两次服务消费者:http://localhost:8083/feign/sayNiHao/zhangsan

结果:

8081接收到了一次请求调用
8082接收到了一次请求调用

8、总结

  • 服务消费者使用 Feign 声明式调用服务服务提供者成功
  • 服务消费者使用 Ribbon 负载均衡成功
  • 服务消费者从注册中心加载服务服务提供者的服务实例成功

9、注意事项

1、一次请求消费多次的情况

在debug调试时, 消费者调用一次, 但是提供者收到多次请求, 原因时feign的超时时间, debug时间超过了超时时间,会触发feign的重试功能。 所以在调试时把超时时间设大一点。

ribbon:
  ConnectTimeout: 1000 # 请求的连接超时时间,单位:毫秒。默认为 1000
  ReadTimeout: 1000 # 请求的读取超时时间,单位:毫秒。默认为 1000
  OkToRetryOnAllOperations: true # 是否对所有操作都进行重试,默认为 false。
  MaxAutoRetries: 0 # 对当前服务的重试次数,默认为 0 次。
  MaxAutoRetriesNextServer: 1 # 重新选择服务实例的次数,默认为 1 次。注意,不包含第 1 次哈。

2、复杂参数:

@GetMapping("/get") // GET 方式一,最推荐
ResponseVO getDemo(@SpringQueryMap RequestParam request);

@GetMapping("/get") // GET 方式二,相对推荐
ResponseVO getDemo(@RequestParam("username") String username, @RequestParam("password") String password);

@GetMapping("/get") // GET 方式三,不推荐
ResponseVO getDemo(@RequestParam Map<String, Object> params);

@PostMapping("/post") // POST 方式
ResponseVO postDemo(@RequestBody RequestParam request);

GET场景

①【最推荐】方式一,采用 Spring Cloud OpenFeign 提供的 [@SpringQueryMap](https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/SpringQueryMap.java) 注解,并使用 DemoDTO 对象。

默认情况下,Feign 针对 POJO 类型的参数,即使我们声明为 GET 类型的请求,也会自动转换成 POST 类型的请求。如果我们去掉 @SpringQueryMap 注解,就会报如下异常:

feign.FeignException$MethodNotAllowed: status 405 reading DemoProviderFeignClient#getDemo(DemoDTO)
  • Feign 自动转换成了 POST /get 请求,而服务提供者提供的 /get 只支持 GET 类型,因此返回响应状态码为 405 的错误。

@SpringQueryMap 注解的作用,相当于 Feign 的 @QueryMap 注解,将 POJO 对象转换成 QueryString

②【较推荐】方式二,采用 SpringMVC 提供的 @RequestParam 注解,并将所有参数平铺开。

参数较少的时候,可以采用这种方式。如果参数过多的话,还是采用方式一更优。

③【不推荐】方式三,采用 SpringMVC 提供的 @RequestParam 注解,并使用 Map 对象。非常不推荐,因为可读性差,都不知道传递什么参数。

POST场景

唯一方式,采用 SpringMVC 提供的 @RequestBody 注解,并使用 DemoDTO 对象。

源码解析

Feign的主要组件

Encoder编码器

Encoder 接口,编码器,负责将一个对象转换成 HTTP 请求体。

spring cloud对feign的默认实现:SpringEncoder

Decoder解码器

Decoder 接口,解码器,负责将 HTTP 响应转换成一个对象。

spring cloud对feign的默认实现:ResponseEntityDecoder

Logger日志

Logger 抽象类,日志记录器,负责请求信息的日志打印。

spring cloud对feign的默认实现:Slf4jLogger

Contract契约

Contract 接口,契约,负责解析 API 接口的方法元数据,例如说注解、方法参数、方法返回类型等等。

比如:feign本来是没法支持spring web mvc(@PathVariable、@RequestMapping、@RequestParam等)注解的,但是有Contract契约组件之后,这个组件负责解释这些注解,让feign可以跟这些注解结合起来使用。

spring cloud对feign的默认实现:SpringMvcContract

Feign.Builder

Feign.Builder 类,Feign 构造器,可以设置各种配置,最终构建出指定 API 接口的 HTTP “客户端”。示例代码如下:

RemoteService service = Feign.builder()
            .options(new Options(1000, 5000)) // 请求的连接和读取超时时间
            .retryer(new Retryer.Default(5000, 5000, 3)) // 重试策略
            .target(RemoteService.class, "http://localhost:8080"); // 目标 API 接口和目标地址

spring cloud对feign的默认实现:HystrixFeign.Builder

FeignClient

Client 接口,定义提交 HTTP 请求的方法。它里面包含了一系列组件,比如说Encoder、Decoder、Logger、Contract等等。

spring cloud对feign的默认实现:LoadBalancerFeignClient,该实现类对 Ribbon 进行了集成。

原理图:

3.1 细节分析 - 图2

1、Feign整体分析

我们使用feign时就用了两个注解:@EnableFeignClients和@FeignClient,入口肯定就是这两个。

2、@EnableFeignClients入口分析

我们来思考,我们只定义了一个接口,feign是如何给我调用其他服务的接口发送http请求的呢?

我觉得fegin会给我生成了一个动态代理,让这个动态代理来给我去处理http请求了。

我们再来思考一下,feign中是通过@FeignClient来标注了要请求的接口,那么肯定会有一个地方来对这些@FeignClient标注的接口做了一些处理,比如生成动态代理。我们下面就来找一下这个地方在哪里?

用屁股猜想论来猜想一下:一共就两个入口,肯定就是在@EnableFeignClients 做扫描的喽。

我们去看看@EnableFeignClients

3.1 细节分析 - 图3

通过看出@EnableFeignClients的引用可以看出,源码中只有FeignClientsRegistrar.java引用了,而且在EnableFeignClients注解类中使用了@Import(FeignClientsRegistrar.class)把此类给引入进来了。我们去看看这个类有什么名堂:

3.1 细节分析 - 图4

FeignClientRegistrar实现了两个XXXAware接口,这是Spring中实现这些接口方法后,然后让spring给他注入对象,就持有了对应的引用。

比如:ResourceLoaderAware接口就是实现一下setResourceLoader方法,此类就可以拥有ResourceLoader引用了。

进入registerDefaultConfiguration(metadata, registry);,该方法会获取所有@EnableFeignClients的配置项,定义了一些配置类,就不做深入分析啦。

3.1 细节分析 - 图5

再进入registerFeignClients()方法中看一下:

3.1 细节分析 - 图6

我们发现了一个ClassPathScanningCandidateComponentProvider,此类根据类名也知道是扫描候选组件的提供者,这不就是扫描组件的类嘛!看一下getScanner()方法,此处下面将会讲到。

3.1 细节分析 - 图7

代码继续走到在117行时,读取@EnableFeignClients配置的clients参数,咱们没配就是null。

在上图代码中可以看到,方法中创建了一个FeignClient.class过滤器给了这个扫描者。这一步肯定用于从扫描器里面过滤处理被@FeignClient标注的类嘛!

走到119行总获取了要扫描的包名basePackages。从下图中可知先去valuebasePackagesbasePackageClasses中读取要扫描的包,如果读不到就设置标注了**@EnableFeignClients**的包路径

所以此处就要注意了:@EnableFeignClients默认扫描的包路径是此注解标注的所在的包,所以尽量把此注解放到最外层的包类中!

3.1 细节分析 - 图8

接下来就对所有的包进行扫描处理了。

3.1 细节分析 - 图9

Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);此行代码就是用组件扫描器(搭载了@FeignClient注解过滤器)在指定的包名下进行扫描包含了@FeignClient注解的接口。它是怎么进行过滤的呢?就要回到上面所看到的getScanner方法里面了。

在执行140行代码Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);会跳入到下面的方法体中判断是否符合条件:

3.1 细节分析 - 图10

beanDefinition.getMetadata().isIndependent()这个是判断是否是个独立的接口,是否是独立的(能够创建对象的) 比如是Class、或者内部类、静态内部类。

beanDefinition.getMetadata().isAnnotation()这个是判断是否是注解。

进过一系列的验证,最终会返回被@FeignClient标注并符合条件的类数组,代码此时就走到了下面:

3.1 细节分析 - 图11

最终所有符合条件的类都汇集到了Set<BeanDefinition> candidateComponents集合当中了。

代码走到143时,去判断了此类是否被打了注解。

代码走到145、146行,这里看下面的Assert信息就知道,这里是判断是否是接口!

代码走到150行,去获取了我们@FeignClient注解加入的属性。我们啥也没放就写了一个@FeignClient注解。

代码走到155行,根据@FeignClient配置属性+服务名称去BeanDefinitionRegistry注册了一下,感觉后面可能会用到。

3.1 细节分析 - 图12

代码走到158行,registerFeignClient(registry, annotationMetadata, attributes);此处看名字是跟注册Feign有关系,我们进去看看:

3.1 细节分析 - 图13

在这里面并没有找到什么有用的信息,但是我注意到上图红框中的一行代码BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); 这里有个小技巧,一般像FactoryBean都是极为关键的组件所以要重点关注一下。

FeignClientFactoryBean,一看就是靠的是这个东西作为一个工厂,在后面spring容器初始化的某个阶段,根据之前扫描出来的信息完成GreetingFeignApi的feign动态代理的构造。

我们来看看这个FeignClientFactoryBean类:

3.1 细节分析 - 图14

先映入眼帘的是一堆的参数,这些参数都很眼熟,跟@FeignClient类中的配置极为相似!为何会有这么多相同的参数呢?因为它要根据这些参数来动态代理啊!

然后我们来找找这个代理的入口在哪里:

3.1 细节分析 - 图15

这入口有个技巧:一般都会有@Override注解。如果有这个注解,一般是实现的接口或者父类定义的抽象方法,这种方法一般是给别人来调用的,很可能是一个入口方法。

总代码:

按照上面的技巧寻找入口,很快就找到了getObject()这个方法,初步判断就是一个入口方法。我们来看看这个方法的代码:

3.1 细节分析 - 图16

获取FeignContext

代码走到232行,为了避免多个 Feign 客户端级别的配置类创建的 Bean 之间互相冲突,Spring Cloud OpenFeign 通过 FeignContext 类,为每一个 Feign 客户端创建一个 Spring 容器。

不同的服务使用@FeignClient,都是可以自定义不同的Configuration! 比如:我们要调用GreetingFeignApi,那么GreetingFeignApi就会关联一个独立的spring容器,容器中关联着自己独立的一些组件:独立的Logger组件,独立的Decoder组件,独立的Encoder组件等。这个FeignContext就是存着每个Feign客户端与Spring容器对应关系的Map.

在FeignContext内部就定义了一个Map用于存储独立的这些组件:

3.1 细节分析 - 图17

构造Feign.Builder

代码走到233行,进入了feign方法,我们来看一下:

3.1 细节分析 - 图18

在这个方法中先去get了一下,这个方法是什么呢?

3.1 细节分析 - 图19

原来就是根据服务名称(GreetingFeignApi)去FeignContext里面去获取对应的FeignLoggerFactory。

context.getInstance里的逻辑,就是根据GreetingFeignApi服务名称,先在Map中获取对应的spring容器,再根据Spring容器获取自己独立的一个FeignLoggerFactory。

然后就根据拿到的这个FeignLoggerFactory创建了一个feign中重要的组件:Logger(feign关联的一个记录日志的组件)

这里有个问题FeignLoggerFactory默认是哪个呢?我们来找一下吧,找spring中的注入类肯定要去找xxxConfiguration或者xxxAutoConfiguration。果然我在FeignClientsConfiguration找到了:

3.1 细节分析 - 图20

创建完毕DefaultFeignLoggerFactory之后,就又去FeignContext获取了一个Feign.Builder对象:

3.1 细节分析 - 图21

我发现:Feign.Builder创建需要Logger、Encoder、Decoder、Contract这些对象。这些对象可都是feign的重要组件啊!那这个Feign.Builder也是一个极为重要的东西。

其实这里所有的代码都是在为以后做准备,初始化一些类注入一些东西!我们接下来看看Encoder、Decoder、Contract、Feign.Builder这些对象的默认实现是什么:SpringEncoder、ResponseEntityDecoder、SpringMvcContract、HystrixFeignConfiguration。

有两个Feign.Builder的注入,debug一下就知道默认用的是HystrixFeignConfiguration啦。

3.1 细节分析 - 图22

接下来代码走到了configureFeign(context, builder);

3.1 细节分析 - 图23

从方法名字可知,此方法就是配置Feign的。

3.1 细节分析 - 图24

通过代码,可以看到,其实就是配置Feign.Builder中的属性。

FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);代码中去获取了feign.client开头的配置文件。

3.1 细节分析 - 图25

注意到,下图中的三行代码。其实这里涉及到配置加载优先级的问题。

3.1 细节分析 - 图26

第一行,采用@FeignClient中configuration的配置项,优先级最低。

第二行,采用application.yml中针对所有feignClient配置的参数,优先级第二高。

第三行,采用针对当前的这个服务进行的feignClient的配置,当前服务的配置优先级最高。

到这一步为止Feign.Builder全部配置完毕!

画图理解一下:

3.1 细节分析 - 图27

关联ribbon

总代码中,我们走完了Feign.Builder builder = feign(context);代码,接下来走哪里呢?看图:

3.1 细节分析 - 图28

在这里判断的是什么呢?我们看一下this.name是啥:

3.1 细节分析 - 图29

原来这里判断的是@FeignClient注解中的配置项:url。因为这里我们没有配置就会采用ribbon来进行负载均衡。代码接着往下走:

3.1 细节分析 - 图30

这里在做一下url的准备,把服务名称与http://拼接起来,在`cleanPath()`中会拼接@FeignClient注解中的path配置。

比如:@FeignClient(value = “ServiceA”, path = “/user”)

此处拼接请求URL地址的时候,就会拼接成:http://ServiceA/user

先构建了一个HardCodedTarget,这个HardCodedTarget其实就是用它来代理目标接口类。然后进入了loadBalance方法:

3.1 细节分析 - 图31

此方法中,获取了Client对象,在FeignContext中获取的。它就是用来发送feign.Request这个Http请求的,请注意:实现此接口的子类实现请确保是线程安全的(因为可能是多线程发送)。默认的Client对象是谁呢?我们来找一下:

3.1 细节分析 - 图32

默认的是LoadBalancerFeignClientDefaultFeignLoadBalancedConfiguration进行注册的。

3.1 细节分析 - 图33

代码走到了Targeter targeter = get(context, Targeter.class);中,此处进行判断了是否存在feign.hystrix.HystrixFeign类,如果有则创建HystrixTargeter如果没有则创建DefaultTargeter,是否有feign.hystrix.HystrixFeign类呢?

3.1 细节分析 - 图34

我们来找找:找到了一个feign-hystrix-9.5.1.jar存在HystrixFeign,所以说在classpath路径下,可以找到feign.hystrix.HystrixFeign。所以说,在上面是用了HystrixTargeter的。

代码继续执行到targeter.target(this, builder, context, target);进去跟踪一下:
target方法跟踪1.png

在build()方法中,创建了ReflectiveFeign,然后ReflectiveFeign对象调用了newInstance方法。
Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);代码中:
ReflectiveFeign方法跟踪1.png
其实是基于我们配置的Contract、Encoder等一堆组件,加上Target对象(GreetingFeignApi接口),去获取GreetingFeignApi接口,进行spring mvc注解的解析,以及接口中各个方法的一些解析,获取了这个接口中所有需要被代理的接口,返回到一个Map中。map的key是方法名字、value是处理该方法的对象(SynchronousMethodHandler)。
apply方法分析.png
在apply方法中,contract(SpringMvcContract)对SpringMVC的注解进行了解析!

比如:sayNiHao()这个接口

(1)方法的定义:GreetingFeignApi#sayNiHao(String)

(2)方法的返回类型:class java.lang.String

(3)发送HTTP请求的模板:GET /sayNiHao/{name} HTTP/1.1\n

3.1 细节分析 - 图38

这里面存在一个invoke方法,我猜测可能每次请求都会走到这里。经过了一下调试,这里确实会对url等方法进行了处理。

3.1 细节分析 - 图39

接下来的代码:

3.1 细节分析 - 图40

遍历GreetingFeignApi接口中的每个方法(通过反射获取的),生成一个类似于nameToHandler的Map,但是key是Method对象。

接下来代码执行到了:

3.1 细节分析 - 图41

此处就是核心,基于JDK的动态代理。创建出来了一个动态代理:Proxy,这个Proxy是实现了GreetingFeignApi接口。如果学习过JDK的动态代理会知道InvocationHandler就是JDK中的动态代理。InvocationHandler的实现是ReflectiveFeign.FeignInvocationHandler。如下图:

3.1 细节分析 - 图42

new Class<?>[]{target.type()},这个就是GreetingFeignApi接口。意思就是说,基于JDK动态代理的机制,创建一个实现了GreetingFeignApi接口的动态代理。

如果不知道JDK动态代理怎么玩,建议先百度一下!

handler(InvocationHandler)对上面创建的proxy动态代理所有接口方法的调用,进行了拦截先进入InvocationHandler的拦截方法,由这个InvocationHandler中的invoke()方法来提供所有方法的实现的逻辑。

在ReflectiveFeign.FeignInvocationHandler中:

3.1 细节分析 - 图43

说白了就是去调用了Map nameToHandler = targetToHandlersByName.apply(target);中生成的Map里面的MethodHandler(SynchronousMethodHandler)的处理逻辑。

画图理解一下:

3.1 细节分析 - 图44

3、请求过程分析

先贴出寻找过程:

3.1 细节分析 - 图45

3.1 细节分析 - 图46

Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);里面的MethodHandler(SynchronousMethodHandler)是实际的处理逻辑。为什么这么说呢?

3.1 细节分析 - 图47

在newInstance方法中使用了JDK的动态代理,使用InocationHandler来处理,具体处理逻辑如下:

3.1 细节分析 - 图48

由上可知,具体处理逻辑是由MethodHandler(SynchronousMethodHandler)完成的。具体来看一下:

3.1 细节分析 - 图49

RequestTemplate template = buildTemplateFromArgs.create(argv);这段是在处理请求地址:

GET /sayNiHao/{name} HTTP/1.1 处理为:GET /sayNiHao/张三 HTTP/1.1

3.1 细节分析 - 图50

给请求添加请求的拦截器,然后基于HardCodedTarget生成了request。

3.1 细节分析 - 图51

上图中,是极为重要的一段代码。看返回值就知道是一个response,那这个方法肯定就去发送请求去了!发送请求肯定就会去ribbon中获取一个server,ribbon肯定在eureka中获取注册表放入serverList。我们来看看逻辑:

3.1 细节分析 - 图52

首先对url进行了进一步的处理。

3.1 细节分析 - 图53

IClientConfig requestConfig = getClientConfig(options, clientName);这一步是根据服务名称获取ribbon的一些配置。

3.1 细节分析 - 图54

上图中,最后一步去获取了IloadBalancer,这个IloadBalancer就是ribbon的嘛!原来这就直接获取了ribbon的ILoadBalancer!真相大白了。

构建FeignLoadBalancer原理图:

3.1 细节分析 - 图55

3.1 细节分析 - 图56

构建完FeignLoadBalancer后要执行executeWithLoadBalancer(ribbonRequest,requestConfig)这段代码应该是去发送http请求去啦。

3.1 细节分析 - 图57

其中LoadBalancerCommand是负责发送请求的一个组件,之后会调用一个submit方法,在submit方法中创建了一个ServerOperation类,此类中含有call方法,而且submit方法又调用了.toBlocking().single();看着好乱。

我们来简单分析一下:call方法很明显就是发送物理请求最终的一块代码:它构造出来了具体的http请求的地址,然后基于底层的http通信组件,发送了请求。这个call方法最终会提交到了LoadBalancerCommand中去了。然后调用了一个toBlocking().single()方法,看着像是阻塞式同步执行,然后获取一个响应结果。

我们来大胆猜测一下:ServerOperation封装了负载均衡选择出来的server,然后直接基于这个server替换掉请求URL中的eureka-client,然后直接拼接出来最终的请求URL地址,最后基于底层的http组件发送请求。

最终请求时在LoadBalancerCommand进行的,我们去看看他的submit方法去:

3.1 细节分析 - 图58

在276行中,我们发现有一个selectServer的方法,根据方法名称很像根据ILoadBalancer获取server,我们去看看:

3.1 细节分析 - 图59

这里使用loadBalancerContext获取了一个server其实里面就是用ILoadBalancer获取server。如下图:

获取到Server后,交给了rxJava的组件,然后去调用ServerOperation.call()方法,由call()方法根据server发送http请求。如下图:

这里rxJava就是一个响应式编程框架,大量的实现了观察者模式。

3.1 细节分析 - 图60

在ServerOperation.call()方法中:

3.1 细节分析 - 图61

首先去解析请求的URL

GET http:///sayNiHao/chengge HTTP/1.1\n

解析成:

GET http://localhost:8080/sayNiHao/chengge HTTP/1.1\n

然后调用FeignLoadBalancer的execute()方法执行请求:

3.1 细节分析 - 图62

这里有很重要的参数:connectTimeOut(发送请求的超时时间),请求时间最好不要超过200ms,这里默认是1s。

最终在Response response = request.client().execute(request.toRequest(), options);中发起http请求!

Feign 是如何给 Java API 接口创建动态代理,从而生成调用远程 HTTP API 接口的实现类。

Feign 和 Ribbon 是如何集成的,并实现前者负责 HTTP 接口的声明与调用,后者负责服务实例的负载均衡。