OpenFeign为什么只需要写一个接口就能远程调用? 是怎么生成代理类的 整个流程又是怎么样的?
为什么使用openFeign时,超时时间是一秒钟? 而有时候超时时间又是60秒钟 ?
不知道你会不会有这样的困惑 如果有的话,或许这篇博文能给你答案
1.添加测试案例
1.1 服务提供者
controller
@RestController
public class HelloController {
@GetMapping("/say")
public String say()
{
return "Hello World";
}
}
主启动类
@SpringBootApplication
public class ProviderApp {
public static void main(String[] args) {
SpringApplication.run(ProviderApp.class);
}
}
配置信息
server:
port: 9000
spring:
application:
name: provider
1.2 open接口
@FeignClient(name = "providerFeign",url = "http://localhost:9000")
public interface ProviderFeign {
@GetMapping("/say")
public String say();
}
1.3 OpenFeign调用者
主启动类
@EnableFeignClients(basePackages = "com.sgg.feign")
@SpringBootApplication
public class ConsumerApp {
public static void main(String[] args) {
SpringApplication.run(ConsumerApp.class);
}
}
配置信息
server:
port: 10000
spring:
application:
name: consumer
测试类
@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 超时时间测试
1.1 OpenFeign的超时时间为60秒的情况
相信刚学SpringCloud OpenFeign的时候,你可能听说过 远程调用默认超时时间时一秒钟,超过一秒钟就会抛出异常
那好 我现在演示一个例子给你看
远程调用say 中间我让它睡10秒钟
这是测试类
这是Feign接口
远程调用成功,并没有抛出超时异常
到底超时时间时多久呢 我们改下服务业务代码 睡200秒 看下到底超时时间是多久
结果输出60秒
1.2 60秒超时时间源码浅析
DEBUG模式启动 跟进
进入 invoke(Object proxy, Method method, Object[] args) 方法
首先是 三个判断 当前是否是调用 Object的 equals hashCode toString 方法
如果都不是 调用 SynchronousMethodHandler 的 invoke方法 并把参数传过去 我们这里feign接口没有传任何参数 所以 args是null
首先创建了一个RequestTemplate模板对象
我们看下这个template对象的属性
封装了请求相关的参数信息 比如请求方式url地址 请求体等等
注意一个点 openfeign会丢失请求头 原因就是创建RequestTemplate模板对象的时候就没有封装请求头数据
跟进方法 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秒