Spring IOC容器的启动

119 阅读4分钟

生命周期管理

容器启动

源码阅读

  1. 调用静态方法, 传入主配置源(配置类), 这里主配置源primartSource不一定是含有main方法的类, 只要是配置类并用@SpringBootApplication标记(或者是可以等价替换的注解)即可。
 public static ConfigurableApplicationContext run(Class<?> primarySource,
   String... args) {
  return run(new Class<?>[] { primarySource }, args);
 }
  1. 容器的初始化, 调用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机制奠定了基础。

  2. 调用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程序中可以通过添加关闭钩子,实现在程序退出时关闭资源、平滑退出的功能。

      1. 程序正常退出
      2. 使用System.exit()
      3. 终端使用Ctrl+C触发的中断
      4. 系统关闭
      5. 使用Kill pid命令干掉进程(kill -9 不会触发)

      houbb.github.io/2019/10/30/…

    • 对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方法。