zoukankan      html  css  js  c++  java
  • SpringBoot是如何启动的?这篇文章告诉你答案!

    本文是通过查看SpringBoot源码整理出来的SpringBoot大致启动流程,整体大方向是以简单为出发点,不说太多复杂的东西,内部实现细节本文不深扣因为每个人的思路、理解都不一样,我个人看的理解跟大家看的肯定不一样,到时候表达的出来的云里雾里也没啥用。

    首先我将SpringBoot的启动流程整理成以下阶段:

    • SpringApplicaiton初始化
      • 审查ApplicationContext类型
      • 加载ApplicationContextInitializer
      • 加载ApplicationListener
    • Environment初始化
      • 解析命令行参数
      • 创建Environment
      • 配置Environment
      • 配置SpringApplication
    • ApplicationContext初始化
      • 创建ApplicationContext
      • 设置ApplicationContext
      • 刷新ApplicationContext
    • 运行程序入口

    省去了一些不影响主流程的细节,在查看SpringBoot源码之前,不得不提一下spring.factories这个文件的使用和功能。

    关于spring.factories

    spring.factories是一个properties文件,它位于classpath:/META-INF/目录里面,每个jar包都可以有spring.factories的文件。Spring提供工具类SpringFactoriesLoader负责加载、解析文件,如spring-boot-2.2.0.RELEASE.jar里面的META-INF目录里面就有spring.factories文件:

    # PropertySource Loaders
    org.springframework.boot.env.PropertySourceLoader=
    org.springframework.boot.env.PropertiesPropertySourceLoader,
    org.springframework.boot.env.YamlPropertySourceLoader
    
    # Run Listeners
    org.springframework.boot.SpringApplicationRunListener=
    org.springframework.boot.context.event.EventPublishingRunListener
    ...
    

    关于spring.factories需要知道些什么?

    • spring.factories是一个properties文件
    • spring.factories里的键值对的value是以逗号分隔的完整类名列表
    • spring.factories里的键值对的key是完整接口名称
    • spring.factories键值对的value是key的实现类
    • spring.factories是由SpringFactoriesLoader工具类加载
    • spring.factories位于classpath:/META-INF/目录
    • SpringFactoriesLoader会加载jar包里面的spring.factories文件并进行合并

    知道spring.factories的概念后,继续来分析SpringBoot的启动。

    SpringApplicaiton初始化

    Java程序的入口在main方法SpringBoot的同样可以通过main方法启动,只需要少量的代码加上@SpringBootApplication注解,很容易的就启动SpringBoot:

    @SpringBootApplication
    @Slf4j
    public class SpringEnvApplication {
    	public static void main(String[] args) {
    		ConfigurableApplicationContext context = SpringApplication.run(SpringEnvApplication.class, args);
     	}
    
    }
    

    SpringApplicaiton初始化位于SpringApplication的构造函数中:

        public SpringApplication(Class<?>... primarySources) {
    		this(null, primarySources);
    	}
     
    	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    		this.resourceLoader = resourceLoader;
    		Assert.notNull(primarySources, "PrimarySources must not be null");
    		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    		this.webApplicationType = WebApplicationType.deduceFromClasspath();
    		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    		this.mainApplicationClass = deduceMainApplicationClass();
    	}
    

    简单的说下SpringApplication的构造函数干了些啥:

    • 基础变量赋值(resourceLoader、primarySources、...)
    • 审查ApplicationContext类型如(Web、Reactive、Standard)
    • 加载ApplicationContextInitializer
    • 加载ApplicationListener
    • 审查启动类(main方法的类)

    然后再来逐个分析这些步骤。

    审查ApplicationContext类型

    SpringBoot会在初始化阶段审查ApplicationContext的类型,审查方式是通过枚举WebApplicationTypededuceFromClasspath静态方法:

    	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;
    	}
    

    WebApplicationType枚举用于标记程序是否为Web程序,它有三个值:

    • NONE:不是web程序
    • SERVLET:基于Servlet的Web程序
    • REACTIVE:基于Reactive的Web程序

    简单的来说该方法会通过classpath来判断是否Web程序,方法中的常量是完整的class类名:

    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";
    

    例如通过pom.xml文件引入spring-boot-starter-web那classpath就会有org.springframework.web.context.ConfigurableWebApplicationContextjavax.servlet.Servlet类,这样就决定了程序的ApplicationContext类型为WebApplicationType.SERVLET

    加载ApplicationContextInitializer

    ApplicationContextInitializer会在刷新context之前执行,一般用来做一些额外的初始化工程如:添加PropertySource、设置ContextId等工作它只有一个initialize方法:

    public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
    	void initialize(C applicationContext);
    }
    

    SpringBoot通过SpringFactoriesLoader加载spring.factories中的配置读取key为org.springframework.context.ApplicationContextInitializer的value,前面提到过spring.factoies中的配置的value都为key的实现类:

    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
    

    上面列出的是spring-boot-2.2.0.RELEASE.jar中包含的配置,其他jar包也有可能配置org.springframework.context.ApplicationContextInitializer来实现额外的初始化工作。

    加载ApplicationListener

    ApplicationListener用于监听ApplicationEvent事件,它的初始加载流程跟加载ApplicationContextInitializer类似,在spring.factories中也会配置一些优先级较高的ApplicationListener

    # Application Listeners
    org.springframework.context.ApplicationListener=
    org.springframework.boot.ClearCachesApplicationListener,
    org.springframework.boot.builder.ParentContextCloserApplicationListener,
    org.springframework.boot.context.FileEncodingApplicationListener,
    org.springframework.boot.context.config.AnsiOutputApplicationListener,
    org.springframework.boot.context.config.ConfigFileApplicationListener,
    org.springframework.boot.context.config.DelegatingApplicationListener,
    org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,
    org.springframework.boot.context.logging.LoggingApplicationListener,
    org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener
    

    ApplicationListener的加载流程跟ApplicationContextInitializer类似都是通过SpringFactoriesLoader加载的。

    小结

    完成初始化阶段后,可以知道以下信息:

    • ApplicationContext是Web还是其他类型
    • SpringApplication中有一些ApplicationContextInitializer实现类
    • SpringApplication中有一些ApplicationListener的实现类

    Environment初始化

    初始化工作完成后SpringBoot会干很多事情来为运行程序做好准备,SpringBoot启动核心代码大部分都位于SpringApplication实例的run方法中,在环境初始化大致的启动流程包括:

    • 解析命令行参数
    • 准备环境(Environment)
    • 设置环境

    当然还会有一些别的操作如:

    • 实例化SpringApplicationRunListeners
    • 打印Banner
    • 设置异常报告
    • ...

    这些不是重要的操作就不讲解了,可以看完文章再细细研究。

    解析命令行参数

    命令行参数是由main方法的args参数传递进来的,SpringBoot在准备阶段建立一个DefaultApplicationArguments类用来解析、保存命令行参数。如--spring.profiles.active=dev就会将SpringBoot的spring.profiles.active属性设置为dev。

    public ConfigurableApplicationContext run(String... args) {
        ...
    	ApplicationArguments applicationArguments = new DefaultApplicationArguments(args); 
    	...
    }
    

    SpringBoot还会将收到的命令行参数放入到Environment中,提供统一的属性抽象。

    创建Environment

    创建环境的代码比较简单,根据之前提到过的WebApplicationType来实例化不同的环境:

    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();
    	}
    }
    

    准备Environment

    环境(Environment)大致由Profile和PropertyResolver组成:

    • Profile是BeanDefinition的逻辑分组,定义Bean时可以指定Profile使SpringBoot在运行时会根据Bean的Profile决定是否注册Bean
    • PropertyResolver是专门用来解析属性的,SpringBoot会在启动时加载配置文件、系统变量等属性

    SpringBoot在准备环境时会调用SpringApplicationprepareEnvironment方法:

    private ConfigurableEnvironment prepareEnvironment(SpringApplicationRunListeners listeners,
    		ApplicationArguments applicationArguments) {
    	// Create and configure the environment
    	ConfigurableEnvironment environment = getOrCreateEnvironment();
    	configureEnvironment(environment, applicationArguments.getSourceArgs());
    	ConfigurationPropertySources.attach(environment);
    	listeners.environmentPrepared(environment);
    	bindToSpringApplication(environment);
        ...
    	return environment;
    }
    

    prepareEnvironment方法大致完成以下工作:

    • 创建一个环境
    • 配置环境
    • 设置SpringApplication的属性

    配置Environment

    创建完环境后会为环境做一些简单的配置:

    protected void configureEnvironment(ConfigurableEnvironment environment, String[] args) {
    	if (this.addConversionService) {
    		ConversionService conversionService = ApplicationConversionService.getSharedInstance();
    		environment.setConversionService((ConfigurableConversionService) conversionService);
    	}
    	configurePropertySources(environment, args);
    	configureProfiles(environment, args);
    }
    
    protected void configurePropertySources(ConfigurableEnvironment environment, String[] args) {
    	
    	if (this.addCommandLineProperties && args.length > 0) {
    		    ...
    			sources.addFirst(new SimpleCommandLinePropertySource(args));
    			...
    		}
    	}
    	protected void configureProfiles(ConfigurableEnvironment environment, String[] args) {
    		Set<String> profiles = new LinkedHashSet<>(this.additionalProfiles);
    		profiles.addAll(Arrays.asList(environment.getActiveProfiles()));
    		environment.setActiveProfiles(StringUtils.toStringArray(profiles));
    	}
    	
    

    篇幅有限省去一些不重要的代码,配置环境主要用于:

    • 设置ConversionService: 用于属性转换
    • 将命令行参数添加到环境中
    • 添加额外的ActiveProfiles

    SpringApplicaton属性设置

    配置SpringApplicaton主要是将已有的属性连接到SpringApplicaton实例,如spring.main.banner-mode属性就对应于bannerMode实例属性,这一步的属性来源有三种(没有自定义的情况):

    • 环境变量
    • 命令行参数
    • JVM系统属性

    SpringBoot会将前缀为spring.main的属性绑定到SpringApplicaton实例:

    protected void bindToSpringApplication(ConfigurableEnvironment environment) {
    	try {
    		Binder.get(environment).bind("spring.main", Bindable.ofInstance(this));
    	}
    	catch (Exception ex) {
    		throw new IllegalStateException("Cannot bind to SpringApplication", ex);
    	}
    }
    

    Environment初始化小结

    总结下环境准备阶段所做的大致工作:

    • 根据WebApplicationType枚举创建环境
    • 设置ConversionService用于转换属性变量
    • 将命令行参数args添加到环境
    • 将外部设置的Profiles添加到环境
    • 绑定SprinngApplicaiton属性
    • 发送环境Prepared事件

    ApplicationContext初始化

    前面提到的一些步骤大部分都是为了准备ApplicationContext所做的工作,ApplicationContext提供加载Bean、加载资源、发送事件等功能,SpringBoot在启动过程中创建、配置好ApplicationContext不需要开发都作额外的工作(太方便啦~~)。

    本文不打算深入ApplicationContext中,因为与ApplicationContext相关的类很多,不是一两篇文章写的完的,建议按模块来看,最后再整合起来看ApplicationContext源码

    创建ApplicationContext

    创建ApplicationContext的过程与创建环境基本模相似,根据WebApplicationType判断程序类型创建不同的ApplicationContext

    protected ConfigurableApplicationContext createApplicationContext() {
    	Class<?> contextClass = this.applicationContextClass;
    	if (contextClass == null) {
    		try {
    			switch (this.webApplicationType) {
    			case SERVLET:
    				contextClass = Class.forName(DEFAULT_SERVLET_WEB_CONTEXT_CLASS);
    				break;
    			case REACTIVE:
    				contextClass = Class.forName(DEFAULT_REACTIVE_WEB_CONTEXT_CLASS);
    				break;
    			default:
    				contextClass = Class.forName(DEFAULT_CONTEXT_CLASS);
    			}
    		}
    		catch (ClassNotFoundException ex) {
    			throw new IllegalStateException(
    					"Unable create a default ApplicationContext, please specify an ApplicationContextClass", ex);
    		}
    	}
    	return (ConfigurableApplicationContext) BeanUtils.instantiateClass(contextClass);
    }
    

    前面提到过WebApplicationType有三个成员(SERVLET,REACTIVE,NONE),分别对应不同的context类型为:

    • SERVLET: AnnotationConfigServletWebServerApplicationContext
    • REACTIVE: AnnotationConfigReactiveWebServerApplicationContext
    • NONE: AnnotationConfigApplicationContext

    准备ApplicationContext

    创建完ApplicationContext完后需要初始化下它,设置环境、应用ApplicationContextInitializer、注册Source类等,SpringBoot的准备Context的流程可以归纳如下:

    • ApplicationContext设置环境(之前创建的环境)
    • 基础设置操作设置BeanNameGenerator、ResourceLoader、ConversionService等
    • 执行ApplicationContextInitializerinitialize方法(ApplicationContextInitializer是在初始化阶段获取的)
    • 注册命令行参数(springApplicationArguments)
    • 注册Banner(springBootBanner)
    • 注册sources(由@Configuration注解的类)

    准备ApplicationContext的代码如下所示:

    private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment,
    		SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
    	context.setEnvironment(environment);
    	postProcessApplicationContext(context);
    	applyInitializers(context);
    	listeners.contextPrepared(context);
    	if (this.logStartupInfo) {
    		logStartupInfo(context.getParent() == null);
    		logStartupProfileInfo(context);
    	}
    	// Add boot specific singleton beans
    	ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
    	beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
    	if (printedBanner != null) {
    		beanFactory.registerSingleton("springBootBanner", printedBanner);
    	}
    	if (beanFactory instanceof DefaultListableBeanFactory) {
    		((DefaultListableBeanFactory) beanFactory)
    				.setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
    	}
    	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]));
    	listeners.contextLoaded(context);
    }
    

    注意注册sources这一步,sources是@Configuration注解的类SpringBoot根据提供的sources注册Bean,基本原理是通过解析注解元数据,然后创建BeanDefinition然后将它注册进ApplicationContext里面。

    刷新ApplicationContext

    如果说SpringBoot的是个汽车,那前面所做的操作都是开门、系安全带等基本操作了,刷新ApplicationContext就是点火了,没刷新ApplicationContext只是保存了一个Bean的定义、后处理器啥的没有真正跑起来。刷新ApplicationContext这个内容很重要,要理解ApplicationContext还是要看刷新操作的源码,
    这里先简单列一下基本步骤:

    • 准备刷新(验证属性、设置监听器)
    • 初始化BeanFactory
    • 执行BeanFactoryPostProcessor
    • 注册BeanPostProcessor
    • 初始化MessageSource
    • 初始化事件广播
    • 注册ApplicationListener
    • ...

    刷新流程步骤比较多,关联的类库都相对比较复杂,建议先看完其他辅助类库再来看刷新源码,会事半功倍。

    运行程序入口

    context刷新完成后Spring容器可以完全使用了,接下来SpringBoot会执行ApplicationRunnerCommandLineRunner,这两接口功能相似都只有一个run方法只是接收的参数不同而以。通过实现它们可以自定义启动模块,如启动dubbogRPC等。

    ApplicationRunnerCommandLineRunner的调用代码如下:

    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);
    		}
    	}
    }
    

    callRunners执行完后,SpringBoot的启动流程就完成了。

    总结

    通过查看SpringApplication的源码,发现SpringBoot的启动源码还好理解,主要还是为ApplicationContext提供一个初始化的入口,免去开发人员配置ApplicationContext的工作。SpringBoot的核心功能还是自动配置,下次分析下SpringBoot Autoconfig的源码,要充分理解SpringBoot看源码是少了的。

    看完SpringApplication的源码还有些问题值得思考:

    • SpringBoot是启动Tomcat的流程
    • SpringBoot自动配置原理
    • SpringBoot Starter自定义
    • BeanFactoryPostProcessor和BeanPostProcessor实现原理
    • ...



    《架构文摘》每天一篇架构领域重磅好文,涉及一线互联网公司应用架构(高可用、高性 能、高稳定)、大数据、机器学习等各个热门领域。

  • 相关阅读:
    python开源项目
    Appscan 10用户安装手册
    20201201-k8s的node节点和独立nginx部署会冲突
    k8s-更换证书(apiserver新添加了VIP)
    20201224-修改pod网段(calico)
    深-宝的一梦
    洛谷-P3383 【模板】线性筛素数
    洛谷-P3913 车的攻击
    洛谷-P1866 编号
    洛谷-P1100 高低位交换
  • 原文地址:https://www.cnblogs.com/xwgblog/p/11798560.html
Copyright © 2011-2022 走看看