生命周期管理
容器启动
源码阅读
- 调用静态方法, 传入主配置源(配置类), 这里主配置源primartSource不一定是含有main方法的类, 只要是配置类并用@SpringBootApplication标记(或者是可以等价替换的注解)即可。
public static ConfigurableApplicationContext run(Class<?> primarySource,
String... args) {
return run(new Class<?>[] { primarySource }, args);
}
-
容器的初始化, 调用SpringApplication的构造方法
-
推断应用类型,支持三种应用类型,web flux的web应用,mvc的web应用,非web应用。
-
利用SPI机制从META-INF/spring.factories加载ApplicationContextInitializer。
-
利用SPI机制从META-INF/spring.factories加载ApplicationListener。
推断应用类型是如何做的
主要是通过判断对应的依赖的Class是否存在。通过加载对应的依赖Class到jvm中,简单来说就是调用class.forName, 如果不存在这个依赖的话,就会抛出异常。
为啥要利用SPI的思想?为什么不采用spring bean的定义方式。
主要是考虑到jar包的扫描。如果ApplicationContextInitializer是从jar包中加载的,SpringBoot是不会主动去进行注入的,除非我们通过@Import注解手动导入jar包的配置类。同时使用SPI这种思想也为后面使用starter机制奠定了基础。
-
-
调用SpringApplication#run方法,进行容器的启动
-
利用SPI加载从META-INF/spring.factories加载SpringApplicationRunListeners类型的监听器并启动,个人认为这个地方可以优化下,构造函数调用后所有的监听器都已经到SpringApplication里来了,所以不用再增加一次调用了。
-
解析启动参数args, 这一步主要是为了启动参数可以覆盖内部的一些配置
-
Environment准备,主要是上面提到的三种环境类型,web flux的web应用,mvc的web应用,非web应用。
-
创建ApplicationContext, 不同的应用类型,会生成不同的环境。
protected ConfigurableApplicationContext createApplicationContext() { Class<?> contextClass = this.applicationContextClass; if (contextClass == null) { try { switch (this.webApplicationType) { case SERVLET: // DEFAULT_WEB_CONTEXT_CLASS 这是一个字符串,是容器类的全限定类名。 contextClass = Class.forName(DEFAULT_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); }问:如注释所言,为什么DEFAULT_WEB_CONTEXT_CLASS容器类的全限定类名, 直接new 一个类可不可以?
答: 我们不清楚容器的类型,如果通过new的方式就要在编译时就引入三种对应的依赖,这是不合理的。采用反射在编译时可以不引入依赖,引入哪些依赖完全由用户决定,运行时才判断相应的依赖是否存在。这也是我自己觉得反射的优点之一。
-
对ApplicationContext上下文准备阶段
-
ApplicationContext设置Environment,不同的应用设置不同的环境。
-
ApplicationContext后置处理。这种方式在Spring中很常见,定义一个接口或者注解,然后定义对应的后置处理器。后置处理时对后置处理器的类型做一个判断,进行不同的处理。由于后置处理器的处理方法可能不一样,所以无法对后置处理器定义接口进行统一,只能通过类型判断。
-
使用ApplicationContextInitializer进行初始化,这个地方留了一个扩展,允许开发者实现容器创建后一些动作。
-
SpringApplicationRunListeners 进行contextPrepare操作。
SpringApplicationRunListener 是SpringBoot利用SPI加载的IOC容器的监听器(当然也可以自定义),内置了很多个不同的方法用户监听不同阶段,如启动,运行失败,关闭容器等。这个思路很值得借鉴,当你的关注点要和代码主逻辑抽离出来,就可以使用时间监听机制,监听任何时候。
-
注册SpringApplicationArguments,SpringBootBanner到容器中
-
资源加载,如加载xml中bean的定义,扫描带有注解的Bean,得到BeanDefinition
-
-
refresh容器,调用refreshContext(context),完成容器的启动。
- 调用ApplicationContext的refresh方法,这个牵涉到BeanFactory和Bean的初始化,后面单独说明。
- 注册shutdown hook
@Override public void registerShutdownHook() { if (this.shutdownHook == null) { // No shutdown hook registered yet. this.shutdownHook = new Thread() { @Override public void run() { synchronized (startupShutdownMonitor) { doClose(); } } }; Runtime.getRuntime().addShutdownHook(this.shutdownHook); } }shutdownHook是一个线程,注册进JVM里面。在Java程序中可以通过添加关闭钩子,实现在程序退出时关闭资源、平滑退出的功能。
- 程序正常退出
- 使用System.exit()
- 终端使用Ctrl+C触发的中断
- 系统关闭
- 使用Kill pid命令干掉进程(kill -9 不会触发)
-
对ApplicatinContext进行后置处理,暂时实现为空,留给开发者扩展。
-
其他的一些就是调用监听器SpringBootRunner对应的方法进行监听。
-
处理启动参数,对应方法的调用。
-
对外扩展点
-
Bean名称的统一管理, 消除硬编码
自定义BeanNameGenerator或者实现Aware接口
-
自定义容器监听器
-
自定义Initializar
一些实现方式
其他
技巧
- 关于不通过反射获取某一个方法所在类的方法, 通过调用栈。
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;
}
- 依赖的判断, 通过jvm加载看是否报错,jvm加载时详细参考Spring的ClassUtils的forName方法。