SpringBoot 启动流程(细节拉满)

SpringBoot 启动流程(细节拉满)

网上搜了一下,这方面网上的资料不少,有些从@SpringBootApplication来入手讲。个人不能苟同吧,讲一讲我的理解。还有一些讲的比较笼统,我来写一篇比较细节一些的启动流程,从源码角度分析,满足大家的各个好奇心。


进入源码

随便建一个 Spring Boot 项目,会自动生成一个主启动类,如下所示:

@SpringBootApplication
public class XXXApplication {
    public static void main(String[] args) {
        SpringApplication.run(XXXApplication.class, args);
    }
}

我们启动整个项目就是启动这个主方法对吧。现在我们发现,主方法其实就是执行了一个动作,调用 SpringApplication 类的静态 run(…) 方法,把参数穿进去。那么我们要研究 SpringBoot 启动过程中的细节,我们只要研究这个方法就好。我们借助 IDEA,找到这个run(…) 方法。

public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
	return run(new Class<?>[] { primarySource }, args);
}

能看到只是把class对象封装成了数组,咱继续跟踪

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

现在看就比较明确了,上述源码可以看到两个步骤,new 了一个 SpringApplication 对象,直接调用他的 run() 成员方法。那我们接下来就按照这两个步骤去查看就能清楚地看到我们的 SpringBoot 的启动流程了不是。

想跟着动手看看代码的,自己从自己项目里进 class 文件,利用 idea 下载一下源代码(class 反编译的最好不要用有许多地方被编译器优化过了,或者很多参数、局部变量失去了自己的名字),也可以自己上 github 拉一份源码下来,这样能自己注释一下自己的想法和理解。我直接用前几天拉的一份最新版本代码来分析了。

给大家附上一个源码地址:https://github.com/spring-projects/spring-boot

SpringApplication构造方法

定位到我们上面分析到的代码位置,然后咱点进去看看代码:

public SpringApplication(Class<?>... primarySources) {
	this(null, primarySources);
}

调用另外的构造方法,第一个参数为null。继续跟踪:(我已经加上我自己的注释)

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
	// ResourceLoader
	this.resourceLoader = resourceLoader;
	Assert.notNull(primarySources, "PrimarySources must not be null");
	// 主入口类
	this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
	// web 类型 NONE | SERVLET | REACTIVE
	this.webApplicationType = WebApplicationType.deduceFromClasspath();
	// 设置 initializers(org.springframework.context.ApplicationContextInitializer)
	setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
	// 设置 listeners(org.springframework.context.ApplicationListener,与run方法中的不同)
	setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
	// main 函数所处的类
	this.mainApplicationClass = deduceMainApplicationClass();
}

现在我们找到了我们想看的东西。上面我已经加上注释,主要分为下面这几步,我们来一步一步看。

1. 保存参数传入信息

这也是大部分构造方法都有的特征,将传入的参数状态保存到对象中。我们看到前几行代码,将 resourceLoader,入口资源类保存到对应状态中,没什么太大的问题。

2. 分析并保存项目类型

我们都知道,Springboot 对于 web 项目会启动内置 Tomcat。但其实 SpringBoot 对于非 web 项目来说,也可以使用,而且不会启动内置的 Tomcat,设计者肯定考虑到了这一点,用不到白白启动那不是造孽吗,SpringBoot 这么大项目的设计者是一定已经考虑到了。我们点进去 WebApplicationType.deduceFromClasspath() 看一下他是怎么实现的:

NONE,
SERVLET,
REACTIVE;
private static final String[] SERVLET_INDICATOR_CLASSES = { "javax.servlet.Servlet",
		"org.springframework.web.context.ConfigurableWebApplicationContext" };

private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";

private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";

private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";

private static final String SERVLET_APPLICATION_CONTEXT_CLASS = "org.springframework.web.context.WebApplicationContext";

private static final String REACTIVE_APPLICATION_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.ReactiveWebApplicationContext";

static WebApplicationType deduceFromClasspath() {
	if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
			&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
		return WebApplicationType.REACTIVE;
	}
	for (String className : SERVLET_INDICATOR_CLASSES) {
		if (!ClassUtils.isPresent(className, null)) {
			return WebApplicationType.NONE;
		}
	}
	return WebApplicationType.SERVLET;
}

要看懂上面代码,其中最关键的是 ClassUtils.isPresent(…) 方法,先把它理解了就通了。我们点进去看看:

public static boolean isPresent(String className, @Nullable ClassLoader classLoader) {
	try {
		forName(className, classLoader);
		return true;
	}
	catch (IllegalAccessError err) {
		throw new IllegalStateException("Readability mismatch in inheritance hierarchy of class [" +
				className + "]: " + err.getMessage(), err);
	}
	catch (Throwable ex) {
		// Typically ClassNotFoundException or NoClassDefFoundError...
		return false;
	}
}

我们看到,其实就是反射去加载了一些类,成功就是true,否则就是false。

我们回到 deduceFromClasspath() 方法,我们发现,现在再看就能懂很多了。可以看自己的代码听我讲,利用 idea 更容易看懂,博客上面代码没有提示什么的。

我们可以发现,上面有三个枚举对象,而且也是我们的返回值类型,我们很容易知道他就是我们所说的类型:NONE、SERVLET、REACTIVE。接下来是好多种类全限定名(包括他的路径的类名)。然后就是判断,哪种类型能够被类加载器所加载。根据加载到的不同类型,返回不同的类型。大概就是这个情况,咱下面详细分析一下。

咱重点看一下这个思路。这个思路特别巧妙,他判断什么类型,就是通过预定的全限定名去加载类。你想,如果咱是 web 类型,项目中肯定至少会引用 javax.servlet.Servlet 类型对吧。那不是web项目,项目中大概率是不会引用的(通过多个具有项目类型特性的代表类,如 DispatcherServlet 等,基本上就可以确定)。既然不引用,当然我们类加载器按照这个全限定名去加载,肯定是加载不到的。那么根据这个思路,我们再结合代码,很容易就理解了,他是根据项目中引用的类,来判断我们的项目类型,最后返回结果。结果有什么用?在哪里用?我们后面说 run(…) 方法的时候再分析。

3. 设置 initializers

能看到他是给自己 setInitializers,那么参数哪里来呢?我们能看到,他是调用了 getSpringFactoriesInstances(Class type) 方法,方法时怎么加载的呢?

我们直接点进去看源代码:

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();
	// Use names and ensure unique to protect against duplicates
	// 从 META-INF/spring.factories 中加载监听器类型对象
	Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
	List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
	AnnotationAwareOrderComparator.sort(instances);
	return instances;
}

从我学习时候的注释中,大家应该能看得出来,这个就是我们 SpringBoot 的一个比较有代表性的功能,Spring 中的 SPI机制,从 META-INF/spring.factories 中加载提前申明的类。

其实 SpringBoot 项目中,这种用法很常见,Springboot 项目源代码本身就大量使用了这种特性:

在这里插入图片描述

要不我们看看他是怎么实现的,其实我觉得不难,META-INF/spring.factories 已经申明了全限定名,那么只需要把文件加载进来,然后用类加载器去加载,然后通过反射构建出对象,不就完成了吗?

这种做法有什么好处吗,最简单的好处就是,我们可以通过修改、增加 META-INF/spring.factories 来增加我们想要加载的类。比如说我们想增加一个初始化器,我们使用这种方法,直接修改 META-INF/spring.factories 文件就好,而不用在每个类中去修改。你看我们下载的MySQL、kafka,想要修改东西,去配置类里修改不就好了吗,而不需要你去修改源代码以后,再重新编译整个 MySQL,这样就很符合我们开闭原则的设计理念,这也就是我们有时候一直聊的 SPI 机制的一种体现,相比较于 JDK本身的 SPI 机制是不是更加强大呢?

4. 设置 listeners

上面的方法看懂的话,setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class)); 其实也是一个道理,就不过多描述了。

5. 保存main函数所处的类

点进去看源代码,就会发现很简单:

private Class<?> deduceMainApplicationClass() {
	try {
		StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
		for (StackTraceElement stackTraceElement : stackTrace) {
			if ("main".equals(stackTraceElement.getMethodName())) {
				return Class.forName(stackTraceElement.getClassName());
			}
		}
	} catch (ClassNotFoundException ex) {
		// Swallow and continue
	}
	return null;
}

其实就是找到 main 函数包含的类,加载出来并返回。

至此,我们的 SpringApplication 对象,也就初始化出来了。

run方法的执行

前面其实都是准备工作,直到 run 方法的执行,占SpringBoot 整个启动流程的大部分工作,到这里大家都提提神,需要我们好好研究。

其实在我看来,run 方法的执行有一条线索,它是整个 run 方法流程的一个体现。它就是监听器 SpringApplicationRunListener。看一下代码其实就能发现,这个东西其实获取出来就是一个局部变量,并不会保存到其他地方,run 方法结束,他也就是回收的对象了,那么我们可以称它的生命周期就是陪伴着 run 方法的执行。我们下面的分析,我们可以通过这条线索来记忆或者整理整个 run 方法的流程。

首先先给大家看我加了注释的源代码:(看不懂就先往下看,不着急,毕竟都不是神仙嘛)

public ConfigurableApplicationContext run(String... args) {
	StopWatch stopWatch = new StopWatch();
	// 计时器开始启动
	stopWatch.start();
	ConfigurableApplicationContext context = null;
	Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
	configureHeadlessProperty();
	// 初始化监听器并启动(要在参数环境对象初始化前)
	// 要是用,就要自己继承 SpringApplicationRunListener,注册到 META-INF\spring.factories 中
	SpringApplicationRunListeners listeners = getRunListeners(args);
	// listeners 六个方法:
	// 1. 刚执行 run 方法将自己初始化时
	// 2. 环境对象建立好时候
	// 3. 上下文建立好的时候
	// 4. 上下文载入配置时候
	// 5. 下文刷新完成后
	// 6. 上下文running
	listeners.starting();
	try {
		// 封装参数对象
		ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
		// 准备环境
		ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
		configureIgnoreBeanInfo(environment);
		// 打印 banner
		Banner printedBanner = printBanner(environment);
		// 根据参数创建不同 ApplicationContext
		context = createApplicationContext();
		// 获取异常处理器
		exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
				new Class[]{ConfigurableApplicationContext.class}, context);
		// Context 的前期准备
		prepareContext(context, environment, listeners, applicationArguments, printedBanner);
		// context 执行 refresh 方法
		refreshContext(context);
		// 留给子类实现
		afterRefresh(context, applicationArguments);
		// 停止计时
		stopWatch.stop();
		if (this.logStartupInfo) {
			new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
		}
		// 发布 started 事件
		listeners.started(context);
		// 执行所有 Runner 运行器
		// ApplicationRunner、CommandLineRunner。前者接受参数对象,后者接受原参数,容器启动末尾时的某些操作
		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;
}

我按照难易程度,分几个过程给大家分析一下。

1. 前期准备工作

首先是 run 方法的前期准备。从代码里面可以明确看到,首先 new 了一个计时器,然后调用计时器的 start 方法来启动计时器。然后申明了一个 context 的引用(只是申明,没有赋值)。接着初始化了异常处理 list。接着设置 java.awt.headless 属性(是否有显示器,表示运行在服务器器端,在没有显示器器和鼠标键盘的模式下工作,模拟输入输出设备功能,暂时跳过)。

接着最关键来了,getRunListeners(args) 来获取监听器。一定得点进去看一下。点进去就发现了,好熟悉的代码,马上就看懂了是不?

private SpringApplicationRunListeners getRunListeners(String[] args) {
	Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};
	return new SpringApplicationRunListeners(logger,
			getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

对哈,咱上面刚刚分析过的,getSpringFactoriesInstances 方法。忘了就说明你不相信我的水平,没好好读我的文章,上面刚说过的你就忘了。咱知道,这个是从 META-INF\spring.factories 取出全限定名来加载并实例化对象对吧。OK,都理解了,不过有一点要注意,有心的兄弟肯定发现了,咱上面已经加载过一次监听器了,这又加载一次,几个意思?OK,咱要注意哈,咱之前构造方法中的那个监听器跟这里的监听器没有关系哈,虽然他们都是监听器,但联系也就只有这一个了。两个都是接口,其中构造器中保存的是 ApplicationListener,监听 run 方法的是 SpringApplicationRunListeners 二者不仅自身没有关系,我研究过了,亲戚也没有关系哈。

接下来执行监听器的第一个方法,listeners.starting();

2. 环境的准备

接下来的代码也简单,将主方法传入的参数封装成参数对象。然后就是我们重点要看的第一个方法了:prepareEnvironment(…)。点进去看看源代码:

private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
												   ApplicationArguments applicationArguments) {
	// Create and configure the environment
	// 创建环境对象
	ConfigurableEnvironment environment = getOrCreateEnvironment();
	// 装配 Profiles 配置、将参数配置进环境对象
	configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 建立 ConfigurationPropertySources 与 environment 之间的绑定关系
	ConfigurationPropertySources.attach(environment);
	// 监听器 environmentPrepared
	listeners.environmentPrepared(environment);
	// 绑定到 SpringApplication
	bindToSpringApplication(environment);
	if (!this.isCustomEnvironment) {
		environment = new EnvironmentConverter(getClassLoader()).convertEnvironmentIfNecessary(environment,
				deduceEnvironmentClass());
	}
	ConfigurationPropertySources.attach(environment);
	return environment;
}

第一步 getOrCreateEnvironment() 方法,就是利用我们在构造方法中检测到的项目类型,来初始化不同的环境对象。感兴趣我们也可以点进去看一看,

private ConfigurableEnvironment getOrCreateEnvironment() {
	if (this.environment != null) {
		return this.environment;
	}
	switch (this.webApplicationType) {
		case SERVLET:
			return new StandardServletEnvironment();
		case REACTIVE:
			return new StandardReactiveWebEnvironment();
		default:
			return new StandardEnvironment();
	}
}

能看到上面两种环境其实都是下面两种环境的拓展。至于环境对象的探究,很有意思,这里包含咱项目不同配置的处理。咱这篇文章就先不讲了,因为我发现了一篇非常有意思的文章,无私的我可以给大家分享一下这篇文章,写的蛮好的,就是对环境的分析,他分析得也很有意思,我就不重复造轮子了。链接:https://baijiahao.baidu.com/s?id=1633656538828437931&wfr=spider&for=pc

看了文章以后,结合我的注释,也能很轻松看懂接下来的代码了对吧,把 Profiles 和我们之前封装好的参数对象配置进环境对象,然后 ConfigurationPropertySources 与 environment 绑定,并且绑定到 SpringApplication。其中有一个我们很在意的方法,大家都发现了 listeners.environmentPrepared(environment); 监听器生命周期的第二个步骤,留个意哈。

3. context的准备

接下来的代码就很好读了,打印 banner(SpringBoot 超有意思的设计),根据项目类型加载不同类型的 ApplicationContext(反射加载),利用 SPI 机制来获取异常处理器。你要是想现在就探究清楚三种 ApplicationContext 的不同,我觉得咱可以缓一缓,君子报仇十年不晚,这个比较复杂,咱之后可以再出篇文章来说这哥仨的不同。

然后咱就看到了咱类似于上面的一个方法:prepareContext。这个就是另一个重要的方法了,咱点进去好好研究。我把我注释过的给大家:

private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
							SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
	// 上下文设置环境,设置 resourceLoader
	context.setEnvironment(environment);
	postProcessApplicationContext(context);
	// 应用初始化器(META-INF\spring.factories中加载,new 的时候加入 SpringApplication)
	applyInitializers(context);
	// 监听器执行 contextPrepared
	listeners.contextPrepared(context);
	if (this.logStartupInfo) {
		logStartupInfo(context.getParent() == null);
		logStartupProfileInfo(context);
	}
	// Add boot specific singleton beans
	ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
	// 向 beanFactory 中注册 applicationArguments、printedBanner
	beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
	if (printedBanner != null) {
		beanFactory.registerSingleton("springBootBanner", printedBanner);
	}
	// 设置是否允许 bean 重写
	if (beanFactory instanceof DefaultListableBeanFactory) {
		((DefaultListableBeanFactory) beanFactory)
				.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
	}
	// 懒加载就在全局添加 BeanFactoryPostProcessor 使所有bean懒加载
	if (this.lazyInitialization) {
		context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
	}
	// Load the sources
	Set<Object> sources = getAllSources();
	Assert.notEmpty(sources, "Sources must not be empty");
	load(context, sources.toArray(new Object[0]));
	// 监听器 contextLoaded
	listeners.contextLoaded(context);
}

一起分析一下:向上下文中设置环境对象,然后执行 postProcessApplicationContext(context)。看到 postProcess 这个字眼,大家肯定都是比较熟悉的,BeanPostProcess、BeanFactoryPostProcess 等等,都是大家的老朋友了。(读过 Spring 源码的兄弟应该是亲人,没读过的也都见过,以后也会是亲人的)他的思想咱是很清楚的。还有点好奇的话,咱不妨点进去看看:

protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
	// 以单例模式注册 beanNameGenerator
	if (this.beanNameGenerator != null) {
		context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR,
				this.beanNameGenerator);
	}
	// 为上下文设置资源加载器和类加载器
	if (this.resourceLoader != null) {
		if (context instanceof GenericApplicationContext) {
			((GenericApplicationContext) context).setResourceLoader(this.resourceLoader);
		}
		if (context instanceof DefaultResourceLoader) {
			((DefaultResourceLoader) context).setClassLoader(this.resourceLoader.getClassLoader());
		}
	}
	// 为上下文设置转换服务
	if (this.addConversionService) {
		context.getBeanFactory().setConversionService(ApplicationConversionService.getSharedInstance());
	}
}

看到注释就很容易理解了,把 beanNameGenerator 的 BeanDefinition 注册进 beanFactory 中,给 context 设置资源加载器和类加载器,设置上下文转换器。都没什么好解释的,有兄弟不理解的话,咱别急,一步一步慢慢来,跟 Spring 多谈几天恋爱就都听过了,我在这儿光秃秃地讲的话,你直接听应该也听不太清楚。

然后就是给 context 应用初始化器,咱之前 SPI 加载出来的,都记得哈,然后就是我们的很想念的 listeners.contextPrepared(context);

然后就是处理了一下日志,取出 context 中的 BeanFactory 来,把参数对象,Banner 对象给注册进去。然后就是设置是否允许 Bean 名字重写覆盖,也就是我们常见的 allowBeanDefinitionOverriding 属性。有兄弟可能有疑问了,这 bean 名字不是不能重复吗?是的,在 loadBeanDefinition(…) 解析方法中也有判断,但是后面注册过程就不一样了,注册的时候就会查这个 allowBeanDefinitionOverriding 参数,如果允许的话,会把 bean 给覆盖掉。咱之后要是有机会讲源码的话咱再细谈。

然后就是是否设置全局 Bean 懒加载,实现也是比较简单吧,给 context 加一个 LazyInitializationBeanFactoryPostProcessor,等到 context 刷新上下文的时候,执行他的 postProcessBeanFactory(…) 方法,可以点进 LazyInitializationBeanFactoryPostProcessor 看一下,我就不放源码了,很好理解,就是从 beanDefinitionNames 属性中(这里面是按注册顺序填写的所有注册到 BeanFactory 的 beanName)获取所有注册 BeanDefinition,然后设置懒加载标志位。

然后接下来的步骤就是给 context 把 sources 加载进来,然后执行 listeners.contextLoaded(context); 方法。没错,就是我们期待的另一个监听器生命周期。

4. refreshContext

这部分看一下就懂了哈,直接刷新上下文,执行 Spring 中 ApplicationContext 的 refresh() 方法。有不懂的兄弟别着急哈,网上介绍这几种方法的很多博客,想看可以看一下,过段时间我也可以写一篇 ApplicationContext 的 refresh() 方法的博客,这篇就不过多介绍了。不过有意思的是,SpringBoot 并不是简单调用了一下 refresh(),他还加了一个钩子函数:

private void refreshContext(ConfigurableApplicationContext context) {
	if (this.registerShutdownHook) {
		try {
			// 钩子函数,JVM 关闭时将 上下文 销毁
			context.registerShutdownHook();
		} catch (AccessControlException ex) {
			// Not allowed in some environments.
		}
	}
	refresh(context);
}

context.registerShutdownHook() 这个方法,我们可以研究一下。我们点进去看 AbstractApplicationContext 的实现:

@Override
public void registerShutdownHook() {
	if (this.shutdownHook == null) {
		// No shutdown hook registered yet.
		this.shutdownHook = new Thread(SHUTDOWN_HOOK_THREAD_NAME) {
			@Override
			public void run() {
				synchronized (startupShutdownMonitor) {
					doClose();
				}
			}
		};
		Runtime.getRuntime().addShutdownHook(this.shutdownHook);
	}
}

先分析 Runtime.getRuntime().addShutdownHook(this.shutdownHook) ,能看到他是添加了一个钩子函数给 JVM,参数传了一个我们上面写出来的 Thread 对象。

我们先看一下钩子函数相关:

因为谷歌翻译插件目前大陆用不了,我在 JDK11 的 API 上给大家把方法解释拿过来。

public static Runtime getRuntime()
返回与当前Java应用程序关联的运行时对象。 类Runtime大多数方法都是实例方法,必须针对当前运行时对象调用。
结果
与当前Java应用程序关联的 Runtime对象。
    
public void addShutdownHook​(Thread hook)
注册新的虚拟机关闭挂钩。
Java虚拟机关闭以响应两种事件:

在程序正常退出 ,当最后一个非守护线程退出时,或者当exit (等同于System.exit )方法被调用,或
虚拟机将响应用户中断(例如键入^C )或系统范围的事件(例如用户注销或系统关闭)而终止 。
关闭钩子只是一个初始化但未启动的线程。 当虚拟机开始其关闭序列时,它将以某种未指定的顺序启动所有已注册的关闭挂钩,并让它们同时运行。 当所有挂钩完成后,它将停止。 请注意,守护程序线程将在关闭序列期间继续运行,如果通过调用exit方法启动关闭,则非守护程序线程也将继续运行。

关闭序列开始后,只能通过调用强制终止虚拟机的halt方法来停止它。

关闭序列开始后,无法注册新的关闭挂钩或取消注册先前注册的挂钩。 尝试这些操作中的任何一个都会导致抛出IllegalStateException 。

关闭挂钩在虚拟机的生命周期中的微妙时间运行,因此应该以防御方式编码。 特别是它们应该被编写为线程安全的并且尽可能避免死锁。 他们也不应盲目依赖可能已经注册了自己的关机钩子的服务,因此他们自己可能正在关闭。 例如,尝试使用其他基于线程的服务(例如AWT事件派发线程)可能会导致死锁。

关机挂钩也应该快速完成工作。 当程序调用exit时 ,期望虚拟机将立即关闭并退出。 当虚拟机因用户注销或系统关闭而终止时,底层操作系统可能只允许一段固定的时间来关闭和退出。 因此,不建议尝试任何用户交互或在关闭钩子中执行长时间运行的计算。

通过调用线程ThreadGroup对象的uncaughtException方法,就像在任何其他线程中一样,在关闭钩子中处理未捕获的异常。 此方法的默认实现将异常的堆栈跟踪打印到System.err并终止该线程; 它不会导致虚拟机退出或停止。

在极少数情况下,虚拟机可能会中止 ,即停止运行而不会干净地关闭。 当外部终止虚拟机时会发生这种情况,例如Unix上的SIGKILL信号或Microsoft Windows上的TerminateProcess调用。 如果本机方法因例如破坏内部数据结构或尝试访问不存在的内存而出错,则虚拟机也可能中止。 如果虚拟机中止,则无法保证是否将运行任何关闭挂钩。

参数
hook - 初始化但未启动的Thread对象
异常
IllegalArgumentException - 如果已注册指定的挂钩,或者可以确定挂钩已在运行或已经运行
IllegalStateException - 如果虚拟机已在关闭过程中
SecurityException - 如果存在安全管理器且它拒绝RuntimePermission (“shutdownHooks”)
从以下版本开始:
1.3
另请参见:
removeShutdownHook(java.lang.Thread)halt(int)exit(int)

OK ,大概了解了哈,再看上面代码,其实就是先通过 getRuntime() 获取到 Runtime 对象,然后注册 addShutdownHook(Thread hook) 钩子函数,在虚拟机关闭时,执行我们传入的 hook 对象。

思路清晰了,我们肯定迫不及待想看一下到底给虚拟机传了什么样的一个 thread 对象,通过上面代码,问题自然而然就来到了 doClose() 方法:

protected void doClose() {
	// Check whether an actual close attempt is necessary...
	if (this.active.get() && this.closed.compareAndSet(false, true)) {
		if (logger.isDebugEnabled()) {
			logger.debug("Closing " + this);
		}

			LiveBeansView.unregisterApplicationContext(this);

			try {
				// Publish shutdown event.
				publishEvent(new ContextClosedEvent(this));
			}
			catch (Throwable ex) {
				logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
			}

		// Stop all Lifecycle beans, to avoid delays during individual destruction.
		if (this.lifecycleProcessor != null) {
			try {
				this.lifecycleProcessor.onClose();
			}
			catch (Throwable ex) {
				logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
			}
		}

		// Destroy all cached singletons in the context's BeanFactory.
		destroyBeans();

		// Close the state of this context itself.
		closeBeanFactory();
            
		// Let subclasses do some final clean-up if they wish...
		onClose();
            
		// Reset local application listeners to pre-refresh state.
		if (this.earlyApplicationListeners != null) {
			this.applicationListeners.clear();
			this.applicationListeners.addAll(this.earlyApplicationListeners);
		}
            
		// Switch to inactive.
		this.active.set(false);
	}
}

看到这里,不管是英语好的还是 java 好的,都差不多能看懂了,(显然我不属于前者,后者的话还能争辩一番,哈哈哈)

给监听器发布 ContextClosedEvent 事件,有 lifecycleProcessor 的话(也是 Spring 做的拓展口)执行他的 onClose() 方法,执行 destroyBeans() 方法(销毁 BeanFactory 中所有 bean,并且执行 registerDisposableBeanIfNecessary() 时候注册的 bean 销毁方法,销毁方法咱项目中也很常用),关闭 BeanFactory,留给子类拓展执行的 onClose() 方法(例如:servlet 类型项目最后关闭 web 服务),最后设置活跃状态位为 false。到此钩子函数和注册的销毁线程我们了解了,那为什么要这么做呢?答案是为了优雅,生活要有仪式感,写代码也一样,都要有仪式感,要有我们程序员自己的仪式感。打个比方,关电脑有几种方法?没有电池的直接拔电源、直接按住物理关机键强制关机、点击关机按钮关机、先把所有程序关掉,再点关机,亦或者 linux 的 shutdown -h now、poweroff等等。都能实现我们的功能,但是我们看看最优雅的程序员怎么关机: who 命令查看用户在线数,然后 netsat -a 查看连接数,ps -aux 命令确定后台进程 shutdown -h now 等等命令来关机。哈哈,虽然做不到他们这么优雅,但咱还是能尽力做的更优雅一些不是?Spring 中也是这样,在 JVM 关闭前,注销许多我们创建好的东西,甚至可以由此来增加拓展功能,来实现虚拟机关闭前的一些特定方法,比如我们 WebSocket 的关闭,这次项目不关,下次启动就有麻烦了~~~

接下来的一句代码就超级简单了:afterRefresh(context, applicationArguments),留给子类来进行 refresh 方法以后的操作,可以进行补充等等功能。这也是我们老生常谈的一个功能,只要不是 final 类,就要考虑给子类留拓展方法。

然后就是计时器停止计时、记录日志、发布 listeners.started(context); 事件。对的,是咱关心的监听器生命周期。到了 context 的 started 阶段了。

5. 收尾阶段

剩下来的就很简单了,执行 callRunners(context, applicationArguments) 方法。这个方法时干什么的?好问题,点进去看看:

private void callRunners(ApplicationContext context, ApplicationArguments args) {
	List<Object> runners = new ArrayList<>();
	runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
	runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());
	AnnotationAwareOrderComparator.sort(runners);
	for (Object runner : new LinkedHashSet<>(runners)) {
		if (runner instanceof ApplicationRunner) {
			callRunner((ApplicationRunner) runner, args);
		}
		if (runner instanceof CommandLineRunner) {
			callRunner((CommandLineRunner) runner, args);
		}
	}
}

能看到三行四行,从 context 中获取出 ApplicationRunner、CommandLineRunner 类型的 bean,然后通过比较器排序,再按顺序,先执行 ApplicationRunner 的 callRunner 方法,再执行 CommandLineRunner 的 callRunner 方法。这个也是我们常用的 SpringBoot 的拓展功能,在容器启动末尾执行的一些方法,直接可以直接使用主方法的参数。使用方法就是直接实现上面两个接口,重写 run 方法,然后注入到 BeanFactory 中就好,在容器启动末尾,也就是咱现在这个阶段就会自动执行。二者的区别是啥呢,我们需要点进去看看 callRunner 方法:

private void callRunner(ApplicationRunner runner, ApplicationArguments args) {
	try {
		(runner).run(args);
	} catch (Exception ex) {
		throw new IllegalStateException("Failed to execute ApplicationRunner", ex);
	}
}

private void callRunner(CommandLineRunner runner, ApplicationArguments args) {
	try {
		(runner).run(args.getSourceArgs());
	} catch (Exception ex) {
		throw new IllegalStateException("Failed to execute CommandLineRunner", ex);
	}
}

一对比就能看出来了,一边是使用外面封装好的参数对象,一边是使用源参数,也就是主函数传入的字符串。这个什么区别不说大家也都懂了哈,就这一丢丢区别,大部分使用的时候还是两种选择都可以的,只是执行顺序的不同而已。

然后就是我们最后等到的 listeners.running(context) 了。没错,最后一个生命周期了,执行完这个以后,SpringApplicationRunListeners 也就走向了自己准备回收的阶段。

下面就是对捕获到的异常进行处理(我们之前 SPI 加载进来的异常处理器,都记得哈),然后返回 context。

至此我们 SpringBoot 的启动流程也就结束了,一层一层返回给我们刚刚启动的主方法。

有脑筋转的快的反骨仔肯定想到了,那执行完了,意思是我们 SpringBoot 就关闭了?主方法返回不就是结束了吗?但是对于实际应用来说,这才刚刚开始呀,我们的实际经验上看,也并没有就这么结束呀?OK,一系列的问题,我也有过这样的疑惑,但把往后研究了一段时间就在源码里找到原因了,我们下面来给大家分析。

到底有没有运行结束

按理来说,到了这儿,主方法肯定是运行结束了,这个没问题,我打断点一步一步看,确实走到最后 return 返回回去就再也走不动了,但是也没有看到项目结束呀,而且这个时候请求 controller 还是能处理的。事情越来越奇怪了。

直到我看 SpringBoot 源码的时候,看到启动内置 Tomcat 的地方,才发现 SpringBoot 项目 main 方法结束了,但项目没运行结束的原因。下面咱们详细说说。

SpringBoot 内置 Tomcat 使用的也是 Spring 在 context 在 refresh 过程中留下来的一个拓展接口,onRefresh() 方法。熟悉源码的肯定知道,这个是 Spring 留给子类来拓展,用来初始化特殊 bean 的一个拓展口,咱看看 AbstractApplicationContext 中的源代码:

protected void onRefresh() throws BeansException {
	// For subclasses: do nothing by default.
}

很简单哈,然后咱借助 idea 看他的重写类,咱先看这个 GenericWebApplicationContext 类,他是 AnnotationConfigServletWebApplicationContext 的父类。

@Override
protected void onRefresh() {
	this.themeSource = UiApplicationContextUtils.initThemeSource(this);
}

简单看一下,有兴趣的看一下, 他是处理 web 项目的 UI 主题的步骤,也就是大部分人学习 SpringBoot 的时候学过的 thymeleaf 这种,现在不怎么使用,而且没那么有意思吧,咱就不看了,简单贴一下代码:

public static ThemeSource initThemeSource(ApplicationContext context) {
	if (context.containsLocalBean(THEME_SOURCE_BEAN_NAME)) {
		ThemeSource themeSource = context.getBean(THEME_SOURCE_BEAN_NAME, ThemeSource.class);
		// Make ThemeSource aware of parent ThemeSource.
		if (context.getParent() instanceof ThemeSource && themeSource instanceof HierarchicalThemeSource) {
			HierarchicalThemeSource hts = (HierarchicalThemeSource) themeSource;
			if (hts.getParentThemeSource() == null) {
				// Only set parent context as parent ThemeSource if no parent ThemeSource
				// registered already.
				hts.setParentThemeSource((ThemeSource) context.getParent());
			}
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Using ThemeSource [" + themeSource + "]");
		}
		return themeSource;
	}
	else {
		// Use default ThemeSource to be able to accept getTheme calls, either
		// delegating to parent context's default or to local ResourceBundleThemeSource.
		HierarchicalThemeSource themeSource = null;
		if (context.getParent() instanceof ThemeSource) {
			themeSource = new DelegatingThemeSource();
			themeSource.setParentThemeSource((ThemeSource) context.getParent());
		}
		else {
			themeSource = new ResourceBundleThemeSource();
		}
		if (logger.isDebugEnabled()) {
			logger.debug("Unable to locate ThemeSource with name '" + THEME_SOURCE_BEAN_NAME +
					"': using default [" + themeSource + "]");
		}
		return themeSource;
	}
}

其实咱刚刚也提到过, GenericWebApplicationContext 类是 AnnotationConfigServletWebApplicationContext 的父类,咱之前也分析过,在 run 方法中,返回不同的 context 的时候,如果检测到是 web 类型,返回的 context 其实就是 AnnotationConfigServletWebApplicationContext,因此核心逻辑肯定在这个里面。那咱直接看 AnnotationConfigServletWebApplicationContext 重写的 onrefresh() 方法:

@Override
protected void onRefresh() {
	super.onRefresh();
	try {
		createWebServer();
	}
	catch (Throwable ex) {
		throw new ApplicationContextException("Unable to start web server", ex);
	}
}

挺简洁的,先调用父类的 onRefresh() 方法,然后调用自己的 createWebServer() 方法。自然,咱跟踪到 createWebServer() 方法:

private void createWebServer() {
	WebServer webServer = this.webServer;
	ServletContext servletContext = getServletContext();
	if (webServer == null && servletContext == null) {
		ServletWebServerFactory factory = getWebServerFactory();
        // 启动获取 web 服务
		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();
}

咱这里先不过多看,咱只看一下核心实现:factory.getWebServer(getSelfInitializer()),贴上源码(以后有机会再展开讲,其实也不是很有意思):

@Override
public WebServer getWebServer(ServletContextInitializer... initializers) {
	if (this.disableMBeanRegistry) {
		Registry.disableRegistry();
	}
	Tomcat tomcat = new Tomcat();
	File baseDir = (this.baseDirectory != null) ? this.baseDirectory : createTempDir("tomcat");
	tomcat.setBaseDir(baseDir.getAbsolutePath());
	Connector connector = new Connector(this.protocol);
	connector.setThrowOnFailure(true);
	tomcat.getService().addConnector(connector);
	customizeConnector(connector);
	tomcat.setConnector(connector);
	tomcat.getHost().setAutoDeploy(false);
	configureEngine(tomcat.getEngine());
	for (Connector additionalConnector : this.additionalTomcatConnectors) {
		tomcat.getService().addConnector(additionalConnector);
	}
	prepareContext(tomcat.getHost(), initializers);
	return getTomcatWebServer(tomcat);
}

看到这儿就很明显能感觉到这就是 Tomcat 的启动过程了哈,这些代码主要就是给 Tomcat 设置启动前的属性对吧,然后最后一步,getTomcatWebServer(tomcat) 来启动并获取 Tomcat 服务,咱看看源码:

protected TomcatWebServer getTomcatWebServer(Tomcat tomcat) {
	return new TomcatWebServer(tomcat, getPort() >= 0);
}

不用过多解释,new 了一个 TomcatWebServer,那咱看看他的构造方法:

public TomcatWebServer(Tomcat tomcat, boolean autoStart) {
   Assert.notNull(tomcat, "Tomcat Server must not be null");
   this.tomcat = tomcat;
   this.autoStart = autoStart;
   initialize();
}

不难哈,设置标志位,然后进行初始化,那关键逻辑就来到了 initialize() 方法:

private void initialize() throws WebServerException {
	logger.info("Tomcat initialized with port(s): " + getPortsDescription(false));
	synchronized (this.monitor) {
		try {
			addInstanceIdToEngineName();

			Context context = findContext();
			context.addLifecycleListener((event) -> {
				if (context.equals(event.getSource()) && Lifecycle.START_EVENT.equals(event.getType())) {
					// Remove service connectors so that protocol binding doesn't
					// happen when the service is started.
					removeServiceConnectors();
				}
			});

			// Start the server to trigger initialization listeners
			this.tomcat.start();

			// We can re-throw failure exception directly in the main thread
			rethrowDeferredStartupExceptions();

				try {
					ContextBindings.bindClassLoader(context, context.getNamingToken(), getClass().getClassLoader());
				}
			catch (NamingException ex) {
				// Naming is not enabled. Continue
			}

				// Unlike Jetty, all Tomcat threads are daemon threads. We create a
				// blocking non-daemon to stop immediate shutdown
				startDaemonAwaitThread();
			}
			catch (Exception ex) {
			stopSilently();
			destroySilently();
			throw new WebServerException("Unable to start embedded Tomcat", ex);
		}
	}
}

大多数代码咱都先不研究,有机会咱出个博客好好分析分析 Tomcat,咱直接看 startDaemonAwaitThread() 方法,这个才是我们 main 方法执行结束,项目却还在运行的原因

private void startDaemonAwaitThread() {
	Thread awaitThread = new Thread("container-" + (containerCounter.get())) {

		@Override
		public void run() {
			TomcatWebServer.this.tomcat.getServer().await();
		}

	};
	awaitThread.setContextClassLoader(getClass().getClassLoader());
	awaitThread.setDaemon(false);
	awaitThread.start();
}

其实看到这个方法名,可能脑子转的快的人就想到是为什么了,startDaemonAwaitThread,能联想到的东西太多了。我们知道,怎么样能够让 JVM 进程结束呢,显示调用 System.exit() 或 Runtime.exit() 肯定可以,然后呢,其实就是非守护线程全部执行结束。我们平常刷算法题,主方法直接执行到最后一行就能结束是为啥,其实就是因为只有主线程一个非守护线程在跑,到最后一行不就跑完了吗。但是 SpringBoot 在这个地方加了不是主方法线程的非守护线程,你看,直接给 Thread 写了一个匿名内部类,run 方法直接执行 TomcatWebServer.this.tomcat.getServer().await()。然后就是设置线程上下文类加载器,设置为非守护线程,启动线程。这不就是除主方法以外的非守护线程了吗。咱只要这个方法不结束,那么咱的项目的 JVM 就不会运行结束。这行代码的意思也很容易理解,就是取出 Tomcat 服务,然后执行 await() 方法。OK,那咱先在点进去发现是个接口,咱们直接看实现类:

@Override
public void await() {
    // Negative values - don't wait on port - tomcat is embedded or we just don't like ports
    if (getPortWithOffset() == -2) {
        // undocumented yet - for embedding apps that are around, alive.
        return;
    }
    if (getPortWithOffset() == -1) {
        try {
            awaitThread = Thread.currentThread();
            while(!stopAwait) {
                try {
                    Thread.sleep( 10000 );
                } catch( InterruptedException ex ) {
                    // continue and check the flag
                }
            }
        } finally {
            awaitThread = null;
        }
        return;
    }

    // Set up a server socket to wait on
    try {
        awaitSocket = new ServerSocket(getPortWithOffset(), 1,
                InetAddress.getByName(address));
    } catch (IOException e) {
        log.error(sm.getString("standardServer.awaitSocket.fail", address,
                String.valueOf(getPortWithOffset()), String.valueOf(getPort()),
                String.valueOf(getPortOffset())), e);
        return;
    }

    try {
        awaitThread = Thread.currentThread();

        // Loop waiting for a connection and a valid command
        while (!stopAwait) {
            ServerSocket serverSocket = awaitSocket;
            if (serverSocket == null) {
                break;
            }

            // Wait for the next connection
            Socket socket = null;
            StringBuilder command = new StringBuilder();
            try {
                InputStream stream;
                long acceptStartTime = System.currentTimeMillis();
                try {
                    socket = serverSocket.accept();
                    socket.setSoTimeout(10 * 1000);  // Ten seconds
                    stream = socket.getInputStream();
                } catch (SocketTimeoutException ste) {
                    // This should never happen but bug 56684 suggests that
                    // it does.
                    log.warn(sm.getString("standardServer.accept.timeout",
                            Long.valueOf(System.currentTimeMillis() - acceptStartTime)), ste);
                    continue;
                } catch (AccessControlException ace) {
                    log.warn(sm.getString("standardServer.accept.security"), ace);
                    continue;
                } catch (IOException e) {
                    if (stopAwait) {
                        // Wait was aborted with socket.close()
                        break;
                    }
                    log.error(sm.getString("standardServer.accept.error"), e);
                    break;
                }

                // Read a set of characters from the socket
                int expected = 1024; // Cut off to avoid DoS attack
                while (expected < shutdown.length()) {
                    if (random == null)
                        random = new Random();
                    expected += (random.nextInt() % 1024);
                }
                while (expected > 0) {
                    int ch = -1;
                    try {
                        ch = stream.read();
                    } catch (IOException e) {
                        log.warn(sm.getString("standardServer.accept.readError"), e);
                        ch = -1;
                    }
                    // Control character or EOF (-1) terminates loop
                    if (ch < 32 || ch == 127) {
                        break;
                    }
                    command.append((char) ch);
                    expected--;
                }
            } finally {
                // Close the socket now that we are done with it
                try {
                    if (socket != null) {
                        socket.close();
                    }
                } catch (IOException e) {
                    // Ignore
                }
            }

            // Match against our command string
            boolean match = command.toString().equals(shutdown);
            if (match) {
                log.info(sm.getString("standardServer.shutdownViaPort"));
                break;
            } else
                log.warn(sm.getString("standardServer.invalidShutdownCommand", command.toString()));
        }
    } finally {
        ServerSocket serverSocket = awaitSocket;
        awaitThread = null;
        awaitSocket = null;

        // Close the server socket and return
        if (serverSocket != null) {
            try {
                serverSocket.close();
            } catch (IOException e) {
                // Ignore
            }
        }
    }
}

代码方面就不过多解释了,上面说过,找机会咱再出一篇关于 Tomcat 的文章,然后咱再详细分析每一行代码,但是我们能清楚地看到,他有 while 循环,会一直检查 stopAwait 标志位,只要是 false,就会一直自旋,直到标志位变成 true。什么时候会变成 true 呢,结束的时候肯定就是 true 了。后期咱写 Tomcat 的文章的时候,再好好介绍。标志位为 true 以后,退出循环,这个方法执行就会结束,方法执行结束了,线程也就该被回收了。一旦这个线程回收,那么 SpringBoot 项目也就直接没有非守护线程还在运行了,项目也就停止运行了。

到此为止呢,SpringBoot 的整个过程,大概就是这些了。


以上呢,基本都是我个人学习过程中思考总结出来的,难免有片面的地方,各位看官要是有看不懂的,或者有不同想法意见的,咱讨论交流!!!

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值