背景

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

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

public class FeignClientFactoryBean
implements FactoryBean<Object>

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

创建过程

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

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的代码如下:

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

debug进入getObject方法后:

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

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

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

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

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

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容器,各自独立。

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容器。

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

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

创建对应的Feign spring容器

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

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

继续上图的2处:

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容器其实还没创建呢:

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

创建代码就是上图中的:

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

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

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

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

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

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

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

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

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

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

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

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

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,它就是我们的默认配置类。

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

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

这里简单列举:

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:

// 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处。跟进如下:

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

@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是空的:

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

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

根据url决定FeignClient的类型

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

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

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

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

loadBalance方法

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

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

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

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

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

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

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

这其中,LoadBalancerClientFactory来自于依赖:

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

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

注入了LoadBalancerClient和LoadBalancerClientFactory这两个bean。

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

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

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

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

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

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

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

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

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

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

总结

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

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

Feign源码解析:初始化过程(三)的更多相关文章

  1. Feign源码解析

    1. Feign源码解析 1.1. 启动过程 1.1.1. 流程图 1.1.2. 解释说明 Feign解析过程依赖Spring的初始化,它通过实现ImportBeanDefinitionRegistr ...

  2. bitcoin 源码解析 - 交易 Transaction(三) - Script

    bitcoin 源码解析 - 交易 Transaction(三) - Script 之前的章节已经比较粗略的解释了在Transaction体系当中的整体运作原理.接下来的章节会对这个体系进行分解,比较 ...

  3. Feign源码解析系列-注册套路

    感谢不知名朋友的打赏,感谢你的支持! 开始 在追寻Feign源码的过程中发现了一些套路,既然是套路,就可以举一反三,所以值得关注. 这篇会详细解析Feign Client配置和初始化的方式,这些方式大 ...

  4. mybatis源码-解析配置文件(三)之配置文件Configuration解析

    目录 1. 简介 1.1 系列内容 1.2 适合对象 1.3 本文内容 2. 配置文件 2.1 mysql.properties 2.2 mybatis-config.xml 3. Configura ...

  5. junit源码解析--初始化阶段

    OK,我们接着上篇整理.上篇博客中已经列出的junit的几个核心的类,这里我们开始整理junit完整的生命周期. JUnit 的完整生命周期分为 3 个阶段:初始化阶段.运行阶段和结果捕捉阶段. 这篇 ...

  6. springmvc源码解析-初始化

    1.      概述 对于Web开发者,MVC模型是大家再熟悉不过的了,SpringMVC中,满足条件的请求进入到负责请求分发的DispatcherServlet,DispatcherServlet根 ...

  7. 分布式事务_02_2PC框架raincat源码解析-启动过程

    一.前言 上一节已经将raincat demo工程运行起来了,这一节来分析下raincat启动过程的源码 主要包括: 事务协调者启动过程 事务参与者启动过程 二.协调者启动过程 主要就是在启动类中通过 ...

  8. mybatis源码阅读-初始化过程(七)

    说明 mybatis初始化过程 就是解析xml到封装成Configuration对象 供后续使用 SqlSessionFactoryBuilder 代码例子 SqlSessionFactoryBuil ...

  9. Feign源码解析系列-那些注解们

    开始 Feign在Spring Cloud体系中被整合进来作为web service客户端,使用HTTP请求远程服务时能就像调用本地方法,可见在未来一段时间内,大多数Spring Cloud架构的微服 ...

  10. Feign源码解析系列-最佳实践

    前几篇准备写完feign的源码,这篇直接给出Feign的最佳实践,考虑到目前网上还没有一个比较好的实践解释,对于新使用spring cloud的同学会对微服务之间的依赖产生一些迷惑,也会走一些弯路.这 ...

随机推荐

  1. modbus转profinet网关连接ABB变频器在博图程序案例

    modbus转profinet网关连接ABB变频器在博图程序案例 在博图里PLC无需编程利用兴达易控modbus转Profinet网关将ABB变频器接入到西门子网络中,用到设备为西门子1200PLC, ...

  2. LSP 链路状态协议

    转载请注明出处: 链路状态协议(Link State Protocol)是一种在计算机网络中用于动态计算路由的协议.它的主要作用是收集网络拓扑信息,为每个节点构建一个准确的网络图,并基于这些信息计算出 ...

  3. [知识管理] Obsidian + Remotely Save插件 + 第三方存储/OSS(七牛云)的同步方案

    0 序言 在几经选择.对比之后,我选择:Obsidian + Remotely Save插件 + 第三方存储/OSS(七牛云) 的方案来搭建自己的[知识管理系统]. 对比分析知识管理工具的过程,详情参 ...

  4. 前端三件套系例之JQuery——JQuery动画效果、JQuery插件、

    文章目录 1 JQuery动画效果 1. 基本效果 2. 滑动效果 3 淡入淡出效果 4 自定义动画 5 动画控制 6 设置 7 事件 7-1 常用事件 7-2 事件绑定 7-3 移除事件 7-4 阻 ...

  5. TOP GP 把已经编译的per反编回对应版本的4fd(画面档)

    由于GP5.1,5.2,5.3的genero对应版本画面档开发互不兼容,下面提供各版本之间互转的操作方法: xshell切换到要反编译的per档目录,执行以下命令,就会在同目录下生成对应4fd档资料 ...

  6. 漏洞扫描与安全加固之Apache Axis组件

    一.Apache Axis组件高危漏洞自查及整改 Apache Axis组件存在由配置不当导致的远程代码执行风险. 1. 影响版本 Axis1 和Axis2各版本均受影响 2. 处置建议 1)禁用此服 ...

  7. 【Unity3D】动态路径特效

    1 前言 ​ 本文通过导航系统(NavMeshAgent)和线段渲染器(LineRenderer)实现了角色走迷宫和绘制路径功能,同时实现动态路径特效. ​ 导航系统的介绍详见博客:导航系统.分离路面 ...

  8. 以效率为导向:用ChatGPT和HttpRunner实现敏捷自动化测试(二)

    1.前言 在上一篇文章: 利用ChatGPT提升测试工作效率--测试工程师的新利器(一)中,我们提到了如何通过chatGPT生成单接口测试用例,然后再让chatGPT去根据测试用例去生成接口自动化脚本 ...

  9. Zuul 2.1.5 设计分析

    前言 https://github.com/Netflix/zuul zuul 是 SpringCloud 家族老兵,使用 Java 微服务大部分都在使用 zuul 作为网关.既然他如此重要,那么我们 ...

  10. 从零用VitePress搭建博客教程(5) - 如何自定义页面模板、给页面添加独有的className和使页面标题变成侧边目录?

    接上一节:从零用VitePress搭建博客教程(4) – 如何自定义首页布局和主题样式修改? 上一节其实我们也简单说了自定义页面模板,这一节更加详细一点说明,开始之前我们要知道在vitePress中, ...