OpenFeign#1 - FeignClient 是如何注册的?

news2024/11/23 21:39:08

文章目录

  • @EnableFeignClients
    • FeignClientsRegistrar
      • registerDefaultConfiguration
      • registerFeignClients
  • @FeignClient
    • FeignClientFactoryBean
      • FeignContext
      • feign(FeignContext)

@EnableFeignClients

该注解会导致 FeignClientsRegistrar 的注入.

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
   ...
   Class<?>[] defaultConfiguration() default {};
   ...
}

FeignClientsRegistrar

这是一个 FeignClient 的注册器, 实现了接口 ImportBeanDefinitionRegistrarregisterBeanDefinition(AnnotationMetadata metadata, BeanDefinitionRegistry registry)方法:

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
                                    BeanDefinitionRegistry registry) {
   registerDefaultConfiguration(metadata, registry); // 1
   registerFeignClients(metadata, registry); // 2
}

registerDefaultConfiguration

其中, registerDefaultConfiguration 负责注册 @EnableFeignClients 的属性 defaultConfiguration 指定的配置类 (注意 name 属性的约定方式 -> FeignClientSpecification).
在这里插入图片描述

调用链:registerBeanDefinitions-registerDefaultConfiguration-registerClientConfiguration:

private void registerClientConfiguration(BeanDefinitionRegistry registry, 
                                         Object name,
                                         Object configuration) {
   // 配置类被注入成 "FeignClientSpecification"
   BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
   builder.addConstructorArgValue(name);
   builder.addConstructorArgValue(configuration);
   registry.registerBeanDefinition(
     name + "." + FeignClientSpecification.class.getSimpleName(),
     builder.getBeanDefinition()
   );
}

registerFeignClients

在这里插入图片描述

registerFeignClients则负责注册所有@FeignClient(将标注了该注解的接口注册为 FeignClientFactoryBean). 同时以 name 为标识注册该 FeignClient 的配置类. 调用链:registerBeanDefinitions-registerFeignClients-registerFeignClient

其中, registerFeignClients 方法的具体职责是:

  1. 获取 @EnableFeignClients 的 clients 属性指定的"标注了 @FeignClient 的 Class, 该属性非空时会和第二步互斥.
  2. 如果 @EnableFeignClients 的 clients 属性没有指定, 则会启用基于属性 basePackages 或者服务本身 basePackage 对 @FeignClient 的候选类的扫描.
  3. 注册基于 clientName 的配置对象:
    1. 获取 clientName: contextId || value || name || serviceId [详见: org.springframework.cloud.openfeign.FeignClientsRegistrar#getClientName)]
    2. 调用 registerClientConfiguration(BeanDefinitionRegistry, clientName, 注解上配置的 configuration[])
// org.springframework.cloud.openfeign.FeignClientsRegistrar#registerClientConfiguration
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
   BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
   builder.addConstructorArgValue(name);
   builder.addConstructorArgValue(configuration);
   registry.registerBeanDefinition(
         // 注意 name 的定义方式与 `registerDefaultConfiguration` 的区别
         name + "." + FeignClientSpecification.class.getSimpleName(),
         builder.getBeanDefinition());
}
  1. 注册通过第1步或者第2步获取到的"候选类", 调用 registerFeignClient:
// org.springframework.cloud.openfeign.FeignClientsRegistrar#registerFeignClient
private void registerFeignClient(BeanDefinitionRegistry registry,
                                 AnnotationMetadata annotationMetadata, 
                                 Map<String, Object> attributes) {
   String className = annotationMetadata.getClassName();
   // 本质上也是注册成 FeignClientFactoryBean
   BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
   ... /* 将注解属性添加到 BeanDefinitionBuilder */
   String alias = contextId + "FeignClient";
   AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
   beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
   // has a default, won't be null
   boolean primary = (Boolean) attributes.get("primary");
   beanDefinition.setPrimary(primary);
   String qualifier = getQualifier(attributes);
   if (StringUtils.hasText(qualifier)) {
      alias = qualifier;
   }
   BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
   BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

至此, @EnableFeignClients 的流程结束. 最终会输出默认的 FeignClientSpecification 与 FeignClient 配对的 FeignClientSpecification, 和 FeignClientFactoryBean. 整体流程图如下:
在这里插入图片描述
从上一节我们知道, @EnabelFeignclients 会扫描并将标注了 @FeignClient 注解的 Bean 注册成 FeignClientFactoryBean, 下面我们主要看看这个 FactoryBean 的能力.

@FeignClient

从上一节我们知道, @EnabelFeignclients 会扫描并将标注了 @FeignClient 注解的 Bean 注册成 FeignClientFactoryBean, 下面我们主要看看这个 FactoryBean 的能力.

FeignClientFactoryBean

上一节已经提到, @FeignClient 标识的接口会被 “根据 @EnableFeignClients 配置的规则扫描出来” 随后 “注册成 FeignClientFacotryBean(在 registerFeignClient 方法中)”.

补充说明
在 Spring 中,FactoryBean 是一个工厂 Bean,用来创建代理 Bean。工厂 Bean 是一种特殊的 Bean,对于需要获取 Bean 的消费者而言,它是不知道 Bean 是普通 Bean 或是工厂 Bean 的。工厂 Bean 返回的实例不是工厂 Bean 本身,而是会返回执行了工厂 Bean 中 FactoryBean#getObject 逻辑的实例.
来看看 FeignClientFactoryBean 对接口 FactoryBean#getObject() 的实现方式:

<T> T getTarget() {
   FeignContext context = applicationContext.getBean(FeignContext.class);
   Feign.Builder builder = feign(context);
   if (!StringUtils.hasText(url)) { // 查看@FeignClient的url是否为空
      if (!name.startsWith("http")) {
         url = "http://" + name;
      } else {
         url = name;
      }
      url += cleanPath();
      return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
   }
   if (StringUtils.hasText(url) && !url.startsWith("http")) {
      url = "http://" + url;
   }
   String url = this.url + cleanPath();
   Client client = getOptional(context, Client.class);
   if (client != null) {
      if (client instanceof LoadBalancerFeignClient) {
         // not load balancing because we have a url, but ribbon is on the classpath, so unwrap
         client = ((LoadBalancerFeignClient) client).getDelegate();
      }
      if (client instanceof FeignBlockingLoadBalancerClient) {
         // not load balancing because we have a url, but Spring Cloud LoadBalancer is on the classpath, so unwrap
         client = ((FeignBlockingLoadBalancerClient) client).getDelegate()
      }
      builder.client(client);
   }
   Targeter targeter = get(context, Targeter.class);
   return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
}

FeignContext

首先看第一行: applicationContext.getBean(FeignContext.class) 从 IOC 容器中获取 FeignContext 的实例. FeignContext 是在 OpenFeignStarter 的 spring.factories 被配置为自动注入的:
在这里插入图片描述
FeignAutoConfiguration 会将 FeignContext 注册到容器中, 同时将 "通过 registerClientConfiguration 注入的由用户指定的配置类(@EnableFeignClients 的 configuration 和 @FeignClient 的 configuration) (FeignClientSpecification) " 设值到 FeignContext 中:
在这里插入图片描述

feign(FeignContext)

接下来我们再回到 FeignClientFactoryBean 的 getTarget() 方法, feign(context) 负责组织 Feign.Builder:
首先, encoder, decoder 和 contract 由 @EnableFeignClients@FeignClient 注解属性 configuration 指定. 其中:

  • encoder: feign.codec.Encoder. 负责在当方法参数没有被标注 @Param 时, 将对象解析成 HTTP 请求体.
  • decoder: feign.codec.Decoder. 负责将 HTTP 响应解析成具体的返回值. 当状态码是 2xx, 并且返回类型不是 void 和 Response 时, 被调用.
  • contract: feign.Contract. 定义了接口能够识别的注解和值.
// org.springframework.cloud.openfeign.FeignClientFactoryBean#feign
protected Feign.Builder feign(FeignContext context) {
  FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
  Logger logger = loggerFactory.create(type);

  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));

  configureFeign(context, builder);
  return builder;
}

补充说明
org.springframework.cloud.openfeign.FeignClientFactoryBean#get
get(FeignContext, Class) 方法和其后续调用逻辑的核心在于基于 FeignContext (extends NamedContextFactory) 创建一个基于 contextId 的 AnnotationConfigApplicationContext, 进而注册当前 (contextId 指定) FeignContext 持有的 List 中对应的 FeignClient 的配置类及其中 @Bean, 见下图:
在这里插入图片描述
在这里插入图片描述
另外, FeignContext 也持有了所有已创建的 AnnotationConfigApplicationContext 与 contextId 的对应关系的 “缓存”:
在这里插入图片描述
整体调用流程如下:
在这里插入图片描述


接着, configureFeign(FeignContext, Feign.Builder), 其作用是根据属性或者配置类(@EnableFeignClients 的 configuration 和 @FeignClient 的 configuration) 配置 Feign.Builder:

// org.springframework.cloud.openfeign.FeignClientFactoryBean#configureFeign
protected void configureFeign(FeignContext context, Feign.Builder builder) {
  FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
  FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
  setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
  if (properties != null && inheritParentContext) {
    if (properties.isDefaultToProperties()) {
      configureUsingConfiguration(context, builder);
      configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
      configureUsingProperties(properties.getConfig().get(contextId), builder);
    } else {
      configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
      configureUsingProperties(properties.getConfig().get(contextId), builder);
      configureUsingConfiguration(context, builder);
    }
  } else {
    configureUsingConfiguration(context, builder);
  }
}

重点关注 getOptional(context, FeignClientConfigurer.class) 子句, 跟进去:
1.org.springframework.cloud.openfeign.FeignClientFactoryBean#getOptional:

protected <T> T getOptional(FeignContext context, Class<T> type) {
   return context.getInstance(contextId, type);
}

2.org.springframework.cloud.context.named.NamedContextFactory#getInstance(java.lang.String, java.lang.Class<T>):

public <T> T getInstance(String name, Class<T> type) {
   AnnotationConfigApplicationContext context = getContext(name);
   try {
      return context.getBean(type);
   } catch (NoSuchBeanDefinitionException e) {}
   return null;
}

3.org.springframework.cloud.context.named.NamedContextFactory#getContext:

protected AnnotationConfigApplicationContext getContext(String name) {
   if (!this.contexts.containsKey(name)) {
      synchronized (this.contexts) {
         if (!this.contexts.containsKey(name)) {
            this.contexts.put(name, createContext(name));
         }
      }
   }
   return this.contexts.get(name);
}

4.org.springframework.cloud.context.named.NamedContextFactory#createContext:

protected AnnotationConfigApplicationContext createContext(String name) {
   // AnnotationConfigApplicationContext 是一个 application context. 其继承了 GenericApplicationContext 的全部实现, 同时实现了 AnnotationConfigRegistry. 它额外扩展了能通过扫描 package 或是直接调用注册方法的方式来加载带有特定 Annotation Bean 的功能
   AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
   if (this.configurations.containsKey(name)) {
      for (Class<?> configuration : this.configurations.get(name).getConfiguration()) {
         context.register(configuration); // 注册配置类及其内部 @Bean
      }
   }
   for (Map.Entry<String, C> entry : this.configurations.entrySet()) {
      if (entry.getKey().startsWith("default.")) {
         for (Class<?> configuration : entry.getValue().getConfiguration()) {
            context.register(configuration); // 注册默认配置
         }
      }
   }
   context.register(PropertyPlaceholderAutoConfiguration.class, this.defaultConfigType);
   context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(this.propertySourceName, Collections.<String, Object>singletonMap(this.propertyName, name)));
   if (this.parent != null) {
      // Uses Environment from parent as well as beans
      context.setParent(this.parent);
      context.setClassLoader(this.parent.getClassLoader());
   }
   context.setDisplayName(generateDisplayName(name));
   context.refresh(); // ! 刷新 AnnotationConfigApplicationContext
   return context;
}

结束, 现在我们知道了 FeignClient 是如何注册并应用配置类的.
下一篇, 我们讨论如何在 Starter 中注册 FeignClient: OpenFeign#2 - 在 Starter 中手动注册 FeignClient

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/422286.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Redis AOF

一、Redis AOF 1.简介 目前&#xff0c;redis的持久化主要应用AOF(Append Only File)和RDB两大机制。AOF以日志的形式来记录每个写操作(增量保存),将redis执行过的所有写指令全部记录下来(读操作不记录)。只许追加文件&#xff0c;但不可以改写文件。redis启动之初会读取该文…

自动化测试学习(七)-正则表达式,你真的会用吗?

目录 一、正则表达式在python中如何使用 二、用正则表达式匹配更多模式 三、常用字符分类的缩写代码 总结 所谓正则表达式&#xff08;regex&#xff09;&#xff0c;就是一种模式匹配&#xff0c;学会用正则匹配&#xff0c;就可以达到事半功倍的效果。 一、正则表达式在…

工程管理系统软件 自主研发,工程行业适用

Java版工程项目管理系统 Spring CloudSpring BootMybatisVueElementUI前后端分离 功能清单如下&#xff1a; 首页 工作台&#xff1a;待办工作、消息通知、预警信息&#xff0c;点击可进入相应的列表 项目进度图表&#xff1a;选择&#xff08;总体或单个&#xff09;项目显示…

在 Web3 里如何寻找靠谱的创新路径——Solv V3 阶段性复盘(上)

作者&#xff1a;Solv 核心团队Solv V3 是 3 月 21 日发布的&#xff0c;到这周一整整三个星期。三周时间产生的实际交易额超过 6,000 万美元&#xff0c;预计在本月内能够破亿。而且从我们手上的 pipeline 来看&#xff0c;这个增长的势头还将持续下去。在几个月内&#xff0c…

「Long HK, Long Crypto」主题酒会在香港圆满举行!Web3er齐聚一堂共叙行业发展

「2023 香港 Web3 嘉年华」是香港有史以来规模最大的加密货币活动之一&#xff0c;邀请了300多位行业领袖、区块链项目创始人、投资人、监管机构代表、专家学者&#xff0c;为每一位参与者带来前沿的 Web3 技术&#xff0c;共同探讨 Web3 未来发展的关键问题。 Web3相关的各大…

OpenCV实战(19)——特征描述符

OpenCV实战&#xff08;19&#xff09;——特征描述符0. 前言1. 特征描述符2. 提升匹配集质量2.1 交叉检查匹配2.2 比率测试2.3 距离阈值3. 完整代码小结系列链接0. 前言 SURF 和 SIFT 关键点检测算法为每个检测到的特征计算位置、方向和比例&#xff0c;比例因子信息可用于定…

RestClient查询文档

文章目录1、RestClient查询文档----快速入门2、查询文档--match、term、range、bool3、查询文档-排序和分页4、高亮1、RestClient查询文档----快速入门 基本步骤 1.先创建SearchRequest对象&#xff0c;调用source方法&#xff08;DSL&#xff09;——>相当于红框内的大jso…

最前端|什么是低代码?与传统开发的区别是什么?

目录一、低代码介绍二、背景趋势三、低代码与传统代码开发&#xff08;一&#xff09;低代码能否替代传统开发低代码页面传统开发页面&#xff08;二&#xff09;相同业务不同方式对比1.低代码开发&#xff08;1&#xff09;优点&#xff08;2&#xff09;缺点2.传统代码开发&a…

实现一个登录功能方案设计2

需求MySQL表实现方案index页面home页面需求 实现一个登录功能 实现的功能 注册(邮箱注册)登录(邮箱密码)重置密码查看操作记录(登录, 注册, 重置密码, 登出. 都算操作)登出在第一版的基础上进行优化:\ 优化点: 存操作信息请求的post使用中间件进行储存操作信息.避免重复代码 因…

【面试】如何设计SaaS产品的数据权限?

文章目录前言数据权限是什么&#xff1f;设计原则整体方案RBAC模型怎么控制数据权限&#xff1f;1. 数据范围权限控制2. 业务对象操作权限控制3. 业务对象字段权限控制总结前言 一套系统的权限可以分为两类&#xff0c;数据权限和功能权限&#xff0c;今天我们从以下几个点&am…

阿里云服务器安装宝塔面板搭建网站全流程(一步步详解)

阿里云服务器安装宝塔面板教程&#xff0c;云服务器吧以阿里云Linux系统云服务器安装宝塔Linux面板为例&#xff0c;先配置云服务器安全组开放宝塔所需端口8888、888、80、443、20和21端口&#xff0c;然后执行安装宝塔面板命令脚本&#xff0c;最后登录宝塔后台安装LNMP&#…

UML 简易使用教程

最近刚好有空&#xff0c;遂决定对应 UML 常用的一些图进行整理&#xff0c;供自己以及需要的人查阅。 UML 分为静态模型与动态模型。静态模型描述一个系统的静态特征&#xff0c;固定的框架结构。包括用例图、类图、对象图、组件图、部署图&#xff1b;动态模型包括时序图、协…

TensorFlow 智能移动项目:11~12

原文&#xff1a;Intelligent mobile projects with TensorFlow 协议&#xff1a;CC BY-NC-SA 4.0 译者&#xff1a;飞龙 本文来自【ApacheCN 深度学习 译文集】&#xff0c;采用译后编辑&#xff08;MTPE&#xff09;流程来尽可能提升效率。 不要担心自己的形象&#xff0c;只…

Flutter 第一个界面

第一个页面 app首页 入口函数 一个Flutter工程的入口函数与Dart命令行工程一样是main&#xff0c;不同的是在Flutter中执行runApp(ArticleApp()) 就能够在手机屏幕上展示这个Widget。 import package:flutter/material.dart; void main() > runApp(new ArticleApp()); Ar…

OpenAI不能访问有什么方法解救呢?试试这方法吧

最近发现国内不挂代理是不能访问到openAI的接口的&#xff0c;为了解决这个问题&#xff0c;我一直在github上需在解决方案&#xff0c;今天终于被我找到一个大神开源了一个解决方案。下面就来看看如何做吧。 整个项目的代码很简单只有几行代码&#xff1a; {"rewrites&q…

几种在Python中List添加、删除元素的方法

嗨害大家好鸭&#xff01;我是爱摸鱼的芝士❤ 一、python中List添加元素的几种方法 List 是 Python 中常用的数据类型&#xff0c; 它一个有序集合&#xff0c; 即其中的元素始终保持着初始时的定义的顺序 &#xff08;除非你对它们进行排序或其他修改操作&#xff09;。 …

进程互斥的实现方式

1.进程互斥的软件实现方法 1.单标志法 算法思想&#xff1a;两个进程在访问完临界区后会把使用临界区的权限转让给另一个进程&#xff0c;也就是说每个进程进入临界区的权限只能被另一个进程赋予 局限性 2.双标志先检查法 算法思想&#xff1a;设置一个布尔数组flag[]&#xff…

python 笔记:PyTrack(将GPS数据和OpenStreetMap数据进行整合)【官网例子解读】

论文笔记&#xff1a;PyTrack: A Map-Matching-Based Python Toolbox for Vehicle Trajectory Reconstruction_UQI-LIUWJ的博客-CSDN博客4 0 包的安装 官网的两种方式我都试过&#xff0c;装是能装成功&#xff0c;但是python import PyTrack包的时候还是显示找不到Pytrack …

Altova MapForce 2023 Crack

Altova MapForce 2023 Crack 数据映射项目中的注释-除了支持对数据映射项目的单个连接进行注释外&#xff0c;现在还可以向源组件和目标组件添加注释&#xff0c;以帮助记录映射的作用和实现方式。 支持XML输出中的standalone“yes”声明-在独立文档声明中&#xff0c;值“yes”…

Chat-GLM 详细部署(GPU显存>=12GB)

建议配置: ( Windows OS 11 部署 )CPU-i7 13700F ~ 13700KF RAM: 16GB DDR4 GPU: RTX3080(12G) 安装 conda: 1. 下载安装 miniconda3 &#xff1a; https://docs.conda.io/en/latest/miniconda.html conda是一个包和环境管理工具&#xff0c;它不仅能管理包&#xff0c;还能隔…