Why does OpenFeign only need to write an interface to be able to call it remotely? How does the whole process of generating the proxy class look like?
Why is the timeout period of one second when using openFeign? And sometimes the timeout period is 60 seconds?
I don’t know if you will have such confusion. If so, maybe this blog post can give you the answer
1. Add a test case
1.1 Service Providers
controller
@RestController
public class HelloController {
@GetMapping("/say")
public String say()
{
return "Hello World";
}
}
复制代码
main startup class
@SpringBootApplication
public class ProviderApp {
public static void main(String[] args) {
SpringApplication.run(ProviderApp.class);
}
}
复制代码
configuration information
server:
port: 9000
spring:
application:
name: provider
复制代码
1.2 open interface
@FeignClient(name = "providerFeign",url = "http://localhost:9000")
public interface ProviderFeign {
@GetMapping("/say")
public String say();
}
复制代码
1.3 OpenFeign caller
main startup class
@EnableFeignClients(basePackages = "com.sgg.feign")
@SpringBootApplication
public class ConsumerApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerApp.class);
}
}
复制代码
configuration information
server:
port: 10000
spring:
application:
name: consumer
复制代码
test class
@SpringBootTest
public class ConsumerTest {
@Autowired
private ProviderFeign providerFeign;
@Test
public void feignTest(){
String say = providerFeign.say();
System.out.println("say = " + say);
}
}
复制代码
controller
@RestController
public class ConsumerController {
@Autowired
private ProviderFeign providerFeign;
@GetMapping("/say")
public String say()
{
return providerFeign.say();
}
}
复制代码
2. OpenFeign timeout test
1.1 When OpenFeign has a timeout of 60 seconds
I believe that when you first learned SpringCloud OpenFeign, you may have heard that the default timeout for remote calls is one second, and an exception will be thrown if it exceeds one second.
Well, I'll show you an example now
In the middle of calling say remotely, I let it sleep for 10 seconds
This is the test class
This is the Feign interface
The remote call succeeded and no timeout exception was thrown
How long is the timeout period? Let's change the service code and sleep for 200 seconds to see how long the timeout period is.
The result is output in 60 seconds
1.2 Source code analysis of 60 second timeout time
DEBUG mode start follow-up
Enter the invoke(Object proxy, Method method, Object[] args) method
The first is to judge whether the current equals hashCode toString method of Object is called
If neither call the invoke method of SynchronousMethodHandler and pass the parameters, we do not pass any parameters in the feign interface here, so args is null
First create a RequestTemplate template object
Let's look at the properties of this template object
It encapsulates request-related parameter information, such as the request method, url address, request body, etc.
Note that a point of openfeign will lose the request header because the request header data is not encapsulated when the RequestTemplate template object is created
Follow up method executeAndDecode(RequestTemplate template, Options options)
我们跟进下 创建Request 的 targetRequest(template) 方法
遍历请求拦截器,执行拦截器的apply方法
所有当我们需要在项目中使用openFeign时,又不想丢失请求头信息,可以创建一个requestInterceptor 拦截器 实现 RequestInterceptor接口,将请求头数据添加到 RequestTemplate模板中
继续看
看下这个方法是怎么拿到返回对象的
首先看下这个 提交请求的Client 对象 , 有四个实现类对象
我们这里是调用 Default实现类的方法
跟进 execute 方法
调用了本类的 convertAndSend 返回一个 Http连接对象
跟进 convertAndSend 方法
可以看到在这设置了连接参数信息 看下options封装的参数信息
在这设置的读取超时为60秒
2.1 OpenFeign的超时时间为1 秒的情况
之前我们的feign接口设置了url地址,通过url远程调用服务
现在我们引用nacos作为注册中心 改下feign接口
nacos注册中心的服务名作为基准地址
实际上就是一秒超时 这种统计时间不精确 但我们知道超时时间发生了变化就行了
2.2 1 秒超时时间源码浅析
再次debug
我们先看下模板对象
重点看 executeAndDecode(template, options);
注意这时 options的连接超时还是10秒 读取超时是60秒
跟进方法
可以看到此时是 LoadBalancerFeignClient 实现类调用 execute() 方法
看下获取客户端配置的方法
getClientConfig(options, clientName)
复制代码
看下这个getClientConfig(String name) 方法的返回结果
在这把超时时间改成了1秒 也就是1000毫秒
3. 对超时时间影响的小结论
@FeignClient的url属性是否设置值会影响Client到底是那个实现类对象 不同的实现类对象的Client的客户端配置是不一样的
默认的是60秒超时,但当是 LoadBalancerFeignClient 实现类时,Client的默认超时时间时1秒
4 .Feign接口是怎么被实例化到容器的
使用openFeign时,要求我们在主启动类上添加 @EnableFeignClients 注解 ,并且指定feign接口所在的包
那我们就来看下这个 @EnableFeignClients 注解干了什么事情
导入了一个组件 FeignClientsRegistrar Feign客户端的注册器
实现了三个接口 来动态的注册bean
ResourceLoaderAware
复制代码
可以拿到资源加载器
EnvironmentAware
复制代码
获取环境变量与配置文件的属性
看下动态注册bean的 ImportBeanDefinitionRegistrar接口 的 registerBeanDefinitions 方法
两个参数
AnnotationMetadata metadata //类的注解元信息
BeanDefinitionRegistry registry // bean定义信息注册器
两个方法
registerDefaultConfiguration(metadata, registry) // 注册Feign默认的配置信息
registerFeignClients(metadata, registry) //注册所有的feign客户端
首先看下这个 registerDefaultConfiguration(metadata, registry) 是怎么注册 Feign的配置信息的
首先拿到@EnableFeignClients 注册的属性与属性值
再判断是不是有外部类,有的话name值为 "default."+外部类的全限定类名
没有的话为 "default."+当前类的全限定类名
此时name 为 default.com.sgg.ConsumerApp
然后再调用 registerClientConfiguration 方法
registerClientConfiguration(registry, name,
defaultAttrs.get("defaultConfiguration"));
复制代码
看下这个bean的定义信息
org.springframework.cloud.openfeign.FeignClientSpecification
回到 ImportBeanDefinitionRegistrar接口 的 registerBeanDefinitions 方法
接下来看下 registerFeignClients(metadata, registry) 方法是怎么注册feign客户端的
复制代码
然后遍历所有的basePackages
将符合条件的BeanDefinition添加到容器中
注册客户端环境
registerClientConfiguration(registry, name,
attributes.get("configuration"));
复制代码
直接看注册feign客户端对象的方法
registerFeignClient(registry, annotationMetadata, attributes);
复制代码
这都是在给beanDefinition 设置属性值
简单看下 生成的 beanDefinition
我们看下这个FeignClientFactoryBean
class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware
复制代码
实现了 FactoryBean 接口 ,就去看他的getObject()方法
调用了本类的getTarget()方法
看下这个loadBalance方法
之前我们已经知道Client是个接口 有四个实现类
看下这里返回的是那个实现类对象
跟进getOptional(context, Client.class);方法
实例一个LoadBalancerFeignClient对象 到这明白了为什么没有设置url属性值,实例的是LoadBalancerFeignClient对象
设置Feign抽象类的内部抽象类Builder 的 client属性
最终生成的bean 由target方法生成 点进去
调用了feign.target方法
最终走到了 ReflectiveFeign 的 newInstance 方法
用JDK动态代理 生成了一个代理对象放到容器中
最终执行feign接口的方法都是执行代理类也就是 FeignInvocationHandler 的invoke方法
发送Http请求都是由client接口的实现类对象完成的
5.如何拿到不同的Client实现类对象
再回头看下 FeignClientFactoryBean.getTarget()方法
如果我们设置了@FeignClient注解的url属性值
执行下面的业务代码
看下这里设置的client
此时Client是Default实现类对象
这就验证了 @FeignClient的url属性是否设置值会影响Client到底是那个实现类对象 不同的实现类对象的Client的客户端配置是不一样的
通过之前的源码知道了调用feign接口的方法会走代理类的invoke方法
我们现在看下 这个invoke方法
这个dispatch也就是一个map缓存
拿到缓存中的MethodHandler 对象,调用它的invoke方法
MethodHandler 是一个接口,有两个实现类对象 如果调用的是feign接口中的Default修饰的方法 将由 DefaultMethodHandler处理
我们看非 Default修饰的接口方法,由SynchronousMethodHandler处理
看下SynchronousMethodHandler的invoke方法
是不是就是之前超时时间源码浅析的时候看到的代码
之前的源码解析可以看到Client 是由 FeignClientFactoryBean.getOptional方法获取的
我们看下这个方法
它从context中获取的Client实例
context是 FeignContext 创建 feign 类实例的工厂。它为每个客户端名称创建一个 Spring ApplicationContext,并从那里提取它需要的 bean
那我们就看自动配置相关信息
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoConfiguration
复制代码
有6个自动配置类
最终发现由@Import导入DefaultFeignLoadBalancedConfiguration再进行注入的
同时还能在 FeignRibbonClientAutoConfiguration 自动配置类看到 注入了一个 Request.Options
默认的读取超时时间60秒,连接超时10秒在这设置的
如果没没有设置url,会走LoadBalancerFeignClient的execute方法
看这个
IClientConfig requestConfig = getClientConfig(options, clientName);
复制代码
点进去 getClientConfig(options, clientName)方法
看下这个 RibbonClientConfiguration 配置信息类
这个值会去读取配置文件的属性值
所以我们设置openFeign超时时间有两种方式
6.设置openFeign超时时间的两种方式
-
feign: client: config: default : //feign客户端名,default则对所有的客户端有效 connectTimeout: 5000 readTimeout: 5000 loggerLevel: FULL 复制代码
-
ribbon: ConnectTimeout: 5000 ReadTimeout: 5000 复制代码
7.openFeign源码总结
在主启动类上添加 @EnableFeignClients 注解 会扫描所有指定包下(basePackages = " ") 如果没有指定,就是主启动类所在包及其子包下,添加了@FeignClient 注解的接口通过jdk动态代理生成代理类,调用代理类的方法会执行 ReflectiveFeign .invoke 方法
然后调用 SynchronousMethodHandler.invoke方法,发送http请求,获取响应对象
生成 feign client客户端的时候 在FeignClientFactoryBean.getTarget()方法中 ,如果没有设置url ,则生成的是 LoadBalancerFeignClient 负载均衡客户端
如果设置了url 则生成的是 Default默认客户端
两个客户端的默认读取时间是不一样的 LoadBalancerFeignClient 的读取时间时 1s 响应时间也是1秒\
可以看 LoadBalancerFeignClient. execute 方法中的 getClientConfig(options, clientName)
最终返回的client配置类对象是 RibbonClientConfiguration.ribbonClientConfig()方法返回的 IClientConfig 对象
如果是 Default 也就是Client 的内部实现类对象
则客户端配置类是 Request.Options 默认连接超时10秒 读取超时60秒