SpringBoot启动原理

123 阅读5分钟

背景

当前(2022年-6月)企业级应用开发,使用 SpringBoot 已经很多的。SpringBoot 工程项目上手很简单。
今天我们来解开 SpringBoot 项目启动的神秘面纱 。

核心点概览

Spring Ioc 容器

@Configuration 注解

@Bean 注解

@SpringBootApplication 注解

@EnableAutoConfiguration 注解

@CompomentScan 注解

SpringApplication#run 方法

xml 方式 bean 注册和 annotation 方式 bean 注册 差异

Spring 提供的 @EnableXxx 注解,如 @EnableScheduling 、@EnableMBeanExport 等

@Import 注解

Spring 扩展方案, SpringFactoriesLoader , META-INF/spring.factories

java 反射

ConfigurableWebApplicationContext 类

ApplicationContext 类,Spring的上下文

ApplicationContextInitializer 类

ApplicationListener 类

SpringApplicationRunListener 类

Spring 的 Environment

SpringBoot 的 Banner

分析过程,推演过程

我们从一个简单的 SpringBoot工程项目开始(普通jar项目,非web 项目),工程的启动代码如下:

@SpringBootApplication  // SpringBoot启动类注解
public class Start {

    public static void main(String[] args) { // main函数,程序的入口
        SpringApplication.run(Start.class, args);

    }
}

maven 配置如下 (gradle 不说了):

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.2.6.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
</dependencies>

SpringBoot 工程启动注解,@SpringBootApplication ,程序的入口 main 函数。会执行SpringApplication#run 静态方法。

SpringApplication#run静态方法,接收2个参数,一个是程序主类,另一个是动态参数,默认是null。接下来我们 debug 进去到 SpringApplication#run 函数里,源码如下:

// 返回 ApplicationContext , ConfigurableApplicationContext 继承了 ApplicationContext
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
   return run(new Class<?>[] { primarySource }, args); // 调用重载方法
}

public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
       // 做了2个事情 :  1、创建 SpringApplication 实例 2、执行run方法 完成 spring Ioc的操作
       return new SpringApplication(primarySources).run(args); // 构建SpringApplication 实例对象,然后执行它的run方法,接收一个动态参数
	
}

SpringApplication#run方法,有2个重载方法,一个是静态的,一个是普通方法。这个普通 run 方法

// 重载的run 方法,一个普通方法,上面2个 run 方法是静态的方法 。
public ConfigurableApplicationContext run(String... args) {
		StopWatch stopWatch = new StopWatch(); 
		stopWatch.start(); // 启动计时器
		ConfigurableApplicationContext context = null; // applicationContext
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>(); // 异常记录
		configureHeadlessProperty();
                // 运行监听器 
		SpringApplicationRunListeners listeners = getRunListeners(args); 
                
		listeners.starting(); // 启动监听
		try {
                 
                        // 参数、环境变量、 banner
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			configureIgnoreBeanInfo(environment);
			Banner printedBanner = printBanner(environment);
                        
			context = createApplicationContext(); // applicationContext 实例化
			
                        exceptionReporters = getSpringFactoriesInstances(
                                SpringBootExceptionReporter.class,
					new Class[] { 
                                        ConfigurableApplicationContext.class },
                                        context);
                                        
			prepareContext(context,
                                environment,
                                listeners, 
                                applicationArguments, 
                                printedBanner);
                        
                        // 刷新 spring 上下文 
			refreshContext(context); 
                        // spring 上下文刷新后置处理
			afterRefresh(context, applicationArguments); 
                        
                        
			stopWatch.stop(); // 计时器-停止,记录了 SpringBoot 工程的启动耗时
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
                        
			listeners.started(context); // 启动监听,里面是for循环方式挨个启动
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}

		try {
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		return context;
	
}

回顾 Spring 的注解

Spring Ioc 容器是一个 Map ,存放实例化并且初始化好的java bean (这块不了解的,可以去看 Spring Ioc 容器启动过程, 以及 Spring bean 创建过程)。

@Configuration 注解

把一个普通的java类标记为配置类

@Bean 注解

向 Spring Ioc 容器中注册 bean 。

正常使用的情况下 @Configuration 注解会和 @Bean 一起使用 。参考如下案例:

@Configuration
public class OdConfig{

    @Bean
    public OderTempalte orderTemplate{
        return new OderTemplate();
    }
    
    @Bean
    public WmsTemplate wmsTemplate{
        return new WmsTemplate();
    }

}

@Import 注解 @Import 注解是把实例注册到Spring Ioc 容器中。可以导入本程序中的 bean ,也可以导入第三方库中的 bean(@Bean 也可以做到,注意它们的差异,灵活使用)

@Import 注解的3种使用姿势

// TODO 补充 @Import 的使用, 以及 @Import的源码说明

建议:java bean 注册到 Spring Ioc 容器的很多方式,推荐看 Spring 的 bean 注册方式。

SpringBoot 组件存在的价值说明

为什么要使用 SpringBoot 组件,直接 Spring 也是可以达到同样的效果,但是配置较多,多了就变得复杂化。我们开发应用追求操作简单。SpringBoot 就是为了简化 Spring 应用的配置(开箱即用、约定优于配置的思想)。
所以,在这个大的前提下,SpringBoot 的很多地方都会遵循Spring的约定,复用 Spring 的注解、在 Spring 的扩展点中进行迭代新内容。一句话,SpringBoot 是为 Spring 服务的。

SpringBoot factories 机制

SpringBoot factories 机制是 springBoot 的扩展机制之一,参考了 Java 的 SPI机制。
Java SPI 机制:为接口 interface 找到具体的实现类。

factories 机制的作用是为 SpringBoot 工程项目注入 bean 到 spring Ioc 容器中,这些 bean 不在当前的 SpringBoot 工程项目中。

为什么要有 spring.factories 扩展机制 ?

@CompomentScan 注解的作用是只扫描加了 @SpringBootApplication 注解类所在包下的所有加了@Compoment 注解和扩展了 @Compoment 注解的类,并注册到 Spring Ioc 容器中。那么如果这个 java bean 是在外部组件中(不在当前工程中)如何把其注册到 Spring Ioc 容器中?有2种方式:

1、使用@Import 注解,要求程序员手动的方式在当前工程中指定,没有自动配置的效果。
2、把要注册到Spring Ioc 容器中的外部类,放到 resources/META-INF/spring.factoriesspring.factories 文件中,通过 Spring 的机制自动装载。(spring 3.2 版本开始提供的机制)

spring factories 实现原理

spring-core 包中有一个SpringFactoriesLoader类,它试下了检索resources/META-INF/spring.factoriesspring.factories 文件,然后获取指定接口的配置功能。他有2个关键方法,如下: 1、loadFactories()方法,根据接口类获取其实现类的实例,这个方法返回的是对象列表。
2、loadFacoryNames(),根据接口获取其接口类的名称,这个方法返回的是类名的列表。

public final class SpringFactoriesLoader {

        // 默认就是到 META-INF/spring.factories 目录下找
	public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";

	private static final Map<ClassLoader, MultiValueMap<String, String>> cache = new ConcurrentReferenceHashMap<>();

        // 找所有实现类
	public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
		Assert.notNull(factoryType, "'factoryType' must not be null");
		ClassLoader classLoaderToUse = classLoader;
		if (classLoaderToUse == null) {
			classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
		}
		List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
		if (logger.isTraceEnabled()) {
			logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
		}
		List<T> result = new ArrayList<>(factoryImplementationNames.size());
		for (String factoryImplementationName : factoryImplementationNames) {
			result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
		}
		AnnotationAwareOrderComparator.sort(result);
		return result;
	}

       // 找所有名称
	public static List<String> loadFactoryNames(Class<?> factoryType,
        @Nullable ClassLoader classLoader) {
		String factoryTypeName = factoryType.getName();
		return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
	}
}

SpringBoot 是如何找到所有可能要装配的类呢?

SrpingBoot 复用了 Spring 的 factories 机制,这样就能找到所有可能要装配的类了。在如上的2个方法中,会遍历 SprtingBoot 工程项目下的 classpath 下的 classLoader 中所有jar包下的spring.factories 文件(先找到所有可能的)。

spring.factories 文件格式和样例如下:
格式: 格式 : 接口全路径=实现类全路径(换行符号使用\)

# 格式 : 接口全路径=实现类全路径(换行符号使用\)
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

# 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

SpringBoot 是如何知道具体要装配哪些类呢?

SpringBoot 使用 @EnableAutoConfiguration 注解开启自动配置,会复用Spring 的 @EnableXxx 注解。把要自动装配的类在 SpringBoot 工程项目的启动类上手动的方式指定(如果没有指定会按照约定的格式自动装配 如mysql)。

@EnableAutoConfiguration 注解里又是使用了 Spring 的 @Import 注解的 selector 方式,也可以使用 @Import 注解的 实现 ImportBeanDefinitionRegistrar 接口方式 。

Spring cloud 整合的 Xxx-start 组件中的字段装配原理同 @EnableAutoConfiguration 一样,会提供类似这样的注解 @EnableXxx 注解,如 @EnableFeignClients 注解。

SpringBoot 自动装配总结:

1、找到可能要装载的 java bean , 从 spring.factories 文件中找到。

2、确定具体要装载哪些 bean 和 配置,配置没有就取默认值,有的话就按照约定的方式装配, @EnableAutoConfiguration 注解方式。

@SpringBootApplication 注解源码如下:
它是一个复合注解

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM,
                classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM,
                classes = AutoConfigurationExcludeFilter.class) 
         }
     )
public @interface SpringBootApplication {
}

@EnableAutoConfiguration 源码如下:
重点是关注@Import 注解

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

springBoot 如何内嵌 servlet 服务器的

SpringBoot // TODO 待续 2022-6-16