[Spring Cloud] OpenFeign remote call process and timeout source code analysis

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

image-20220323211936628

In the middle of calling say remotely, I let it sleep for 10 seconds

image-20220323212006936

This is the test class

image-20220323212020742

This is the Feign interface

idea64_56ltYsRLcy

The remote call succeeded and no timeout exception was thrown

image-20220323212352051

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.

image-20220323220257339

The result is output in 60 seconds

image-20220323220341675

1.2 Source code analysis of 60 second timeout time

DEBUG mode start follow-up

image-20220323220850042

Enter the invoke(Object proxy, Method method, Object[] args) method

image-20220323220954682

The first is to judge whether the current equals hashCode toString method of Object is called

image-20220323221327001

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

image-20220323222458602

First create a RequestTemplate template object

Let's look at the properties of this template object

image-20220323224109731

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)

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

Guess you like

Origin juejin.im/post/7078558310298812447