溯水流光 发表于 2025-5-29 13:37:39

自动装配源码解析 底层实现的细节 springboot

本帖最后由 溯水流光 于 2025-5-29 13:39 编辑

> 下载和备份: https://github.com/HHsomeHand/springboot-code-reading/tree/main

## 前言

使用 springboot 开发时

只需要导入 starter 就 ok 了

starter 会帮我们, 自动装配 (自动配置组件到 IOC 容器中)

如:

+ spring mvc starter 会自动装配 dispatchServlet, 并映射到 / 路径
+ mybatis starter 会自动装配 连接池(DataSource) 事物管理器

这些装配的操作都是由一个个的配置类实现的

而这些配置类在 starter 相关包下

这些包不在我们的 @SpringBootApplication 标注类的路径下

所以自动扫描, 无法扫描到

所以 starter 会在 jar 包的 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 这个文件里面

登记所有要加载的配置类, springboot 会加载所有的配置类, 于是便实现了自动装配



## 源码分析, 探寻背后的实现原理

>这里分析的源码版本为 3.0.4, springboot 新旧源码差异大, 特此说明

创建一个 springboot 项目

```
<dependencies>
      <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>3.0.4</version>
      </dependency>
      <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
</dependencies>
```

``` java
@SpringBootApplication
public class MainSpringApplication {

    public static void main(String[] args) {
      SpringApplication.run(MainSpringApplication.class, args);
    }

}
```

IDEA 按住 ctrl 并点击 @SpringBootApplication, 转到定义

乍一看, 花花绿绿, 但源码分析的要义就是抓重点看

所以其他内容, 我们晚点再分析, 先看主线剧情

```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@EnableAutoConfiguration // 看这里
public @interface SpringBootApplication {
```

转到 `@EnableAutoConfiguration ` 的定义

```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({AutoConfigurationImportSelector.class}) // 看这里
public @interface EnableAutoConfiguration {
```

`@Import` 是用于将一个类注入 IOC 容器

```java
public class AutoConfigurationImportSelector implements DeferredImportSelector
```

AutoConfigurationImportSelector 是一个 DeferredImportSelector

DeferredImportSelector 是一个 ImportSelector

ImportSelector 有一个成员方法: `selectImports`

这个成员方法, 返回一个字符串数组, 内容为类的权限定名, 也就是完整的`包名 + 类名`

spring 收到后, 会把这些类装配到 IOC 容器中

我们可以通过 ImportSelector 来将外部的配置类, 装配到 IOC 容器中, 使其生效

ImportSelector 无法通过 @Component 包扫描生效, 必须用 @Import 导入才能生效

```java
public String[] selectImports(AnnotationMetadata annotationMetadata) {
```

annotationMetadata 为 @Import({ImportSelector.class}) 下面的注解

这里便是 EnableAutoConfiguration

```java
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}
```

EnableAutoConfiguration 携带了一些信息, 用于排除不想自动装配的项

继续看这个 ImportSelector 的具体实现

```java
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // 先看这里
    var autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
   
    // ...
}
```

```java
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
    // 先看这里
    List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
        // ...
}
```

```java
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 看这里
    ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader());
   
    // ...
}
```

```java
public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
   
    // annotation 为 AutoConfiguration.class
    // 所以 location 为 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
    String location = String.format("META-INF/spring/%s.imports", annotation.getName());
   
    Enumeration<URL> urls = findUrlsInClasspath(classLoader, location);
    List<String> importCandidates = new ArrayList();

    while(urls.hasMoreElements()) {
      URL url = (URL)urls.nextElement();
      
      // readCandidateConfigurations 为按行读取配置文件, 返回一个字符串数组
      importCandidates.addAll(readCandidateConfigurations(url));
    }

    return new ImportCandidates(importCandidates);
}

```

这里 urls 为配置文件的路径名

一个 springboot 项目经常使用多个 starter

每个 starter 相关包 都提供了配置文件

配置文件 按行存放 配置类路径

且该配置文件在 jar 包下的 `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 路径

```java
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 也就是读取了一个字符串数组, 这个数组存储了全部要加载的配置类
    ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation, this.getBeanClassLoader());
   
    List<String> configurations = importCandidates.getCandidates();
   
    return configurations;
}
```

```java
// annotationMetadata 为 @EnableAutoConfiguration
// @EnableAutoConfiguration 有 exclude 和 excludeName, 两个属性
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
    AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
   
    // 获取要加载的配置类路径
    List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
   
    // removeDuplicates 中文意思 -> 移除重复项
    configurations = this.removeDuplicates(configurations);
   
    // 获取要排除的配置类
    Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
   
    // 删除要排除的项目
    configurations.removeAll(exclusions);
   
    return new AutoConfigurationEntry(configurations, exclusions);
}

```

```java
public String[] selectImports(AnnotationMetadata annotationMetadata) {
    // 获取要加载的 配置类 的 全限定名 数组
    var autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
   
    return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
```

到此, 算是完全分析完毕了, 功德圆满了



## 其他的噪音

分析源码的一大忌讳便是钻牛角尖, 扣细节

这完全是无意义的内耗, 扣那些噪音干什么?

难不成看到 HashMap, 还要顺便去扣 HashMap 的具体实现?

然后发现需要数据结构和算法的知识.

于是恶补一下数据结构和算法, 然后发现需要数学

然后去专研数学, 最后成为了数学大师, 从此功德圆满.

这太荒诞了, 而且也不可能.

研究数学, 也去扣细节, 于是开始研究人类的逻辑, 开始研究心理学.

最后在无尽的内耗和痛苦中, 成为了神经病, 于是变成了行为艺术家.

当然上面都是极端情况的逻辑推演, 如有雷同, 纯属巧合, 我只是在开玩笑

这些噪音在进行源码分析的时候, 最好排除, 以免被干扰



### 再看 @SpringBootApplication

```java
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
```

@SpringBootConfiguration 就是 @Configuration

TypeExcludeFilter 用于加载我们注入 IOC 容器中的类型过滤器, 来辅助 包扫描 排除项目, 这里不做探讨

AutoConfigurationExcludeFilter 的实现, 成员方法 match 是用来做匹配的, 如果返回 true, 包扫描工具 则排除此项

AutoConfigurationExcludeFilter 的作用为排除所有的 自动配置类

让包扫描工具, 不去加载这些配置类

这些配置类留给 AutoConfigurationImportSelector 用 importSelector 去加载, 这么设计的因由, 我后文分析, 先读源码:

```java
public class AutoConfigurationExcludeFilter implements TypeFilter, BeanClassLoaderAware {
    private ClassLoader beanClassLoader;
    private volatile List<String> autoConfigurations;

    public void setBeanClassLoader(ClassLoader beanClassLoader) {
      this.beanClassLoader = beanClassLoader;
    }

    public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory) throws IOException {
      return this.isConfiguration(metadataReader) && this.isAutoConfiguration(metadataReader);
    }

    private boolean isConfiguration(MetadataReader metadataReader) {
      // 判断是否带 Configuration 注解
      return metadataReader.getAnnotationMetadata().isAnnotated(Configuration.class.getName());
    }

    // 判断是否是 自动配置类
    private boolean isAutoConfiguration(MetadataReader metadataReader) {
      // 判断是否带 AutoConfiguration 注解
      boolean annotatedWithAutoConfiguration = metadataReader.getAnnotationMetadata().isAnnotated(AutoConfiguration.class.getName());
      
      return annotatedWithAutoConfiguration || this.getAutoConfigurations().contains(metadataReader.getClassMetadata().getClassName());
    }

    protected List<String> getAutoConfigurations() {
      if (this.autoConfigurations == null) {
            // 上面分析过 ImportCandidates.load 的逻辑
            // 这里的功能为读取所有 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
            // 读取后, 可以获取到要加载的 配置类全限定名 数组
            ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, this.beanClassLoader);
            this.autoConfigurations = importCandidates.getCandidates();
      }

      return this.autoConfigurations;
    }
}
```

上面有简单提到过, AutoConfigurationImportSelector 为 DeferredImportSelector

DeferredImportSelector 相较于 ImportSelector 的区别是:

DeferredImportSelector 是 `Deferred` 延迟执行的

DeferredImportSelector 会在所有 @Configuration 类解析完成后执行

DeferredImportSelector 这么做的好处就是不与用户配置产生冲突

如用户配置了阿里的连接池德鲁伊, starter 内用于 连接池初始化的配置类, 就不应该生效了

因为用户都配置了连接池, starter 就不应该再配置了

下面是一段`DataSourceAutoConfiguration.class`摘录:

```java
@Configuration
@ConditionalOnMissingBean({DataSource.class, XADataSource.class})
@Import({DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class, DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class, DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class})
protected static class PooledDataSourceConfiguration {
```

逻辑为, 如果 DataSource.class 在容器中**不存在**, 再去使用`DataSourceConfiguration.Hikari.class`配置类

HikariCP 是 DataSource, 也就是连接池



## 找找 AutoConfiguration.imports

IDEA 项目(左侧的树状结构) -> 外部库 -> spring-boot-autoconfigure-3.5.0.jar -> META-INF.spring -> org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件

打开的内容:

```
// 连接池的配置类
org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration

// DispatcherServlet 的配置类
org.springframework.boot.autoconfigure.web.servlet.DispatcherServletAutoConfiguration
```

按 Ctrl + 点击名字, 可以转到定义

我们这里来简单看看 DispatcherServletAutoConfiguration 的内容

```java
@Bean(
    name = {"dispatcherServlet"}
)
public DispatcherServlet dispatcherServlet(WebMvcProperties webMvcProperties) {
    DispatcherServlet dispatcherServlet = new DispatcherServlet();
   
    // ...
   
    return dispatcherServlet;
}

@Bean(
    name = {"dispatcherServletRegistration"}
)
public DispatcherServletRegistrationBean dispatcherServletRegistration(DispatcherServlet dispatcherServlet, WebMvcProperties webMvcProperties, ObjectProvider<MultipartConfigElement> multipartConfig) {
    DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath());
   
    // ...
   
    return registration;
}
```

综上所述, springboot 的 自动配置类 并不神秘, 其实就是配置类(@Configuration)

只是通过了 ImportSelector 进行了加载

也就是通过 配置文件, 指明要加载的配置类, springboot 会自动帮我们加载

加载了配置类, 自然就可以把组件加载到 IOC 容器中

而那些 starter 都提供了这样的配置文件, 于是只用添加了这些 starter, 便可以实现自动装配



## 尾声

笔者以前也写过许多源码解析, elmGetter, duilib 之类的云云

我也阅读了大量且优质的源码解析, 并在持续创作的时候, 总结了一些方法论, 比如如何简化源码, 如何突出重点, 如何层层递进

所以我写的这篇, 我个人感觉是相当好的, 当然, 以后会更好.

文有文风, 曲有曲风, 一篇文章不可能满足所有人.

所以希望这篇文章能带给有缘人, 知识和收获吧!



### 关于 AI

写源码解析的另外一个忌讳就是, 完全使用 AI

AI 没有人类的逻辑, 完全是靠概率堆叠词汇, 所以可能会出错, 尤其是缺乏上下文

你冷不丁地去问 AI, springboot 源码的细节, AI 大概率会胡说八道

不过等未来, AI 强大了, 个人 PC 也可以跑 AI, 让 AI 做源码"陪读", 也是完全现实的

而且因为部署在本地, 可以把上下文 token 拉满, 让 AI 记忆细节, 就不会像现在一样, 胡说八道, 一堆幻觉



### 是时候 Say Goodbye 了

最后的最后, 感谢你的阅读!

王一之 发表于 2025-5-29 13:43:09

Java真繁琐啊

溯水流光 发表于 2025-5-29 15:10:09

王一之 发表于 2025-5-29 13:43
Java真繁琐啊

毕竟是后端屠龙刀, 个人轻量化的场景还是适合用 GO

拿战斧切肉片, 多少有点不合适

SpringMVC 确实是有很多前后端未分离, 遗留下来的遗产 (Legacy)

但注解化 + springboot, 开发也还算"轻量化"了

当年 Spring 打着 "轻量化" 的旗号革掉了 EJB 的命.

虽然 EJB 还有小部分大型公司在用, 但已经不是主流了.

现在 GO 会不会取代 Spring?

未来的日子实在是不好说, 只能等历史的潮流滚滚向前, 把前浪拍死在沙滩上.

李恒道 发表于 2025-5-29 23:01:31

虽然看不懂
但是支持哥哥!

溯水流光 发表于 2025-5-30 08:56:11

李恒道 发表于 2025-5-29 23:01
虽然看不懂
但是支持哥哥!

感谢哥哥的支持!!

zyhust 发表于 2025-6-3 05:56:06

我的偶像 tampermonkey 710.9551488118273
页: [1]
查看完整版本: 自动装配源码解析 底层实现的细节 springboot