Spring Boot 启动过程:
1. 创建 SpringApplication 对象。
2. 执行对象的 run() 方法。
3. 将 class 变成 beanDefinition。
4. 将 beanDefinition 变成 bean
5. 图解 循环依赖
6. 图解 bean 的生命周期
7. 图解 aop 拦截器链调用
@SpringBootApplication
public class AutoconfigApplication {
public static void main(String[] args) {
SpringApplication.run(AutoconfigApplication.class, args);
}
}
1. 创建 SpringApplication对象
一路调用下来,执行到了 SpringApplication 的构造方法中。
// resourceLoder = null; primarySources = AutoconfigApplication.class;
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 应用程序需要一个资源加载器,来加载外部的文件。
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
// 给应用程序设定主配置类。
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 根据关键字获取当前 SpringApplication 的类型,结果是:SERVLET。
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 设置引导启动器。
this.bootstrappers = new ArrayList<>(getSpringFactoriesInstances(Bootstrapper.class));
// 设置初始化器。
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 设置监听器。
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 设置主类。
this.mainApplicationClass = deduceMainApplicationClass();
}
构造器中做了什么呢?给应用程序的属性设置值。
- 设置资源加载器。
- 设定主配置类,最关键的是主配置类上的
@SpringBootApplication
注解,此处先不展开描述。 - 设置应用程序类型为 SERVLET,表示当前应用程序是内嵌了 servlet 的 web 应用程序。
The application should run as a servlet-based web application and should start an embedded servlet web server.
SERVLET,
- 设置引导启动器,应用程序在执行后面的 run() 方法时,会创建引导上下文(一个对象),引导启动器要向这个上下文中加载资源。因为 Spring 框架没有默认的引导启动器,并且开发者也没有自定义引导启动器,所以这个属性值是个空 list。
- 设置初始化器。应用程序在执行 run() 方法时会创建 IOC容器(这是一个 ApplicationContext 对象),初始化器是后面用来向 IOC 容器中加载资源的。
- 设置监听器。监听器的作用是监控应用程序的运行状态,并且在一些指定状态出现时发布事件,比如:向终端打印启动时的 log。
- 设置主类。
SpringApplication 启动时开发者如果要做些操作,那么就应该自定义类并实现Bootstrapper.java
接口,如果开发着要在 IOC 容器中干些什么事情,那么应该自定义类并实现 ApplicationContextInitializer.java
接口。
2. 引导启动器、初始化器、监听器都是怎么加载进来的。
从上面的代码可以看到,引导启动器、初始化器、监听器的加载过程是一样的,那就看看getSpringFactoriesInstances() 是怎么把这些都加载进来的。 以 ApplicationContextInitializer.class 为例。
// type = ApplicationContextInitializer.class
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
return getSpringFactoriesInstances(type, new Class<?>[] {});
}
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
// 类加载器。
ClassLoader classLoader = getClassLoader();
// 获取到所有 ApplicationContextInitializer.class 子类的全限定名。
Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
// 使用全限定名通过反射给每个初始化器创建一个实例。并将所有实例存放到集合中。
List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
// 对集合中的初始化器排序。
AnnotationAwareOrderComparator.sort(instances);
// 返回所有的初始化器。
return instances;
}
初始化器就是这样被加载进来了,但只描述到这里,恐怕连自己都说服不了。初始化器们到底放在哪里了?全限定名怎么被拿到的?它们又是怎么反射的?
2.1 初始化器放在哪里?
Spring 默认的初始化配置器都放在 jar 包的 "META-INF/spring.factories"
文件中。书写格式:全限定接口名 = 全限定子类名
,等号右边的类全都是等号左边接口的实现类。
spring-boot-autoconfigure-2.4.4.jar/MERA-INF/spring.factories
中有 2 个初始化器:
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
spring-boot-2.4.4.jar/MERA-INF/spring.factories
中有 5 个初始化器:
# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
框架总共默认了 7 个初始化器。不只是默认的初始化器,默认的监听器、过滤器等等都在 MERA-INF/spring.factories
中写着。
2.2 初始化器的全限定名怎么被加载到了内存中?
默认的全限定名是被写到文件中的,而且有那么多种类,为什么这次只唯独拿到了所有初始化器全限定名,而下次又只拿监听器的全限定名?
//factoryType = org.springframework.context.ApplicationContextInitializer
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
// 类加载器。
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// factoryTypeName = org.springframework.context.ApplicationContextInitializer
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
这行代码很明能看出来,loadSpringFactories
方法使用 类加载器 classLoaderToUse
加载外部文件并且将信息构建成 map 类型,factoryTypeName
作为 key 去 map 中拿信息,如果能拿到就返回,如果 key 不存在就返回 Collections.emptyList()
。这就解释了为什么这次只拿初始化器,下次只拿监听器,因为两次传的参数不一样啊。
那再看看 map 是怎么来的,里面都有些啥?
先说结论:classLoader 将所 jar 包中的 spring.factories 文件都加载了进来(我测试了,只有三个)。并将文件中的信息读到了 Map<String, List<String>> result
中,key 是全限定接口名,value 是该接口子类的全限定类名组成的 list,最后将 result 返回。
再看,一次启动就载入了引导启动器,初始化器、监听器,如果每载入一个XXX器,就将spring.factories文件全部加载一次,那也太浪费了。所以源码在这里做了个小优化,第一次全加载并且构建 result,然后将 result 放到 cache 中缓存起来,等第二次再需要加载 XXX器时,先去 cache 中找下有没有 result,如果有了那直接在 result 中通过 key 值拿 values,如果 result 不存在,那再加载全部 spring.factories 文件。
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
// 先判断缓存中有没有 result。
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
// 有,那就直接返回。
return result;
}
// 如果没有,创建个空 map 准备填数据。
result = new HashMap<>();
// try块里面的代码就做了一件事情,读全部spring.factories文件,并将其中的信息构建成键值对放到result。
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());
}
}
}
// 这里是去重(不同文件中有时会定义相同的接口实现类)
result.replaceAll((factoryType, implementations) -> implementations.stream().distinct()
.collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList)));
// 将构建好的 result 放到缓存中。
cache.put(classLoader, result);
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
// 返回
return result;
}
打个断点看下 result 里面到底有什么?
从这里可以看出,全部 spring.factories 文件中总共默认了 16 个接口(暂且这么叫吧),默认了 7 个 ApplicationContextInitializer,1 个 ApplicationListener ,0 个 Bootstrapper。
2.3 类的全限定名怎么反射到实例的?
这块代码还是很简单的,大体来看下:
// names 里面存放的都是类的全限定名。
private <T> List<T> createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) {
// 创建 instances 用于存放实例。
List<T> instances = new ArrayList<>(names.size());
// 循环拿到每一个全限定名
for (String name : names) {
try {
// 根据全限定名获取到这个类的 class对象。
Class<?> instanceClass = ClassUtils.forName(name, classLoader);
Assert.isAssignable(type, instanceClass);
// 从类对象中拿构造器。
Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
// 用构造器创建类的实例。
T instance = (T) BeanUtils.instantiateClass(constructor, args);
// 添加到集合中。
instances.add(instance);
}
catch (Throwable ex) {
throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
}
}
// 返回 instances
return instances;
}
通过这样的方式,应用程序默认的初始化器、监听器等就被加载进来了。
最后设置了主类,创建 SpringApplication 对象的代码就走完了,要执行 run 方法。