为什么SpringBoot什么都不需要配置就可以运行起来
写在前面
SpringBoot毋庸置疑是目前流行度最高的框架。而这原因,即是大家最清楚的0配置化,不需要配置便可以启动一个tomcat服务。
我相信,只要你用过Spring Boot,就会对这样一个现象非常的好奇:
引入一个组件依赖,加个配置,这个组件就生效了。
为了故事的发展,我们举个例子来说,比如我们常用的Redis, 在Spring Boot中就是这样使用的
引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置yml文件
spring:
redis:
host: 127.0.0.1
port: 6379
password: 123456
这就配置OK了,可以直接引用了,神奇嘛
@Autowired
private RedisTemplate redisTemplate;
就这样,两步搞定,中间没有做任何事情。这里面我们应该提出几个问题。
第一,pom文件redis是在哪里,怎么连版本号都没有,reids版本是什么
第二,RedisTemplate 是怎么交给Spring注入管理的,怎么就能用了呢。
不知道你们刚接触Boot的时候,有没有听说过约定大于配置
。我们要明白一个道理,事情总要有人做,自己不做,肯定是别人帮着做了,我们带着这些疑问去深入看源码。
SPI
先不着急,讲流程源码之前,我们先来学习下SPI,这个非常重要,Boot里面大都是靠这个机制在做事情。上一篇只是简单提了下,今天发现绕不开了。
SPI ,全称为 Service Provider Interface(服务提供者接口),是一种服务发现机制。它通过在classpath路径下的
META-INF/services
文件夹查找文件,自动加载文件中所定义的类。
为了故事的继续发展 我们建个工程

我们用医生给我们做核酸的场景:
spi-doctor 为服务提供方,可以理解为我们的框架
spi-person 为使用方,因为我的服务提供接口叫PlayHeSuan(做核酸),所以所有实现都是人~
我们看下代码
在spi-doctor模块中定义接口PlayHeSuan
/**
* 服务提供者,做核酸
* @Date 2022/4/29 5:50 下午
* @Author shushi
*/
public interface PlayHeSuan {
//做核酸
void playHeSuan();
}
在spi-person模块中定义两个实现类ZhangSan
,Lisi
/**
* 李四做核酸
* @Date 2022/4/29 5:57 下午
* @Author shushi
*/
public class Lisi implements PlayHeSuan {
@Override
public void playHeSuan() {
System.out.println("李四做核酸~~~~");
}
}
/**
* 张三做核酸
* @Date 2022/4/29 5:56 下午
* @Author shushi
*/
public class ZhangSan implements PlayHeSuan {
@Override
public void playHeSuan() {
System.out.println("张三做核酸 ~~~~");
}
}
编写配置文件
新建文件夹META-INF/services
在文件夹下新建文件cn.shushi.spi.doctor.PlayHeSuan
这里要特别说明 建配置文件一定要直接写 META-INF/services,不能直接写META-INF.services,否则会无效
文件里面写实现类的全路径

测试
public class SpiTest {
public static void main(String[] args) {
ServiceLoader<PlayHeSuan> load = ServiceLoader.load(PlayHeSuan.class);
load.forEach(PlayHeSuan::playHeSuan);
}
}

SPI就讲到这里,我们回过头来结合实例理解下。SPI的目的就是借助SPI理解自动装配
借助SPI理解自动装配
回顾一下我们做了什么,我们在resources下创建了一个文件,里面放了些实现类,然后通过ServiceLoader
这个类加载器就把它们加载出来了。
假设有人已经把编写配置之类的前置步骤完成了,那么我们是不是只需要使用下面的这部分代码,就能将PlayHeSuan
有关的所有实现类调度出来。
// 使用Java的ServiceLoader进行加载
ServiceLoader<PlayHeSuan> load = ServiceLoader.load(PlayHeSuan.class);
load.forEach(PlayHeSuan::playHeSuan);
再进一步讲,如果再有人把上面这部分代码也给写了,然后把这些实现类全部注入到Spring容器里,那会发生什么?
相信到这里大家看到这里心里都已经有个谱了,我们接下来进入SpringBoot开始深入理解
SpringBoot的配置文件
在回想下SPI机制,他们都是通过在组件下放入一个配置文件完成的,那么Spring Boot是不是也这样的呢?。

是的 没错,我们可以通过pom文件的组名称来maven看到在这个下面,我们可以看到spring.factories
,翻译过来是spring工厂,是不是有那个感觉了,这个就是实例化的呗

很明显的看到最下面有个自动配置的注释,key还是个EnableAutoConfiguration,开启自动配置!
找到了!
我们接着找我们最开始说的redis

进入RedisAutoConfiguration类,看看里面是些什么代码

哈哈 破案了,终于破案了,这里不就是加载进去了嘛,配置文件我们也找到了:spring.factories,也实锤了就是通过这个配置文件进行的自动配置。
接下来,我们来尝试还原一下案情经过:
-
第一 通过某种方式读取spring.factories文件,紧接着把里面所有的自动配置类加载到Spring容器中
-
第二 然后就可以通过Spring的机制将配置类的@Bean注入到容器中了。
那接下来,我们就看是通过什么样的方式读取spring.factories,并且注入的,看到这里是不是感觉刚要进去的感觉,坚持看,相信都能看明白
如何注入
其实读取配置,就是注入,我们一起来看下看看Spring中有哪些注入方式
类似于@Component,@Bean这些,阿鉴就不说了,大家肯定见过一种这样的注解:EnableXxxxx
比如:EnableAsync
开启异步,大家好不好奇这样的注解是怎么生效的?一起来看下

大家有没有发现,框架中到处是Import注解,这里先不展开细讲,到时候讲Spring的时候,再细说,总之一句话,import就是将该类交给Spring管理,实例化该bean。
那回过头来进去 AsyncConfigurationSelector
,接着进去ProxyAsyncConfiguration

真相了,EnableAsync
就是通过这里(AsyncAnnotationBeanPostProcessor
)生效的
好的 我们接着研究最后一个注解,也是SpringBoot的启动核心注解
SpringBootApplication注解
我们在使用SpringBoot项目时,用到的唯一的注解就是@SpringBootApplication,所以我们唯一能下手的也只有它了,打开它看看吧。


进入AutoConfigurationImportSelector
核心逻辑selectImports
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
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);
//过滤掉不具备注入条件的配置类,通过Conditional注解
configurations = filter(configurations, autoConfigurationMetadata);
//通知自动配置相关的监听器
fireAutoConfigurationImportEvents(configurations, exclusions);
//返回所有自动配置类
return StringUtils.toStringArray(configurations);
}
如何从配置文件读取的(getCandidateConfigurations)
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
AnnotationAttributes attributes) {
//是不是有刚才那个SPI感觉了
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
//通过这个也能看出来 是去读取META-INF/spring.factorie
Assert.notEmpty(configurations,
"No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
getSpringFactoriesLoaderFactoryClass
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
return EnableAutoConfiguration.class;
}
结合上一步,就是加载配置文件,并且读取key为EnableAutoConfiguration的配置
接下来再看loadFactoryNames
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
try {
// FACTORIES_RESOURCE_LOCATION的值为:META-INF/spring.factories
// 这步就是意味中读取classpath下的META-INF/spring.factories文件
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
// 接下来就是读取出文件内容,封装成map的操作了
result = new LinkedMultiValueMap<>();
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();
for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryTypeName, factoryImplementationName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
ok, 后面的过滤逻辑就不在这里说了,毕竟本节的重点是自动装配机制,小伙伴明白了原理就ok啦
好的 今天的自动装配原理就讲到这里,我们下期见
总结回顾
本篇介绍了关于SpringBoot的自动装配原理,我们先通过SPI机制进行了小小的热身,然后再根据SPI的机制进行推导Spring的自动装配原理,中间还带大家回顾了一下@Import注解的使用,最后成功破案~
最后给张图做下总结
