tips:每个 springBoot 的版本不同,代码的实现存会存在不同。
上一章,我们聊到 mybatis-spring-boot-starter; 简单分析了它的结构。 这一章我们将着重分析 Starter 的加载机制,并结合源码进行分析理解。
一、加载实际
1.1 何时被加载
引入的 Starter 何时被加载到 Spring 容器里面。 解决了这个过程,那么 Starter 的加载机制就明白大半了。
1.2 加载时机
给出几个步骤。(为了突出重点,时序图中忽略后置处理器等,下一章会给出全局时序图)
- 应用启动始于主启动类(通常使用 @SpringBootApplication 注解)中的main方法,调用SpringApplication.run(...)
- 在自动配置过程中,Spring Boot会使用 SpringFactoriesLoader 类来加载所有可见的 spring.factories 文件。SpringFactoriesLoader 查找类路径上所有 META-INF/spring.factories 实例
- 通过后置处理器,spring.factories文件中的自动配置类(通过EnableAutoConfiguration指定)将被实例化。这些自动配置类通常使用@Configuration注解,且可能包含@Conditional注解
整个加载过程,比较关键的两个类:
- SpringFactoriesLoader 加载 spring.factories 文件
- EnableAutoConfigurationImportSelector 导入 autoconfiguration 并进行加载
spring.factories 文件是帮助 SpringBoot 项目包以外的bean(即在pom文件中添加依赖中的bean)注册到SpringBoot 项目的 Spring 容器中。由于 @ComponentScan注解只能扫描 spring-boot 项目包内的 bean 并注册到 Spring 容器中,因此需要 @EnableAutoConfiguration 注解来注册项目包外的 bean。而spring.factories文件,则是用来记录项目包外需要注册的 bean 类名。
1.3 SPI 机制
SPI(Service Provider Interface)服务提供接口,在Java中是一种发现和加载可插拔实现的标准机制。目的是实现对组件的动态发现和加载
通过java.util.ServiceLoader类来发现和加载META-INF/services/目录下相应接口的实现类。
关于 Java 本身提供的 SPI 实现细节
在 SpringBoot 中,SPI 机制允许开发人员在模块中定义一些服务接口,并且可以为这些接口提供多个可插拔的实现。实现了进一步的便利性和更强大的整合特性。 通过定义约定的 spring.factories 文件,来实现自动配置和条件装配。
接下来,我们通过 debug 的方式来探索 Starter 被加载的过程。
特别说明:本文 debug 的源码是:mybatis-starter-apply
, 如果需要跟着 debug 走步骤流程, 下载相关源码。uzong-starter-learning: 学习 SpringBoot Starter 的工程案例
二、源码理解
在理解整个 Starter 之前,我们先来了解一下几个核心类。它将是整个解密 Starter 最为关键的几个类
2.1 SpringFactoriesLoader 类
这个类的作用, 解析 META-INF/spring.factories
文件,并将结果方法到一个 Map 中。
spring.factories 的结果和 properties 非常相似,我们可以查看 mybatis-spring-boot-starter
文件中的 spring.factories
注意: spring.factories 中的 value 值是可以用逗号分隔。
使用的分割方法是 org.springframework.util.StringUtils#commaDelimitedListToStringArray
public static String[] commaDelimitedListToStringArray(@Nullable String str) {
return delimitedListToStringArray(str, ",");
}
将 spring.factories 中的 key = value 解析后放入到 MultiValueMap<String, String>
。 这是一个特殊的 map。 它的 value 值是一个 List
public interface MultiValueMap<K, V> extends Map<K, List<V>> {
.....
}
继承至 Map<K, List<V>>。
SpringFactoriesLoader 将当前类加载器下所有的 "META-INF/spring.factories" 文件进行解析,并将解析结果放到一个 Map 中进行缓存。注意:目前都是 String, 还不是 class
附上部分 SpringFactoriesLoader 源码以及注释。
核心逻辑:加载 META-INF/spring.factories 文件数据; 把里面的key、value值放入到一个特殊的 Map 中。
public abstract class SpringFactoriesLoader {
// JAR 文件的路径
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// 将所有类路径的名称加载到这个 Map 中
private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();
// 根据类名获取value值。注意返回的是 list
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
// 解析 spring.factories 文件,将解析的 key=value 放入到 cache 中。
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null)
return result;
try {
// jar 文件路径
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
// 遍历所路径,加载文件资源
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
// 解析成 Properties 对象,即 key = value
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
// 逗号分割值
List<String> factoryClassNames = Arrays.asList(
StringUtils.commaDelimitedListToStringArray((String) entry.getValue()));
result.addAll((String) entry.getKey(), factoryClassNames);
}
}
cache.put(classLoader, result);
return result;
}
......
}
// 反射,加载对象
@SuppressWarnings("unchecked")
private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
try {
// 反射创建对象
Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
if (!factoryClass.isAssignableFrom(instanceClass)) {
throw new IllegalArgumentException(
"Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
}
return (T) ReflectionUtils.accessibleConstructor(instanceClass).newInstance();
}
.....
}
}
通过 debug 断点,查看 cache 中的数据。
以 debug 方式启动 com.uzong.instance.MyApplication 类,查看 Map 数据。
断点地址:SpringFactoriesLoader#142 行
可以看到,mybatis-spring-boot-starter中的 auto-configuration 类被加载了。
回到 mybatis-spring-boot-starter 的 spring.factories 确认一下。
org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration
确认一致。
读取 spring.factories 抽取了几个关键步骤。如下所示:
补充: idea 中 debug 窗口,为执行栈帧,如下所示。可以清楚的知道运行经过的链路。
总结两个要点:
- cache 包含多种类型,不仅仅是 Starter 中指定的
org.springframework.boot.autoconfigure.EnableAutoConfiguration
类型。 后续通过指定类型获取 list 值。 - 此处的 Map 中的值还是 String,不是 Class,那么哪一步才能将 String 变成 Class 呢,接下来关注 AutoConfigurationImportSelector
2.2 AutoConfigurationImportSelector 类
org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports
这个方法的作用是将 org.springframework.boot.autoconfigure.EnableAutoConfiguration
对应的 List 值进行加载到 application 中进行类的初始化
相关源码,主要从 cache map 中读取 org.springframework.boot.autoconfigure.EnableAutoConfiguration 值。并进行加载
入口:org.springframework.boot.autoconfigure.AutoConfigurationImportSelector#selectImports
public String[] selectImports(AnnotationMetadata annotationMetadata) {
.....
try {
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 加载 org.springframework.boot.autoconfigure.EnableAutoConfiguration
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
.....
return StringUtils.toStringArray(configurations);
}
......
}
在这个类中,会读取 spring.factories 中的 org.springframework.boot.autoconfigure.EnableAutoConfiguration
值进行加载。
getCandidateConfigurations()
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
......
return configurations;
}
返回 org.springframework.boot.autoconfigure.EnableAutoConfiguration
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
特别注意:不同版本源码略有不同;在 selectImports 中, 也对返回的 list 做了过滤。
在 filter() 方法中,过滤一些不满足的 configuration,不进一步加载。比如:ConditionalOnClass 条件注解。如果找不到依赖的那个类,则直接过滤掉。
·
下面就是基于ConditionalOnClass org.springframework.boot.autoconfigure.condition.OnClassCondition#getOutcomes
规则做过滤。
如果我们引入的 Starter 发现没有生效,则可以通过断点到这一行,来排查对应的文件是否被引入。
2.3 加载过程
找到符合的全路径名称后,就交给 org.springframework.context.annotation.ConfigurationClassParser
类进行解析。然后把整个 configuration 所在的 Bean 都一一进行加载。
关于 ConfigurationClassParser
是非常重要的,它是 springframework core 中的核心类。以递归性的解析加载所有类;也是非常繁琐和重要的,后面会用单独章节进行详细讲解。
2.4 整个加载过程
加载过程,包括 ConfigurationClassParser
以及后置处理器也添加进来的时序图。
时序图中的几个关键方法,可以关注一下:
关键方法一:refreshContext
org.springframework.boot.SpringApplication#prepareContext
关键方法二:invokeBeanFactoryPostProcessors。 激活各种后置处理器
org.springframework.context.support.AbstractApplicationContext#invokeBeanFactoryPostProcessors
关键方法三:processDeferredImportSelectors,处理延迟导入 importSelector 。
org.springframework.context.annotation.ConfigurationClassParser#processDeferredImportSelectors
Spring 的核心扩展接口。 SpringBoot 的 AutoConfigurationImportSelector 类,实现 ImportSelector(DeferredImportSelector)方法,从而实现 Starter 的扩展装配能力。
三、本章小结
本文只通过局部了解到 Starter 中的类被加载的时机,主要有两个核心类
- SpringFactoriesLoader
- AutoConfigurationImportSelector
一个是加载 spring.factories 加载资源放入 Map ; 另外一个触发 Map 中读取数据并交给 ConfigurationClassParser
解析。
如果仅仅从局部理解加载过程是局限的。接下来,我们从整个 SpringBoot 的加载顺序理解全貌。
已同步发布到公众号:面汤放盐 第四节 Starter 加载时机和源码理解 (qq.com)
掘金账号:第四节 Starter 加载时机和源码理解 - 掘金 (juejin.cn)