[toc]

SpringBoot 源码总结

SpringApplication.run(Application.class, args) 这一行方法到底干了什么?

本文前置知识:读 Spring 源码总结SPI 机制,基于 jdk11

注册初始化器和监听器

关键词:加载并设置初始化器和监听器、记录主配置类、META-INF/spring.factories、getSpringFactoriesInstances、loadFactoryNames、loadSpringFactories。

点进 run 方法,事实上调用了:

  1. public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
  2. return new SpringApplication(primarySources).run(args);
  3. }

此处 primarySources是我们的主类,跟进查看new SpringApplication()的逻辑:

  1. public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
  2. this.resourceLoader = resourceLoader;
  3. this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
  4. this.webApplicationType = WebApplicationType.deduceFromClasspath();
  5. setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
  6. setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
  7. this.mainApplicationClass = deduceMainApplicationClass();
  8. }

这个初始化代码干了啥呢?首先添加了主要资源,然后设置了初始化器和监听器,最后跟踪栈堆信息推导出主配置类 mainApplicationClass,getSpringFactoriesInstances 方法很重要,看看具体的逻辑:

  1. private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
  2. ClassLoader classLoader = getClassLoader();
  3. Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
  4. List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
  5. AnnotationAwareOrderComparator.sort(instances);
  6. return instances;
  7. }

getSpringFactoriesInstances 方法通过 loadFactoryNames 获取了一系列的 names,然后反射创造实例返回,继续跟进 SpringFactoriesLoader.loadFactoryNames 方法,发现主要核心是 loadSpringFactories 方法:

  1. public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
  2. String factoryTypeName = factoryType.getName();
  3. return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
  4. }
  5. private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
  6. MultiValueMap<String, String> result = cache.get(classLoader);
  7. if (result != null) return result;
  8. Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
  9. result = new LinkedMultiValueMap<>();
  10. while (urls.hasMoreElements()) {
  11. URL url = urls.nextElement();
  12. UrlResource resource = new UrlResource(url);
  13. Properties properties = PropertiesLoaderUtils.loadProperties(resource);
  14. for (Map.Entry<?, ?> entry : properties.entrySet()) {
  15. String factoryTypeName = ((String) entry.getKey()).trim();
  16. for (String factoryImplementationName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
  17. result.add(factoryTypeName, factoryImplementationName.trim());
  18. }
  19. }
  20. }
  21. cache.put(classLoader, result);
  22. return result;
  23. }

其中 String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories",loadSpringFactories 函数查找了 classPath 下 META-INF/spring.factories 文件,并把所有的资源以键值对的形式放进缓存中,loadFactoryNames 返回对应键的所有元素(全限定名)。

现在来回顾下这两行代码是干啥的:

  1. setInitializers(getSpringFactoriesInstances(ApplicationContextInitializer.class));
  2. setListeners(getSpringFactoriesInstances(ApplicationListener.class));

getSpringFactoriesInstances(ApplicationContextInitializer.class) 加载了所有 META-INF/spring.factories 文件下的所有名称为 ApplicationContextInitializer 的元素,例如:

读 SpringBoot源码总结 - 图1

也就是说这些类会在这一步被加载进去,至于这些类都是干嘛的,读者可点进对应的类查看对应的注解。

再次说明 loadFactoryNames、loadSpringFactories 这两个方法很重要!

loadSpringFactories 加载 MEAT-INFO/factories 下的所有键值对,每个值都是一个类的全限定名,loadFactoryNames 根据 loadSpringFactories 加载的 Map 选取对应的值返回!

设置系统环境变量

关键词:设置系统环境变量、StandardEnvironment 类

进入 public ConfigurableApplicationContext run(String… args) 方法,其中有一行代码是:

  1. ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);

这一行代码将帮我们设置环境变量,在 prepareEnvironment 方法内调用了 getOrCreateEnvironment() 方法,跟进方法发现是一个 switch 判断,具体返回环境属性类什么取决于具体的情况,例如 Web 应用将返回 StandardServletEnvironment()。

  1. private ConfigurableEnvironment getOrCreateEnvironment() {
  2. if (this.environment != null) {
  3. return this.environment;
  4. }
  5. switch (this.webApplicationType) {
  6. case SERVLET:
  7. return new StandardServletEnvironment();
  8. case REACTIVE:
  9. return new StandardReactiveWebEnvironment();
  10. default:
  11. return new StandardEnvironment();
  12. }
  13. }

但不管怎么样都是继承了 StandardEnvironment 类,而 StandardEnvironment 类又继承了 AbstractEnvironment 类,因此无论如何都会触发 AbstractEnvironment 类的初始化:

  1. public AbstractEnvironment() {
  2. customizePropertySources(this.propertySources);
  3. }

而初始化又调用了子类 StandardEnvironment 的方法,

  1. protected void customizePropertySources(MutablePropertySources propertySources) {
  2. propertySources.addLast(
  3. new PropertiesPropertySource(SYSTEM_PROPERTIES_PROPERTY_SOURCE_NAME, getSystemProperties()));
  4. propertySources.addLast(
  5. new SystemEnvironmentPropertySource(SYSTEM_ENVIRONMENT_PROPERTY_SOURCE_NAME, getSystemEnvironment()));
  6. }

getSystemProperties() 和 getSystemEnvironment() 其实就是返回了 System.getProperties() 和 System.getenv(),这就是我们的系统环境变量。

注册注解处理增强器并注册 beanFactory

反射、createApplicationContext、DefaultListableBeanFactory、注册默认的增强器、ConfigurationClassPostProcessor。

还是在 public ConfigurableApplicationContext run(String… args) 方法中,有一行代码 context = createApplicationContext(); 创建了 ConfigurableApplicationContext 上下文,点进这个方法:

  1. protected ConfigurableApplicationContext createApplicationContext() {
  2. Class<?> contextClass = this.applicationContextClass;
  3. if (contextClass == null) {
  4. switch (this.webApplicationType) {
  5. case SERVLET:
  6. contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
  7. break;
  8. case REACTIVE:
  9. contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
  10. break;
  11. default:
  12. contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
  13. }
  14. }
  15. return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
  16. }

不要看这个方法很简单,但细节是魔鬼,我这里是进入了第一个 case 语句,也就是调用了 Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS) 语句,DEFAULT_SERVLET_WEB_CONTEXT_CLASS 是:

  1. "org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext"

也就是说我应该在 BeanUtils.instantiateClass 方法中实例化这个上下文容器,点进这个上下文容器的构造器:

  1. public AnnotationConfigServletWebServerApplicationContext() {
  2. this.reader = new AnnotatedBeanDefinitionReader(this);
  3. this.scanner = new ClassPathBeanDefinitionScanner(this);
  4. }

首先不要看这个代码,要知道这肯定会向上调用父类构造器,跟踪发现持续调用了 ServletWebServerApplicationContext、GenericWebApplicationContext、GenericApplicationContext 类的构造器方法,在 GenericApplicationContext 类中:

  1. public GenericApplicationContext() {
  2. this.beanFactory = new DefaultListableBeanFactory();
  3. }

此时,DefaultListableBeanFactory 被注册。

然后在回到 AnnotationConfigServletWebServerApplicationContext() 方法中,这里注册了两个解析器,点入 AnnotatedBeanDefinitionReader 类的构造器,持续调用父类的构造器,最终调用了一行代码:

  1. AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry);

点入这行代码,跳到了 AnnotationConfigUtils 类中执行,代码很长,但逻辑很简单,就是将几个硬编码的增强器加到容器上下文中,例如:

读 SpringBoot源码总结 - 图2

SpringBoot 中最最最重要的增强器 ConfigurationClassPostProcessor 在这里诞生了。

ClassPathBeanDefinitionScanner 这一行代码类似,添加了一些过滤器到容器内。

加载源类到容器中

关键词:prepareContext 方法、加载主配置类到容器中(未实例化)。

继续回到 run 方法中,可以发现 SpringBoot 又从 factorys 文件注册了异常播报者,但这不是我们关心的,注意方法:prepareContext,在这个方法内有一个核心逻辑:

  1. Set<Object> sources = getAllSources(); // allSources.addAll(this.primarySources);
  2. load(context, sources.toArray(new Object[0]));

getAllSources 其实加载了一些源,默认情况下就是我们传入的 primarySources,即主类,在 load 方法内被添加到容器中(未实例化)。

回到 run 方法:

读 SpringBoot源码总结 - 图3成功下载了源类!

注册 JVM 关闭钩子

你一定很好奇为什么关闭虚拟机时 springboot 还会输出一些日志,这是因为它向 JVM 注册了关闭钩子!

  1. private void refreshContext(ConfigurableApplicationContext context) {
  2. if (this.registerShutdownHook) {
  3. try {
  4. // 注册关闭狗子
  5. context.registerShutdownHook();
  6. }
  7. catch (AccessControlException ex) {
  8. // Not allowed in some environments.
  9. }
  10. }
  11. refresh((ApplicationContext) context);
  12. }

执行 ConfigurationClassPostProcessor 增强器

关键词:refresh 方法、ConfigurationClassPostProcessor 增强器、主配置类 — 候选者、ConfigurationClassParser.parse 方法。

现在终于进入 refresh 方法,这是 spring 的知识点,就不再说了,现在到执行增强器的时候了,spirngboot 会调用 ConfigurationClassPostProcessor 类的 postProcessBeanDefinitionRegistry 方法(这个方法与 postProcessBeanFactory 方法具有同样的语义),然后调用了 processConfigBeanDefinitions 方法,在这个方法内,我们调用了:

  1. ConfigurationClassParser parser = new ConfigurationClassParser(
  2. this.metadataReaderFactory, this.problemReporter, this.environment,
  3. this.resourceLoader, this.componentScanBeanNameGenerator, registry);
  4. do {
  5. parser.parse(candidates);
  6. } while (!candidates.isEmpty());

现在开始执行 ConfigurationClassParser.parse 了,而 candidates 就是我们的主配置类,这与之前 springboot 费尽心思加载主配置类也对应上了。

跟进 parse 方法内,核心代码是:

  1. public void parse(Set<BeanDefinitionHolder> configCandidates) {
  2. for (BeanDefinitionHolder holder : configCandidates) {
  3. BeanDefinition bd = holder.getBeanDefinition();
  4. parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
  5. }
  6. this.deferredImportSelectorHandler.process();
  7. }

this.deferredImportSelectorHandler.process() 这个方法很重要,先记下来,等会再说。

再跟进重载的 parse 方法,主要调用了 processConfigurationClass 方法,这个方法内有一个很重要的逻辑:

  1. SourceClass sourceClass = asSourceClass(configClass, filter);
  2. do {
  3. sourceClass = doProcessConfigurationClass(configClass, sourceClass, filter);
  4. }
  5. while (sourceClass != null);

doProcessConfigurationClass 是真正做事的方法了,并且,如果 doProcessConfigurationClass 方法返回不是 null,那么会循环调用!

doProcessConfigurationClass 解析 Configuration 类上的注解

关键词:解析主配置类上的注解、@Import、getImports 方法、找到 @EnableAutoConfiguration 注解中的 @Import(AutoConfigurationImportSelector.class)。

这里面会执行对 @ComponentScan 的扫描,我们的启动类上的注解@SpringBootApplication 注解,这个注解内就包含了 @ComponentScan 注解:

  1. @ComponentScan(excludeFilters = {
  2. @Filter(type = FilterType.CUSTOM,
  3. classes = TypeExcludeFilter.class),
  4. @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

也就是说这里 springboot 会扫描和主类在同一个包下的所有被@Controller,@Service,@Repository,@Component 标注的类,将它们注册到容器中,然后 递归的对这些类执行 parse 方法重新解析。

现在假设容器已经递归的解析好了其他的类,重新开始解析我们的主类,有一行关键的代码是:

  1. processImports(configClass, sourceClass, getImports(sourceClass), filter, true);

重点是这个函数的参数:getImports(sourceClass) 方法,跟进方法内:

  1. private Set<SourceClass> getImports(SourceClass sourceClass) throws IOException {
  2. Set<SourceClass> imports = new LinkedHashSet<>();
  3. Set<SourceClass> visited = new LinkedHashSet<>();
  4. collectImports(sourceClass, imports, visited);
  5. return imports;
  6. }
  7. private void collectImports(SourceClass sourceClass, Set<SourceClass> imports, Set<SourceClass> visited) {
  8. if (visited.add(sourceClass)) {
  9. for (SourceClass annotation : sourceClass.getAnnotations()) {
  10. String annName = annotation.getMetadata().getClassName();
  11. if (!annName.equals(Import.class.getName())) {
  12. collectImports(annotation, imports, visited);
  13. }
  14. }
  15. imports.addAll(sourceClass.getAnnotationAttributes(Import.class.getName(), "value"));
  16. }
  17. }

主要逻辑在 collectImports 方法内,这个方法啥意思?看if (!annName.equals(Import.class.getName())),其实这个方法就是搜集类上的注解,然后继续递归的跟踪这些注解,看看注解上有没有 @Import,有就加入集合,然后继续跟踪注解……递归调用,同时用 visited 防止死循环。

简言之,就是搜集类上或从其他类继承的所有 @Import 注解,将这些注解信息添加到集合内。我们的启动类上应该是有两个 @Import 的,他们在 @EnableAutoConfiguration 和 @AutoConfigurationPackage 中。

读 SpringBoot源码总结 - 图4

拿到资源后将执行 processImports 方法,这个方法会执行普通的 @Import 注解类,但不是我们的重点,这个我们马上会说。

自动装配

请先了解 @Import 基本知识

关键词:延迟导入、getAutoConfigurationEntry 方法、loadFactoryNames 、 loadSpringFactories、获取自动装配类、EnableAutoConfiguration.class、过滤自动装配类、META-INF/spring-autoconfigure-metadata.properties。

@Import(AutoConfigurationImportSelector.class) 中的注解并不会在 processImports 中执行,这是因为 AutoConfigurationImportSelector 实现了 DeferredImportSelector 接口,标志自己被延迟导入

回到 parse 方法:

  1. public void parse(Set<BeanDefinitionHolder> configCandidates) {
  2. for (BeanDefinitionHolder holder : configCandidates) {
  3. BeanDefinition bd = holder.getBeanDefinition();
  4. parse(((AnnotatedBeanDefinition) bd).getMetadata(), holder.getBeanName());
  5. }
  6. this.deferredImportSelectorHandler.process();
  7. }

this.deferredImportSelectorHandler.process() 是处理延迟导入的关键方法,跟进 process() 方法,这个方法调用了:

  1. DeferredImportSelectorGroupingHandler handler = new DeferredImportSelectorGroupingHandler();
  2. handler.processGroupImports();

点进 handler.processGroupImports() 方法内有一行:

  1. grouping.getImports().forEach(......)

跟进 grouping.getImports() 内:

  1. public Iterable<Group.Entry> getImports() {
  2. for (DeferredImportSelectorHolder deferredImport : this.deferredImports) {
  3. this.group.process(deferredImport.getConfigurationClass().getMetadata(),
  4. deferredImport.getImportSelector());
  5. }
  6. return this.group.selectImports();
  7. }

核心方法是 this.group.process,继续跟进:

  1. @Override
  2. public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
  3. AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector).getAutoConfigurationEntry(annotationMetadata);
  4. this.autoConfigurationEntries.add(autoConfigurationEntry);
  5. for (String importClassName : autoConfigurationEntry.getConfigurations()) {
  6. this.entries.putIfAbsent(importClassName, annotationMetadata);
  7. }
  8. }

至关重要的一行代码:getAutoConfigurationEntry(annotationMetadata),跟进这个方法:

  1. protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
  2. AnnotationAttributes attributes = getAttributes(annotationMetadata);
  3. // 获取候选类
  4. List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  5. // 过滤候选类
  6. configurations = getConfigurationClassFilter().filter(configurations);
  7. return new AutoConfigurationEntry(configurations, exclusions);
  8. }

这个方法主要就两个逻辑:获取自动装配类和过滤自动装配类,一个一个看!

获取自动装配类

点进 getCandidateConfigurations 方法:

  1. protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  2. List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
  3. getBeanClassLoader());
  4. return configurations;
  5. }

看来核心是 SpringFactoriesLoader.loadFactoryNames 方法了,跟进这个方法,发现竟然调用了 loadFactoryNames 和 loadSpringFactories 方法!这两个方法我们在第一步的时候就已经详细讲了!我们来看看具体的参数(key):getSpringFactoriesLoaderFactoryClass():

  1. protected Class<?> getSpringFactoriesLoaderFactoryClass() {
  2. return EnableAutoConfiguration.class;
  3. }

是 EnableAutoConfiguration!也就是说这一步会拿到所有 key = EnableAutoConfiguration 的值:

读 SpringBoot源码总结 - 图5

类多的数不胜数,所有全限定名都被加载返回,这就是自动装配核心!

\ 是换行的意思,按照上面的分析,这里肯定走缓存了。

过滤自动装配类

须熟悉 OnCondition 相关知识

跟进 getConfigurationClassFilter().filter(configurations) 的 filter 方法,configurations 是我们刚刚获得的自动装配类的全限定名:

  1. List<String> filter(List<String> configurations) {
  2. String[] candidates = StringUtils.toStringArray(configurations);
  3. for (AutoConfigurationImportFilter filter : this.filters) {
  4. boolean[] match = filter.match(candidates, this.autoConfigurationMetadata);
  5. for (int i = 0; i < match.length; i++) {
  6. if (!match[i]) {
  7. candidates[i] = null;
  8. }
  9. }
  10. }
  11. List<String> result = new ArrayList<>(candidates.length);
  12. for (String candidate : candidates) {
  13. if (candidate != null) {
  14. result.add(candidate);
  15. }
  16. }
  17. }

这个方法遍历 this.filters 来执行 match 方法,只要有一个不符合就过滤掉,this.filters 是什么呢?

读 SpringBoot源码总结 - 图6

看到 OnCondition 应该很熟悉了,这里面的方法比较绕,就不放源码了,大致的是在 AutoConfigurationMetadataLoader 类内,对 PATH = “META-INF/spring-autoconfigure-metadata.properties“ 下的文件进行了资源加载,然后根据对应的规则(onClass 还是 onBean),去判断对应的 key 是否符合条件:

读 SpringBoot源码总结 - 图7

手写自动装配组件

定义一个类 Hello:

  1. public class Hello {
  2. public void hello() {
  3. System.out.println("Hello World!");
  4. }
  5. }

按照要求建立资源文件:

读 SpringBoot源码总结 - 图8

按照要求填写类的全限定名:

  1. org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  2. com.happysnaker.Hello

打 jar 包,然后重启另一个项目导入 jar 包,运行 test(爆红别怕):

  1. @SpringBootTest
  2. class ApplicationTests {
  3. @Autowired
  4. Hello hello;
  5. @Test
  6. void contextLoads() {
  7. hello.hello();
  8. }
  9. }

成功输出:Hello World!

再建个文件:

读 SpringBoot源码总结 - 图9

填入:

  1. com.happysnaker.Hello=
  2. com.happysnaker.Hello.ConditionalOnClass=com.happysnaker.H

重新打包,运行程序报错!说明被过滤了!

新建一个类 H:

  1. public class H {
  2. }

重新打包运行,成功输出:Hello World!

总结

按照目录的顺序下来就是启动的所有流程了,如果能围绕着其中的关键字思考应该没多大问题了。