你曾经想过为什么在搭建一个 Spring Boot 项目时,它似乎自带一种魔法,能够为你自动完成大量的配置工作吗?那么,这一切是如何做到的呢?今天,我们来揭开这背后的秘密。
你是否还记得,初次入门 Spring Boot 时,完成基本项目搭建后怀着激动的心情点下运行按钮的时候没出现 Bug 时的喜悦?
如果你能够看到下面美丽的 Banner 图案,那么恭喜你 !🎉 你已经完全掌握 Spring Boot 了:
但是你不知道的是,仅仅是这简单的启动,Spring Boot 究竟在背后为你操碎了多少心 💔。它会帮你自动去索类路径(CLASSPATH)中的所有 JAR 包和类,帮你了解到哪些库已经被包含进来。拿到这些信息,Spring Boot 开始它的自动配置工作,生成相应的 Bean,并将它们加入到 Spring 上下文中。
是的,当你还在随意拿着一个个为你准备好的 Spring Bean 随意操作的时候,你却忘了背后是谁在为你默默付出。于是,有一天你突然良心发现,心想:为什么我每次啥也不用做,就能信手拈来好些 Bean?它们是从哪儿凭空出现的?于是你准备一探究竟 … …
于是你准备从曾经的故乡,那个熟悉的 *Application 开始入手,希望能寻找到一些线索:
盯着这简短的几行代码,左看右看只有一个 @SpringBootApplication
注解显得格外突出,于是怀着试一试的心态按下 Control 键点开了它的源码:
看着密密麻麻的源码你有些无从下手,于是决定从头开始看看 Spring 官方团队对这个注解简单的几句注解:
用蹩脚的英文水平尝试翻译了一下:
指示一个配置类,它声明一个或多个 @Bean 方法,并触发自动配置和组件扫描。这是一个方便的注解,相当于声明 @Configuration, @EnableAutoConfiguration 和 @ComponentScan。
嗷 ~你默默拿出了笔记本开始几下几个关键信息 📝
- 它可以声明一个或多个 Bean,也就是我们之前即便没有配置也能使用的 Spring Bean;
- 它可以触发自动配置和组件扫描,“自动配置?” 那不就正好应上了 “不用配置即可用 Spring Bean?”。“组件扫描?” 原来我以前通过
@Configuration
注解在配置类写的 Bean 也是它代劳。 - 它还是个多功能的复合注解,一个人就兼备了
@Configuration
(配置类),@EnableAutoConfiguration
(自动配置)和@ComponentScan
(组件扫描)。
由于你一开始探索的起因正是 “自动”,于是你毫不犹豫的点下了唯一一个带有 “Auto” 字样的 @EnableAutoConfiguration
注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
到这里又看不明白了,于是又开始进行蹩脚的翻译顶部注释:
这次内容比较多,你还特意为你认为的 “重点” 画上了下划线:
启用 Spring 应用上下文的自动配置,试图推测并配置你可能需要的 beans。自动配置类通常基于你的类路径和你已定义的 beans 进行应用。例如,如果你的类路径中有 tomcat-embedded.jar,你可能需要一个 TomcatServletWebServerFactory(除非你已经定义了自己的 ServletWebServerFactory bean)。
当使用 @SpringBootApplication 注解时,上下文的自动配置会自动启用,因此添加此注解不会产生任何额外效果。
自动配置试图尽可能智能地进行操作,并且在你定义更多自己的配置时会逐渐退让。你总是可以手动排除任何你不希望应用的配置(如果你无法访问它们,可以使用 excludeName())。你也可以通过 spring.autoconfigure.exclude 属性来排除它们。在用户定义的 beans 已经注册之后,自动配置总是会被应用。
使用 @EnableAutoConfiguration 注解的类的包,通常通过 @SpringBootApplication,具有特定的意义,并经常用作 “默认值”。例如,它在扫描 @Entity 类时会被使用。通常建议你将 @EnableAutoConfiguration(如果你没有使用 @SpringBootApplication)放在根包中,以便可以搜索所有子包和类。
自动配置类是普通的 Spring @Configuration beans。它们**使用 ImportCandidates 和 SpringFactoriesLoader 机制(针对此类)进行定位。通常,自动配置 beans 是 @Conditional beans(最常使用 @ConditionalOnClass 和 @ConditionalOnMissingBean 注解)**。
从这次翻译中你发现原来它会负责开启自动配置功能,然后推测并配置你可能会用到的 Bean,并且这些 Bean 的优先级似乎并不高,只要自己在配置类中也配置了同名的 Bean 就会覆盖默认自动注册的 Bean。同时,你还发现原来只要将该注解作用在根包中的启动类上,就会自动扫描当前包及其子包下我们定义的 Bean。(So easy~)
但是,到后面你又开始犯难了,“ImportCandidates” 和 “SpringFactoriesLoader 机制” 是什么?“@Conditional beans” 又是什么?
咋一看,你决定使用百试不厌的见名知意大法:
- “ImportCandidates” -> 导入候选者,猜测它可能是用于导入某个地方的 Bean 信息的;
- “SpringFactoriesLoader 机制” -> Spring 的 工厂加载器机制,猜测它可能是用来加载 Beans 的;
- “@Conditional beans” -> 条件 Beans,猜测这些自动配置的 Bean 似乎需要满足某些条件;
可是,左看右看在 EnableAutoConfiguration 源码里能体现出来上述三点的也就 @Import(AutoConfigurationImportSelector.class)
沾边,通过多年经验断定这个 @Import
注解导入了一个 AutoConfigurationImportSelector
类。但是,又发现好像这个只要不瞎都能看出来。
但是为了证明你比别人更加聪明一些,你多给它翻译了一个极像线索的名字 “自动配置导入选择器”。然后继续点开源码:
你欣喜若狂,因为 AutoConfigurationImportSelector
类的注释少得让你不得不喜欢,快速翻译一下:
DeferredImportSelector 处理自动配置。如果需要 @EnableAutoConfiguration 的自定义变体,这个类也可以被子类化。
… … ☹️,果然短小而精悍,看了八百遍你硬是没看懂。于是你跑去请教了一下你的大师兄,他简单为你解释了一下:
DeferredImportSelector 是 Spring 框架中的一个接口,允许延迟选择应该导入的配置类直到所有
@Configuration
类都已被处理。简单来说,它可以用来延迟导入某些配置。为什么要 “延迟自动配置的导入?”
- 顺序和优先级:自动配置的目的是为了为应用提供合理的默认配置。但在某些情况下,你可能已经为某些组件定义了自己的配置。通过延迟自动配置,Spring Boot 可以确保你的明确配置有更高的优先级,并且不会被默认的自动配置覆盖。(前面你自己总结的时候不是已经写过了嘛)
- 条件评估:Spring Boot 的自动配置经常基于条件 —— 如某个类存在于类路径上、某个 bean 尚未定义等。延迟自动配置导入意味着当条件进行评估时,所有的常规配置和定义都已经被处理,这样条件评估就更加精确和有效。(这好像和你前面看到的 “@Conditional beans” 相呼应起来了!)
- 避免循环依赖:在某些情况下,提前导入自动配置可能会导致 bean 之间的循环依赖。延迟导入有助于防止这种情况,因为它确保所有其他的
@Configuration
类都已经被处理和排序。(似乎很有道理,你连连点头)- 性能考虑:一次性评估所有的自动配置条件可能是昂贵的,特别是在大型应用中。延迟自动配置直到所有其他配置已经被处理,可以让 Spring Boot 更有效地批量处理这些条件,从而优化启动时间。(嗷 ~)
“to handle auto-configuration” 意味着
AutoConfigurationImportSelector
是为了处理 Spring Boot 的自动配置功能而设计的。自动配置是 Spring Boot 提供的一种核心功能,可以根据你的项目的类路径、beans 的定义以及各种其他条件,尝试自动配置你的应用程序。后面暂时就不太重要了,大概就是如果你需要自定义
@EnableAutoConfiguration
的行为,你可以通过继承AutoConfigurationImportSelector
来实现。通过继承和定制AutoConfigurationImportSelector
,可以对自动配置的导入过程进行更精细的控制,但是想想你也没这个需求,有需求也没这个能耐,于是大师兄告述你了解即可。
大师兄告别前还提醒你,既然 AutoConfigurationImportSelector
是为了处理 Spring Boot 的自动配置功能而设计的,那你不妨关注一下它的 selectImports()
方法。
然后你又开始了自己的探索之路:(其实你大师兄临走前连注释都给你写好了)
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 检查基于给定的注解元数据,是否启用了自动配置功能。
if (!isEnabled(annotationMetadata)) {
// 如果自动配置没有被启用,直接返回一个空的导入数组。
return NO_IMPORTS;
}
// 基于给定的注解元数据,获取与之相关的自动配置条目。
// 这个条目中包含了所有与这个元数据相关的 自动配置类 的列表。
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
// 将自动配置条目中的配置转换为字符串数组,并返回。
// 这个数组中的每个字符串代表一个要被导入的自动配置类的完全限定名。
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
于是,你拿出笔记本总结出这个方法的几个核心流程:
- 基于注解元数据判断是否应启用自动配置。
- 如果启用了,它会查询与此注解元数据相关的自动配置类。
- 最后,它返回这些自动配置类的完全限定名列表,以便后续进行导入。
那这些自动配置类的完全限定名是如何获取的?从哪儿获取的?于是你又顺藤摸瓜点进了 getAutoConfigurationEntry()
方法的源码:
protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
// 检查基于给定的注解元数据,是否启用了自动配置功能。
if (!isEnabled(annotationMetadata)) {
// 如果自动配置没有被启用,直接返回一个空的自动配置条目。
return EMPTY_ENTRY;
}
// 获取与给定的注解元数据相关的属性。
AnnotationAttributes attributes = getAttributes(annotationMetadata);
// 根据注解元数据和其相关属性,获取候选的自动配置类列表。
// 这是这个方法的核心步骤,因为它确定了可能的自动配置类。
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
// 移除配置列表中的重复项。
configurations = removeDuplicates(configurations);
// 根据注解元数据和其属性,获取应该被排除的自动配置类列表。
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
// 检查配置列表中的类是否被列在排除列表中,如果是的话,将引发一个异常。
checkExcludedClasses(configurations, exclusions);
// 从配置列表中移除那些被排除的配置类。
configurations.removeAll(exclusions);
// 使用配置类过滤器进一步筛选配置列表,以便只包括应该被导入的配置类。
configurations = getConfigurationClassFilter().filter(configurations);
// 发送与自动配置导入相关的事件。
fireAutoConfigurationImportEvents(configurations, exclusions);
// 返回一个新的自动配置条目,其中包含了应该被导入的配置类列表和应该被排除的配置类列表。
return new AutoConfigurationEntry(configurations, exclusions);
}
精明的你一眼就锁定了一个熟悉的单词 “Candidate”,还特意这部分与上下分开一些,生怕自己看不到:
// 根据注解元数据和其相关属性,获取候选的自动配置类列表。
// 这是这个方法的核心步骤,因为它确定了可能的自动配置类。
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
这不就是之前盲猜的 ““ImportCandidates” -> 导入候选者,猜测它可能是用于导入某个地方的 Bean 信息的;” 吗?好歹都有一个 “Candidate” 候选者关键字眼。
二话不说,你就点进了它的源码:(这次你大师兄恰好路过,顺势帮你注释了一波)
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
// 创建一个列表用于存储所有候选的自动配置类名。
List<String> configurations = new ArrayList<>(
// 这是核心点之一。
// 使用 SpringFactoriesLoader 从 META-INF/spring.factories 中加载所有与 getSpringFactoriesLoaderFactoryClass() 对应的自动配置类名。
// 这里的 getSpringFactoriesLoaderFactoryClass() 通常返回 AutoConfigurationImportSelector 类。
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())
);
// 这是另一个核心点。
// 加载所有标记为 @AutoConfiguration 的类,并将其添加到 configurations 列表中。
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
// 断言确保 configurations 列表不为空。
// 如果列表为空,意味着没有在预期的位置找到任何自动配置类,这可能是因为缺少了某些必要的资源或配置错误。
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
+ "are using a custom packaging, make sure that file is correct.");
// 返回包含所有候选自动配置类名的列表。
return configurations;
}
根据大师兄的注释,你提出出了如下两个核心点:
-
SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader())
这行代码从
META-INF/spring.factories
文件中加载了所有与getSpringFactoriesLoaderFactoryClass()
对应的自动配置类名。 -
ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configuratio
这行代码加载了所有标记为
@AutoConfiguration
的类,并将其添加到configurations
列表中。
为了验证大师兄的注释是否正确,你先点开了 getSpringFactoriesLoaderFactoryClass()
看到确实是返回了 EnableAutoConfiguration
类:
然后你也快速找到了 getBeanClassLoader()
原来是一个 ClassLoader
类加载器:
会想起你曾经学过的 JVM 类加载机制相关知识,明白这里大概是应用类加载器(App ClassLoader)。
参数搞定了,接下来你又看到了一个熟悉的类 “ SpringFactoriesLoader” 这玩意儿我们不是曾经盲猜过吗?
点进去,一探究竟!
你虽然看到了一个大师兄上面注释中提到的, SpringFactoriesLoader.loadFactoryNames()
会使用类加载器从 “META-INF/spring.factories” 加载对应的自动配置类名,但是你还是先先了解一下 “ SpringFactoriesLoader” 到底和我们之前猜测的作用是否一致,于是有开始了蹩脚的翻译注释:
SpringFactoriesLoader
是一个通用的、供框架内部使用的工厂加载机制。它的主要功能是从类路径中的多个 JAR 文件的META-INF/spring.factories
文件中加载并实例化给定类型的工厂。META-INF/spring.factories
文件的格式必须键是接口或抽象类的完全限定名,值是实现类名的逗号分隔列表。
然后,你精简的得出了如下结论:SpringFactoriesLoader
允许 Spring 及其组件在不知道具体实现的情况下,通过 META-INF/spring.factories
文件动态地加载和实例化服务/工厂。
顺手你还看了看涉及到的 SpringFactoriesLoader
的 loadFactoryNames()
方法,它负责执行具体的加载逻辑:
// 加载指定类型的所有工厂全类名。
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// 如果传入的类加载器为空,使用 SpringFactoriesLoader 的类加载器
ClassLoader classLoaderToUse = (classLoader != null ? classLoader : SpringFactoriesLoader.class.getClassLoader());
// 获取工厂类型的名称
String factoryTypeName = factoryType.getName();
// 调用 loadSpringFactories 加载所有的工厂并返回特定类型的工厂名称
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
// 这个方法加载 META-INF/spring.factories 文件中定义的所有工厂。
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
// 尝试从缓存中获取结果
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
// 初始化结果
result = new HashMap<>();
try {
// 获取所有的 META-INF/spring.factorie s资源的 URLs
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
// 对于每一个 URL 资源,加载其属性
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
// 解析属性并添加到结果中
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryTypeName = ((String) entry.getKey()).trim();
String[] factoryImplementationNames =
StringUtils.commaDelimitedListToStringArray((String) entry.getValue());
for (String factoryImplementationName : factoryImplementationNames) {
result.computeIfAbsent(factoryTypeName, key -> new ArrayList<>())
.add(factoryImplementationName.trim());
}
}
}
// 确保每一个工厂列表是不可修改的并且只包含唯一的元素
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
// 将结果存入缓存
cache.put(classLoader, result);
}
catch (IOException ex) {
// 在读取 META-INF/spring.factories 文件时发生任何异常都将抛出 IllegalArgumentException
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
代码有点多,但是好在你清楚的明白 “大事化小,小事化了” 的意义,于是你决定拆解它!
你先关注到了第一部分:
这里首先是第一个需要理解的地方便是 String factoryTypeName = factoryType.getName()
,如何理解这里的 factoryTypeName?你不明所以,于是又向大表哥虚心求教。
✏️ 大表哥语录:
这里的
factoryTypeName
实际上是META-INF/spring.factories
文件中的一个键(key),该键表示一个接口或抽象类的完全限定名。对应的值(value)则是这个接口或抽象类的多个实现,这些实现是用逗号分隔的类名列表。大表哥还说:现在先知道有这个东西就行,一会儿你就明白了。
于是你继续关注第二个专注点:
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
咋一看,好像是两部分内容,那不妨就拆开看看:
-
loadSpringFactories(classLoaderToUse):
这部分代码调用了
loadSpringFactories
方法,传入了一个classLoaderToUse
参数。通过翻译注释,你知道了该方法的作用是加载所有在META-INF/spring.factories
文件中定义的工厂类并返回一个映射(Map)。在这个映射中,键是工厂类型(即接口或抽象类的完全限定名),值是该类型的所有实现的类名列表。 -
getOrDefault(factoryTypeName, Collections.emptyList()):
熟悉
Map
的你一眼看出,这是Map
接口中的一个方法,它的工作是尝试从映射中获取给定键的值。在这里,键是factoryTypeName
(要查询的特定工厂类型的名称)。如果映射包含该键,方法将返回与之关联的值(在这种情况下是实现类名的列表)。如果映射不包含该键,则方法将返回其第二个参数,作为默认值。在这里,默认值是一个空列表(Collections.emptyList()
)。
通过合理拆分,你成功的掌握了这行代码的作用:“加载所有的工厂类,然后尝试从中获取与 factoryTypeName
关联的实现类名列表。如果没有找到这样的工厂类型,则返回一个空列表。”
进一步,接着探索 loadSpringFactories()
的关键部分:
嗯哼?怎么还没开始加载就先从缓存拿了呢?这里缓存的意义何在?于是,你又犯嘀咕了,不得不再次请出大表哥 … …
✏️ 大表哥说:
在
SpringFactoriesLoader
中,使用缓存的主要原因是效率和性能。你想一个场景:在 Spring 应用的生命周期中,可能会有多次需要加载和查询spring.factories
文件来获取特定类型的工厂实现。如果每次都从文件系统中重新读取和解析这些文件是不是一种计算和 I/O 的浪费?既然如此,当你第一次查询某个工厂类型并加载其实现时,
loadSpringFactories
方法解析spring.factories
文件并将解析后的结果存储在缓存中不就简单化解了嘛。后续对相同工厂类型的查询可以直接从缓存中获取,而不需要重新解析文件。这大大提高了性能,特别是在大型应用中,其中可能包含大量的 JAR 文件和许多spring.factories
文件,你想想通过缓存是不是就避免了多次频繁且大量的解析和 I/O 操作?
弄明白后,你继续向下探索:
终于到了通过类加载器加载多次出现的 META-INF/spring.factories
文件内容的地方了,你迫不及待的点进了源码:
// java.lang.ClassLoader
public Enumeration<URL> getResources(String name) throws IOException {
// 创建一个 Enumeration 数组,其长度为 2。该数组用于存放两个资源的枚举:
// 一个来自父类加载器,另一个来自当前类加载器。
@SuppressWarnings("unchecked")
Enumeration<URL>[] tmp = (Enumeration<URL>[]) new Enumeration<?>[2];
// 如果有父类加载器,则使用父类加载器获取给定名称的资源。
if (parent != null) {
tmp[0] = parent.getResources(name);
} else {
// 如果没有父类加载器(也就是说,这是根加载器),则从引导类加载器获取资源。
tmp[0] = getBootstrapResources(name);
}
// 使用当前类加载器获取给定名称的资源。
tmp[1] = findResources(name);
// 返回一个 CompoundEnumeration 对象,该对象组合了父类加载器和当前类加载器查找到的资源。
// 这意味着调用者可以透明地迭代所有查找到的资源,无论它们来自哪个类加载器。
return new CompoundEnumeration<>(tmp);
}
看着这段加载逻辑,你内心暗喜(这不就是传说中的双亲委派机制嘛 😎)
于是你熟练的默念出了 Java 类加机制中双亲委派机制的理解:
- 委派:当一个类加载器被调用来加载一个类时,它首先不会自己去加载,而是把这个请求委派给父类加载器去执行。
- 检查:父类加载器会再判断是否委派给更上层的类加载器,或者直接进行加载。如果上层类加载器可以完成类加载任务,就成功返回;否则,子类加载器尝试自己加载。
- 加载:如果父类加载器及其上层加载器都没有加载该类,那么子类加载器才开始尝试自己加载这个类。
OK,真想就快浮出水面,你决定是时候看看这 “META-INF/spring.factories” 的庐山真面目了。那么在哪儿找到它呢?上面多次提及 JAR 包,那么自然是在 JAR 包中去寻找。进一步,既然是 Spring Boot 的自动配置,那不妨就去 Spring Boot 的自动配置 JAR 包中一探究竟。
于是你打开了 IDEA 的 External Libraries 目录:
往下找啊找啊,终于找到了:
你按耐不住的双击进入文件,这时候谁来了也拦不住你,我说的!
这不就对上了嘛,前面说的:
那事实果真如此吗?为了验证你随便点进一个实现,看 RedisAutoConfiguration
比较顺眼,于是你就点了进去:
嗷~你大喊:我知道 Spring Boot 如何知道哪些 Bean 需要被配置的了!
这就是通过 Spring 的一系列 @ConditionalXXX
条件配置来实现的。它为每一个 Bean 定义了一个或多个配置条件,只有当条件满足时,这个 Bean 才会被加载。
于是你还特意去查阅相关资料了解 Spring 的条件化配置:
在开发应用的过程中,我们经常会遇到一些特殊场景,其中某些组件只有在满足特定条件时才会被初始化和使用。在 Spring 4 之前,要实现这种条件化配置有些复杂。但 Spring 4 为我们带来了一个有力的工具:@Conditional
注解,它为此类问题提供了简洁优雅的解决方案。
条件注解 | 说明 |
---|---|
@ConditionalOnBean | 当给定的 bean 存在于 ApplicationContext 时条件成立。 |
@ConditionalOnMissingBean | 当给定的 bean 不存在于 ApplicationContext 时条件成立。 |
@ConditionalOnSingleCandidate | 当 ApplicationContext 中只有一个指定类型的 bean 或指定类型的 bean 存在但也标记为 primary 时条件成立。 |
@ConditionalOnClass | 当给定的类路径上有指定的类时条件成立。 |
@ConditionalOnMissingClass | 当给定的类路径上没有指定的类时条件成立。 |
@ConditionalOnExpression | 当给定的 Spring Expression Language (SpEL) 表达式计算为 true 时条件成立。 |
@ConditionalOnJava | 当 Java 的版本与某个特定的版本匹配时条件成立。 |
@ConditionalOnWebApplication | 当应用是 web 应用程序时条件成立。 |
@ConditionalOnNotWebApplication | 当应用不是 web 应用程序时条件成立。 |
@ConditionalOnProperty | 当给定的配置属性存在,且与指定的值匹配时条件成立。 |
当你以为一切已经浮出水面正在沾沾自喜的时候,大表哥突然一个灵魂发问:你不觉得还缺点什么吗?
嘶~你想了 9981 个小时后仿佛想到了什么 … … 为什么非得在 META-INF/spring.factories
文件下搜索?怎么感觉 “META-INF” 在哪儿见过 🤔
嗷!卧槽!我想起来了,Java 的 SPI 机制!
你想激动的给大表哥说,但是你发现 SPI 你快忘了,于是查阅了一波资料:
SPI,即服务提供者接口,是 Java 提供的一套用于为应用程序(即服务使用者)提供服务的接口。其核心思想是面向接口编程,允许第三方为这些接口提供具体的实现,并将这些实现动态地加载到应用程序中。
SPI 的工作原理:
- 在
META-INF/services/
目录下创建一个以服务接口命名的文件。例如,如果服务接口是com.example.MyService
,那么应该创建一个名为META-INF/services/com.example.MyService
的文件。 - 在这个文件中列出服务接口的所有实现类。这些实现类必须有一个公开的无参构造方法,因为
ServiceLoader
会使用这个构造方法实例化对象。 - 服务使用者通过
ServiceLoader
类动态地加载这些实现。如:ServiceLoader.load(MyService.class)
。
这种机制使得服务提供者可以将服务的实现放在一个 JAR 文件中,然后分发这个 JAR 文件。服务使用者只需要将这个 JAR 文件添加到类路径中,就可以使用 ServiceLoader
加载并使用这个服务,而无需进行任何代码修改。
SPI 在 Spring Boot 中的应用:
Spring Boot 扩展了 Java 的 SPI 机制,使其更加强大和灵活。它使用类似的方式从META-INF/spring.factories
文件中加载配置类,而不是 META-INF/services/
。
在 Spring Boot 的自动配置过程中,spring.factories
文件扮演了关键角色。此文件中列出了所有的自动配置类,Spring Boot 在启动时会自动加载并处理这些配置类。
例如,如果你要为一个库提供自动配置,可以在库中包含一个spring.factories
文件,列出库中的自动配置类。然后,只要将这个库添加到 Spring Boot 项目的类路径中,Spring Boot 就会自动加载并应用这些自动配置。
在具体应用中:
- 当你在项目中加入了某个 starter(如
spring-boot-starter-web
),它会带有一些自动配置的类。 - 这些自动配置类在对应的 JAR 包中的
META-INF/spring.factories
文件中被列出。 - 在 Spring Boot 启动时,通过
SpringFactoriesLoader
读取到这些自动配置类。 - 根据条件注解(如
@ConditionalOnClass
、@ConditionalOnProperty
等),Spring Boot 决定是否要自动配置这些类。
可见,SPI 机制在 Spring Boot 中为自动配置提供了强大的支持,使得自动配置变得灵活和插拔式。
OK,文末,希望对你有所帮助,记得一件三连嗷,毕竟这篇文章是你自己一步步推敲出来的 😏。