目录
Feign简介
Feign的简单使用
Feign原理
Feign设计
Feign源码分析
咋知道哪个接口需要生成实现类?
谁给我生成的实现类?
实现类的InvocationHandler是怎么定义的?啥功能?
调用接口方法时,实现类咋干活的?干了啥?
请求信息咋生成的request信息?
咋把request发出去的?结果咋解析的?
http客户端用的哪个?能不能改?
咋选择的服务器?
request信息能不能人为改变?咋改变?
我们先用一用,看到底多方便。
还看我们的项目。
1、在consumer模块的pom文件添加一个依赖。
org.springframework.cloud
spring-cloud-starter-openfeign
package com.haogenmin.consumer.service;
import com.haogenmin.model.Product;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import java.util.List;
/**
* @author :HaoGenmin
* @Title :ProductClientService
* @date :Created in 2020/6/29 14:14
* @description:
*/
@FeignClient(value = "microservice-product")
public interface ProductClientService {
@RequestMapping(value = "/provider/products", method = RequestMethod.POST)
public boolean add(Product product);
@RequestMapping(value = "/provider/products/{id}", method = RequestMethod.GET)
public Product get(Long id);
@RequestMapping(value = "/provider/products",method = RequestMethod.GET)
public List list();
}
3、修改controller,这时候之前配置的RestTemplate就没用了。
package com.haogenmin.consumer.controller;
import com.haogenmin.consumer.service.ProductClientService;
import com.haogenmin.model.Product;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.List;
@RestController
@RequestMapping("/consumer")
public class ProductController_Consumer {
@Autowired
private ProductClientService productClientService;
@RequestMapping(value = "/products", method = RequestMethod.POST)
public boolean add(Product product) {
return productClientService.add(product);
}
@RequestMapping(value = "/products/{id}", method = RequestMethod.GET)
public Product get(@PathVariable("id") Long id) {
return productClientService.get(id);
}
@RequestMapping(value = "/products",method = RequestMethod.GET)
public List list() {
return productClientService.list();
}
}
4、最后主启动类上面扫描一下我们的Feign客户端这个包。
package com.haogenmin.consumer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
/**
* @author :HaoGenmin
* @Title :ConsumerApplication
* @date :Created in 2020/6/23 19:31
* @description:
*/
@EnableFeignClients(basePackages= {"com.haogenmin.consumer.service"})
@EnableEurekaClient
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
SpringApplication.run(ConsumerApplication.class,args);
}
}
测试:
以上的过程如下:
Feign封装了Http调用流程,更适合面向接口化的变成习惯。
在服务调用的场景中,我们经常调用基于Http协议的服务,而我们经常使用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,这些框架在基于自身的专注点提供了自身特性。而从角色划分上来看,他们的职能是一致的提供Http调用服务。具体流程如下:
要理解以上内容,需要理解动态代理的知识。
1、我们在自己写的接口上面加了注解以及注解里面的参数信息。Feign根据这些信息,生成了一个代理类,这个代理类可以把接口方法的参数,转成request,然后用ribbon找到服务器,用http客户端把请求发出去,再解析请求。等等等等。
2、代理类咋做的呢?我们知道动态代理真正执行的是InvocationHandler里面的invoke方法。实际上,Feign实现了专门用于FeignClient的InvocationHandler,但是,它是构建了一个接口方法到方法处理器的映射表(Method==>MethodHandler),当我们执行接口的method方法的时候,我们通过这个映射表找到对应的MethodHandler,MethodHandler是真正的执行者。
3、MethodHandler就厉害了,它根据参数信息,注解信息,把请求参数等相关信息创建了一个requestTemplate出来。然后执行请求并解析数据。这一切都是MethodHandler做的,当然,它用到了encoder和decoder以及httpClient。
4、Feign定义了拦截器接口,在请求发送之前可以拦截一下,对其进行自定义修改请求信息。
5、httpclient有很多种,默认的性能比较低,生产环境按需修改,而我们使用的一般是一个httpclient的包装类,为啥?因为我们配置的是应用名啊,我们得需要先找到对应的服务器才能发请求。所以这个httpclient是这个包装Client类的一个属性引用,在访问请求的时候,这个包装类先用ribbon根据负载策略找到合适的服务器IP端口。然后,用httpclient进行最终的请求。
如果大家觉得我说的太简单,看不懂。请看这一篇。我这里主要是针对源码进行分析。
原理参考:
https://www.cnblogs.com/crazymakercircle/p/11965726.html
https://www.jianshu.com/p/8c7b92b4396c
首先,我们知道springcloud肯定根据我们的接口注解信息生成了实现类,那么我们现在要探寻的问题有这么几个:
咋知道哪个接口需要生成实现类?
谁给我生成的实现类?
实现类的InvocationHandler是怎么定义的?啥功能?
调用接口方法时,实现类咋干活的?干了啥?
请求信息咋生成的request信息?
咋把request发出去的?结果咋解析的?
http客户端用的哪个?能不能改?
咋选择的服务器?
request信息能不能人为改变?咋改变?
带着问题我们来看看代码:
首先我们知道启动类上面注解了@EnableFeignClients,目的是为了扫描FeignClient接口,所以,能扫描到的接口中,有@FeignClient注解的就是我们要实现代理的接口。
从下图中我们发现除了启动类,只在FeignClientsRegistrar类中引用了EnableFeignClients。看名字就知道这是Feign客户端的注册器。
当应用启动时会首先调用FeignClientsRegistrar的registerBeanDefinitions()方法。
主要看下registerFeignClients()方法。
我们再看一看当找不到clients属性时候,怎么扫描包的:
所以我们@EnableFeignClients(basePackages= {"com.haogenmin.consumer.service"})这个注解就是这个时候被处理的。然后,就把我们的ProductClientService接口注册进去了。
再看一看是怎么注册到容器中的。
到此,应该明白,因为我们的注解,那些接口被扫描,打上了FeignClient的标记,然后加入了Spring容器,就知道哪些借口是FeignClient的类型了。
在spring容器启动时会调用FeignClientFactoryBean的getObject()方法(只有在其他bean注入feign client时才会调用)。
看一下getTarget的注释,根据给定的数据和上下文信息,创建一个FeignClient客户端。从@FeignClient注解上是否指定URL,feign的处理分成了两部分,如果未指定URL,则使用负载均衡去发送请求,指定URL,只会向指定的URL发送请求。
我们再来看看loadBalance函数做了什么?
Targeter默认为DefaultTargeter,client为LoadBalancerFeignClient。再看下DefaultTargeter.target()方法。
feign.target()方法。
这里看到,调用的其实是ReflectiveFeign的newInstance方法。
这个build里面的参数一大堆:
先看看这个newInstance方法做了啥。
由此,我们知道了,在spring依赖注入的时候,生成了接口的代理,处理的类是ReflectiveFeign 。
从这里我们知道handler是这个工厂制造出来的。看一看。发现是构造函数传进来的。回到之前Feign中发现:
你发现兜了一大圈,有回来了。搞了半天FeignInvocationHandler就是ReflectiveFeign的一个静态内部类。
于是我们发现,FeignInvocationHandler是定义好的,传入参数生产出实例,为每个接口生成代理对象。功能就是,当我们调用接口的方法时,调用的其实是FeignInvocationHandler的invoke来执行的。
到这,在启动时候的注入过程就完了。
简单总结下启动时Feign所做的处理:
这样做的好处是:在程序中使用Feign Client时就可以像其他spring 管理的bean一样直接注入即可。
接下来看一下FeignInvocationHandler的invoke方法。
看到,它有一个属性,是传进来的Method与MethodHandler的映射。当我们调用接口的方法时,实际上处理的是对应的MethodHandler。
那么我们看一下MethodHandler是哪个?他又做了啥?
从上图发现,MethodHandler是apply方法得来的,看一看。
到这,发现SynchronousMethodHandler就是最终执行的MethodHandler。
以上是实现类工作的过程,在这里请求被处理,响应被解析。当然,我们不想到这就结束,还想进一步看看,请求咋发出去的?谁发出去的……
那我们先看一看RequestTemplate咋生成的。
找到在这里:
进入这个类瞅一眼,发现又是ReflectiveFeign的静态内部类:
找到它的create方法。
@Override
public RequestTemplate create(Object[] argv) {
// 获取methodMetada的template,这个RequestTemplate是可变的,跟随每次调用参数而变。
RequestTemplate mutable = RequestTemplate.from(metadata.template());
if (metadata.urlIndex() != null) {
//处理@PathVariable在URL上插入的参数
int urlIndex = metadata.urlIndex();
checkArgument(argv[urlIndex] != null, "URI parameter %s was null", urlIndex);
mutable.target(String.valueOf(argv[urlIndex]));
}
//处理调用方法的param参数,追加到URL ?后面的参数
Map varBuilder = new LinkedHashMap();
for (Entry> entry : metadata.indexToName().entrySet()) {
int i = entry.getKey();
Object value = argv[entry.getKey()];
if (value != null) { // Null values are skipped.
if (indexToExpander.containsKey(i)) {
value = expandElements(indexToExpander.get(i), value);
}
for (String name : entry.getValue()) {
varBuilder.put(name, value);
}
}
}
//处理query参数以及body内容
RequestTemplate template = resolve(argv, mutable, varBuilder);
if (metadata.queryMapIndex() != null) {
// add query map parameters after initial resolve so that they take
// precedence over any predefined values
// 当 RequestTemplate处理完参数后,再处理@QueryMap注入的参数,以便优先于任意值。
Object value = argv[metadata.queryMapIndex()];
Map queryMap = toQueryMap(value);
template = addQueryMapQueryParameters(queryMap, template);
}
if (metadata.headerMapIndex() != null) {
//处理RequestTemplate的header内容
template =
addHeaderMapHeaders((Map) argv[metadata.headerMapIndex()], template);
}
return template;
}
可以看到,第一步是根据调用时的参数等构造了RequestTemplate的param、body、header等内容。
再看executeAndDecode方法。
到这,我们发现,我们关于请求的各种信息,终于生成了一个request请求。
继续看executeAndDecode方法,发现在这个地方发送了请求,并处理了响应结果。
接下来,是比较重要的一点。我们用的哪个HTTP客户端执行的请求呢?回顾到第一步,我们知道这个Client是LoadBalancerFeignClient我们看看这个客户端的执行方法。
这个值在配置类里面生成bean的时候传进去了,就是:
Feign 默认底层通过JDK 的 java.net.HttpURLConnection
实现了feign.Client
接口类,在每次发送请求的时候,都会创建新的HttpURLConnection 链接,这也就是为什么默认情况下Feign的性能很差的原因。可以通过拓展该接口,使用Apache HttpClient 或者OkHttp3等基于连接池的高性能Http客户端。所以,大家使用的时候,记得改一下这个客户端。
这部分是ribbon处理的,ribbon的内容很多,见上一篇详解。
其实,到这里大致的流程都已经看完了,除了encoder,decoder,和日志记录之外的过程,这里都在源码里走了一遍了。另外我们还想知道,请求的信息可否自定义添加。
在请求转换的过程中,Feign 抽象出来了拦截器接口,用于用户自定义对请求的操作:
我们可以在apply方法中对request模板进行自定义操作。具体用法,可以参考官方的几个实现类。
对以上过程总结:
然而,我们说的这个过程是Feign没有配置熔断器的时候的过程。配置熔断器的话,在最初的时候得到的Target就不是默认的那个了。回头有时间再看……