12

1.简介

Spring Boot是由Pivotal团队提供的快速开发框架,基于SpringMVC通过注解+内置Http服务器如:tomcat-embed-core,简化了XML配置,快速将一些常用的第三方依赖整合(通过Maven继承依赖关系),最终实现以Java应用程序的方式进行执行。

1.1 SpringBoot起源

  • Spring框架:Spring框架从早期的IOC与AOP衍生出了很多产品例如Spring (boot、security、jpa)等等
  • SpringMVC框架:Spring MVC提供了一种轻度耦合的方式来开发web应用,它是Spring的一个web框架。通过Dispatcher Servlet, ModelAndView 和 View Resolver,开发web应用变得很容易。解决的问题领域是网站应用程序或者服务开发——URL路由、Session、模板引擎、静态Web资源等等,是基于Spring的一个 MVC 框架。
  • SpringBoot框架:Spring Boot实现了自动配置,降低了项目搭建的复杂度。它主要是为了解决使用Spring框架需要进行大量的配置太麻烦的问题,所以它并不是用来替代Spring的解决方案,而是和Spring框架紧密结合用于提升Spring开发者体验的工具。同时它集成了大量常用的第三方库配置(例如Jackson, JDBC, Mongo, Redis, Mail等等),是基于Spring4的条件注册的一套快速开发整合包。

图片描述

1.2 便捷的starter poms (启动器)

starter包含了搭建项目快速运行所需的依赖。它是一个依赖关系描述符的集合。当应用需要一种spring的其它服务时,不需要粘贴拷贝大量的依赖关系描述符。例如想在spring中使用redis,只需要在项目中包含 spring-boot-starter-redis 依赖就可以使用了,所有的starters遵循一个相似的命名模式:spring-boot-starter-,在这里是一种特殊类型的应用程序。该命名结构可以帮你找到需要的starter。很多IDEs集成的Maven允许你通过名称搜索依赖。

1.3 demo

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

这是一个简单的SpringBoot的启动类实现,下文章将围绕这个demo进行扩展分析。

1.4 SpringBoot启动流程

clipboard.png

2 @SpringBootApplication注解分析

以下是注解@SpringBootApplication的源码实现

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = {
        @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
        @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

可以发现它是由众多注解组合而成的,下面具体分析下这里每个注解所起到的作用。

  • @Target Target通过ElementType来指定注解可使用范围的枚举集合(FIELD/METHOD/PARAMETER...)
  • @Retention Retention(保留)注解说明,这种类型的注解会被保留到那个阶段. 有三个值:

    • RetentionPolicy.SOURCE —— 这种类型的Annotations只在源代码级别保留,编译时就会被忽略
    • RetentionPolicy.CLASS —— 这种类型的Annotations编译时被保留,在class文件中存在,但JVM将会忽略
    • RetentionPolicy.RUNTIME —— 这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使
  • @Documented 注解表明这个注解应该被 javadoc工具记录. 默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented,则它会被 javadoc 之类的工具处理, 所以注解类型信息也会被包括在生成的文档中
  • @Inherited 允许子类继承父类的注解,仅限于类注解有用,对于方法和属性无效。
  • @SpringBootConfiguration 注解实际上和@Configuration有相同的作用,配备了该注解的类就能够以JavaConfig的方式完成一些配置,可以不再使用XML配置。
  • @ComponentScan 这个注解完成的是自动扫描的功能,相当于Spring XML配置文件中的:<context:component-scan>,可使用basePackages属性指定要扫描的包,及扫描的条件。如果不设置则默认扫描@ComponentScan注解所在类的同级类和同级目录下的所有类,所以我们的Spring Boot项目,一般会把入口类放在顶层目录中,这样就能够保证源码目录下的所有类都能够被扫描到。
  • @EnableAutoConfiguration 这个注解是让Spring Boot的配置能够如此简化的关键性注解。我把EnableAutoConfiguration的实现端上来了,大家来鉴赏一下!

    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Inherited
    @AutoConfigurationPackage
    @Import(AutoConfigurationImportSelector.class)
    public @interface EnableAutoConfiguration {
    • @AutoConfigurationPackage 注解用于保存自动配置类以供之后的使用,比如给JPA entity扫描器,用来扫描开发人员通过注解@Entity定义的entity类。通俗的讲就是,注册bean定义到容器中。
    • @Import(AutoConfigurationImportSelector.class)是EnableAutoConfiguration注解中最关键的来,它借助AutoConfigurationImportSelector,可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器中。关于@Import注解要说的内容还比较多,改天再聊。

关于注解的话题就先谈到这里,下面开启撸代码环节。

3 剖析代码

查看SpringApplication的源代码可以发现SpringApplication的启动由两部分组成:

  • new SpringApplication(primarySources):创建SpringApplication对象
  • run(args):调用run方法

3.1 实例化SpringApplication对象

源码如下:

    public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
        this.resourceLoader = resourceLoader;//1、初始化资源加载器
        Assert.notNull(primarySources, "PrimarySources must not be null");//2、断言资源加载类不能为 null
        this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));//3、初始化加载资源类集合并去重
        this.webApplicationType = deduceWebApplicationType();//4、 推断应用类型是Standard还是Web
        setInitializers((Collection) getSpringFactoriesInstances(
                ApplicationContextInitializer.class));//5、设置应用上下文初始化器
        setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));//6、设置监听器 
        this.mainApplicationClass = deduceMainApplicationClass();//7、推断应用入口类
    }

下面将针对源码中的重要实现进行详细的分析。

3.1.1 初始化资源加载器

ResourceLoader接口,在 Spring 中用于加载资源,通过它可以获取一个Resouce 对象。使用spring的朋友都知道它加载资源的方式由多种,下面就挑两个常用的继承ResourceLoader的接口与实现类说起。

  • DefaultResourceLoader : 作为 ResourceLoader 接口的直接实现类,该类实现了基本的资源加载功能,可以实现对单个资源的加载。
  • ResourcePatternResolver :该接口继承了 ResourceLoader,定义了加载多个资源的方法, 可以实现对多个资源的加载。
1、DefaultResourceLoader

上面介绍过该类通过实现 ResourceLoader 接口实现了加载单个资源的功能。它的子类通过继承它来实现具体的资源访问策略。下面来探究下该类如何加载单个资源:

    public Resource getResource(String location) {//这里是三种识别location加载出Resource的方式。
        Assert.notNull(location, "Location must not be null");
        //1.先看有没有自定义的ProtocolResolver,如果有则先根据自定义的ProtocolResolver解析location得到Resource
        for (ProtocolResolver protocolResolver : this.protocolResolvers) {
            Resource resource = protocolResolver.resolve(location, this);
            if (resource != null) {
                return resource;
            }
        }
        //2.根据路径是否匹配"/"或"classpath:"来解析得到ClassPathResource
        if (location.startsWith("/")) {
            return getResourceByPath(location);
        }else if (location.startsWith(CLASSPATH_URL_PREFIX)) {//classpath
            return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader());
        }else {
            try {
                //默认传入的location是一个URL路径,加载得到一个UrlResource
                URL url = new URL(location);
                return (ResourceUtils.isFileURL(url) ? new FileUrlResource(url) : new UrlResource(url));
            }
            catch (MalformedURLException ex) {
                // 如果以上三种情况都不满足,则按照“/”来处理
                return getResourceByPath(location);
            }
        }
    }

扫盲:ProtocolResolver是解析location的自定义拓展类,有了它我们才能随意传入不同格式的location,然后根据对应的格式去解析并获得我们的Resource即可。
扩展:在Spring容器初始化过程中,我们可以自定义一个类实现ProtocolResolver接口,然后实现该resolve方法,就可以解析特定的location得到Resoure。

2、ResourcePatternResolver

该接口继承了ResourceLoader接口,在其基础上增加了同时对多个资源的访问功能。

public interface ResourcePatternResolver extends ResourceLoader {
    String CLASSPATH_ALL_URL_PREFIX = "classpath*:";

    // 例如使用 ant 风格的路径,匹配路径下的多个资源
    Resource[] getResources(String locationPattern) throws IOException;
}

PathMatchingResourcePatternResolver是 ResourcePatternResolver 接口的直接实现类,它是基于模式匹配的,默认使用AntPathMatcher 进行路径匹配,它除了支持 ResourceLoader 支持的前缀外,还额外支持 “classpath*” ,下面查看源码看看它如何实现对多个资源的访问。

    public Resource[] getResources(String locationPattern) throws IOException {
        Assert.notNull(locationPattern, "Location pattern must not be null");
         //首先判断资源路径是否是类路径下的资源(以 “classpath*:” 开头)
        if (locationPattern.startsWith(CLASSPATH_ALL_URL_PREFIX)) {
            // 通过 getPathMatcher 方法取得 PathMatcher ,默认只有 AntPathMatcher 一个实现类
           // 通过 isPattern 方法判断路径是否允许存在多个匹配的资源(路径中包含 “*” 或 “?”)
            if (getPathMatcher().isPattern(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()))) {
                //找到所有匹配路径(ant 风格)的资源
                return findPathMatchingResources(locationPattern);
            }
            else {
                //通过类加载器查找
                return findAllClassPathResources(locationPattern.substring(CLASSPATH_ALL_URL_PREFIX.length()));
            }
        }
        else {
            int prefixEnd = (locationPattern.startsWith("war:") ? locationPattern.indexOf("*/") + 1 :
                    locationPattern.indexOf(':') + 1);
            // 判断资源路径 ":" 之后的部分是否包含 "*" 或 "?"
            if (getPathMatcher().isPattern(locationPattern.substring(prefixEnd))) {
                // a file pattern
                return findPathMatchingResources(locationPattern);
            }
            else {
                // 若不存在表示是单个资源,则通过从构造函数传入的 ResourceLoader 取得
                return new Resource[] {getResourceLoader().getResource(locationPattern)};
            }
        }
    }

扩展:查看源码发现ApplicationContext接口也继承了 ResourcePatternResolver 接口,说明它也集成了对对单个或多个资源的访问功能。

  • 当 Spring 需要进行资源访问时,实际上并不需要直接使用 Resource 实现类,而是调用 getResource 方法来获取资源。
  • 当通过 ApplicationContext 实例获取 Resource 实例时,它将会负责选择具体的 Resource 的实现类。代码如下:
    Resource res = ctx.getResource("some/resource/path/myTemplate.txt);

Spring采用和 ApplicationContext 相同的策略来访问资源。也就是说:

  • 如果 ApplicationContext 是 FileSystemXmlApplicationContext,res 就是 FileSystemResource 实例;
  • 如果 ApplicationContext 是 ClassPathXmlApplicationContext,res 就是 ClassPathResource 实例;
  • 如果 ApplicationContext 是 XmlWebApplicationContext,res 是 ServletContextResource 实例。

也就是说 ApplicationContext 将会确定具体的资源访问策略,从而将应用程序和具体的资源访问策略分离开来,这就体现了策略模式的优势。

3.1.2 设置应用上下文初始化器setInitializers

  • 介绍:initializers是SpringApplication中的一个实例属性:List<ApplicationContextInitializer<?>> initializers,每一个initailizer都是一个实现了ApplicationContextInitializer接口的实例。ApplicationContextInitializer是Spring IOC容器中提供的一个接口,源码如下:
    public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext>{
        void initialize(C applicationContext);
    }
  • 作用:ApplicationContextInitializer接口的作用就是在spring prepareContext的时候做一些初始化工作,在spring 初始化的过程中 执行prepareContext方法的时候里面会通过applyInitializers方法回调所有ApplicationContextInitializer接口的实现。所以在SpringApplication的构造方法中执行了setInitializers方法,该方法是把初始化的ApplicationContextInitializer实现类全部加载到SpringApplication内部的集合中。
  • 实现:这些初始化器(initializers)是Spring Boot从本地的META-INF/spring.factories文件和jar文件中的META-INF/spring.factories 文件获取配置的初始化类(注意这边是获取了配置文件的全部配置类),然后通过loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());

}方法获得ApplicationContextInitializer接口全部实现类的完整名称,最后通过反射的机制获得ApplicationContextInitializer实现类。

  • 使用:通过getSpringFactoriesInstances(

ApplicationContextInitializer.class)方法获得实现类

3.1.3 设置监听器(setListeners)

listeners成员变量,是一个ApplicationListener<?>类型对象的集合。可以看到获取该成员变量内容使用的是跟成员变量initializers一样的方法,只不过传入的类型从ApplicationContextInitializer.class变成了ApplicationListener.class。

    public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
        void onApplicationEvent(E event);
    }

这个接口基于JDK中的EventListener接口,实践了观察者模式。对于Spring框架的观察者模式实现,它限定感兴趣的事件类型需要是ApplicationEvent类型的子类,而这个类同样是继承自JDK中的EventObject类。

3.1.4 推断应用入口类

该方法通过构造一个运行时异常,通过异常栈中方法名为main的栈帧来得到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;
    }

3.2 run方法

源码如下:

public ConfigurableApplicationContext run(String... args) {
    //1、计时监控类
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    
    // 2、初始化应用上下文和异常报告集合
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    
    // 3、设置系统属性java.awt.headless的值,默认值为:true(没有图形化界面)
    configureHeadlessProperty();
    
    // 4、创建所有 Spring 运行监听器并发出开始执行的事件
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        // 5、初始化默认应用参数类
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(
                args);
                
        // 6、根据SpringApplicationRunListeners和应用参数来准备 Spring 环境
        ConfigurableEnvironment environment = prepareEnvironment(listeners,
                applicationArguments);
        configureIgnoreBeanInfo(environment);
        
        // 7、准备Banner打印器 - 就是启动Spring Boot的时候打印在console上的ASCII艺术字体
        Banner printedBanner = printBanner(environment);
        
        // 8、创建Spring上下文
        context = createApplicationContext();
        
        // 9、准备异常报告器
        exceptionReporters = getSpringFactoriesInstances(
                SpringBootExceptionReporter.class,
                new Class[] { ConfigurableApplicationContext.class }, context);
                
        // 10、Spring上下文前置处理
        prepareContext(context, environment, listeners, applicationArguments,
                printedBanner);
                
        // 11、刷新Spring上下文
        refreshContext(context);
        
        // 12、Spring上下文后置处理
        afterRefresh(context, applicationArguments);
        
        // 13、停止计时监控类
        stopWatch.stop();
        
        // 14、输出日志记录执行主类名、时间信息
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
        }
        
        // 15、发布应用上下文启动完成事件
        listeners.started(context);
        
        // 16、执行所有 Runner 运行器
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }
 
    try {
        // 17、发布应用上下文就绪事件
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    // 18、返回应用上下文
    return context;
}

下面针对run方法中提到的一些重要步骤进行阐述:

3.2.1 计时监控类

启动方法如下:

    public void start() throws IllegalStateException {
        start("");
    }
    
    public void start(String taskName) throws IllegalStateException {
        if (this.currentTaskName != null) {
            throw new IllegalStateException("Can't start StopWatch: it's already running");
        }
        this.currentTaskName = taskName;
        this.startTimeMillis = System.currentTimeMillis();
    }

你可以看到它传入了一个空字符串给当前任务作为任务名称,然后记录当前Spring Boot应用启动的开始时间。并且它会判断当前任务名是否存在,用于保证Spring Boot应用不重复启动。

3.2.2 初始化应用上下文和异常报告集合

这里仅初始化一个应用上下文对象context和一个空的异常报告集合,具体用途看下文。

3.2.3 设置系统属性

configureHeadlessProperty的方法实现如下:

    private void configureHeadlessProperty() {
        System.setProperty(SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, System.getProperty(
                SYSTEM_PROPERTY_JAVA_AWT_HEADLESS, Boolean.toString(this.headless)));
    }
    private static final String SYSTEM_PROPERTY_JAVA_AWT_HEADLESS = "java.awt.headless";

该方法设置了一个名为java.awt.headless的系统属性,观其源码可以发现它给属性设值System.setProperty(),而它的值来源于System.getProperty(),这并不奇怪,因为System中getProperty()有2个重载方法,其中getProperty()有单参和双参两个方法,这里调用的是双参数方法,该方法在没有的时候会返回一个调用者指定的默认值,所以这里先获取后设置。这个设置的目的是保证即使没有检测到显示器(服务器不需要显示器),也允许程序启动。

3.2.4 创建所有 Spring 运行监听器并发出开始执行的事件

实现如下:

//根据args获取所有SpringApplicationRunListeners监听器
SpringApplicationRunListeners listeners = getRunListeners(args);

//这段代码大家应该已经熟悉了,获取SpringApplicationRunListeners扩展
private SpringApplicationRunListeners getRunListeners(String[] args) {
    Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};
    return new SpringApplicationRunListeners(logger, getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
}

通过Class<?>[] types = new Class<?>[]{SpringApplication.class, String[].class};类型加载对应的监听器,并创建SpringApplicationRunListener实例

下面的内容和之前实例化初始化器的流程是一样,通过getSpringFactoriesInstances方法从spring.factories中获取与SpringApplicationRunListener.class相关的实例类名列表。

3.2.5 初始化默认应用参数类

ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);

args是启动Spring应用的命令行参数,该参数可以在Spring应用中被访问。
如:--server.port=9000

3.2.6 根据运行监听器和应用参数来准备 Spring 环境

创建并配置当前SpringBoot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile(其作用就是指定激活的配置文件,可以区分环境来加载不同的配置)),并遍历调用所有的SpringApplicationRunListener的environmentPrepared()方法,广播Environment准备完毕。
源码如下:

    private ConfigurableEnvironment prepareEnvironment(
            SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments) {
        //获取或创建环境(存在就直接返回,不存在创建一个再返回)
        ConfigurableEnvironment environment = getOrCreateEnvironment();
        //配置环境:配置PropertySources和activeProfiles
        configureEnvironment(environment, applicationArguments.getSourceArgs());
        //listeners环境准备(就是广播ApplicationEnvironmentPreparedEvent事件)。
        listeners.environmentPrepared(environment);
        //将环境绑定到SpringApplication
        bindToSpringApplication(environment);
        //如果非web环境,将环境转换成StandardEnvironment
        if (this.webApplicationType == WebApplicationType.NONE) {
            environment = new EnvironmentConverter(getClassLoader())
                    .convertToStandardEnvironmentIfNecessary(environment);
        }
        //配置PropertySources对它自己的递归依赖
        ConfigurationPropertySources.attach(environment);
        return environment;
    }

prepareEnvironment的作用:加载外部化配置资源到environment,包括命令行参数、servletConfigInitParams、servletContextInitParams、systemProperties、sytemEnvironment、random、application.yml(.yaml/.xml/.properties)等;初始化日志系统。

3.2.7 准备Banner打印器

该功能仅供自娱自乐,不做过多解读,看源码便是。

    private Banner printBanner(ConfigurableEnvironment environment) {
        if (this.bannerMode == Banner.Mode.OFF) {
            return null;
        }
        ResourceLoader resourceLoader = (this.resourceLoader != null ? this.resourceLoader
                : new DefaultResourceLoader(getClassLoader()));
        SpringApplicationBannerPrinter bannerPrinter = new SpringApplicationBannerPrinter(
                resourceLoader, this.banner);
        if (this.bannerMode == Mode.LOG) {
            return bannerPrinter.print(environment, this.mainApplicationClass, logger);
        }
        return bannerPrinter.print(environment, this.mainApplicationClass, System.out);
    }

3.2.8 创建Spring上下文

源码如下:

public static final String DEFAULT_SERVLET_WEB_CONTEXT_CLASS = "org.springframework.boot.web.servlet.context.AnnotationConfigServletWebServerApplicationContext";

public static final String DEFAULT_REACTIVE_WEB_CONTEXT_CLASS = "org.springframework.boot.web.reactive.context.AnnotationConfigReactiveWebServerApplicationContext";

public static final String DEFAULT_CONTEXT_CLASS = "org.springframework.context.annotation.AnnotationConfigApplicationContext";

protected ConfigurableApplicationContext createApplicationContext() {
        // 先判断有没有指定的实现类
        Class<?> contextClass = this.applicationContextClass;
        // 如果没有,则根据应用类型选择
        if (contextClass == null) {
            try {
                //根据webApplicationType的类型去反射创建ConfigurableApplicationContext的具体实例。
                switch (this.webApplicationType) {
                case SERVLET:
                    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);
    }

createApplicationContext()该方法的逻辑较为简单,共两个分支:

  • 自定义ApplicationContext的实现类
  • 根据当前应用的类型webApplicationType来匹配对应的ApplicationContext,是servlet、reactive或者非web应用

3.2.9 准备异常报告器

这一步的逻辑和实例化初始化器和监听器的一样,都是通过调用 getSpringFactoriesInstances 方法来获取配置的异常类名称并实例化所有的异常处理类。

3.2.10 Spring上下文前置处理

源码如下:

    private void prepareContext(ConfigurableApplicationContext context,
            ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
            ApplicationArguments applicationArguments, Banner printedBanner) {
        //设置容器环境,包括各种变量
        context.setEnvironment(environment);
 
        //设置上下文的 bean 生成器和资源加载器
        postProcessApplicationContext(context);
 
        //执行容器中的ApplicationContextInitializer(包括 spring.factories和自定义的实例)
        applyInitializers(context);
 
        //触发所有 SpringApplicationRunListener 监听器的 contextPrepared 事件方法
        listeners.contextPrepared(context);
 
        //记录启动日志
        if (this.logStartupInfo) {
            logStartupInfo(context.getParent() == null);
            logStartupProfileInfo(context);
        }
 
        // 添加特定于引导的单例bean
context.getBeanFactory().registerSingleton("springApplicationArguments",
                applicationArguments);
        if (printedBanner != null) {
            context.getBeanFactory().registerSingleton("springBootBanner", printedBanner);
        }
        // 加载所有资源
        Set<Object> sources = getAllSources();
        Assert.notEmpty(sources, "Sources must not be empty");
        //加载我们的启动类,将启动类注入容器
        load(context, sources.toArray(new Object[0]));
 
        //触发所有 SpringApplicationRunListener 监听器的 contextLoaded 事件方法
        listeners.contextLoaded(context);
    }

这块会对整个上下文进行一个预处理,比如触发监听器的响应事件、加载资源、设置上下文环境等等。

3.2.11 刷新Spring上下文

源码如下:

    private void refreshContext(ConfigurableApplicationContext context) {
        refresh(context);
        if (this.registerShutdownHook) {
            try {
                //向JVM运行时注册一个关机钩子,在JVM关机时关闭这个上下文
                context.registerShutdownHook();
            }
            catch (AccessControlException ex) {
                // Not allowed in some environments.
            }
        }
    }

这块主要做了两件事:

  1. 通过refresh方法对整个IoC容器的初始化(包括Bean资源的定位、解析、注册等等)
  2. 通过context.registerShutdownHook()(向JVM运行时注册一个关机钩子,在JVM关机时关闭这个上下文,除非它当时已经关闭。)

3.2.12 Spring上下文后置处理

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

该方法没有实现,可以根据需要做一些定制化的操作。

3.2.13 停止计时监控类

源码如下:

    public void stop() throws IllegalStateException {
        if (this.currentTaskName == null) {
            throw new IllegalStateException("Can't stop StopWatch: it's not running");
        }
        long lastTime = System.currentTimeMillis() - this.startTimeMillis;
        this.totalTimeMillis += lastTime;
        this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime);
        if (this.keepTaskList) {
            this.taskList.add(this.lastTaskInfo);
        }
        ++this.taskCount;
        this.currentTaskName = null;
    }

该方法主要是做计时监听器停止操作,并统计一些任务执行信息。

3.2.14 输出日志记录执行主类名、时间信息

源码如下:

if (this.logStartupInfo) {
    new StartupInfoLogger(this.mainApplicationClass)
                    .logStarted(getApplicationLog(), stopWatch);
}

用于打印主类信息和时间信息。

3.2.15 发布应用上下文启动完成事件

源码如下:

public void started(ConfigurableApplicationContext context) {
    for (SpringApplicationRunListener listener : this.listeners) {
        listener.started(context);
    }
}

执行所有SpringApplicationRunListener实现的started方法。

3.2.16 执行所有 Runner 运行器

Runner 运行器用于在服务启动时进行一些业务初始化操作,这些操作只在服务启动后执行一次。
Spring Boot提供了ApplicationRunner和CommandLineRunner两种服务接口CommandLineRunner、ApplicationRunner
对比

  • 相同点

    • 两者均在服务启动完成后执行,并且只执行一次。
    • 两者都能获取到应用的命令行参数。
    • 两者在执行时机上是一致的(可以通过Ordered相关的接口或注解来实现自定义执行优先级。)。
  • 不同点

    • 虽然两者都是获取到应用的命令行参数,但是ApplicationRunner获取到的是封装后的ApplicationArguments对象,而CommandLine获取到的是ApplicationArguments中的sourceArgs属性(List<String>),即原始参数字符串列表(命令行参数列表)。

源码如下:

    private void callRunners(ApplicationContext context, ApplicationArguments args) {
        List<Object> runners = new ArrayList<>();
// 从Spring容器中获取ApplicationRunner实现类        runners.addAll(context.getBeansOfType(ApplicationRunner.class).values());
// 从Spring容器中获取CommandLineRunner实现类        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);
            }
        }
    }

ApplicationRunner还是CommandLineRunner,都是在应用启动完成后执行一次业务初始化代码,达到的效果也类似,由于ApplicationRunner的方法参数是ApplicationArguments对象,使用起来更加方便,所以更推荐使用。

3.2.17 发布应用上下文就绪事件

源码如下

    public void running(ConfigurableApplicationContext context) {
        for (SpringApplicationRunListener listener : this.listeners) {
            listener.running(context);
        }
    }

触发所有 SpringApplicationRunListener 监听器的 running 事件方法。

总结

SpirngBoot启动流程到这里就分析完了、接下来的文章会针对SpringBoot的特定场景进行分析,希望每个看到这篇文章的朋友都能有所收获,同时也欢迎大家多提宝贵意见共同成长。


模范青蛙
65 声望15 粉丝

大道无情,唯变化永恒