SpringBoot启动

356 阅读29分钟

SpringBoot 的启动

SpringBoot 的启动过程是比较复杂的,涉及了非常多的组件,这些组件有些是用于定制启动过程,有些是定制 Spring 容器的配置,当这些东西都混杂到一起时,理解 SpringBoot 的启动就是一项非常考验脑力的活动了,我曾经多次研究多 SpringBoot 的启动过程,不过没有记录笔记,有些过程在看的时候是理解的,不过随着时间的流逝,再拾起时又有一些模糊的地方。下面我将和大家一起探究 SpringBoot 的启动过程,并整理成这系列笔记,方便自己和大家理解。

为什么要探究 SpringBoot 的启动过程

是啊,为什么要探究 SpringBoot 的启动过程呢?把它当成一个魔法黑盒,不也可以使用的很好吗?确实如此,即使不去探究 SpringBoot 的启动,也可以将 SpringBoot 使用的非常熟练,SpringBoot 官方提供了非常详尽的操作手册以指导用户正确的使用。不过我个人不太喜欢这种魔法式的效果,我希望打开 SpringBoot 的盒子,看下它是如何工作的,想了解它的工作原理,更进一步的想理解它是为何如此设计。

SpringBoot 的示例代码

示例项目的代码非常简单

  • pom 主要配置

     <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.0.2</version>
     </parent>
     <dependencies>
        <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    
  • 启动类

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

SpringBoot 构造函数

代码 SpringApplication.run(DemoApplication.class, args)的最终调用方法是下面

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

这个方法中,它首先利用传入的DemoApplication.class作为 primarySources 参数创建了一个 SpringApplication 对象,然后调用了 run 方法,参数是 main 方法的 args。所以我们首先看下 SpringBoot 的构造函数,它在构造函数中初始化了哪些内容。

SpringBoot 提供了两个构造函数,第一个构造函数 SpringApplication(Class... primarySources),只接受一组Class作为参数,并在内部调用了第二个构造函数 SpringApplication(ResourceLoader resourceLoader, Class... primarySources).

第二个构造函数有两个参数 ResourceLoader ,和 Class 数组,我把每行的作用注释在代码中,方便大家阅读。

public class SpringApplication {

  //资源加载器
  private ResourceLoader resourceLoader;

  //主配置类数组
  private final Set<Class<?>> primarySources;

  //当前应用类型(枚举 NONE、SERVLET和REACTIVE)
  private WebApplicationType webApplicationType;

  //BootstrapRegistry初始化回调接口数组(SpringBoot独有的)
  private final List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;

  //ApplicationContext初始化回调接口数组(Spring有的)
  private List<ApplicationContextInitializer<?>> initializers;

  //应用事件监听器
  private List<ApplicationListener<?>> listeners;

  //应用主类
  private Class<?> mainApplicationClass;

  //构造函数一
  //参数 primarySources 主配置类数组
	public SpringApplication(Class<?>... primarySources) {
    //内部调用构造函数二,primarySources是直接透传,而resourceLoader的参数为null
		this(null, primarySources);
	}

  //构造函数二
  //参数 resourceLoader 资源加载器
  //参数 primarySources 主配置类数组
  public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // 1. 设置资源加载器(为null也不要紧,后面有默认值)
    this.resourceLoader = resourceLoader;
    // 2. 校验并设置primarySources ,不能为null
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 3. 推测当前应用的类型
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 4. 从spring.factories中加载BootstrapRegistryInitializer
    this.bootstrapRegistryInitializers = new ArrayList<>(
        getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    //5. 从spring.factories中加载ApplicationContextInitializer
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    //6. 从spring.factories中加载ApplicationListener
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    //7. 从调用堆栈中找出应用主类
    this.mainApplicationClass = deduceMainApplicationClass();
  }
}

关于构造函数有几点需要额外介绍的

  • 应用类型推断
  • 从 spring.factories 加载配置
  • 应用主类推断

应用类型推断

应用类型

SpringBoot 使用类 WebApplicationType 定义了三种应用类型

  • NONE
  • SERVLET
  • REACTIVE

分别表示了 普通应用、Servlet Web 应用、和响应式 Web 应用三种类型。这些应用类型分别适用于不同的业务场景。不需要提供 web 访问的应用是普通应用,传统的 JavaServlet 标准的 Servlet Web 应用,和 Spring 基于 reactor 研发的响应式 web 应用。

这三种类型在技术上实际是决定了 SpringBoot 将要使用什么样类型的 ApplicationContext。具体的代码我们在 run 方法中仔细探究,这里先介绍一下

类型常规Native
NONEAnnotationConfigApplicationContextGenericApplicationContext
SERVLETAnnotationConfigServletWebServerApplicationContextServletWebServerApplicationContext
REACTIVEAnnotationConfigReactiveWebServerApplicationContextReactiveWebServerApplicationContext

类型推断方法

类型推断方法是委托给类 WebApplicationType,大致是下面三步

  1. 只有 DispatcherHandler 时,为 Reactive 类型
  2. 没有 Servlet 或 ConfigurableWebApplicationContext 时,为 None 类型
  3. 其余都是 Servlet 类型

这里需要注意的一点是,即使 ClassPath 中同时有 Servlet 和 Reactive 的类,还是会推断成 Servlet 类型。


public enum WebApplicationType {

  private static final String WEBFLUX_INDICATOR_CLASS = "org.springframework.web.reactive.DispatcherHandler";
  private static final String WEBMVC_INDICATOR_CLASS = "org.springframework.web.servlet.DispatcherServlet";
  private static final String JERSEY_INDICATOR_CLASS = "org.glassfish.jersey.servlet.ServletContainer";
  private static final String[] SERVLET_INDICATOR_CLASSES = { "jakarta.servlet.Servlet","org.springframework.web.context.ConfigurableWebApplicationContext" }

  //从ClassPath中推断应用类型
	static WebApplicationType deduceFromClasspath() {

    //1 只有DispatcherHandler 时,为Reactive类型
    // 这里可以看出 如果Servlet和Reactive的类都在时,实际类型推断出来是Servlet类型。
		if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null)
        && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
				&& !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
			return WebApplicationType.REACTIVE;
		}

    //2 没有 Servlet 或 ConfigurableWebApplicationContext时,为None类型
		for (String className : SERVLET_INDICATOR_CLASSES) {
			if (!ClassUtils.isPresent(className, null)) {
				return WebApplicationType.NONE;
			}
		}

    //3 其余都是Servlet类型
		return WebApplicationType.SERVLET;
	}

}

从 spring.factories 加载配置

SpringBoot 中很多配置都是从 spring.factories 中加载的。spring.factories 本质是一个 Java properties 文件,里面定义很多的 KV 映射。key 一般是接口的全限定名,而值则是 key 的实现列表(使用逗号分隔)。其实这里和 Java 的 SPI 机制非常类似,不过 SPI 会将接口全限定名作为文件名,然后实现类的名字直接写在文件内部,位置一般是 META-INF/services/接口全限定名。而 spring.factories 文件位置是META-INF/spring.factories

spring-boot 和 spring-boot-autocofiigure 中都有这个文件。我截取 springboot 的 spring.factories 一部分内容:

# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
...

springboot 使用类 SpringFactoriesLoader 来负责从 spring.factories 中读取 KV,并实例化成对象。

public class SpringFactoriesLoader {
  // 默认资源位置
  public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
  //缓存
  //结构为
  //{
  //   ClassLoaderA: {
  //             loacation1:  SpringFactoriesLoader
  //             loacation2:  SpringFactoriesLoader
  //   },
  //   ClassLoaderB: {
  //             loacation3:  SpringFactoriesLoader
  //   }
  //}
  static final Map<ClassLoader, Map<String, SpringFactoriesLoader>> cache = new ConcurrentReferenceHashMap<>();


  //SpringFactoriesLoader的创建
  public static SpringFactoriesLoader forResourceLocation(String resourceLocation, @Nullable ClassLoader classLoader) {
    //1 确定类加载器
		ClassLoader resourceClassLoader = (classLoader != null ? classLoader : SpringFactoriesLoader.class.getClassLoader());
    //2 从缓存中读取,没有就创建
		Map<String, SpringFactoriesLoader> loaders = cache.computeIfAbsent(resourceClassLoader, key -> new ConcurrentReferenceHashMap<>());
    //3 从缓存中读取,没有就创建SpringFactoriesLoader 并加载资源
		return loaders.computeIfAbsent(resourceLocation, key ->
				new SpringFactoriesLoader(classLoader, loadFactoriesResource(resourceClassLoader, resourceLocation)));
	}

  //从resourceLocation中加载资源
  protected static Map<String, List<String>> loadFactoriesResource(ClassLoader classLoader, String resourceLocation) {
		Map<String, List<String>> result = new LinkedHashMap<>();
		try {
      // 从resourceLocation读取所有的文件,然后依次读取并按照properties类型解析,并且按照分隔符切割成List
			Enumeration<URL> urls = classLoader.getResources(resourceLocation);
			while (urls.hasMoreElements()) {
				UrlResource resource = new UrlResource(urls.nextElement());
				Properties properties = PropertiesLoaderUtils.loadProperties(resource);
				properties.forEach((name, value) -> {
					List<String> implementations = result.computeIfAbsent(((String) name).trim(), key -> new ArrayList<>());
					Arrays.stream(StringUtils.commaDelimitedListToStringArray((String) value))
							.map(String::trim).forEach(implementations::add);
				});
			}
      // 某个接口对应的List去重
			result.replaceAll(SpringFactoriesLoader::toDistinctUnmodifiableList);
		}
		catch (IOException ex) {
			throw new IllegalArgumentException("Unable to load factories from location [" + resourceLocation + "]", ex);
		}
		return Collections.unmodifiableMap(result);
	}

  //SpringFactoriesLoader加载指定的接口实现
  // factoryType 就是接口类型
	public <T> List<T> load(Class<T> factoryType, @Nullable ArgumentResolver argumentResolver,
			@Nullable FailureHandler failureHandler) {

    //从缓存中读取实现列表
 		List<String> implementationNames = loadFactoryNames(factoryType);
		List<T> result = new ArrayList<>(implementationNames.size());
		FailureHandler failureHandlerToUse = (failureHandler != null) ? failureHandler : THROWING_FAILURE_HANDLER;
    //依次实例化
		for (String implementationName : implementationNames) {
			T factory = instantiateFactory(implementationName, factoryType, argumentResolver, failureHandlerToUse);
			if (factory != null) {
				result.add(factory);
			}
		}
    //按照order排序
		AnnotationAwareOrderComparator.sort(result);
		return result;
	}

}

应用主类推断

这个应用主类是 main 方法的所在类,只能在单线程环境和启动过程中才能正确推断。在 Java8 及之前,可以通过创建一个 Throwable 对象,并通过 Throwable 的 getStackTrace()方法可以获取调用的栈结构,从而在遍历整个栈找到 main 方法的所在类。Java9 中加入了类 StackWalker,可以通过该类来边界的遍历整个调用栈。比如下面的示例代码:

public class StackWalkerDemo {

    public static void main(String[] args) {
        new StackWalkerDemo().start();
    }

    private void start() {
        init();
    }

    private void init() {
        //在这里推断
        StackWalker walker = StackWalker.getInstance(Option.RETAIN_CLASS_REFERENCE);
        List<String> walk = walker.walk(stream -> {
            return stream.map(frame -> {
                return frame.getClassName() + "->" + frame.getMethodName();
            }).collect(Collectors.toList());
        });
        System.out.println(walk);
        //[support.StackWalkerDemo->init, support.StackWalkerDemo->start, support.StackWalkerDemo->main]
    }
}

上面的示例代码中,我们在 init 方法中查看调用的栈结构,把类名和方法名收集到 List 中,并最终打印整个 List。结果中显示了我们 init 的栈顺序。

springboot 中也是利用了 StackWalker,来查找 main 方法所在的类,代码如下

public class SpringApplication {
  //推断主类
	private Class<?> deduceMainApplicationClass() {
		return StackWalker.getInstance(StackWalker.Option.RETAIN_CLASS_REFERENCE).walk(this::findMainClass)
				.orElse(null);
	}
  //找主类的方法
	private Optional<Class<?>> findMainClass(Stream<StackFrame> stack) {
    //过滤,找出第一个方法名字是main的类
		return stack.filter((frame) -> Objects.equals(frame.getMethodName(), "main")).findFirst()
				.map(StackWalker.StackFrame::getDeclaringClass);
	}
}

run 方法

run 方法就是启动 springboot 应用的方法。这里面有很多的步骤,示意图如下

2023-02-12_185312.png

run 方法的启动步骤的数量可能和其他博主的有些出入,不过整体基本是一致的,我在示意图中省略了一些不紧要的步骤,并且仅记录了 run 方法内部的代码步骤,没有记录 run 方法调用的方法内的步骤,比如准备环境时,实际上还会发送环境准备完毕事件,这部分我没有记录,我会在下面单独阐述 SpringBoot 的启动过程监听器和事件。

springboot 的 run 方法的代码如下,代码上都有注释

public class SpringApplication {

  public ConfigurableApplicationContext run(String... args) {
    //1 计时开始!
		long startTime = System.nanoTime();

    //2 创建引导上下文
		DefaultBootstrapContext bootstrapContext = createBootstrapContext();

		ConfigurableApplicationContext context = null;
    //配置headless属性(可以忽略)
		configureHeadlessProperty();
    //3 准备好所有的SpringBoot应用启动过程事件监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);

    //4 发送开始启动事件
		listeners.starting(bootstrapContext, this.mainApplicationClass);
		try {
      // 5 准备环境
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);

      //6 打印应用标语
			Banner printedBanner = printBanner(environment);

      //7 创建应用上下文
			context = createApplicationContext();
      //设置启动步骤记录器(可忽略
			context.setApplicationStartup(this.applicationStartup);

      //8 配置应用上下文
			prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);

      //9 刷新应用上下文
			refreshContext(context);

      //10 刷新后操作(springboot预留方法,可忽略
			afterRefresh(context, applicationArguments);

      //11 记录整体耗时
			Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
			}

      //12 发送应用已启动事件
			listeners.started(context, timeTakenToStartup);

      //13 执行所有的ApplicationRunner 和 CommandLineRunner
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
      // 失败处理
			if (ex instanceof AbandonedRunException) {
				throw ex;
			}
			handleRunFailure(context, ex, listeners);
			throw new IllegalStateException(ex);
		}

		try {
      //14 发送应用就绪事件
			if (context.isRunning()) {
				Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
				listeners.ready(context, timeTakenToReady);
			}
		}
		catch (Throwable ex) {
      // 失败处理
			if (ex instanceof AbandonedRunException) {
				throw ex;
			}
			handleRunFailure(context, ex, null);
			throw new IllegalStateException(ex);
		}
		return context;
	}
}

看了上面复杂的启动步骤,我们来看下具体每个步骤做了什么事情。

创建引导上下文

springboot 会创建一个引导上下文,用于存储、共享启动过程中需要的各个组件。不是 ApplicationContext,而是 BootstrapContext,具体的实现类型是 DefaultBootstrapContext。

下面是创建的步骤:

  1. 创建一个类型为 DefaultBootstrapContext 的引导上下文
  2. 依次调用所有的 引导上下文初始化器的回调方法
public class SpringApplication {

	private final List<BootstrapRegistryInitializer> bootstrapRegistryInitializers;

	private DefaultBootstrapContext createBootstrapContext() {
    //1 创建了一个默认引导上下文
		DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();

    //2 调用所有的BootstrapRegistryInitializer来初始化这个引导上下文
    // 这里的 BootstrapRegistryInitializer 是在构造函数中设置的
		this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
		return bootstrapContext;
	}
}

那么 springboot 中默认有多少个 BootstrapRegistryInitializer 呢?答案是 0 个!没错 SpringBoot 和 SpringBootAutoConfigure 中默认没有配置任何的 BootstrapRegistryInitializer,不过在 SpringCloud 中配置了很多,这部分我们后面有机会探究。

准备所有的启动过程事件监听器

springboot 在启动的过程中也有一套事件发布和监听的机制,这部分相比于 Spring 的事件机制来说简单了不少,因为这部分的事件主要是 SpringBoot 启动过程中产生和消费的,与应用本身无关。

public class SpringApplication {

  //SpringBoot 3.0 新增的,可以通过ThreadLocal来配置SpringBoot
  //SpringApplicationHook 目前可以只能添加SpringApplicationRunListener
  private static final ThreadLocal<SpringApplicationHook> applicationHook = new ThreadLocal<>();


	private SpringApplicationRunListeners getRunListeners(String[] args) {
    //1 解析启动的命令行参数
		ArgumentResolver argumentResolver = ArgumentResolver.of(SpringApplication.class, this);
		argumentResolver = argumentResolver.and(String[].class, args);

    //2 从spring.factories中加载所有的 SpringApplicationRunListener
		List<SpringApplicationRunListener> listeners = getSpringFactoriesInstances(SpringApplicationRunListener.class, argumentResolver);

    //3 把应用Hook中的监听器加入进来(3.0.0新增)
		SpringApplicationHook hook = applicationHook.get();
		SpringApplicationRunListener hookListener = (hook != null) ? hook.getRunListener(this) : null;
		if (hookListener != null) {
			listeners = new ArrayList<>(listeners);
			listeners.add(hookListener);
		}

    //4 把所有的监听器包装到一起
		return new SpringApplicationRunListeners(logger, listeners, this.applicationStartup);
	}

}

启动过程中的事件

SpringBoot 的启动过程的事件下面 7 种:

  1. starting
  2. environmentPrepared
  3. contextPrepared
  4. contextLoaded
  5. started
  6. ready
  7. failed

发生的时机见图:

2023-02-12_193402.png

发送开始启动事件

发送 starting 事件的代码只有一行,如下

listeners.starting(bootstrapContext, this.mainApplicationClass);

我们稍稍深入研究下 springboot 启动过程中是如何发布事件。上面谈到过,springboot 会收集所有的 SpringApplicationRunListener,然后统一放置于 SpringApplicationRunListeners 中,然后在 SpringApplicationRunListeners 的内部进行统一的事件发布。下面就是 SpringApplicationRunListeners 的 starting 代码,我们看下它是如何调用的,其他类型的方法也是类似的,理解了 starting 的调用,也就理解了整个 springboot 启动过程中的事件的调用。

class SpringApplicationRunListeners {

	void starting(ConfigurableBootstrapContext bootstrapContext, Class<?> mainApplicationClass) {
    // 直接调用内部函数 doWithListeners
    //
		doWithListeners("spring.boot.application.starting", (listener) -> listener.starting(bootstrapContext),
				(step) -> {
					if (mainApplicationClass != null) {
						step.tag("mainApplicationClass", mainApplicationClass.getName());
					}
				});
	}

  //调用所有的监听器,并触发对应的步骤记录
  //  stepName 当前SpringApplicaiton进行的步骤
  // listenerAction 当前所有监听器需要进行的操作函数
  // stepAction 步骤记录器需要进行的操作
  private void doWithListeners(String stepName, Consumer<SpringApplicationRunListener> listenerAction,	Consumer<StartupStep> stepAction) {
		StartupStep step = this.applicationStartup.start(stepName);

    //调用所有SpringApplicationRunListener的某个方法
		this.listeners.forEach(listenerAction);

		if (stepAction != null) {
			stepAction.accept(step);
		}
		step.end();
	}

}
  • 首先可以看到 SpringApplicationRunListeners 并不是 public 修饰的,所以这个类仅仅用于 springboot 内部,普通用户不需要使用。
  • 在 starting 方法内部调用了 doWithListeners 方法,传入了"spring.boot.application.starting",这表示当前步骤的名字,后面是两个 Lambda 函数,第一个是监听器函数,第二个是启动步骤函数
  • 监听器函数 (listener) -> listener.starting(bootstrapContext) 在 doWithListeners 中被this.listeners.forEach(listenerAction);调用,这意味着调用了所有的 SpringApplicationRunListener 的 starting 方法
  • 启动步骤函数的调用是在doWithListenersstepAction.accept(step);处被调用,关于启动步骤,此文不在进行详细讲解。

这里我们需要扩展一下,springboot 项目和 autoconfigure 项目默认有多少个 SpringApplicationRunListener,他们的每一步的作用又是什么? 首先看下在 spring.factories 中配置了多少 SpringApplicationRunListener

springboot 中

org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

autoconfigure 默认没有监听器。

那我们看下 springboot 的 EventPublishingRunListener 的作用吧。其实从名字就可以看出来这个类的作用大概是发布和广播事件, 它是用来把 springboot 启动的事件转换并广播到上下文中的监听器中,是连接 springboot 事件和 spring 事件的桥梁。

我们看下它的构造函数和 starting 方法,它广播了一个 ApplicationStartingEvent 事件

class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {

  private final SimpleApplicationEventMulticaster initialMulticaster;

  //构造函数
	EventPublishingRunListener(SpringApplication application, String[] args) {
		this.application = application;
		this.args = args;
    //创建一个事件广播器
		this.initialMulticaster = new SimpleApplicationEventMulticaster();
	}
  //
  @Override
	public void starting(ConfigurableBootstrapContext bootstrapContext) {
		multicastInitialEvent(new ApplicationStartingEvent(bootstrapContext, this.application, this.args));
	}

  //广播事件
  private void multicastInitialEvent(ApplicationEvent event) {
    //首先是找出当前时间点所有的 ApplicationListener
		refreshApplicationListeners();
    //然后进行事件的广播
		this.initialMulticaster.multicastEvent(event);
	}
  //
  private void refreshApplicationListeners() {
		this.application.getListeners().forEach(this.initialMulticaster::addApplicationListener);
	}
}

可以看出 springnboot 的启动过程中的各个事件都会被广播到 Spring 中,我总结下如下的 springboot 事件和 spring 事件的对应关系。

springboot 事件spring 事件备注
startingApplicationStartingEvent
environmentPreparedApplicationEnvironmentPreparedEvent
contextPreparedApplicationContextInitializedEvent
contextLoadedApplicationPreparedEvent
startedApplicationStartedEvent还会有事件 AvailabilityChangeEvent
readyApplicationReadyEvent还会有事件 AvailabilityChangeEvent
failedApplicationFailedEvent

上表中的各个 spring 事件,并不是 spring 项目原始的事件,都是 springboot 基于 spring 而扩展的事件类型。

应用存活与就绪状态 在微服务的架构中,我们需要知道每个服务的状态,比如这个服务还有没有,是不是可以访问。springboot 基于此类需求,设计了应用的存活状态和就绪状态。使用枚举类 LivenessState 和 ReadinessState 来表示。

  • LivenessState
    • 应用的存活状态,有 CORRECT 和 BROKEN 两种,分别表示应用正常和失败
  • ReadinessState
    • 应用的可用性状态,有 ACCEPTING_TRAFFIC 和 REFUSING_TRAFFIC 两种,分别表示应用可以提供服务和不能提供服务

springboot 在 started 和 ready 事件中,也会修改上述的存活和就绪状态。在 started 之后应用进入了 LivenessState.CORRECT 状态,在 ready 之后应用进入了 ReadinessState.ACCEPTING_TRAFFIC 状态。

相关代码如下:

class EventPublishingRunListener implements SpringApplicationRunListener, Ordered {
	@Override
	public void started(ConfigurableApplicationContext context, Duration timeTaken) {
		context.publishEvent(new ApplicationStartedEvent(this.application, this.args, context, timeTaken));
    //发布应用存活状态变更事件
    //AvailabilityChangeEvent(state=LivenessState.CORRECT)
		AvailabilityChangeEvent.publish(context, LivenessState.CORRECT);
	}

	@Override
	public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
		context.publishEvent(new ApplicationReadyEvent(this.application, this.args, context, timeTaken));
    //发布应用就绪状态变更事件
    //AvailabilityChangeEvent(state=ReadinessState.ACCEPTING_TRAFFIC)
		AvailabilityChangeEvent.publish(context, ReadinessState.ACCEPTING_TRAFFIC);
	}
}

准备环境

springboot 会把创建一个环境,并设置到后面的上下文中。

springboot 的环境是基于 spring 的 ConfigurableEnvironment,但是增加了很多的扩展点。主要步骤如下:

  1. 查找或创建一个环境
  2. 配置环境,设置类型转换服务、加入其他源
  3. 发送环境准备好事件
  4. 设置当前 SpringApplication 的一些属性值
  5. 如果环境类型和当前应用类型不匹配还需要进行转换
public class SpringApplication {

  private boolean isCustomEnvironment = false;

  //准备环境
	private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
			DefaultBootstrapContext bootstrapContext, ApplicationArguments applicationArguments) {
		// Create and configure the environment
    //1 查找或创建一个环境
		ConfigurableEnvironment environment = getOrCreateEnvironment();

    //2 配置环境
		configureEnvironment(environment, applicationArguments.getSourceArgs());
    // 转换propertySource
		ConfigurationPropertySources.attach(environment);
    //3 发送环境准备好事件
		listeners.environmentPrepared(bootstrapContext, environment);
    // 把defaultProperties移动到最后一位
		DefaultPropertiesPropertySource.moveToEnd(environment);
		Assert.state(!environment.containsProperty("spring.main.environment-prefix"),
				"Environment prefix cannot be set via properties.");

    //4 设置SprnigApplication的一些字段值
		bindToSpringApplication(environment);
    //5 转换环境类型(非用户创建的环境需要进行转换成和当前应用场景适配的环境类型)
		if (!this.isCustomEnvironment) {
			EnvironmentConverter environmentConverter = new EnvironmentConverter(getClassLoader());
			environment = environmentConverter.convertEnvironmentIfNecessary(environment, deduceEnvironmentClass());
		}
		ConfigurationPropertySources.attach(environment);
		return environment;
	}
}

查找或创建一个环境 springboot 提供了 setEnvironment 方法,可以在 run 方法之前调用,来设置用户自己的环境实现,不过用户在多数情况下都不会设置自定义的环境,而是让 springboot 自己创建一个。

  1. 如果用户定义了环境,就直接使用
  2. 根据当前的应用类型来创建不同场景的环境,见表格
  3. 如没有创建,则使用默认策略创建环境
  4. 如没有创建,则使用 ApplicationEnvironment

应用类型与环境类型

类型常规Native
NONEApplicationEnvironment相同
SERVLETApplicationServletEnvironment相同
REACTIVEApplicationReactiveWebEnvironment相同
public class SpringApplication {
	private ConfigurableEnvironment getOrCreateEnvironment() {
    //1 使用用户定义的环境(如果有
		if (this.environment != null) {
			return this.environment;
		}
    //2 按照应用类型创建环境
		ConfigurableEnvironment environment = this.applicationContextFactory.createEnvironment(this.webApplicationType);

    //3 没有创建出来,则使用默认策略创建
		if (environment == null && this.applicationContextFactory != ApplicationContextFactory.DEFAULT) {
			environment = ApplicationContextFactory.DEFAULT.createEnvironment(this.webApplicationType);
		}

    //4 没有创建出来,最后的兜底策略,使用ApplicationEnvironment
		return (environment != null) ? environment : new ApplicationEnvironment();
	}
}

配置环境 环境创建好后,需要一些初始化步骤:

  1. 配置类型转换服务(环境中的值都是字符串,而对象的属性则是各种不同的类型)
  2. 加入其他的配置源(默认配置、命令行参数)
public class SpringApplication {

	private boolean addConversionService = true;
  private boolean addCommandLineProperties = true;


	protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
    //1 设置类型转换服务
		if (this.addConversionService) {
			environment.setConversionService(new ApplicationConversionService());
		}

    //2 加入其他的配置源
		configurePropertySources(environment, args);

    //3 按照profile配置(预留方法,空实现)
		configureProfiles(environment, args);
	}

  //向环境中加入其它配置源
	protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
		MutablePropertySources sources = environment.getPropertySources();
    //1 如有设置默认配置项则加入,名字是 defaultProperties,(一般都没有,可以调用setDefaultProperties设置)
    // 这个源的顺序是最后一位
		if (!CollectionUtils.isEmpty(this.defaultProperties)) {
			DefaultPropertiesPropertySource.addOrMerge(this.defaultProperties, sources);
		}

    //2 加入命令行参数配置源,名字为 commandLineArgs,顺序是第一位
		if (this.addCommandLineProperties && args.length > 0) {
			String name = CommandLinePropertySource.COMMAND_LINE_PROPERTY_SOURCE_NAME;
			if (sources.contains(name)) {
				PropertySource<?> source = sources.get(name);
				CompositePropertySource composite = new CompositePropertySource(name);
				composite.addPropertySource(new SimpleCommandLinePropertySource("springApplicationCommandLineArgs", args));
				composite.addPropertySource(source);
				sources.replace(name, composite);
			}
			else {
				sources.addFirst(new SimpleCommandLinePropertySource(args));
			}
		}
	}

	protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
	}

}

不同类型环境的默认配置源

环境中可以有多种配置源,也就是一组 PropertySource,并且他们的顺序是非常重要的,越在前面的优先级越高。环境被创建时,都会内置一些默认的配置源,不同类型的环境的配置源可能不同。 下面列出了 springboot 中使用的不同类型环境的内置的配置源列表

ApplicationEnvironment 的默认 PropertySource 如下:

  1. systemProperties (系统属性)
  2. systemEnvironment (系统环境变量)

ApplicationServletEnvironment 的默认 PropertySource 如下:

  1. servletConfigInitParams (Servle Config 的参数,创建是占位的,后面创建 webServer 才再填充值)
  2. servletContextInitParams (Servlet Context 初始化参数,创建是占位的,后面创建 webServer 才再填充值)
  3. systemProperties (系统属性)
  4. systemEnvironment (系统环境变量)

如果使用 JNDI 的话,还有一个 jndiProperties 的 PropertySource 会排在 servletContextInitParams 之后,不过一般 springboot 的项目都没有使用 JNDI。

ApplicationReactiveWebEnvironment 的默认 PropertySource 和 ApplicationEnvironment 是一样的:

  1. systemProperties (系统属性)
  2. systemEnvironment (系统环境变量)

转换 propertySource

转换 propertySource,即代码ConfigurationPropertySources.attach(environment);,springboot 支持键的松散绑定,即环境变量 A_B_C 是可以绑定到 a.b.c 类型的值上的。 提供这种松散绑定的能力需要 PropertySourcesPropertyResolver 的支持,springboot 把所有的 propertySoure 组合到一起,创建了一个名为'configurationProperties',并放置到环境 propertysource 的首位,统一提供带松散绑定能力的键值获取。

public final class ConfigurationPropertySources {

  private static final String ATTACHED_PROPERTY_SOURCE_NAME = "configurationProperties";

	public static void attach(Environment environment) {
		Assert.isInstanceOf(ConfigurableEnvironment.class, environment);
    //1 读取出当前环境的所有propertysource
		MutablePropertySources sources = ((ConfigurableEnvironment) environment).getPropertySources();

    //2 查看当前的环境是不是已经有 configurationProperties了
		PropertySource<?> attached = getAttached(sources);

    //3 如果已经有了,则复合propertysource即可
		if (attached == null || !isUsingSources(attached, sources)) {
			attached = new ConfigurationPropertySourcesPropertySource(ATTACHED_PROPERTY_SOURCE_NAME,
					new SpringConfigurationPropertySources(sources));
		}
    //4 把configurationProperties放置到环境首位,优先级最高
    // 内部propertysource的顺序依旧是保持的
		sources.remove(ATTACHED_PROPERTY_SOURCE_NAME);
		sources.addFirst(attached);
	}

}

设置 SprnigApplication 的一些字段值 springboot 应用支持一些前缀为spring.main的属性,这些属性就在此时会设置到当前 SpringApplication 对象的字段上。绑定的属性值示例如下:

  • spring.main.sources
  • spring.main.web-application-type=none
  • spring.main.banner-mode=off

SpringApplication 中几乎所有的属性值都可以这样设置

public class SpringApplication {

	protected void bindToSpringApplication(ConfigurableEnvironment environment) {
		try {
      //使用 Binder 把environment中前缀为spring.main的键值设置到this上
			Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
		}
		catch (Exception ex) {
			throw new IllegalStateException("Cannot bind to SpringApplication", ex);
		}
	}
}

转换环境类型 如果 springboot 发现创建的环境类型与当前的应用的场景不匹配的话,则会将创建的环境进行类型的转换。说是类型的转换,实际是创建一个匹配当前场景的环境子类型,然后把当前环境的 properysource、类型转换服务、profile 等参数复制过去而已。

final class EnvironmentConverter {

	ConfigurableEnvironment convertEnvironmentIfNecessary(ConfigurableEnvironment environment,
			Class<? extends ConfigurableEnvironment> type) {
    // 类型一致不需要转换
		if (type.equals(environment.getClass())) {
			return environment;
		}
    // 类型不一致,需要转换
		return convertEnvironment(environment, type);
	}

	private ConfigurableEnvironment convertEnvironment(ConfigurableEnvironment environment,
			Class<? extends ConfigurableEnvironment> type) {
    //1 创建一个指定类型的环境
		ConfigurableEnvironment result = createEnvironment(type);
    //2 复制profile
		result.setActiveProfiles(environment.getActiveProfiles());
    //3 复制类型转换服务
		result.setConversionService(environment.getConversionService());
    //4 复制propertysource
		copyPropertySources(environment, result);
		return result;
	}

}

打印应用标语

打印标语是 springboot 一项非常有意思的内容,但不是核心的内容,我对此不再深入展开,简要介绍下。

标语默认支持三种模式,定义在 Banner.Mode 中

  1. OFF 关闭
  2. CONSOLE 标准输出
  3. LOG 日志

sprinboot 默认会从 banner.txt 中读取标语内容,不过也可以通过参数spring.banner.location指定

public class SpringApplication {

  private Banner.Mode bannerMode = Banner.Mode.CONSOLE;


	private Banner printBanner(ConfigurableEnvironment environment) {

    //1 如果用户设置了关闭标语,则直接退出
		if (this.bannerMode == Banner.Mode.OFF) {
			return null;
		}

    //2 查找资源加载器(下面加载banner文件需要)
		ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
				: new DefaultResourceLoader(null);

    //3 创建一个Banner打印器
		SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(resourceLoader, this.banner);

    //4,按照不同的标语模式输出标语
		if (this.bannerMode == Mode.LOG) {
			return bannerPrinter.print(environment, this.mainApplicationClass, logger);
		}
		return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
	}
}

创建应用上下文

springboot 会根据不同的场景创建不同的应用上下文。创建的过程是交给上下文工厂 ApplicationContextFactory 来进行的,默认使用的是 DefaultApplicationContextFactory。

public class SpringApplication {

	private ApplicationContextFactory applicationContextFactory = ApplicationContextFactory.DEFAULT;

	protected ConfigurableApplicationContext createApplicationContext() {
    //直接交给工厂按照类型创建即可
		return this.applicationContextFactory.create(this.webApplicationType);
	}

}

DefaultApplicationContextFactory 的创建策略

  1. 首先根据 WebApplicationType 的类型,在 spring.factories 查找配置的 ApplicationContextFactory
  2. 如果找到了,就使用找的 ApplicationContextFactory 进行创建
  3. 如果找不到,则自己创建

springboot 中另外提供了 Servlet 和 Reactive 环境的 ApplicationContextFactory,分别是 ServletWebServerApplicationContextFactory 和 ReactiveWebServerApplicationContextFactory。 创建出的 ApplicationContext 的类型,我再次展示下之前的表格

类型工厂常规Native
NONEDefaultApplicationContextFactoryAnnotationConfigApplicationContextGenericApplicationContext
SERVLETServletWebServerApplicationContextFactoryAnnotationConfigServletWebServerApplicationContextServletWebServerApplicationContext
REACTIVEReactiveWebServerApplicationContextFactoryAnnotationConfigReactiveWebServerApplicationContextReactiveWebServerApplicationContext

配置应用上下文

配置应用上下文即设置上下文的各项属性,也是非常复杂的过程,基本步骤如下:

  1. 设置环境
  2. 初始化一些基本属性
  3. 调用应用上下文初始化器来初始化应用上下文
  4. 发送应用上下文准备好事件
  5. 关闭引导上下文
  6. 注册一些单例对象(命令行参数、标语)
  7. 设置 IOC 的属性(是否支持循环依赖,是否支持 BeanDefintion 覆盖)
  8. 加入延迟初始化工厂后处理器
  9. 加入一个配置源排序工厂后处理器
  10. 加载配置的 source 中的所有 BeanDefinition
  11. 发送应用上下文加载好事件
public class SpringApplication {

	private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
			ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
			ApplicationArguments applicationArguments, Banner printedBanner) {
    //1 设置环境
		context.setEnvironment(environment);
    //2 初始化一些基本属性
		postProcessApplicationContext(context);
    // aot设置(暂时忽略
		addAotGeneratedInitializerIfNecessary(this.initializers);
    //3 调用上下文初始化器来初始化上下文
		applyInitializers(context);
    //4 发送上下文准备好事件
		listeners.contextPrepared(context);
    //5 关闭引导上下文
		bootstrapContext.close(context);
    // 记录启动步骤(暂时忽略
		if (this.logStartupInfo) {
			logStartupInfo(context.getParent() == null);
			logStartupProfileInfo(context);
		}

    //6 注册一些单例对象(命令行参数、标语)
		// Add boot specific singleton beans
		ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
		beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
		if (printedBanner != null) {
			beanFactory.registerSingleton("springBootBanner", printedBanner);
		}

    //7  设置 IOC 的属性(是否支持循环依赖,是否支持 BeanDefintion 覆盖)
		if (beanFactory instanceof AbstractAutowireCapableBeanFactory autowireCapableBeanFactory) {
			autowireCapableBeanFactory.setAllowCircularReferences(this.allowCircularReferences);
			if (beanFactory instanceof DefaultListableBeanFactory listableBeanFactory) {
				listableBeanFactory.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
			}
		}
    //8 加入延迟初始化工厂后处理器
		if (this.lazyInitialization) {
			context.addBeanFactoryPostProcessor(new LazyInitializationBeanFactoryPostProcessor());
		}
    //9 加入一个配置源排序工厂后处理器
		context.addBeanFactoryPostProcessor(new PropertySourceOrderingBeanFactoryPostProcessor(context));
    //10 加载配置的source中的所有BeanDefinition (非native)
    // 一般情形中,仅仅把标注@SpringApplication注册为BeanDefinition,不会解析上面的注解。
		if (!AotDetector.useGeneratedArtifacts()) {
			// Load the sources
			Set<Object> sources = getAllSources();
			Assert.notEmpty(sources, "Sources must not be empty");
			load(context, sources.toArray(new Object[0]));
		}
    //11 发送应用上下文加载好事件
		listeners.contextLoaded(context);
	}
}

初始化一些基本属性

主要是根据当前 SpringApplition 的一些配置来设置应用上下文的属性:

  1. 设置 beanname 生成器
  2. 设置资源加载器
  3. 设置类型转换服务(和环境中使用的同一个类型转换器)
public class SpringApplication {

  	private BeanNameGenerator beanNameGenerator;
	  private ResourceLoader resourceLoader;
	  private boolean addConversionService = true;


		protected void postProcessApplicationContext(ConfigurableApplicationContext context) {
    //1 设置beanname生成器
		if (this.beanNameGenerator != null) {
			context.getBeanFactory().registerSingleton(AnnotationConfigUtils.CONFIGURATION_BEAN_NAME_GENERATOR, this.beanNameGenerator);
		}
    //2 设置资源加载器
		if (this.resourceLoader != null) {
			if (context instanceof GenericApplicationContext genericApplicationContext) {
				genericApplicationContext.setResourceLoader(this.resourceLoader);
			}
			if (context instanceof DefaultResourceLoader defaultResourceLoader) {
				defaultResourceLoader.setClassLoader(this.resourceLoader.getClassLoader());
			}
		}
    //3 设置类型转换服务(和环境中使用的同一个类型转换器)
		if (this.addConversionService) {
			context.getBeanFactory().setConversionService(context.getEnvironment().getConversionService());
		}
	}
}

调用应用上下文初始化器来初始化应用上下文

在 SpringApplication 的构造函数中,我们看到它加载了所有的 ApplicationContextInitializer,这里则是回调这些应用上下文初始化器的位置。

这里会取出所有的 ApplicationContextInitializer,然后依次调用方法 initialize 来初始化

public class SpringApplication {

	protected void applyInitializers(ConfigurableApplicationContext context) {
    //1 遍历所有的ApplicationContextInitializer
		for (ApplicationContextInitializer initializer : getInitializers()) {
      //2 解析并判断类型
			Class<?> requiredType = GenericTypeResolver.resolveTypeArgument(initializer.getClass(), ApplicationContextInitializer.class);
			Assert.isInstanceOf(requiredType, context, "Unable to call initializer.");
      //3 初始化应用上下文
			initializer.initialize(context);
		}
	}
}

此处我列出 springboot 和 autoconfigure 项目中配置的 ApplicationContextInitializer,并探究他们的作用。

在 springboot 中默认在 spring.factories 中配置了一些 ApplicationContextInitializer 的实现,如下

# 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-autoconfigure 中配置如下的实现

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

上面的这些 ApplicationContextInitializer 会在执行前进行排序。下面按照执行顺序依次简单介绍下各个初始化器的功能和作用

  1. DelegatingApplicationContextInitializer

    • 代理应用上下文初始化器,可以从环境中找出配置的 ApplicationContextInitializer。order 为 0,第一个应用的
    • springboot 默认是从 spring.factories 文件中查找 ApplicationContextInitializer 的实现类名的
    • 第一点:这样实际需要外部的 jar 包都遵循 META-INF/spring.facoties 的配置方式,对于一些项目来说可能是不太灵活
    • 第二点:这样的配置太死板,文件中写的类都会被加载并应用,对于需要条件判断来加载的不太适合。
    • 于是 springboot 于是提供了 DelegatingApplicationContextInitializer,可以从环境中读取context.initializer.classes条目的类型,这个值就是另外的 ApplicationContextInitializer 实现类名,比如 在 SpringBoot 启动前,执行如下代码,即可添加进来,并立马执行
     System.setProperty("context.initializer.classes", "com.wpp.run.MyApplicationContextInitializer");
    
  2. SharedMetadataReaderFactoryContextInitializer

    • 创建一个在 ConfigurationClassPostProcessor 和 SpringBoot 中共享的 CachingMetadataReaderFactoryPostProcessor
    • 这个 CachingMetadataReaderFactoryPostProcessor 是一个 BeanDefinitionRegistryPostProcessor,并且优先级高于 ConfigurationClassPostProcessor 还高(因为需要修改 ConfigurationClassPostProcessor 的属性,所以必须高),它实际上会最终向容器中配置一个 ConcurrentReferenceCachingMetadataReaderFactory,用于保存 .class 文件和 MetadataReader 的缓存,可以优化类元数据的读取性能
    • 并且这个 ConcurrentReferenceCachingMetadataReaderFactory 会替换成 Spring 的配置解析类 ConfigurationClassPostProcessor 属性 metadataReaderFactory 上,以达到 Springboot 和 ConfigurationClassPostProcessor 的缓存共享
    • 这个 ConcurrentReferenceCachingMetadataReaderFactory 在 IOC 中的 benaName 是 org.springframework.boot.autoconfigure.internalCachingMetadataReaderFactory
    • 简言之,这个初始化器用于把 ConfigurationClassPostProcessor 的 metadataReaderFactory 设置成 ConcurrentReferenceCachingMetadataReaderFactory
  3. ContextIdApplicationContextInitializer

    • 设置 applicationContext 的 ID
    • 优先使用属性 spring.application.name,没有配置则使用固定字符串application
    • IOC 容器中应用上下文的 Id 是一个 ContextId 类型的对象,并且也会添加到容器中
  4. ConfigurationWarningsApplicationContextInitializer

    • 向上下文中添加 ConfigurationWarningsPostProcessor
    • ConfigurationWarningsPostProcessor 会检查@ComponentScan 的配置,如配置了orgorg.springframework就会打印类似下面的警告信息
     ** WARNING ** : Your ApplicationContext is unlikely to start due to a @ComponentScan of 'org'.
    
    • 原因也很简单,org 或者 org.springframework 包下许多包都是需要各种条件化配置评估的才能运行加载的,如果盲目的写,一般也会缺少各种 class,根本启动不了项目
  5. RSocketPortInfoApplicationContextInitializer

    • 向 ApplicationContext 中加入一个监听·RSocketServerInitializedEvent· 事件的监听器
    • 该监听器听到 RSocketServerInitializedEvent 事件时,会修改(没有就创建)环境(当前和所有父亲环境)的配置源server.ports,向里面添加一个属性local.rsocket.server.port=$port
    • 这里的 port 是 socket 的端口
  6. ServerPortInfoApplicationContextInitializer

    • 这个 ServerPortInfoApplicationContextInitializer 个上面的 RSocketPortInfoApplicationContextInitializer 类似,是加入一个监听 WebServerInitializedEvent 事件的监听器
    • 该监听器听到 WebServerInitializedEvent 事件时,会修改(没有就创建)环境(当前和所有父亲环境)的配置源server.ports,先里面添加一个属性"local.${上下文名字}.port=${webserver.port}"
    • 这里的 port 是 web 服务器的端口
  7. ConditionEvaluationReportLoggingListener

    • 向 ApplicationContext 中加入一个 ConditionEvaluationReportListener 监听器
    • ConditionEvaluationReportListener 会在事件 ContextRefreshedEvent 或者 ApplicationFailedEvent 时,打印出所有的条件评估日志

关闭引导上下文

随着应用上下文的初始化完成,引导上下文也随之需要进行关闭。调用bootstrapContext.close(context);来进行关闭。 在 DefaultBootstrapContext 的 close 实现中,他会广播一个 BootstrapContextClosedEvent 事件。这个事件在 springboot 项目中没有监听器感兴趣,不过在 springcloud 中确实监听的实现。

public class DefaultBootstrapContext implements ConfigurableBootstrapContext {
  private final ApplicationEventMulticaster events = new SimpleApplicationEventMulticaster();

	public void close(ConfigurableApplicationContext applicationContext) {
    // 调用自己的事件广播器进行广播
		this.events.multicastEvent(new BootstrapContextClosedEvent(this, applicationContext));
	}

}

加入延迟初始化工厂后处理器

默认情况下 springboot 不会加入 LazyInitializationBeanFactoryPostProcessor。用户可以通过方法 setLazyInitialization 或属性spring.main.lazyInitialization=true来开启懒加载模式。此处主要探究下 LazyInitializationBeanFactoryPostProcessor。用户可以通过方法是如何设置 Bean 的懒加载的。

  1. 找出当前 IOC 的所有延迟加载排除器
  2. 遍历当前 IOC 所有的 BeanDefinition,
  3. 如果 BeanDefinition 不是被排除的或者延迟加载属性没有被设置过,就设置为 true,表示延迟加载
public final class LazyInitializationBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {

	@Override
	public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {

    //1 找出当前IOC的所有延迟加载排除器
		Collection<LazyInitializationExcludeFilter> filters = getFilters(beanFactory);
    //2 遍历当前IOC所有的BeanDefinition,
		for (String beanName : beanFactory.getBeanDefinitionNames()) {
			BeanDefinition beanDefinition = beanFactory.getBeanDefinition(beanName);
			if (beanDefinition instanceof AbstractBeanDefinition abstractBeanDefinition) {
        //3 如果BeanDefinition不是被排除的或者延迟加载属性没有被设置过,就设置为true,表示延迟加载
				postProcess(beanFactory, filters, beanName, abstractBeanDefinition);
			}
		}
	}

  private void postProcess(ConfigurableListableBeanFactory beanFactory,
			Collection<LazyInitializationExcludeFilter> filters, String beanName,
			AbstractBeanDefinition beanDefinition) {
    //1 如果BeanDefinition的属性lazyInit已经被赋值了,就不处理
		Boolean lazyInit = beanDefinition.getLazyInit();
		if (lazyInit != null) {
			return;
		}
    //2 查看当前bean的类型,是不是被排除的
		Class<?> beanType = getBeanType(beanFactory, beanName);
		if (!isExcluded(filters, beanName, beanDefinition, beanType)) {
			beanDefinition.setLazyInit(true);
		}
	}

}

LazyInitializationExcludeFilter

关于这个延迟加载排除器,它是从 BeanFactory 中 get 出来的,所以我们可以使用@Bean 和其它的方式添加。这个接口的代码如下:

public interface LazyInitializationExcludeFilter {
  //确认是否排除该BeanDefinition
	boolean isExcluded(String beanName, BeanDefinition beanDefinition, Class<?> beanType);
}

在 autoconfigure 中仅有一个实现类 ScheduledBeanLazyInitializationExcludeFilter,它用于排除以下类

  1. 实现了接口 AopInfrastructureBean、TaskScheduler、ScheduledExecutorService 的类
  2. 方法上有 @Scheduled@Schedules注解的类

这部分关于调度的类不能延迟加载也是可以理解的,调度任务本来就必须是自动执行的,如有设置成延迟加载了,那么根本就不会有人来触发这些调度,这会导致所有的调度任务失效。

加入一个配置源排序工厂后处理器

类 PropertySourceOrderingBeanFactoryPostProcessor 是一个内部类,它的作用只有一个就是确保 默认配置源 defaultProperties 一定在所有配置源最末尾。实现代码如下:

private static class PropertySourceOrderingBeanFactoryPostProcessor implements BeanFactoryPostProcessor, Ordered {

  @Override
  public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException {
    //把defaultProperties移动到末尾
    DefaultPropertiesPropertySource.moveToEnd(this.context.getEnvironment());
  }
}

加载配置的 source 中的 BeanDefinition

这里的加载 BeanDefinition 和 Spring 的配置解析并不相同,这里是确保通过 SpringApplition 中的 primarySources 和 sources 设置的配置类自身会被加载成 IOC 的 BeanDefintion。

比如文章前面的的示例代码(如下)中提到的 DemoApplication,仅仅是把 DemoApplication 这个类自己解析成 BeanDefinition,加载到 IOC 中。至于它的注解 @SpringBootApplication 中的自动配置和包扫描等工作不会在此时展开,而是由 Spring 中配置解析类 ConfigurationClassPostProcessor 来完成的。

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

source 支持的类型

springboot 的 source 支持非常多的类型,他们的加载方式略微不同,这部分工作是交给 BeanDefinitionLoader 来完成的。

支持的类型如下

  • 类(直接做成 BeanDefinition)
  • Resource(xml 文件、groovy 脚本)
  • Package 类(会扫描这个 Package)
  • 字符串(实际是字符串表示的类、Resource 或 Package)

刷新应用上下文

在做了那么多的铺垫和准备后,终于到了刷新应用上下文!不过 springboot 并没有重写 spring 的刷新,而是直接调用 AplicationConetext 的 refresh 就完成了刷新。所以说,springboot 的核心还是 spring,springboot 项目的关键是boot,即启动。

public class SpringApplication {

  private boolean registerShutdownHook = true;
	static final SpringApplicationShutdownHook shutdownHook = new SpringApplicationShutdownHook();

	private void refreshContext(ConfigurableApplicationContext context) {
    //1 注册JVM停止的回调钩子
		if (this.registerShutdownHook) {
			shutdownHook.registerApplicationContext(context);
		}
    //2 刷新
		refresh(context);
	}

  protected void refresh(ConfigurableApplicationContext applicationContext) {
    //调用applicationContext的refresh方法完成刷新
		applicationContext.refresh();
	}

}

shutdownHook

Java 的 Runtime 类提供了一个 addShutdownHook(Thread hook) 方法。使用示例代码如下:

  public static void main(String[] args) throws InterruptedException {
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
      System.out.println("停止Hook");
    }));
    Thread.currentThread().join();
  }
  1. 程序正常退出,hook 会被回调
  2. 程序异常退出,hook 会被回调
  3. kill -15 pid ,hook 会被回调 (SIGTERM 信号)
  4. kill -9 pid ,hook 不会被回调 (SIGKILL 信号)

springboot 则使用类 SpringApplicationShutdownHook 把这部分进行了封装,SpringApplicationShutdownHook 本身会被注册到 Hook 中,当 JVM 停止并回调时,会执行 SpringApplicationShutdownHook 的 run 方法:

class SpringApplicationShutdownHook implements Runnable {

  private final Handlers handlers = new Handlers();

  @Override
	public void run() {
		Set<ConfigurableApplicationContext> contexts;
		Set<ConfigurableApplicationContext> closedContexts;
		Set<Runnable> actions;
		synchronized (SpringApplicationShutdownHook.class) {
			this.inProgress = true;
			contexts = new LinkedHashSet<>(this.contexts);
			closedContexts = new LinkedHashSet<>(this.closedContexts);
			actions = new LinkedHashSet<>(this.handlers.getActions());
		}
    //1 遍历调用每个ApplicationContext的 closeAndWait 方法
		contexts.forEach(this::closeAndWait);
		closedContexts.forEach(this::closeAndWait);
    //2 遍历调用注册到Handler上的Runnable
		actions.forEach(Runnable::run);
	}

}

那么如何向 Handlers 中注册我们自己的 Runnable 呢?在 SpringApplication 中提供了静态方法 getShutdownHandlers ,代码如下:

public class SpringApplication {
	public static SpringApplicationShutdownHandlers getShutdownHandlers() {
		return shutdownHook.getHandlers();
	}
}

我们注册 shutdownHook 的使用示例如下:

public class HookDemo {

  public static void main(String[] args) {
    SpringApplication.getShutdownHandlers().add(() -> {System.out.println("my shutdown hook");});
    SpringApplication.run(HookDemo.class, args);
  }

}

刷新后操作(springboot 预留方法,可忽略

这部分是 springboot 的预留方法,暂无作用

public class SpringApplication {
	protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
	}
}

记录整体耗时

代码Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);计算出了从 run 方法开始到 afterRefresh 的中间的耗时. 从事件来分析是同 starting 事件到 started 之间的耗时情况。

发送应用已启动事件

代码listeners.started(context, timeTakenToStartup); 调用监听器的 started,上文已经分析过,此处不再赘述。

执行所有的 ApplicationRunner 和 CommandLineRunner

在容器刷新完成后,容器会依次调用配置的 ApplicationRunner 和 CommandLineRunner.这两种 Runner 的调用顺序是由它们配置的 Order 决定的,与类型无关。 这两种 Runner 的区别如下,仅仅是调用参数的不同

  • ApplicationRunner 把启动命令参数封装成易用的 ApplicationArguments
  • CommandLineRunner 把启动命令参数透传过来
public class SpringApplication {
	private void callRunners(ApplicationContext context, ApplicationArguments args) {
    // 1 实例化所有的 ApplicationRunner 和 CommandLineRunner
		List<Object> runners = new ArrayList<>();
		runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
		runners.addAll(context.getBeansOfType(CommandLineRunner.class).values());

    //2 按照Order排序
		AnnotationAwareOrderComparator.sort(runners);

    //3 依次调用
		for (Object runner : new LinkedHashSet<>(runners)) {
			if (runner instanceof ApplicationRunner applicationRunner) {
				callRunner(applicationRunner, args);
			}
			if (runner instanceof CommandLineRunner commandLineRunner) {
				callRunner(commandLineRunner, args);
			}
		}
	}
}

启动失败处理

springboot 启动过程中如果发送了异常,就会进入失败处理

  1. 确定并处理程序退出码,如果是非正常退出,还会发布 ExitCodeEvent 事件
  2. 发布 failed 事件
  3. 打印当前的错误原因
  4. 关闭上下文,并从 shutdownHook 中移除该上下文(如果上下文是创建成功的情况)
  5. 再从抛出该异常
public class SpringApplication {

  private void handleRunFailure(ConfigurableApplicationContext context, Throwable exception, SpringApplicationRunListeners listeners) {
		try {
			try {
        //1. 确定并处理程序退出码
				handleExitCode(context, exception);
        //2 发布failed事件
				if (listeners != null) {
					listeners.failed(context, exception);
				}
			}
			finally {
        //3 打印当前的错误原因
				reportFailure(getExceptionReporters(context), exception);
				if (context != null) {
          //4 关闭上下文
					context.close();
          //5 JVMshutdown回调中移除该context
					shutdownHook.deregisterFailedApplicationContext(context);
				}
			}
		}
		catch (Exception ex) {
			logger.warn("Unable to close ApplicationContext", ex);
		}
    //6 抛出该异常
		ReflectionUtils.rethrowRuntimeException(exception);
	}

  //处理退出码
  private void handleExitCode(ConfigurableApplicationContext context, Throwable exception) {
    //1 确定退出码
    // 内部会借助ExitCodeGenerators从ExitCodeExceptionMapper中获取退出码
		int exitCode = getExitCodeFromException(context, exception);
		if (exitCode != 0) {
			if (context != null) {
        // 2 如果是非正常退出 ,则发布ExitCodeEvent事件
				context.publishEvent(new ExitCodeEvent(context, exitCode));
			}
      //3 设定exitCode,用于程序结束的退出码
			SpringBootExceptionHandler handler = getSpringBootExceptionHandler();
			if (handler != null) {
				handler.registerExitCode(exitCode);
			}
		}
	}

}

SpringBoot 启动总结

至此 SpringBoot 的启动步骤就介绍完毕了,小结一下 springboot 的启动:

在 SpringApplication 的构造函数中,首先设置了主配置源,然后推断出应用的类型,这个应用类型用于确定 Environment 和 ApplicationContext 的类型,再从 spring.factories 中读取了引导上下文的初始化器、应用上下文的初始化器、应用监听器等,最后从调用栈中找出 main 方法的所在主类。

在启动方法 run 中,首先创建并初始化了引导上下文、创建启动过程监听器,发布 starting 事件表示 springboot 应用启动了,然后创建并配置了环境,并发出 environmentPrepared 事件,之后就打印出标语。在环境都准备好了之后,就要开始创建应用上下文了,这里上下文的类型是由构造函数中推断出的应用类型决定的,调用上下文工厂创建出上下文后,就开始了配置上下文的过程,先是设置应用上下文的环境,然后调用上下文初始化器进行初始化,初始化完毕后,就可以认为上下文是准备好了的,于是发布 contextPrepared 事件。接着关闭引导上下文,向应用上下文 IOC 中添加单例对象和工厂后处理器,随后把配置的 source 全部加载成 IOC 的 BeanDefinition。 在这些源都加载完毕后,就可以刷新上下文了。刷新完毕,发布 started 事件,应用就是启动好了,紧接着把注册的 Runner 全部调用,至此容器就进入了 ready,可以对外提供服务了。


更新记录:

  • 2023-02-17
    • 把BootStrapContext的名字从"启动上下文"改为"引导上下文"