• Feign源码解析:初始化过程(三)


    背景#

    前面两篇讲了下,在一个典型的引入了feign、loadbalancer、nacos等相关依赖的环境中,会有哪些bean需要创建。

    其中第一篇讲了非自动配置的bean,第二篇是自动配置的bean。第一篇中提到,@FeignClient这个注解,就会创建一个beanDefinition,类型为FeignClientFactoryBean,是一个工厂bean,就是用它来创建一个FeignClient。

    Copy
    public class FeignClientFactoryBean implements FactoryBean<Object>

    下面就来看看这个FeignClient是如何创建出来的。

    创建过程#

    这个工厂bean里包含的属性,都是用来创建FeignClient的,它的字段,基本和@FeignClient这个注解里的字段差不多。

    Copy
    private Class<?> type; private String name; private String url; private String contextId; private String path; private Class<?> fallback = void.class; private Class<?> fallbackFactory = void.class; private int readTimeoutMillis = new Request.Options().readTimeoutMillis(); private int connectTimeoutMillis = new Request.Options().connectTimeoutMillis(); private boolean followRedirects = new Request.Options().isFollowRedirects();

    创建bean的代码如下:

    image-20231229165611822

    我们走到上面红框处后,factory的属性如下:

    image-20231229165806435

    debug进入getObject方法后:

    Copy
    public Object getObject() { return getTarget(); }

    然后需要获取一个FeignContext类型的bean:

    Copy
    <T> T getTarget() { FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class) : applicationContext.getBean(FeignContext.class);

    这是从spring容器中获取FeignContext,那么,这个bean是在哪里注册的呢?

    是在如下的自动装配类中:

    Copy
    org.springframework.cloud.openfeign.FeignAutoConfiguration#feignContext @Bean public FeignContext feignContext() { FeignContext context = new FeignContext(); context.setConfigurations(this.configurations); return context; }

    FeignContext#

    该类继承如下,它继承了的类叫做NamedContextFactory,根据名字猜测,这是个工厂类,生产什么东西呢,

    是NamedContext,也就是说,命名spring容器上下文,下面我们就知道,这个类会给每个FeignClient创建一个spring容器,各自独立。

    Copy
    public class FeignContext extends NamedContextFactory<FeignClientSpecification> public FeignContext() { super(FeignClientsConfiguration.class, "feign", "feign.client.name"); } public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>

    这个类的有一个很重要的字段,因为每一个FeignClient最终都会创建一个spring容器,这里就是一个map,key就是FeignClient的名称,value就是对应的spring容器。

    Copy
    private Map<String, AnnotationConfigApplicationContext> contexts = new ConcurrentHashMap<>();

    但是此时,还不会去创建各个FeignClient的spring容器,只是将各个FeignClient的配置保存起来:

    image-20231229171705011

    创建对应的Feign spring容器#

    回到之前的如下代码,拿到FeignContext这个bean之后,要做啥呢:

    Copy
    <T> T getTarget() { FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class) : applicationContext.getBean(FeignContext.class); // 2 Feign.Builder builder = feign(context);

    继续上图的2处:

    Copy
    protected Feign.Builder feign(FeignContext context) { // 2.1 FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(type); // 2.2 Feign.Builder builder = get(context, Feign.Builder.class) .logger(logger) .encoder(get(context, Encoder.class)) .decoder(get(context, Decoder.class)) .contract(get(context, Contract.class)); // 2.3 configureFeign(context, builder); return builder; }

    2.1处,从FeignContext获取FeignLoggerFactory这个类型的bean,但此时,该FeignClient的spring容器其实还没创建呢:

    image-20231229172219659

    此时,我们得先创建对应的spring容器(此处是懒加载模式):

    image-20231229172318349

    创建代码就是上图中的:

    Copy
    this.contexts.put(name, createContext(name));

    这个createContext方法,说白了,也就是下面的内容:

    Copy
    DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(beanFactory);

    容器创建好了,要往里面放什么beanDefinition呢?

    首先,就是这个FeignClient注解中configuration字段指定的class注册为bean:

    Copy
    public @interface FeignClient { ... Class<?>[] configuration() default {};
    Copy
    if (this.configurations.containsKey(name)) { for (Class<?> configuration : this.configurations.get(name).getConfiguration()) { context.register(configuration); } }

    怎么理解呢,比如,如下代码,就会将A这个类注册为bean,且只是注册到echo-service-provider对应的这个spring容器里:

    Copy
    @FeignClient(value = "echo-service-provider",configuration = A.class) // 指向服务提供者应用 public interface EchoService {

    在我们的代码中,实际没配置configuration,所以不会注册:

    image-20231229173249192

    ok,除了这些各个FeignClient中指定的配置类,大家知道,其实我们@enableFeignClients注解,其实也是可以配置这个属性的,这种是配置各个FeignClient都能用的默认配置:

    Copy
    public @interface EnableFeignClients { Class<?>[] defaultConfiguration() default {}; }

    所以,接下来还会检查有没有默认配置:

    Copy
    for (Map.Entry<String, C> entry : this.configurations.entrySet()) { if (entry.getKey().startsWith("default.")) { for (Class<?> configuration : entry.getValue().getConfiguration()) { context.register(configuration); } } }

    我们这边也没有配置(在@enableFeignClients中没配置defaultConfiguration字段),所以也不会注册任何bean。

    但,Feign默认就会配置一堆encoder、decoder等bean,这些配置是怎么来的呢?

    答案就在如下的构造函数中,可以看到,super调用的第一个参数是个class,FeignClientsConfiguration.class,它就是我们的默认配置类。

    Copy
    public FeignContext() { super(FeignClientsConfiguration.class, "feign", "feign.client.name"); }

    里面包含了各种自动配置:

    image-20240107145432892

    这里简单列举:

    feign.codec.Decoder、feign.codec.Encoder、feign.Contract、feign.Retryer、org.springframework.format.support.FormattingConversionService、org.springframework.cloud.openfeign.FeignLoggerFactory、org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer

    配置FeignBuilder#

    既然该FeignClient对应的容器准备好了,接下来就是继续创建FeignClient,创建它是通过FeignBuilder:

    Copy
    // 1 Feign.Builder builder = get(context, Feign.Builder.class) // required values .logger(logger) .encoder(get(context, Encoder.class)) .decoder(get(context, Decoder.class)) .contract(get(context, Contract.class)); // 2 configureFeign(context, builder);

    创建FeignBuilder也是直接从容器获取,然后配置其logger、encoder、decoder、contract。

    这几个大件现在配置好了,接下来开始配置其他东西,也就是上面的2处。跟进如下:

    image-20240107151005183

    这里首先是可以根据用户的配置来设置一些属性:

    Copy
    @ConfigurationProperties("feign.client") public class FeignClientProperties { private boolean defaultToProperties = true; private String defaultConfig = "default"; private Map<String, FeignClientConfiguration> config = new HashMap<>(); private boolean decodeSlash = true; }

    其次,也可以根据FeignClientConfigurer这个bean来配置部分属性,默认情况下,获取到的这个bean是空的:

    Copy
    @Bean @ConditionalOnMissingBean(FeignClientConfigurer.class) public FeignClientConfigurer feignClientConfigurer() { return new FeignClientConfigurer() { // 没有重写任何方法 }; }

    说白了,上图这个方法,要么根据你的properties配置来设置FeignBuilder,要么根据FeignClientConfigurer。

    根据url决定FeignClient的类型#

    完成了FeignBuilder的创建后,来到关键一环:

    image-20240107152205171

    根据url判断是否为空,来决定走哪个路径。如果你url写死了,那就自然是以你为准,不需要去什么服务发现中获取服务实例列表,再用负载均衡来决定走哪个实例;如果url为空,默认认为是把你在FeignClient中指定的名字,认为是服务的名称,就要走服务发现机制+负载均衡机制了。

    一般来说,微服务都是走服务发现机制。咱们这里也是如此。

    此时,在进入上图的loadBalance方法前,我这里url最终为:http://echo-service-provider

    loadBalance方法#

    接下来,开始跟踪loadBalance方法:

    image-20240107152812809

    上图红框,需要从当前FeignClient对应的容器中获取类型为feign.Client的bean,而结合上文,我们知道,我们那个容器中,好像没有这个类型的bean,那还能获取到吗?实际是可以的。

    是从父容器获取,父容器就是spring boot启动时,默认的那个大的容器,里面一般包含了我们的业务bean的,加上框架的bean,经常有大几百个bean。

    image-20240107154106441

    这个bean的定义在哪里呢,如何引入的呢?

    image-20240107154533816

    可以看到,这个bean是在DefaultFeignLoadBalancerConfiguration类中,这个类是在另一个自动配置类中引入的:

    image-20240107154708359

    这个自动装配类上有一些condition,如:

    Copy
    @ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })

    这其中,LoadBalancerClientFactory来自于依赖:

    image-20240107154901095

    你要是没加入spring-loadbalancer的依赖,你自然也就不会激活这个自动装配类,也就不会有feign.Client这个bean,程序也就起不来。

    继续看之前的feignClient的bean,是采用了构造器注入:

    image-20240107154533816

    注入了LoadBalancerClient和LoadBalancerClientFactory这两个bean。

    这两个bean是在哪里定义的呢?

    我发现一个好办法来找bean定义的地方,根据method return type来找,看看哪里返回这个type:

    image-20240107161653116

    发现是在如下自动装配类,这个类是在loadbalancer的相关依赖中:

    image-20240107161801109

    image-20240107162144630

    而这个bean又依赖构造器中的参数,LoadBalancerClientFactory,同样的方式找到它:

    image-20240107162256310

    它则依赖了如下bean,这是个配置属性类:

    Copy
    @ConfigurationProperties("spring.cloud.loadbalancer") public class LoadBalancerClientsProperties extends LoadBalancerProperties {

    通过以上这些步骤,可以说,FeignClient基本就创建好了,最终就是如下红框的几个步骤:

    image-20240107164130568

    最终就是做了些对象封装:

    image-20240107164257163

    创建动态代理对象给业务侧调用:

    image-20240107164625447

    基本的流程就这些,这块的分析就没有太细了,各个FeignClient对应的对象创建完成后,程序也就完成了启动,启动后,feign调用的流程,尤其是loadbalancer部分,是怎么工作的呢,又有哪些坑呢,下篇继续讲讲。

    总结#

    发现写源码是真的枯燥,本来是因为想把某个问题讲清楚,但不结合源码,又讲不清楚,没辙。

    上一篇还是2023年,这篇跨年了,2024年,大家新年快乐。




  • 相关阅读:
    雷达频带概述及其应用
    git bash 常见场景用法
    基于LVM通过添加硬盘实现分区扩容的方法介绍
    PHP导入上千万CSV数据处理
    【clickhouse专栏】对标mongodb存储类JSON数据文档统计分析
    vue实现路由跳转的方法
    跟羽夏学 Ghidra ——窗口
    大厂程序员上班猝死成常态?
    组合数3 - lucas a、b较大的组合数
    基于SpringCloud的面试刷题系统,项目经验统统搞定
  • 原文地址:https://www.cnblogs.com/grey-wolf/p/17950804