Spring 的丰富生态备受开发者青睐,尤其是自从 SpringBoot 出现之后去掉了原来的复杂配置,因为 SpringBoot 的理念就是 约定大于配置 ,这让我们省去了很多需要手动配置的过程,就拿 SpringMVC 来说吧各种 XML 配置直接劝退初学者,但是 SpringBoot 的易用性简直是成为了推广 Spring 生态的利器。本篇文章主要是结合 SpringBoot 的源码,来探究 SpringBoot 应用程序的启动流程!
新建一个 SpringBoot 项目,首先映入眼帘的恐怕就是下面的这个关键的 Main 函数与 @SpringBootApplication 注解吧,我们将从这个注解开始,逐步探究 SpringBoot 应用的启动流程:
@SpringBootApplication
public class SpringBootStartApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootStartApplication.class, args);
}
}
复制代码
@SpringBootApplication 注解实际上是 SpringBoot 提供的一个复合注解,我们来看一看其源码:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
...
}
复制代码
关于这里面某些元注解的功能,可以参考我之前的写的一篇博客 《 注解的原理与实现 》 。在这里我们只需要看 @SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan 这三个注解。在 SpringBoot 应用的启动类上用这个三个注解代替 @SpringBootApplication 注解其实也是没问题的:
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public class SpringBootStartApplication {
public static void main(String[] args) {
SpringApplication.run(SpringBootStartApplication.class, args);
}
}
复制代码
那我们接下来就需要分贝探究这三个注解的功能。
@SpringBootConfiguration
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
复制代码
@SpringBootConfiguration 也是来源于 @Configuration,二者功能都是将当前类标注为配置类,@Configuration 用于定义配置类,可替换 xml 配置文件,被注解的类内部包含有一个或多个被 @Bean 注解的方法,这些方法将会被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 类进行扫描,并用于构建 Bean 定义,初始化 Spring 容器,这个貌似一点都不新奇。
@EnableAutoConfiguration
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
...
}
复制代码
@EnableAutoConfiguration 注解启用自动配置,其中最关键的要属 @Import(AutoConfigurationImportSelector.class),借 AutoConfigurationImportSelector,@EnableAutoConfiguration 可以帮助 SpringBoot 应用将所有符合条件的 @Configuration 配置都加载到当前 SpringBoot 创建并使用的 IoC 容器。
借助于 Spring 框架原有的一个工具类:SpringFactoriesLoader 的支持, @EnableAutoConfiguration 可以智能的自动配置功效才得以大功告成!
关于这个注解可以参考我的另一篇文章 《SpringBoot 自动配置原理》 ,里面有详细介绍并且有例子。
推荐观看:SpringBoot自动配置源码详解
@ComponentScan
@ComponentScan 对应于 XML 配置形式中的 context:component-scan,用于将一些标注了特定注解的 bean 定义批量采集注册到 Spring 的 IoC 容器之中,这些特定的注解大致包括:
- @Controller
- @Entity
- @Component
- @Service
- @Repository
对于该注解可以通过 basePackages 属性来更细粒度的控制该注解的自动扫描范围,比如:
@ComponentScan(basePackages = {"xpu.tim.controller","xpu.tim.entity"})
复制代码
@SpringBootApplication 这个注解看完了, 那么接下来就来看看这个 SpringApplication 以及 run() 方法究竟干了些啥。原始的 SpringCore 中并没有这个类,SpringApplication 里面封装了一套 Spring 应用的启动流程,然而这对用户完全透明,因此我们上手 SpringBoot 时感觉简洁、轻量。
通过阅读 run 方法的源码我们不难发现,其实是需要构造一个 SpringApplication 对象:
/**
* Static helper that can be used to run a {@link SpringApplication} from the
* specified sources using default settings and user supplied arguments.
* @param primarySources the primary sources to load
* @param args the application arguments (usually passed from a Java main method)
* @return the running {@link ApplicationContext}
*/
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}
复制代码
默认的 SpringApplication 执行流程已经可以满足大部分需求,但是若用户想干预这个过程,则可以通过 SpringApplication 在流程某些地方开启的扩展点来完成对流程的扩展,典型的扩展方案那就是使用 set 方法。
@SpringBootApplication
public class SpringBootStartApplication {
public static void main(String[] args) {
//SpringApplication.run(SpringBootStartApplication.class, args);
SpringApplication application = new SpringApplication(SpringBootStartApplication.class);
application.set...(); // 用户自定义扩展点
application.set...(); // 用户自定义扩展点
application.run(args);
}
}
复制代码
这样一拆解后我们发现,我们也需要先构造 SpringApplication 类对象,然后调用该对象的 run() 方法。那么接下来就讲讲 SpringApplication 的构造过程以及其 run() 方法的流程,搞清楚了这个,那么也就搞清楚了 SpringBoot 应用是如何运行起来的! 主要需要看以下四个方法:
1、deduceFromClasspath:用来推断应用的类型:创建的是 REACTIVE 应用、SERVLET 应用、NONE 三种中的一种
NONE 表示当前的应用即不是一个 web 应用也不是一个 REACTIVE 应用,是一个纯后台的应用。SERVLET 表示当前应用是一个标准的 web 应用。REACTIVE 是 spring5 当中的新特性,表示是一个响应式的 web 应用。而判断的依据就是根据 Classloader 中加载的类。如果是 servlet,则表示是 web,如果是 DispatcherHandler,则表示是一个 REACTIVE 应用,如果两者都不存在,则表示是一个非 web 环境的应用。
2、setInitializers:使用 SpringFactoriesLoader 查找并加载 classpath 下 META-INF/spring.factories 文件中所有可用的 ApplicationContextInitializer
使用 SpringFactoriesLoader 查找并加载 classpath 下 META-INF/spring.factories 文件中的所有可用的 ApplicationListener
3、setListeners:使用 SpringFactoriesLoader 查找并加载 classpath 下 META-INF/spring.factories 文件中的所有可用的 ApplicationListener
4、deduceMainApplicationClass:推断并设置 main 方法的定义类
通过这个几个关键步骤,SpringApplication 完成了实例化。
之前我们弄清楚了 SpringApplication 的实例化过程,现在看看它的 run 方法究竟干了什么:
1、通过 SpringFactoriesLoader 加载 META-INF/spring.factories 文件,获取并创建 SpringApplicationRunListener 对象;
2、然后由 SpringApplicationRunListener 来发出 starting 消息;
3、把参数 args 封装成 DefaultApplicationArguments,并配置当前 SpringBoot 应用将要使用的 Environment;
4、完成之后,依然有 SpringApplicationRunListener 来发出 environmentPrepared(环境已准备)消息;
5、创建上下文,根据项目类型创建上下文;
6、初始化 ApplicationContext,并设置 Environment,加载相关配置等;
7、由 SpringApplicationRunListener 来发出 contextPrepared 消息,告知 SpringBoot 应用使用的 ApplicationContext 已准备 OK;
8、将各种 beans 装载入 ApplicationContext,继续由 SpringApplicationRunListener 来发出 contextLoaded 消息,告知 SpringBoot 应用使用的 ApplicationContext 已装填 OK;
9、refresh ApplicationContext,完成 IoC 容器可用的最后一步;
10、由 SpringApplicationRunListener 来发出 started 消息 ;
11、完成最终的程序的启动;
12、SpringApplicationRunListener 来发出 running 消息,告知程序已运行起来了;
步骤 4 和 5 之间还有个 PrintBanner,用来打印 Banner
createApplicationContext()
下面这段代码主要是根据项目类型创建上下文,并且会注入几个核心组件类:
protected ConfigurableApplicationContext createApplicationContext() {
Class<?> contextClass = this.applicationContextClass;
if (contextClass == null) {
try {
switch (this.webApplicationType) {
case SERVLET:
contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
break;
case REACTIVE:
contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
break;
default:
contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
}
}
catch (ClassNotFoundException ex) {
throw new IllegalStateException(
"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
}
}
return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
}
复制代码
Web 类型项目创建上下文对象 AnnotationConfigServletWebServerApplicationContext 。这里会把 ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等一些核心组件加入到 Spring 容器。
refreshContext()
下面一起来看下 refreshContext(context) 这个方法,这个方法启动 spring 的代码加载了 bean,还启动了内置 web 容器:
private void refreshContext(ConfigurableApplicationContext context) {
refresh(context);
if (this.registerShutdownHook) {
try {
context.registerShutdownHook();
}
catch (AccessControlException ex) {
// Not allowed in some environments.
}
}
}
复制代码
点击跟进后发现方法里面是 spring 容器启动代码:
我们可以看到一个 onRefresh 方法,点进去需要看的是子类实现,我们只看其中一个子类实现:
@Override
protected void onRefresh() {
super.onRefresh();
try {
createWebServer();
}
catch (Throwable ex) {
throw new ApplicationContextException("Unable to start web server", ex);
}
}
private void createWebServer() {
WebServer webServer = this.webServer;
ServletContext servletContext = getServletContext();
if (webServer == null && servletContext == null) {
// 这个获取webServerFactory还是要进去看看
ServletWebServerFactory factory = getWebServerFactory();
this.webServer = factory.getWebServer(getSelfInitializer());
}
else if (servletContext != null) {
try {
getSelfInitializer().onStartup(servletContext);
}
catch (ServletException ex) {
throw new ApplicationContextException("Cannot initialize servlet context", ex);
}
}
initPropertySources();
}
复制代码
我们继续看下 getWebServletFactory() 这个方法,这个里面其实就是选择出哪种类型的 web 容器了:
protected ServletWebServerFactory getWebServerFactory() {
// Use bean names so that we don't consider the hierarchy
String[] beanNames = getBeanFactory().getBeanNamesForType(ServletWebServerFactory.class);
if (beanNames.length == 0) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to missing "
+ "ServletWebServerFactory bean.");
}
if (beanNames.length > 1) {
throw new ApplicationContextException("Unable to start ServletWebServerApplicationContext due to multiple "
+ "ServletWebServerFactory beans : " + StringUtils.arrayToCommaDelimitedString(beanNames));
}
return getBeanFactory().getBean(beanNames[0], ServletWebServerFactory.class);
}
复制代码
我们再去看 factory.getWebServer(getSelfInitializer()) ,转到定义就会看到很熟悉的名字 tomcat:
内置的 Servlet 容器就是在 onRefresh() 方法里面启动的,至此一个 Servlet 容器就启动 OK 了。
1、new 了一个 SpringApplication 对象,使用 SPI 技术加载加载 ApplicationContextInitializer、ApplicationListener 接口实例;
2、调用 SpringApplication.run() 方法;
3、调用 createApplicationContext() 方法创建上下文对象,创建上下文对象同时会注册 spring 的核心组件类(ConfigurationClassPostProcessor 、AutowiredAnnotationBeanPostProcessor 等);
4、调用 refreshContext() 方法启动 Spring 容器和内置的 Servlet 容器;
原文链接:https://juejin.cn/post/6921226309792169991
如果觉得本文对你有帮助,可以关注一下我公众号,回复关键字【面试】即可得到一份Java核心知识点整理与一份面试大礼包!另有更多技术干货文章以及相关资料共享,大家一起学习进步!