背景
当前(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