SpringBoot 自动装配流程以及核心源码剖析

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
全局流量管理 GTM,标准版 1个月
简介: SpringBoot 自动装配流程以及核心源码剖析

前言

Spring Framework 一直在致力于解决一个问题,就是如何让 bean 管理变得更简单,如何让开发者尽可能的少关注一些基础化的 bean 配置,从而实现自动装配。所谓的自动装配,实际上就是如何自动将 bean 装载到 Ioc 容器中来

实际上在 spring 3.x 版本中,Enable 模块驱动注解的出现,已经有了一定的自动装配的雏形,而真正能够实现这一机制,还是在 spirng 4.x 版本中,conditional 条件注解的出现;@EnableXxx 注解其实本质上就是 @Import 注解的体现,而 @Import 注解是为了替代之前的 <import> 标签而出现的.

@Import 可以根据添加的不同类型来作出不一样的操作

  • 普通类型:直接注入该类型的对象
  • 实现了 ImportBeanDefinitionRegistrar 接口:不注入该类型的对象,调用 registerBeanDefinitions 方法,通过注册器进行注入
  • 实现了 ImportSelector 接口:不注入该类型的对象,调用 selectImports 方法,将返回的数据注入到容器中

深入分析装配过程

启动类注解:@SpringBootApplication->内置注解:@EnableAutoConfiguration

EnableAutoConfiguration:主要作用就是帮助 SpringBoot 应用把所有符合条件的 @Configuration 配置都加载到当前创建且使用的 IOC 容器中

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

如上可以看到,通过 Import 导入的类型不仅仅是一个普通的配置类,而是一个实现 ImportSelector 接口的类型,它基于动态 bean 加载的功能;由于其接口下的核心方法是 selectImports,可以追踪一下其源码实现

selectImports 方法

public String[] selectImports(AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return NO_IMPORTS;
  }
  // 从配置文件(spring-autoconfigure-metadata.properties)中加载
  AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
      .loadMetadata(this.beanClassLoader);
  // 获取所有候选配置类 EnableAutoConfiguration
  AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(autoConfigurationMetadata,
      annotationMetadata);
  return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}

getAutoConfigurationEntry 方法

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata,
      AnnotationMetadata annotationMetadata) {
  if (!isEnabled(annotationMetadata)) {
    return EMPTY_ENTRY;
  }
  // 获取元注解中的属性
  AnnotationAttributes attributes = getAttributes(annotationMetadata);
  // 使用 SpringFactoriesLoader 加载 classpath 路径下 META-INF\spring.factories中,
  // key= org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的value
  List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  // 去重
  configurations = removeDuplicates(configurations);
  // 应用 exclusion 属性
  Set<String> exclusions = getExclusions(annotationMetadata, attributes);
  checkExcludedClasses(configurations, exclusions);
  configurations.removeAll(exclusions);
  // 过滤,检查候选配置类上的注解 @ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载
  configurations = filter(configurations, autoConfigurationMetadata);
  // 广播事件
  fireAutoConfigurationImportEvents(configurations, exclusions);
  return new AutoConfigurationEntry(configurations, exclusions);
}

本质上来说,其实 EnableAutoConfiguration 会帮助 SpringBoot 应用把所有符合 @Configuration 配置都加载到当前 SpringBoot 创建的 IOC 容器,而这里面借助了 Spring 框架提供的一个工具类 SpringFactoriesLoader 的支持;以及用到了 Spring 提供的条件注解 @Conditional,选择性的针对需要加载的 bean 进行条件过滤

SpringFactoriesLoader 其实和 SPI 实现机制的是一样的,只不过它不会像 SPI 一次性把所有的类全部加载完,而是通过 key「全限定类名」加载其对应 value,加载的文件名称:classpath:META-INF/spring.factories

条件过滤配置类

在 spring.factories 文件中配置的类有很多,有时候配置类又要依赖于其他的类型存在才得以生存,所以在 SpringBoot 自动装配中还提供了 ConditionalOnClass、ConditionalOnBean 这些条件来过滤所加载的配置类,摘取部分 spring-boot-autoconfigure 模块下 spring-autoconfigure-metadata.properties 文件源码

org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration=
org.springframework.boot.autoconfigure.data.redis.LettuceConnectionConfiguration.ConditionalOnClass=io.lettuce.core.RedisClient
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration=
org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration.ConditionalOnClass=org.springframework.data.redis.core.RedisOperations

通过这种条件过滤可以有效的减少 @Configuration 类数量从而降低 SpringBoot 启动时间

小结

通过启动类注解下自动配置注解实现的 ImportSelector 选择器,去扫描指定文件下所有的配置类,按照系统需要去加载和过滤相关的配置类,满足系统运行其他中间件的需要,通过以下图来小结上面所介绍的内容,接下来就是介绍容器是在那个过程中去执行扫描工作的

深入解析过程

以上内容只是介绍配置类是如何装配进去的,具体的解析还是交由我们容器去处理的,而且这些配置类也不是说在拿到以后就直接去注入的,它们是等待所依赖的类型先注入以后,到最后才去处理的.

通过类图可以看出,@Import 导入的选择器并不是直接实现 ImportSelector 接口,而是实现的 DeferredImportSelector,其字面含义就是延迟导入,对父接口做了增强处理

DeferredImportSelector 接口

通过以上的类结构可以看出 DeferredImportSelector 接口是基于 ImportSelector 接口的一个扩展

DeferredImportSelector 接口本身也有 ImportSelector 接口的功能,如果我们仅仅是实现了DeferredImportSelector 接口,重写了 selectImports 方法,那么 selectImports 方法还是会被执行的,来看代码

public class MyImportSelector implements DeferredImportSelector {
  @Override
  public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    System.out.println("MyImportSelector implements DeferredImportSelector >>>");
    return new String[0];
  }
}
@Configuration
@Import(MyImportSelector.class)
public class MyAutoConfig {
  public static void main(String[] args) {
    ApplicationContext applicationContext = new AnnotationConfigApplicationContext(MyAutoConfig.class);
  }
}

但是当我们重写了 DeferredImportSelector 中的 Group 接口,并重写了 getImportGroup 方法,那么容器在启动时就不会执行 selectImports 方法了,而是执行 getImportGroup 方法,进而执行 Group 接口中重写的方法,代码如下:

public class MyImportSelector implements DeferredImportSelector {
  @Override
  public String[] selectImports(AnnotationMetadata importingClassMetadata) {
    System.out.println("MyImportSelector implements DeferredImportSelector >>>");
    return new String[0];
  }
  @Override
  public Class<? extends Group> getImportGroup() {
    System.out.println("MyImportSelector#getImportGroup");
    return MyGroup.class;
  }
  public static class MyGroup implements Group {
    private List<Entry> imports = new ArrayList<>();
    @Override
    public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
      System.out.println("MyImportSelector->MyGroup#process");
    }
    @Override
    public Iterable<Entry> selectImports() {
      System.out.println("MyImportSelector->MyGroup#selectImports");
      return imports;
    }
  }
}

ImportSelector 实例的 selectImports 方法的执行时机,是在 @Configuration 注解中的其他逻辑被处理之前,所谓的其他逻辑,包括对 @ImportResource、@Bean 这些注解的处理(注意,这里只是对 @Bean 修饰方法的处理,并不是立即调用 @Bean 修饰的方法,这个区别很重要!)

上面的结论以及从流程图分析,我们可以直接在源码中找到到对应的答案,首先定位到 ConfigurationClassParser#parse 方法

  • 首先看到调用的是 doProcessConfigurationClass:循环遍历每一个处理配置类
  • 处理 @Import 注解方法中,在这个方法可以看到 @Import 注解的实现逻辑,处理 ImportSelector 接口、子接口不同类型的实现:DeferredImportSelector、ImportSelector,在处理前者时将对应的实例存储了起来
  • 等待其他的配置类都已经处理完成以后,到 parse 方法块后面,执行 deferredImportSelectorHandler#process 方法

1、先处理的是 register 方法,获取我们重写的 importGroup 方法的返回值,如果为空说明没有重写 Group 接口,那么就使用原来的 ImportSelector 实现类对象且创建默认的 Group 实现 DefaultDeferredImportSelectorGroup,否则就是使用自定义的 Group 对象

2、再看 processGroupImports 方法,主要看的是方法块里面的 grouping.getImports 方法,在这里面会根据 Group 实现类的不同来执行 process 方法,如果是默认实现,那么调用的就是 ImportSelector 实现类的 selectImports 方法返回,否则就调用自定义 Group 对象的 process 方法,在这里面会看到 getAutoConfigurationEntry 装配的核心方法被调用,返回自动装配的那些配置类

3、到这里,就可以清晰的了解到自动装配里面核心的解析过程是什么的!!!

同时,从以上可以看出,ImportSelector 与 DeferredImportSelector 的区别,就是执行 selectImports 方法时有所区别,这个差别期间,Spring 容器对此 Configuration 配置类做了其他的逻辑:包括 @ImportResource、@Bean 这些注解处理

关于 ConfigurationClassPostProceesor 核心类更多核心流程讲解,可以看博主这篇文章:Spring 核心类 ConfigurationClassPostProcessor 流程讲解及源码全面分析

自动装配示例

使用 HttpEncodingAutoConfiguration 来解释自动装配原理

/*
表名这是一个配置类,
*/
@Configuration(proxyBeanMethods = false)
/*
启动指定类的 ConfigurationProperties 功能,进入 HttpProperties 查看,将配置文件中对应的值和 HttpProperties 绑定起来,并把 HttpProperties 加入到 ioc 容器中
*/
@EnableConfigurationProperties(HttpProperties.class)
/*
spring 底层 @Conditional 注解,根据不同的条件判断,如果满足指定的条件,整个配置类里面的配置就会生效
此时表示判断当前应用是否是 web 应用,如果是,那么配置类生效
*/
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
/*
判断当前项目由没有这个类 CharacterEncodingFilter,springmvc 中进行乱码解决的过滤器
*/
@ConditionalOnClass(CharacterEncodingFilter.class)
/*
判断配置文件中是否存在某个配置:spring.http.encoding.enabled
如果不存在,判断也是成立的,
即使我们配置文件中不配置spring.http.encoding.enabled=true,也是默认生效的
*/
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
    // 和 springboot 配置文件映射
  private final HttpProperties.Encoding properties;
    // 只有一个有参构造器的情况下,参数的值就会从容器中拿
  public HttpEncodingAutoConfiguration(HttpProperties properties) {
    this.properties = properties.getEncoding();
  }
    // 给容器中添加一个组件,这个组件的某些值需要从 properties 中获取
  @Bean
  @ConditionalOnMissingBean//判断容器中是否有此组件
  public CharacterEncodingFilter characterEncodingFilter() {
    CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
    filter.setEncoding(this.properties.getCharset().name());
    filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
    filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
    return filter;
  }
  @Bean
  public LocaleCharsetMappingsCustomizer localeCharsetMappingsCustomizer() {
    return new LocaleCharsetMappingsCustomizer(this.properties);
  }
  private static class LocaleCharsetMappingsCustomizer
      implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>, Ordered {
    private final HttpProperties.Encoding properties;
    LocaleCharsetMappingsCustomizer(HttpProperties.Encoding properties) {
      this.properties = properties;
    }
    @Override
    public void customize(ConfigurableServletWebServerFactory factory) {
      if (this.properties.getMapping() != null) {
        factory.setLocaleCharsetMappings(this.properties.getMapping());
      }
    }
    @Override
    public int getOrder() {
      return 0;
    }
  }
}

根据当前不同的条件判断,决定这个配置类是否生效

1、springboot启动会加载大量的自动配置类

2、查看需要的功能有没有在springboot默认写好的自动配置类中华

3、查看这个自动配置类到底配置了哪些组件

4、给容器中自动配置类添加组件的时候,会从properties类中获取属性

@Conditional:自动配置类在一定条件下才能生效

@Conditional扩展注解 作用
@ConditionalOnJava 系统的java版本是否符合要求
@ConditionalOnBean 容器中存在指定Bean
@ConditionalOnMissingBean 容器中不存在指定Bean
@ConditionalOnExpression 满足SpEL表达式
@ConditionalOnClass 系统中有指定的类
@ConditionalOnMissingClass 系统中没有指定的类
@ConditionalOnSingleCandidate 容器中只有一个指定的Bean,或者是首选Bean
@ConditionalOnProperty 系统中指定的属性是否有指定的值
@ConditionalOnResource 类路径下是否存在指定资源文件
@ConditionOnWebApplication 当前是web环境
@ConditionalOnNotWebApplication 当前不是web环境
@ConditionalOnJndi JNDI存在指定项

总结

最后,在解析自动装配的过程中涉及到的比较重要的类 ConfigurationClassPostProcessor,它既实现了 BeanDefinitionRegisterPostProcessor 同时也实现了 BeanFactoryPostProcessor,这个类后面会单独写一篇文章来对其里面的核心处理过程分析。

如果觉得博文不错,关注我 vnjohn,后续会有更多实战、源码、架构干货分享!

大家的「关注❤️ + 点赞👍 + 收藏⭐」就是我创作的最大动力!谢谢大家的支持,我们下文见!

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
1月前
|
数据采集 监控 前端开发
二级公立医院绩效考核系统源码,B/S架构,前后端分别基于Spring Boot和Avue框架
医院绩效管理系统通过与HIS系统的无缝对接,实现数据网络化采集、评价结果透明化管理及奖金分配自动化生成。系统涵盖科室和个人绩效考核、医疗质量考核、数据采集、绩效工资核算、收支核算、工作量统计、单项奖惩等功能,提升绩效评估的全面性、准确性和公正性。技术栈采用B/S架构,前后端分别基于Spring Boot和Avue框架。
|
2月前
|
前端开发 Java
表白墙/留言墙 —— 初级SpringBoot项目,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
文章通过一个表白墙/留言墙的初级SpringBoot项目实例,详细讲解了如何进行前后端开发,包括定义前后端交互接口、创建SpringBoot项目、编写前端页面、后端代码逻辑及实体类封装的全过程。
87 3
表白墙/留言墙 —— 初级SpringBoot项目,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
2月前
|
前端开发 Java 数据安全/隐私保护
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
文章通过一个简单的SpringBoot项目,详细介绍了前后端如何实现用户登录功能,包括前端登录页面的创建、后端登录逻辑的处理、使用session验证用户身份以及获取已登录用户信息的方法。
276 2
用户登录前后端开发(一个简单完整的小项目)——SpringBoot与session验证(带前后端源码)全方位全流程超详细教程
|
1月前
|
JavaScript Java 项目管理
Java毕设学习 基于SpringBoot + Vue 的医院管理系统 持续给大家寻找Java毕设学习项目(附源码)
基于SpringBoot + Vue的医院管理系统,涵盖医院、患者、挂号、药物、检查、病床、排班管理和数据分析等功能。开发工具为IDEA和HBuilder X,环境需配置jdk8、Node.js14、MySQL8。文末提供源码下载链接。
|
2月前
|
NoSQL Java Redis
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
这篇文章介绍了如何使用Spring Boot整合Apache Shiro框架进行后端开发,包括认证和授权流程,并使用Redis存储Token以及MD5加密用户密码。
36 0
shiro学习四:使用springboot整合shiro,正常的企业级后端开发shiro认证鉴权流程。使用redis做token的过滤。md5做密码的加密。
|
2月前
|
XML Java 应用服务中间件
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
【Spring】运行Spring Boot项目,请求响应流程分析以及404和500报错
204 2
|
2月前
|
前端开发 Java 数据库连接
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
本文是一份全面的表白墙/留言墙项目教程,使用SpringBoot + MyBatis技术栈和MySQL数据库开发,涵盖了项目前后端开发、数据库配置、代码实现和运行的详细步骤。
65 0
表白墙/留言墙 —— 中级SpringBoot项目,MyBatis技术栈MySQL数据库开发,练手项目前后端开发(带完整源码) 全方位全步骤手把手教学
|
2月前
|
JSON 前端开发 JavaScript
优雅!Spring Boot 3.3 实现职责链模式,轻松应对电商订单流程
本文介绍如何使用 Spring Boot 3.3 实现职责链模式,优化电商订单处理流程。通过将订单处理的各个环节(如库存校验、优惠券核验、支付处理等)封装为独立的处理器,并通过职责链将这些处理器串联起来,实现了代码的解耦和灵活扩展。具体实现包括订单请求类 `OrderRequest`、抽象处理器类 `OrderHandler`、具体处理器实现(如 `OrderValidationHandler`、`VerifyCouponHandler` 等)、以及初始化职责链的配置类 `OrderChainConfig`。
|
2月前
|
机器学习/深度学习 移动开发 自然语言处理
基于人工智能技术的智能导诊系统源码,SpringBoot作为后端服务的框架,提供快速开发,自动配置和生产级特性
当身体不适却不知该挂哪个科室时,智能导诊系统应运而生。患者只需选择不适部位和症状,系统即可迅速推荐正确科室,避免排错队浪费时间。该系统基于SpringBoot、Redis、MyBatis Plus等技术架构,支持多渠道接入,具备自然语言理解和多输入方式,确保高效精准的导诊体验。无论是线上医疗平台还是大型医院,智能导诊系统均能有效优化就诊流程。
|
Java Spring
SpringBoot自动配置源码调试
SpringBoot自动配置源码调试
542 0