本帖最后由 溯水流光 于 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>
@SpringBootApplication
public class MainSpringApplication {
public static void main(String[] args) {
SpringApplication.run(MainSpringApplication.class, args);
}
}
IDEA 按住 ctrl 并点击 @SpringBootApplication, 转到定义
乍一看, 花花绿绿, 但源码分析的要义就是抓重点看
所以其他内容, 我们晚点再分析, 先看主线剧情
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@EnableAutoConfiguration // 看这里
public @interface SpringBootApplication {
转到 @EnableAutoConfiguration
的定义
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({AutoConfigurationImportSelector.class}) // 看这里
public @interface EnableAutoConfiguration {
@Import
是用于将一个类注入 IOC 容器
public class AutoConfigurationImportSelector implements DeferredImportSelector
AutoConfigurationImportSelector 是一个 DeferredImportSelector
DeferredImportSelector 是一个 ImportSelector
ImportSelector 有一个成员方法: selectImports
这个成员方法, 返回一个字符串数组, 内容为类的权限定名, 也就是完整的包名 + 类名
spring 收到后, 会把这些类装配到 IOC 容器中
我们可以通过 ImportSelector 来将外部的配置类, 装配到 IOC 容器中, 使其生效
ImportSelector 无法通过 @Component 包扫描生效, 必须用 @Import 导入才能生效
public String[] selectImports(AnnotationMetadata annotationMetadata) {
annotationMetadata 为 @Import({ImportSelector.class}) 下面的注解
这里便是 EnableAutoConfiguration
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
EnableAutoConfiguration 携带了一些信息, 用于排除不想自动装配的项
继续看这个 ImportSelector 的具体实现
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 先看这里
var autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
// ...
}
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
// 先看这里
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
// ...
}
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 看这里
ImportCandidates importCandidates = ImportCandidates.load(AutoConfiguration.class, this.getBeanClassLoader());
// ...
}
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
路径
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 也就是读取了一个字符串数组, 这个数组存储了全部要加载的配置类
ImportCandidates importCandidates = ImportCandidates.load(this.autoConfigurationAnnotation, this.getBeanClassLoader());
List<String> configurations = importCandidates.getCandidates();
return configurations;
}
// 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);
}
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 获取要加载的 配置类 的 全限定名 数组
var autoConfigurationEntry = this.getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
到此, 算是完全分析完毕了, 功德圆满了
其他的噪音
分析源码的一大忌讳便是钻牛角尖, 扣细节
这完全是无意义的内耗, 扣那些噪音干什么?
难不成看到 HashMap, 还要顺便去扣 HashMap 的具体实现?
然后发现需要数据结构和算法的知识.
于是恶补一下数据结构和算法, 然后发现需要数学
然后去专研数学, 最后成为了数学大师, 从此功德圆满.
这太荒诞了, 而且也不可能.
研究数学, 也去扣细节, 于是开始研究人类的逻辑, 开始研究心理学.
最后在无尽的内耗和痛苦中, 成为了神经病, 于是变成了行为艺术家.
当然上面都是极端情况的逻辑推演, 如有雷同, 纯属巧合, 我只是在开玩笑
这些噪音在进行源码分析的时候, 最好排除, 以免被干扰
再看 @SpringBootApplication
@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 去加载, 这么设计的因由, 我后文分析, 先读源码:
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
摘录:
@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 的内容
@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 了
最后的最后, 感谢你的阅读!