要理解SpringBoot自动装配原理肯定离不开@SpringBootApplication
注解,如下图所示。
点进去之后可以发现@SpringBootApplication
注解是由许多注解组成,但真正核心的注解只有下面三个:
点进@SpringBootConfiguration
注解,可以发现其核心注解为@Configuration
注解:
@Configuration
是一个类级别的注释,表明一个对象是 bean 定义的来源。@Configuration
类通过带@Bean
注解的方法声明 bean 。@Bean
对@Configuration
类方法的调用也可用于定义 bean 间的依赖关系
@Configuration
在spring的注解开发中占有很重要的地位,你当你想要定义一个配置类并交给spring管理的时候你就可以在相关类上面加这个注解,并配合@Bean
注解把对象交个spring去管理。
所以@SpringBootConfiguration
注解本质上就是一个@Configuration
注解,用来标注某个类为 JavaConfig 配置类,有了这个注解就可以在 SpringBoot 启动类中使用```@Bean``标签配置类了,如下图所示。
@ComponentScan
是Spring注解之一,用于在Spring应用程序上下文中启用组件扫描。组件扫描是自动检测和注册 Spring bean(组件)到应用程序上下文中的过程。
这个扫描的范围是:SpringBoot 主启动类的同级路径及子路径,扫描到特定的@Component
、@Service
、@Controler
、@Repository
、@Configuration
等等注解后,会做相应的bean注册和配置文件bean注册工作。
点进这个注解可以发现 @Import(AutoConfigurationImportSelector.class)
,如下图所示。
@Import
是Spring注解之一,用于在配置类中导入其他配置类或者普通的Java类。
通过@Impor
注解,我们可以将其他配置类或者普通的Java类导入到当前配置类中,从而实现对这些类的引用和使用。可以用于将多个配置类组合在一起,或者引入第三方库中的配置类。
说白了在这里@Import
注解的作用就是将 AutoConfigurationImportSelector 这个类导入当前类,这个类就是实现自动装配的核心。
进入 AutoConfigurationImportSelector 类,找到 selectImports() 方法,见名知意,不难想到这个方法就是选择要加载什么。
可以发现这个方法的参数是 AnnotationMetadata ,AnnotationMetadata 是 Spring 框架中的一个接口,用于表示一个类或者方法的注解信息。通过 AnnotationMetadata接口,我们可以获取类或方法上的注解信息,并根据注解信息进行相应的操作,如动态创建bean、实现AOP等等。
不难想到这个方法就是根据注解信息加载一些东西,至于加载什么,我们可以看到这个方法的返回值类型是字符串数组,这个字符串数组里面到底要放哪些内容?答案是加载某些配置文件中的全包名列表,这些配置信息包含了Spring Boot自动装配需要的各种配置类,当Spring Boot应用启动时,会根据这些配置信息自动装配相应的组件和配置。至于要加载的这些信息在哪里,我会带大家一点一点的找到。
分析到此处,我们不难想到,要想得到需要的配置信息,必然会有一个方法的参数是注解信息,返回的就是我们需要的配置信息,结合方法源码,果然可以找到这个方法——getAutoConfigurationEntry()。
我们发现 autoConfigurationEntry 中保存着我们需要的配置信息,它是通过 getAutoConfigurationEntry 方法获取的,于是我们继续深入,进入 getAutoConfigurationEntry 方法。
这个方法有很多代码,而且看着长得都差不多,很容易让然眼花缭乱,反正我是这么觉得的,那我们该如何分析呢?既然内容太多,不如先看看结果,看看他想要什么东西。通过方法返回值可以知道我们需要返回一个 AutoConfigurationEntry 对象,再看看最后的 return ,果然是一个新创建的 AutoConfigurationEntry 对象,我们再看看构造这个对象需要的参数:
configurations, exclusions
从名字上来看,显然 configurations 是我们需要的配置信息,而 exclusions 字面意思是排除,也就是不需要的,那我们接下来应该关注configurations 到底是怎么来的。
根据前面的分析可以知道,我们是根据注解类名来从一个配置文件中读取出我们需要的Config配置类,这里configurations就代表了Config配置类,那么我们应该找到一个入口,这个入口跟注解相关,并且返回了 configurations 这个参数。
目光回到源码,可以发现方法参数是注解的方法有三个,但是返回值是 configurations 的,只有一个:
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
没错,getCandidateConfigurations 方法就是那个我们要找的。在这里我先多说一下:Candidate 候选数据,getCandidateConfigurations 获得候选配置数据,这是什么意思的,在这里我们可以先这样理解,通过这个方法我们获得的配置信息数据是一个全集,我们还要从这里排除一些不需要的,或者是我们自己设置的要排除哪些配置,其实要排除的不就是 exclusions ,不过这里大家留个印象即可,后面我们详细将这里的。
接下来我们继续进入 getCandidateConfigurations 方法:
走到这里,如果你还没有忘记我们的初衷——“找到要加载的配置信息在哪里”,那我们可以选择走一条捷径,仔细看源码:
这个断言很巧妙,他告诉我们在哪里没有找到什么东西,要发出警告,我们反其道而行之,去这个路径下看一看。
妙啊,我们发现了一个类似配置文件的东西(其实就是),我们点开他看一看。
这个文件里内容填充率爆表啊,至少我刚刚接触代码时,看到这么密密麻麻的东西肯定很难受,所以我们换个角度来看这个文件。
spring.factories文件是Spring框架和Spring Boot中用来配置自动装配的文件之一,它的格式是标准的Java属性文件格式,通常包含以下几个部分:
注释:以#或!开头的注释行,用于描述文件的作用和内容。
配置项:以key=value的形式定义,用于配置自动装配的相关信息。其中,key表示自动装配的配置类或者接口,value表示该配置类或者接口的实现类或者子类。
哦,原来如此,不就是个 key-value 结构吗,key表示自动装配的配置类或者接口,value表示该配置类或者接口的实现类或者子类。这个文件包含了我们包含了我们要的配置信息,只要通过 EnableAutoConfiguration 这个注解(key),我们就可以打包带走该配置类或者接口的实现类或者子类这些信息(value)。
到这里,自动装配到底是什么,应该比较清楚了,原来是 SpringBoot 帮我们加载了各种已经写好的Config类文件,实现了这些JavaConfig配置文件的重复利用和组件化。我们不需要再为到底需要哪些类而伤神了,我们只需要通过这个配置文件就可以获得我们需要的类,甚至我们不需要关心怎样获取项目启动所需要的类,只需要一个注解—— @EnableAutoConfiguration。
但是这毕竟是走捷径分析出来的,所以我们还要继续下去,把路走完。但前路漫长,走之前我们要带上一样东西,通过之前的分析,我们不难发现,这个配置文件的 key-value 结构有些奇怪,一个 key 对应着 许多 value,所以我们要通过那种数据结构封装这些信息呢?答案是——Map
我们虽然通过断言信息走捷径,知道了配置文件的位置,但是我们还是不知道 SpringBooot 具体是怎样获得这些配置信息的,所以我们要继续分析getCandidateConfigurations 方法,到此时我们显然只有一条路可以走了,那就是进入 loadFactoryNames 方法。
这个方法中代码并不多,我们只需要关注他的返回值,我们需要继续进入 loadSpringFactories 方法。
loadSpringFactories 方法内容有点多,为了清晰展示就不截屏了,直接把源码放到这里。
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 {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
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());
}
}
}
// Replace all lists with unmodifiable lists containing unique elements
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
return result;
}
注意看这个方法的第一行代码,Map
在这里进行断点调试:
果然如此,这个 result 就是我们想要的配置文件中的配置信息,我们之前看的 spring.factories 中关于 @EnableAutoConfiguration注解 的相关信息也被封装到 result 中。
现在我们已经很明确,我们想要的配置信息已经拿到了,所以在这个方法中一定有读取配置文件的地方,那怎么找到它呢?想要读取配置文件,一定要有配置文件的路径,我们继续寻找,果然发现了使用地址信息的地方:
继续找到这个常量,看,我们发现了什么,“META-INF/spring.factories”,这不正是我们之前走捷径通过断言发现的位置吗?
继续往下看,有了配置文件的路径,并且通过这个路径拿到了一个,urls 集合,接下来我们要怎么办,当然是遍历 urls ,获取配置信息,然后添加到 result 中进行返回。
看,有源码中显然也是这么做的,通过循环遍历 urls ,然后将结果添加到 result 中。
我们当然还要继续分析,但是在此之前,我想看看 urls 里面到底是个什么?怎么看呢?当然是断点调试!
单看 urls 的内容可能比较复杂,但是我们可以单独看一个 url ,这不就是 jar 包对应配置文件的绝对路径吗?有了这个,再去配置文件获取配置信息还不是手到擒来!
其实到这了我们的分析已经可以结束了,我们已经知道 SpringBoot 加载配置信息,通过这些配置信息自动装配需要的类,这个基本流程我们已经走通了,但是我是个有强迫症的人,我已我们继续来看一些细节。
我对上面两个地方加了断点,因为我比较好奇,他怎么通过 url 加载配置信息的内容,在此之前我们先了解一下 UrlResource类 和 loadProperties 方法。
UrlResource 是 Spring 框架中的一个类,用于表示一个 URL 资源,它的作用主要有以下几个方面:
封装 URL 资源:UrlResource 将 URL 资源封装为一个对象,方便在程序中处理和操作。
加载资源:UrlResource 可以加载指定 URL 的资源,例如文件、图片、视频等等,可以通过 getInputStream 方法获取资源输入流,然后进行读取和处理。
支持多种协议:UrlResource 支持多种 URL 协议,包括 file、http、https、ftp 等等,可以根据需要进行选择。
用于 Spring 框架中的资源加载:UrlResource 主要用于 Spring 框架中的资源加载,例如加载 Spring 配置文件、加载 bean 定义文件等等。
上面都是一些废话,网上一搜就可找到,总之 UrlResource 就是对 URL 资源进行了封装,方法数据处理和操作。
loadProperties 方法是用来加载 Java 属性文件的方法。它能够将属性文件中的键值对读取到一个 Properties 对象中,方便在程序中使用。
结合上面的知识,我们再看看 resource 对象和 properties 对象,这时我们应该比较清楚了,原来是 loadProperties 方法通过 url 资源将属性文件中的键值对读取到一个 Properties 对象中,我们后面只需要操作 properties 对象内容即可,我们要的配置信息都封装在这个里面了。
再看看这个循环,不就是从 properties 对象中取出内容,然后经过一些操作,然后添加到 result 中吗。至于做了哪些处理,感兴趣的可以自己看一看,这个不是重点,我就不带着大家一起看了。
最后还有一个地方需要提一下:
上面两张图是 loadSpringFactories 方法的开头和结尾,也没什么太值得强调的,就是 result 是放在缓存里的,需要的时候先去缓存里找,缓存里没有,再通过 loadProperties 方法去加载配置信息,加载完成并添加到 result 后,在方法返回之前,再将 result 存入缓存。
至于为什么这么做,当然是为了减少了磁盘频繁的读写I/O,提高读取速度!
俗话说好马不吃回头草,但是我们毕竟不是马(可能是牛马),我们还是回头看一看 loadFactoryNames 方法,这个方法调用了我们上面分析了好久的 loadSpringFactories 方法,如果你没有忘记 loadSpringFactories 方法返回的应该是那个 result 把,他不是 Map
所以,不难想到,SpringBoot 必然调用了一个容器类方法,将 Map
没错这个方法就是 getOrDefault 方法,我在上面的图中已经标注出来了,接下来,进去看看吧。
好短啊!来,看看他的作用吧。
getOrDefault 是 Java Map 接口中的一个方法,它的作用是获取指定键对应的值,如果该键不存在,则返回一个默认值。
该方法的语法如下:
V getOrDefault(Object key, V defaultValue)
其中,key 表示要获取的键,defaultValue 表示默认值。
当 Map 中存在指定的键时,该方法返回该键对应的值;当 Map 中不存在指定的键时,该方法返回 defaultValue。
原来 SpringBoot 通过 loadSpringFactories 方法获得了 Map
最后,我们再回过头来,看一看 getAutoConfigurationEntry 方法。
可以看到,这里对加载进来的配置配置信息 configurations 进行了去重、排除的操作,这是为了使得用户自定义的排除包生效,同时避免包冲突异常,在SpringBoot的入口函数中我们可以通过注解指定需要排除哪些不用的包,如下图所示。
至此,整个 SpringBoot 自动装配流程全部分析完成,全文一万多字,纯手敲,如果大家喜欢我的讲解风格,可以给个点赞吗,哈哈。
有任何问题,或者文章有任何错误,请在评论区@我。