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

感谢不知名朋友的打赏,感谢你的支持!

开始

在追寻Feign源码的过程中发现了一些套路,既然是套路,就可以举一反三,所以值得关注。
这篇会详细解析Feign Client配置和初始化的方式,这些方式大多依赖Spring的游戏规则,在和Spring相关的各个组件中都可以看到类似的玩法,都是可以举一反三。所以熟悉这些套路大有益处。

内容

在上一篇中,我们提到了注解FeignClient引入了FeignClientsRegistrar,它继承ImportBeanDefinitionRegistrar。
在Spring中,使用ImportBeanDefinitionRegistrar动态组装注册BeanDefinition,就是套路之一,像FeignClientsRegistrar一样的类还有很多,比如:org.springframework.cloud.netflix.ribbon.RibbonClientConfigurationRegistrar,org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesRegistrar

FeignClientsRegistrar实现ImportBeanDefinitionRegistrar的registerBeanDefinitions方法:

public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
   registerDefaultConfiguration(metadata, registry);
   registerFeignClients(metadata, registry);
}

从入口代码调用的两个方法看,从方法名上也可以看出来,要做的事可以分为两个:

1,注册@EnableFeignClients中定义defaultConfiguration属性下的类,包装成FeignClientSpecification,注册到Spring容器。
private void registerDefaultConfiguration(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   Map defaultAttrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName(), true);
   if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
      String name;
      // 注解只用的类进行判断是否为内部类或者方法内的本地类
      if (metadata.hasEnclosingClass()) {
         name = "default." + metadata.getEnclosingClassName();
      }
      else {
         name = "default." + metadata.getClassName();
      }
      registerClientConfiguration(registry, name,
            defaultAttrs.get("defaultConfiguration"));
   }
}
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
      Object configuration) {
   BeanDefinitionBuilder builder = BeanDefinitionBuilder
         .genericBeanDefinition(FeignClientSpecification.class);
   builder.addConstructorArgValue(name);
   builder.addConstructorArgValue(configuration);
   registry.registerBeanDefinition(
         name + "." + FeignClientSpecification.class.getSimpleName(),
         builder.getBeanDefinition());
}

registerClientConfiguration方法中,使用FeignClientSpecification生成BeanDefinitionBuilder,放入构造函数的两个参数,然后构造了bean注册的名称。
这里的名称是类似这样的:default.xxx.TestApplication.FeignClientSpecification。
假如你还记得在@FeignClient中有一个属性:configuration,这个属性是表示各个FeignClient自定义的配置类,后面也会通过调用registerClientConfiguration方法来注册成FeignClientSpecification到容器。
所以,这里可以完全理解在@EnableFeignClients中配置的是做为兜底的配置,在各个@FeignClient配置的就是自定义的情况。

2,对于每个@FeignClient进行解析,并将他们注册到spring容器。
public void registerFeignClients(AnnotationMetadata metadata,
      BeanDefinitionRegistry registry) {
   ClassPathScanningCandidateComponentProvider scanner = getScanner();
   scanner.setResourceLoader(this.resourceLoader);
   Set basePackages;
   Map attrs = metadata
         .getAnnotationAttributes(EnableFeignClients.class.getName());
   AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
         FeignClient.class);
   final Class[] clients = attrs == null ? null
         : (Class[]) attrs.get("clients");
   if (clients == null || clients.length == 0) {
      scanner.addIncludeFilter(annotationTypeFilter);
      basePackages = getBasePackages(metadata);
   }
   else {
      final Set clientClasses = new HashSet<>();
      basePackages = new HashSet<>();
      for (Class clazz : clients) {
         basePackages.add(ClassUtils.getPackageName(clazz));
         clientClasses.add(clazz.getCanonicalName());
      }
      AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
         @Override
         protected boolean match(ClassMetadata metadata) {
            String cleaned = metadata.getClassName().replaceAll("\\$", ".");
            return clientClasses.contains(cleaned);
         }
      };
      scanner.addIncludeFilter(
            new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
   }
   for (String basePackage : basePackages) {
      Set candidateComponents = scanner
            .findCandidateComponents(basePackage);
      for (BeanDefinition candidateComponent : candidateComponents) {
         if (candidateComponent instanceof AnnotatedBeanDefinition) {
            // verify annotated class is an interface
            AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
            AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
            Assert.isTrue(annotationMetadata.isInterface(),
                  "@FeignClient can only be specified on an interface");
            Map attributes = annotationMetadata
                  .getAnnotationAttributes(
                        FeignClient.class.getCanonicalName());
            String name = getClientName(attributes);
            registerClientConfiguration(registry, name, attributes.get("configuration"));
            registerFeignClient(registry, annotationMetadata, attributes);
         }
      }
   }
}

我们知道@FeignClient的扫描路径在@EnableFeignClients上是可以通过basePackages,basePackageClasses,client这三个参数进行配置的。所以在扫描@FeignClient之前就都是这个逻辑。确认好basePackages后,就遍历basePackages,利用扫描器扫出各个路径下的@FeignClient注解。

特别注意,@FeignClient是需要在定义扫描位置才能被解析的,如果你的feign客户端接口不在扫描范围是不会被入住到容器中,从而无法被使用。而且没有入口可以更改配置的扫描路径,在实际开发中需要注意。

这个扫描器ClassPathScanningCandidateComponentProvider又是spring的套路,通过配置的filters,找出需要的结果类。这个能力在自定义注解+扫描路径可配置的场景非常合适。
在确认好@FeignClient注解的是否为接口后,最后会解析配置,先调用registerClientConfiguration方法,后调用registerFeignClient方法。

registerClientConfiguration方法,前面已经提到过,这里会对每个FeignClient都进行调用,所以会把@FeignClient上配置的configuration包装成FeignClientSpecification到容器中。
registerFeignClient方法把FeignClientFactoryBean注入到容器,FeignClientFactoryBean用于生产FeignClient,后续详细。

private void registerFeignClient(BeanDefinitionRegistry registry,
      AnnotationMetadata annotationMetadata, Map attributes) {
   String className = annotationMetadata.getClassName();
   BeanDefinitionBuilder definition = BeanDefinitionBuilder
         .genericBeanDefinition(FeignClientFactoryBean.class);
   validate(attributes);
   definition.addPropertyValue("url", getUrl(attributes));
   definition.addPropertyValue("path", getPath(attributes));
   String name = getName(attributes);
   definition.addPropertyValue("name", name);
   definition.addPropertyValue("type", className);
   definition.addPropertyValue("decode404", attributes.get("decode404"));
   definition.addPropertyValue("fallback", attributes.get("fallback"));
   definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
   definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
   String alias = name + "FeignClient";
   AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
   boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null
   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);
}

FeignClientSpecification继承NamedContextFactory.Specification,而NamedContextFactory用与创建子上下文将NamedContextFactory.Specification放入其中。
NamedContextFactory中维护一个context的map,value是AnnotationConfigApplicationContext即子上下文。
在feign中定义了一个FeignContext继承NamedContextFactory,来统一维护feign中各个feign客户端相互隔离的上下文。

类相互依赖图:

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

FeignContext的代码:

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

NamedContextFactory中的defaultConfigType被设置为FeignClientsConfiguration。
这里我们先看一下NamedContextFactory中的createContext方法的实现:

protected AnnotationConfigApplicationContext createContext(String name) {
   // 每次调用new一个AnnotationConfigApplicationContext
   AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
   //在子上下文上就注册name对应的configuration
   if (this.configurations.containsKey(name)) {
      for (Class configuration : this.configurations.get(name)
            .getConfiguration()) {
         context.register(configuration);
      }
   }
  //注册default configuration
   for (Map.Entry entry : this.configurations.entrySet()) {
      if (entry.getKey().startsWith("default.")) {
         for (Class configuration : entry.getValue().getConfiguration()) {
            context.register(configuration);
         }
      }
   }
   // 将this.defaultConfigType即FeignClientsConfiguration也注册上
   context.register(PropertyPlaceholderAutoConfiguration.class,
         this.defaultConfigType);
   context.getEnvironment().getPropertySources().addFirst(new MapPropertySource(
         this.propertySourceName,
         Collections. singletonMap(this.propertyName, name)));
   // 父上下文设置,所有的子上下文都是一个父上下文,当子上下文找不到时,就去父上下文找
   if (this.parent != null) {
      // Uses Environment from parent as well as beans
      context.setParent(this.parent);
   }
   context.refresh();
   return context;
}

FeignContext注册到容器是在FeignAutoConfiguration上完成的:

@Autowired(required = false)
private List configurations = new ArrayList<>();
@Bean
public FeignContext feignContext() {
   FeignContext context = new FeignContext();
   context.setConfigurations(this.configurations);
   return context;
}

在初始化FeignContext时,会把configurations在容器中放入FeignContext中。configurations的来源就是在前面registerFeignClients方法中将@FeignClient的配置configuration。

结束

关键需要理解的是在feign中为每一个client准备了FeignContext,内部维护这个自定义配置的内容比如Encoder,Decoder等,从而实现对每个client自定义能力。

Original: https://www.cnblogs.com/killbug/p/10447625.html
Author: 每当变幻时
Title: Feign源码解析系列-注册套路

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/609301/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

  • SpringbootTest注入失败

    正确方法: java;gutter:true; /<strong> * <em>@author:sawsh * </em>@date:2021/…

    Java 2023年5月30日
    0124
  • 【game】1、pacman利用bfs进行搜索路径自动吃豆

    1.设计思路 设计思路有几个,一步步优化来的 v0.1 比较复杂,而且进行了2次bfs,浪费了大量时间 v0.2 简化了2次bfs的操作,但是有很多不必要的判断逻辑,并且考虑不够全…

    Java 2023年6月5日
    094
  • 【SpringCloud-Alibaba系列教程】1.环境搭建以及注意事项

    一、开发环境 JDK 1.8SpringBoot 2.1.7.RELEASESpringCloud-Alibaba 2.1.2.RELEASE数据库MySQL 5.8如果需要修改版…

    Java 2023年6月5日
    095
  • 开源C# WPF控件库《AduSkin – UI》

    追求极致,永臻完美 A Beautiful WPF Control UI 一款简单漂亮的WPF UI,融合部分开源框架的组件,为个人定制的UI,可供学者参考。https://www…

    Java 2023年5月30日
    0107
  • MySQL五:InnoDB线程模型

    转载~ 一、InnoDB线程模型的组成 在Innodb存储引擎中,后台线程的主要作用是 「负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据」。此外它会将已经修改的数据文…

    Java 2023年6月8日
    087
  • Java开发笔记(一百四十七)通过JDBC管理数据库

    前面介绍了如何通过JDBC获取数据库连接,可是Connection对象不能直接执行SQL语句,需要引入Statement报告对象才能操作SQL。Statement对象由Connec…

    Java 2023年6月6日
    098
  • JDK1.8新特性汇总

    Java 8 新特性 Java 8 (又称为 jdk 1.8) 是 Java 语言开发的一个主要版本。 Oracle 公司于 2014 年 3 月 18 日发布 Java 8 ,它…

    Java 2023年6月9日
    090
  • Jackson: java.util.LinkedHashMap cannot be cast to X

    Jackson是一个广泛使用的 Java 库,它允许我们方便地序列化/反序列化 JSON 或 XML。 有时,当我们尝试将 JSON 或 XML 反序列化为对象集合时,可能会遇到&…

    Java 2023年5月29日
    0112
  • TreeMap源码分析

    TreeMap源码分析 数据结构 TreeMap使用红黑树来存储数据,红黑树是一种平衡二叉查找树,它是一种高效的搜索算法,它的算法时间复杂度是O(lgn) 增删改查 增改 publ…

    Java 2023年6月16日
    091
  • [Java]ArrayList源码解析

    ArrayList源码解析 1. 核心源码解读 package java.util; import java.util.function.Consumer; import java…

    Java 2023年6月5日
    077
  • java类库

    Java的应用程序接口(API)以包的形式来组织,每个包提供大量的相关类、接口和异常处理类,这些包的集合就是Java的类库。 Java类库可以分为两种 包名以java开始的包是Ja…

    Java 2023年6月7日
    096
  • 使用Python定时清理运行超时的pdflatex僵尸进程

    问题 在我们之前的《基于texlive定制chemfig化学式转换Python服务镜像》定制的pdflatex在线转换的镜像已经运行在生产环境了,但是最近总有人反馈服务跑着跑着就慢…

    Java 2023年6月7日
    0100
  • 从源码中理解Spring Boot自动装配原理

    SpringBoot 定义了一套接口规范,这套规范规定: SpringBoot在启动时会扫描外部引用jar包中的 META-INF/spring.factories文件,将文件中配…

    Java 2023年6月16日
    099
  • RPC学习–C#使用Thrift简介,C#客户端和Java服务端相互交互

    本文主要介绍两部分内容: C#中使用Thrift简介 用Java创建一个服务端,用C#创建一个客户端通过thrift与其交互。 用纯C#实现Client和Server C#服务端,…

    Java 2023年5月29日
    0102
  • Linux 进程管理

    Linux 进程管理 在 LINUX 中,每个执行的程序都称为一个进程。每一个进程都分配一个 ID 号(pid,进程号)。 每个进程都可能以两种方式存在的。前台与后台,所谓前台进程…

    Java 2023年6月5日
    081
  • Docker学习笔记二(linux下安装Docker)

    Docker学习笔记二(linux下安装Docker) 1.在线安装linux Docker 这种方式首先要保证linux 环境下可以上网,当然,小编是在自己的电脑上安装了虚拟机,…

    Java 2023年6月16日
    0104
亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球