概述
Feign 是由 Netflix 开源的声明式的 HTTP 客户端,目前已经捐献给 OpenFeign社区。
原生Feign
直接使用main方法来测试一把:
import feign.Feign;import feign.Param;import feign.RequestLine;/*** @author weimaomao* @createTime 2021-01-31日* @Description TODO*/public class TestNativeFeign {public static void main(String[] args) {// 创建 SayHelloAPI 对象SayHelloAPI sayHelloAPI = Feign.builder().target(SayHelloAPI.class,"http://localhost:8081");String product = sayHelloAPI.sayNiHao("hahaha");System.out.println(product);}interface SayHelloAPI {@RequestLine("GET /sayNiHao/{name}")String sayNiHao(@Param("name") String name);}}
我们发现,这样的写法需要把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个子项目:

注册中心:eureka-server
服务提供者:eureka-client
服务公共api:eureka-client-api
服务消费者:feign-client
2、搭建父工程
pom.xml文件
<modelVersion>4.0.0</modelVersion><packaging>pom</packaging><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.0.3.RELEASE</version><relativePath/> <!-- lookup parent from repository --></parent><groupId>org.example</groupId><artifactId>feign-demo</artifactId><version>1.0-SNAPSHOT</version><properties><project.build.sourceEncoding>UTF-8</project.build.sourceEncoding></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>Finchley.RELEASE</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><modules><module>feign-client</module><module>eureka-server</module><module>eureka-client</module><module>eureka-client-api</module></modules>
3、注册中心:eureka-server
pom.xml文件
<parent><artifactId>feign-demo</artifactId><groupId>org.example</groupId><version>1.0-SNAPSHOT</version></parent><modelVersion>4.0.0</modelVersion><artifactId>eureka-server</artifactId><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-netflix-eureka-server</artifactId></dependency></dependencies>
application.yml
server:port: 8761spring:application:name: eureka-servereureka:client:# 表示是否将自己注册到Eureka Server,默认为true。register-with-eureka: false# 表示是否从Eureka Server获取注册信息,默认为true。fetch-registry: false# 设置与Eureka Server交互的地址,查询服务和注册服务都需要依赖这个地址。默认是http://localhost:8761/eureka ;多个地址可使用,分隔service-url: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 进行了集成。
原理图:

1、Feign整体分析
我们使用feign时就用了两个注解:@EnableFeignClients和@FeignClient,入口肯定就是这两个。
2、@EnableFeignClients入口分析
我们来思考,我们只定义了一个接口,feign是如何给我调用其他服务的接口发送http请求的呢?
我觉得fegin会给我生成了一个动态代理,让这个动态代理来给我去处理http请求了。
我们再来思考一下,feign中是通过@FeignClient来标注了要请求的接口,那么肯定会有一个地方来对这些@FeignClient标注的接口做了一些处理,比如生成动态代理。我们下面就来找一下这个地方在哪里?
用屁股猜想论来猜想一下:一共就两个入口,肯定就是在@EnableFeignClients 做扫描的喽。
我们去看看@EnableFeignClients :

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

FeignClientRegistrar实现了两个XXXAware接口,这是Spring中实现这些接口方法后,然后让spring给他注入对象,就持有了对应的引用。
比如:
ResourceLoaderAware接口就是实现一下setResourceLoader方法,此类就可以拥有ResourceLoader引用了。
进入registerDefaultConfiguration(metadata, registry);,该方法会获取所有@EnableFeignClients的配置项,定义了一些配置类,就不做深入分析啦。

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

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

代码继续走到在117行时,读取@EnableFeignClients配置的clients参数,咱们没配就是null。
在上图代码中可以看到,方法中创建了一个FeignClient.class过滤器给了这个扫描者。这一步肯定用于从扫描器里面过滤处理被@FeignClient标注的类嘛!
走到119行总获取了要扫描的包名basePackages。从下图中可知先去value、basePackages、basePackageClasses中读取要扫描的包,如果读不到就设置标注了**@EnableFeignClients**的包路径。
所以此处就要注意了:@EnableFeignClients默认扫描的包路径是此注解标注的所在的包,所以尽量把此注解放到最外层的包类中!

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

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

beanDefinition.getMetadata().isIndependent()这个是判断是否是个独立的接口,是否是独立的(能够创建对象的) 比如是Class、或者内部类、静态内部类。
beanDefinition.getMetadata().isAnnotation()这个是判断是否是注解。
进过一系列的验证,最终会返回被@FeignClient标注并符合条件的类数组,代码此时就走到了下面:

最终所有符合条件的类都汇集到了Set<BeanDefinition> candidateComponents集合当中了。
代码走到143时,去判断了此类是否被打了注解。
代码走到145、146行,这里看下面的Assert信息就知道,这里是判断是否是接口!
代码走到150行,去获取了我们@FeignClient注解加入的属性。我们啥也没放就写了一个@FeignClient注解。
代码走到155行,根据@FeignClient配置属性+服务名称去BeanDefinitionRegistry注册了一下,感觉后面可能会用到。

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

在这里面并没有找到什么有用的信息,但是我注意到上图红框中的一行代码BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); 这里有个小技巧,一般像FactoryBean都是极为关键的组件所以要重点关注一下。
FeignClientFactoryBean,一看就是靠的是这个东西作为一个工厂,在后面spring容器初始化的某个阶段,根据之前扫描出来的信息完成GreetingFeignApi的feign动态代理的构造。
我们来看看这个FeignClientFactoryBean类:

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

这入口有个技巧:一般都会有@Override注解。如果有这个注解,一般是实现的接口或者父类定义的抽象方法,这种方法一般是给别人来调用的,很可能是一个入口方法。
总代码:
按照上面的技巧寻找入口,很快就找到了getObject()这个方法,初步判断就是一个入口方法。我们来看看这个方法的代码:
获取FeignContext
代码走到232行,为了避免多个 Feign 客户端级别的配置类创建的 Bean 之间互相冲突,Spring Cloud OpenFeign 通过 FeignContext 类,为每一个 Feign 客户端创建一个 Spring 容器。
不同的服务使用@FeignClient,都是可以自定义不同的Configuration! 比如:我们要调用
GreetingFeignApi,那么GreetingFeignApi就会关联一个独立的spring容器,容器中关联着自己独立的一些组件:独立的Logger组件,独立的Decoder组件,独立的Encoder组件等。这个FeignContext就是存着每个Feign客户端与Spring容器对应关系的Map.
在FeignContext内部就定义了一个Map用于存储独立的这些组件:

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

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

原来就是根据服务名称(GreetingFeignApi)去FeignContext里面去获取对应的FeignLoggerFactory。
context.getInstance里的逻辑,就是根据GreetingFeignApi服务名称,先在Map中获取对应的spring容器,再根据Spring容器获取自己独立的一个FeignLoggerFactory。
然后就根据拿到的这个FeignLoggerFactory创建了一个feign中重要的组件:Logger(feign关联的一个记录日志的组件)
这里有个问题FeignLoggerFactory默认是哪个呢?我们来找一下吧,找spring中的注入类肯定要去找xxxConfiguration或者xxxAutoConfiguration。果然我在FeignClientsConfiguration找到了:

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

我发现:Feign.Builder创建需要Logger、Encoder、Decoder、Contract这些对象。这些对象可都是feign的重要组件啊!那这个Feign.Builder也是一个极为重要的东西。
其实这里所有的代码都是在为以后做准备,初始化一些类注入一些东西!我们接下来看看Encoder、Decoder、Contract、Feign.Builder这些对象的默认实现是什么:SpringEncoder、ResponseEntityDecoder、SpringMvcContract、HystrixFeignConfiguration。
有两个Feign.Builder的注入,debug一下就知道默认用的是HystrixFeignConfiguration啦。

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

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

通过代码,可以看到,其实就是配置Feign.Builder中的属性。
在FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);代码中去获取了feign.client开头的配置文件。

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

第一行,采用@FeignClient中configuration的配置项,优先级最低。
第二行,采用application.yml中针对所有feignClient配置的参数,优先级第二高。
第三行,采用针对当前的这个服务进行的feignClient的配置,当前服务的配置优先级最高。
到这一步为止Feign.Builder全部配置完毕!
画图理解一下:

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

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

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

这里在做一下url的准备,把服务名称与http://拼接起来,在`cleanPath()`中会拼接@FeignClient注解中的path配置。
比如:@FeignClient(value = “ServiceA”, path = “/user”)
此处拼接请求URL地址的时候,就会拼接成:http://ServiceA/user
先构建了一个HardCodedTarget,这个HardCodedTarget其实就是用它来代理目标接口类。然后进入了loadBalance方法:

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

默认的是LoadBalancerFeignClient在DefaultFeignLoadBalancedConfiguration进行注册的。

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

我们来找找:找到了一个feign-hystrix-9.5.1.jar存在HystrixFeign,所以说在classpath路径下,可以找到feign.hystrix.HystrixFeign。所以说,在上面是用了HystrixTargeter的。
代码继续执行到targeter.target(this, builder, context, target);进去跟踪一下:
在build()方法中,创建了ReflectiveFeign,然后ReflectiveFeign对象调用了newInstance方法。
在Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);代码中:
其实是基于我们配置的Contract、Encoder等一堆组件,加上Target对象(GreetingFeignApi接口),去获取GreetingFeignApi接口,进行spring mvc注解的解析,以及接口中各个方法的一些解析,获取了这个接口中所有需要被代理的接口,返回到一个Map中。map的key是方法名字、value是处理该方法的对象(SynchronousMethodHandler)。
在apply方法中,contract(SpringMvcContract)对SpringMVC的注解进行了解析!
比如:sayNiHao()这个接口
(1)方法的定义:GreetingFeignApi#sayNiHao(String)
(2)方法的返回类型:class java.lang.String
(3)发送HTTP请求的模板:GET /sayNiHao/{name} HTTP/1.1\n

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

接下来的代码:

遍历GreetingFeignApi接口中的每个方法(通过反射获取的),生成一个类似于nameToHandler的Map,但是key是Method对象。
接下来代码执行到了:

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

new Class<?>[]{target.type()},这个就是GreetingFeignApi接口。意思就是说,基于JDK动态代理的机制,创建一个实现了GreetingFeignApi接口的动态代理。
如果不知道JDK动态代理怎么玩,建议先百度一下!
handler(InvocationHandler)对上面创建的proxy动态代理所有接口方法的调用,进行了拦截先进入InvocationHandler的拦截方法,由这个InvocationHandler中的invoke()方法来提供所有方法的实现的逻辑。
在ReflectiveFeign.FeignInvocationHandler中:

说白了就是去调用了Map
画图理解一下:

3、请求过程分析
先贴出寻找过程:


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

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

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

RequestTemplate template = buildTemplateFromArgs.create(argv);这段是在处理请求地址:
GET /sayNiHao/{name} HTTP/1.1 处理为:GET /sayNiHao/张三 HTTP/1.1

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

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

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

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

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


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

其中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方法去:

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

这里使用loadBalancerContext获取了一个server其实里面就是用ILoadBalancer获取server。如下图:
获取到Server后,交给了rxJava的组件,然后去调用ServerOperation.call()方法,由call()方法根据server发送http请求。如下图:
这里rxJava就是一个响应式编程框架,大量的实现了观察者模式。

在ServerOperation.call()方法中:

首先去解析请求的URL
GET http:///sayNiHao/chengge HTTP/1.1\n
解析成:
GET http://localhost:8080/sayNiHao/chengge HTTP/1.1\n
然后调用FeignLoadBalancer的execute()方法执行请求:

这里有很重要的参数:connectTimeOut(发送请求的超时时间),请求时间最好不要超过200ms,这里默认是1s。
最终在Response response = request.client().execute(request.toRequest(), options);中发起http请求!
Feign 是如何给 Java API 接口创建动态代理,从而生成调用远程 HTTP API 接口的实现类。
Feign 和 Ribbon 是如何集成的,并实现前者负责 HTTP 接口的声明与调用,后者负责服务实例的负载均衡。
