【Spring Cloud】OpenFeign 远程调用流程及超时时间源码解析

1,381 阅读8分钟

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的时候,你可能听说过 远程调用默认超时时间时一秒钟,超过一秒钟就会抛出异常

那好 我现在演示一个例子给你看

image-20220323211936628

远程调用say 中间我让它睡10秒钟

image-20220323212006936

这是测试类

image-20220323212020742

这是Feign接口

idea64_56ltYsRLcy

远程调用成功,并没有抛出超时异常

image-20220323212352051

到底超时时间时多久呢 我们改下服务业务代码 睡200秒 看下到底超时时间是多久

image-20220323220257339

结果输出60秒

image-20220323220341675

1.2 60秒超时时间源码浅析

DEBUG模式启动 跟进

image-20220323220850042

进入 invoke(Object proxy, Method method, Object[] args) 方法

image-20220323220954682

首先是 三个判断 当前是否是调用 Object的 equals hashCode toString 方法

image-20220323221327001

如果都不是 调用 SynchronousMethodHandler 的 invoke方法 并把参数传过去 我们这里feign接口没有传任何参数 所以 args是null

image-20220323222458602

首先创建了一个RequestTemplate模板对象

我们看下这个template对象的属性

image-20220323224109731

封装了请求相关的参数信息 比如请求方式url地址 请求体等等

注意一个点 openfeign会丢失请求头 原因就是创建RequestTemplate模板对象的时候就没有封装请求头数据

跟进方法 executeAndDecode(RequestTemplate template, Options options)

image-20220323223614067

我们跟进下 创建Request 的 targetRequest(template) 方法

image-20220323223716738

遍历请求拦截器,执行拦截器的apply方法

所有当我们需要在项目中使用openFeign时,又不想丢失请求头信息,可以创建一个requestInterceptor 拦截器 实现 RequestInterceptor接口,将请求头数据添加到 RequestTemplate模板中

继续看

image-20220323224352671

看下这个方法是怎么拿到返回对象的

image-20220323224440304

首先看下这个 提交请求的Client 对象 , 有四个实现类对象

image-20220323224607926

我们这里是调用 Default实现类的方法

跟进 execute 方法

image-20220323224742052

调用了本类的 convertAndSend 返回一个 Http连接对象

跟进 convertAndSend 方法

image-20220323225007955

可以看到在这设置了连接参数信息 看下options封装的参数信息

image-20220323225139151

在这设置的读取超时为60秒

2.1 OpenFeign的超时时间为1 秒的情况

之前我们的feign接口设置了url地址,通过url远程调用服务

现在我们引用nacos作为注册中心 改下feign接口

image-20220323230447460

nacos注册中心的服务名作为基准地址

image-20220324125130269

实际上就是一秒超时 这种统计时间不精确 但我们知道超时时间发生了变化就行了

2.2 1 秒超时时间源码浅析

再次debug

image-20220323230550258

我们先看下模板对象

image-20220323230729916

重点看 executeAndDecode(template, options);

注意这时 options的连接超时还是10秒 读取超时是60秒

image-20220323230816509

跟进方法

image-20220323230948448

可以看到此时是 LoadBalancerFeignClient 实现类调用 execute() 方法

image-20220323231108883

看下获取客户端配置的方法

image-20220323231349808

getClientConfig(options, clientName)

image-20220323231428572

看下这个getClientConfig(String name) 方法的返回结果

image-20220323231813578

在这把超时时间改成了1秒 也就是1000毫秒

3. 对超时时间影响的小结论

@FeignClient的url属性是否设置值会影响Client到底是那个实现类对象 不同的实现类对象的Client的客户端配置是不一样的

默认的是60秒超时,但当是 LoadBalancerFeignClient 实现类时,Client的默认超时时间时1秒

4 .Feign接口是怎么被实例化到容器的

使用openFeign时,要求我们在主启动类上添加 @EnableFeignClients 注解 ,并且指定feign接口所在的包

image-20220324095105287

那我们就来看下这个 @EnableFeignClients 注解干了什么事情

image-20220324095229769

导入了一个组件 FeignClientsRegistrar Feign客户端的注册器

实现了三个接口 来动态的注册bean

image-20220324095359552

ResourceLoaderAware   

可以拿到资源加载器

image-20220324095950711

EnvironmentAware

获取环境变量与配置文件的属性

image-20220324100058085

看下动态注册bean的 ImportBeanDefinitionRegistrar接口 的 registerBeanDefinitions 方法

image-20220324100222621

两个参数

​ AnnotationMetadata metadata //类的注解元信息

​ BeanDefinitionRegistry registry // bean定义信息注册器

两个方法

​ registerDefaultConfiguration(metadata, registry) // 注册Feign默认的配置信息

​ registerFeignClients(metadata, registry) //注册所有的feign客户端

首先看下这个 registerDefaultConfiguration(metadata, registry) 是怎么注册 Feign的配置信息的

image-20220324101918753

首先拿到@EnableFeignClients 注册的属性与属性值

再判断是不是有外部类,有的话name值为 "default."+外部类的全限定类名

没有的话为 "default."+当前类的全限定类名

image-20220324102534430

此时name 为 default.com.sgg.ConsumerApp

然后再调用 registerClientConfiguration 方法

registerClientConfiguration(registry, name,
					defaultAttrs.get("defaultConfiguration"));

image-20220324102855650

看下这个bean的定义信息

image-20220324103016418

org.springframework.cloud.openfeign.FeignClientSpecification

回到 ImportBeanDefinitionRegistrar接口 的 registerBeanDefinitions 方法

接下来看下 registerFeignClients(metadata, registry) 方法是怎么注册feign客户端的

image-20220324103634806

image-20220324105703545

然后遍历所有的basePackagesimage-20220324110008113

将符合条件的BeanDefinition添加到容器中

image-20220324110332587

注册客户端环境

registerClientConfiguration(registry, name,
      attributes.get("configuration"));

直接看注册feign客户端对象的方法

registerFeignClient(registry, annotationMetadata, attributes);

image-20220324111521912

这都是在给beanDefinition 设置属性值

简单看下 生成的 beanDefinition

image-20220324111119994

我们看下这个FeignClientFactoryBean

class FeignClientFactoryBean
		implements FactoryBean<Object>, InitializingBean, ApplicationContextAware

实现了 FactoryBean 接口 ,就去看他的getObject()方法

image-20220324111719296

调用了本类的getTarget()方法

image-20220324112235717

image-20220324112050806

看下这个loadBalance方法

image-20220324112318388

之前我们已经知道Client是个接口 有四个实现类

看下这里返回的是那个实现类对象

跟进getOptional(context, Client.class);方法

image-20220324112824366

实例一个LoadBalancerFeignClient对象 到这明白了为什么没有设置url属性值,实例的是LoadBalancerFeignClient对象

image-20220324113800416

设置Feign抽象类的内部抽象类Builder 的 client属性

image-20220324130325745

最终生成的bean 由target方法生成 点进去

image-20220324113855429

调用了feign.target方法

image-20220324113933751

最终走到了 ReflectiveFeign 的 newInstance 方法

用JDK动态代理 生成了一个代理对象放到容器中

image-20220324114037806

最终执行feign接口的方法都是执行代理类也就是 FeignInvocationHandler 的invoke方法

image-20220324114735326

发送Http请求都是由client接口的实现类对象完成的

5.如何拿到不同的Client实现类对象

再回头看下 FeignClientFactoryBean.getTarget()方法

如果我们设置了@FeignClient注解的url属性值

image-20220324115221198

执行下面的业务代码

image-20220324115240927

看下这里设置的client

image-20220324115621946

image-20220324115704812

此时Client是Default实现类对象

这就验证了 @FeignClient的url属性是否设置值会影响Client到底是那个实现类对象 不同的实现类对象的Client的客户端配置是不一样的

通过之前的源码知道了调用feign接口的方法会走代理类的invoke方法

我们现在看下 这个invoke方法

image-20220324132613192

这个dispatch也就是一个map缓存

image-20220324132716518

拿到缓存中的MethodHandler 对象,调用它的invoke方法

MethodHandler 是一个接口,有两个实现类对象 如果调用的是feign接口中的Default修饰的方法 将由 DefaultMethodHandler处理

我们看非 Default修饰的接口方法,由SynchronousMethodHandler处理

image-20220324132744347

看下SynchronousMethodHandler的invoke方法

image-20220324132938861

是不是就是之前超时时间源码浅析的时候看到的代码

之前的源码解析可以看到Client 是由 FeignClientFactoryBean.getOptional方法获取的

image-20220324133225559

我们看下这个方法

image-20220324133337840

它从context中获取的Client实例

context是 FeignContext 创建 feign 类实例的工厂。它为每个客户端名称创建一个 Spring ApplicationContext,并从那里提取它需要的 bean

那我们就看自动配置相关信息

image-20220324133820041

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个自动配置类

image-20220324134009468

image-20220324134019468

最终发现由@Import导入DefaultFeignLoadBalancedConfiguration再进行注入的

同时还能在 FeignRibbonClientAutoConfiguration 自动配置类看到 注入了一个 Request.Options

image-20220324134228543

image-20220324134243004

image-20220324134251240

默认的读取超时时间60秒,连接超时10秒在这设置的

如果没没有设置url,会走LoadBalancerFeignClient的execute方法

image-20220324134720789

看这个

IClientConfig requestConfig = getClientConfig(options, clientName);

点进去 getClientConfig(options, clientName)方法

image-20220324141716889

看下这个 RibbonClientConfiguration 配置信息类

image-20220324141735546

image-20220324142220791

这个值会去读取配置文件的属性值

所以我们设置openFeign超时时间有两种方式

6.设置openFeign超时时间的两种方式

  1. feign:
      client:
        config:
          default :  //feign客户端名,default则对所有的客户端有效
            connectTimeout: 5000
            readTimeout: 5000
            loggerLevel: FULL
    
  2. ribbon:
     ConnectTimeout: 5000
     ReadTimeout: 5000
    

7.openFeign源码总结

在主启动类上添加 @EnableFeignClients 注解 会扫描所有指定包下(basePackages = " ") 如果没有指定,就是主启动类所在包及其子包下,添加了@FeignClient 注解的接口通过jdk动态代理生成代理类,调用代理类的方法会执行 ReflectiveFeign .invoke 方法

image-20220324143336093

然后调用 SynchronousMethodHandler.invoke方法,发送http请求,获取响应对象

image-20220324143852932

生成 feign client客户端的时候 在FeignClientFactoryBean.getTarget()方法中 ,如果没有设置url ,则生成的是 LoadBalancerFeignClient 负载均衡客户端

如果设置了url 则生成的是 Default默认客户端

image-20220324143952513

两个客户端的默认读取时间是不一样的 LoadBalancerFeignClient 的读取时间时 1s 响应时间也是1秒\

可以看 LoadBalancerFeignClient. execute 方法中的 getClientConfig(options, clientName)

image-20220324144317790

最终返回的client配置类对象是 RibbonClientConfiguration.ribbonClientConfig()方法返回的 IClientConfig 对象

image-20220324142220791

如果是 Default 也就是Client 的内部实现类对象

则客户端配置类是 Request.Options 默认连接超时10秒 读取超时60秒

image-20220324134243004

image-20220324134251240

Feign接口代理类生成和远程调用源码.png