[深入学习]Spring Boot中的SpringApplication【转载】

在Spring Boot的入口类中,我们通常是通过调用SpringApplication的run方法来启动Spring Boot项目。这节我们来深入学习下SpringApplication的一些细节。

  1. 自定义SpringApplication
    1.1 通过SpringApplication API 调整
    1.2 通过SpringApplicationBuilder API调整
  2. SpringApplication准备阶段
    2.1 配置源
    2.2 推断应用类型
    2.3 加载应用上下文初始器
    2.4 加载应用事件监听器
    2.5 推断入口类
  3. SpringApplication运行阶段
    3.1 开启事件监听
    3.2 开启运行监听器
    3.3 创建Environment
    3.4 是否打印Banner
    3.5 创建Context
    3.6 装配Context
    3.7 Refresh Context
    3.8 广播应用已开启
    3.9 执行Runner
    3.10 广播应用运行中

自定义SpringApplication

默认的我们都是直接通过SpringApplication的run方法来直接启动Spring Boot,其实我们可以通过一些API来调整某些行为。

通过SpringApplication API调整

创建一个SpringBoot项目,添加web 依赖。
然后将入口类代码修改为:

        SpringApplication application = new SpringApplication(Application.class);
        application.setBannerMode(Banner.Mode.OFF);
        application.setWebApplicationType(WebApplicationType.NONE);
        application.setAdditionalProfiles("dev");
        application.run(args);

通过调用SpringApplication的方法,我们关闭了Banner的打印,设置应用环境为非WEB应用,profiles指定为dev。除此之外,SpringApplication还包含了许多别的方法,具体可以查看源码或者官方文档:


image.png

通过SpringApplicationBuilder API调整

SpringApplicationBuilder提供了Fluent API,可以实现链式调用,下面的代码和上面的效果一致,但在编写上较为方便:

        new SpringApplicationBuilder(Application.class)
                .bannerMode(Banner.Mode.OFF)
                .web(WebApplicationType.NONE)
                .profiles("dev")
                .run(args);

SpringApplication准备阶段

SpringApplication的生命周期阶段大致可以分为准备阶段和运行阶段。
我们通过源码来查看SpringApplication的有参构造器:

    public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {
        this.sources = new LinkedHashSet();
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.addConversionService = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = new HashSet();
        this.isCustomEnvironment = false;
        this.resourceLoader = resourceLoader;
        Assert.notNull(primarySources, "PrimarySources must not be null");
        this.primarySources = new LinkedHashSet(Arrays.asList(primarySources));
        this.webApplicationType = WebApplicationType.deduceFromClasspath();
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }

通过有参构造器里的代码我们可以将SpringApplication的准备阶段分为以下几个步骤:

配置源

构造器中this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));这行代码用于加载我们配置的Spring Boot Bean源。通常我们使用SpringApplication或者SpringApplicationBuilder的构造器来直接指定源。

所谓的Spring Boot Bean源指的是某个被@SpringBootApplication注解标注的类,比如入口类:

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        SpringApplication application = new SpringApplication(Application.class);
        application.run(args);
}

我们也可以将上面的代码改为下面这种方式:

public class Application {

    public static void main(String[] args) {
       SpringApplication application = new SpringApplication(ApplicationResource.class);
        application.run(args);
}
    @SpringBootApplication
    public static class ApplicationResource {

    }
}

这样也是可行的。查看SpringApplication的单个参数构造器:

    public SpringApplication(Class... primarySources) {
        this((ResourceLoader)null, primarySources);
    }

说明我们除了配置单个源外,还可以配置多个源。

推断应用类型

构造器中这行this.webApplicationType = WebApplicationType.deduceFromClasspath();代码用于推断当前Spring Boot应用类型

Spring Boot 2.0后,应用可以分为下面三种类型:

  1. WebApplicationType.NONE:非WEB类型;
  2. WebApplicationType.REACTIVE:Web Reactive类型;
  3. WebApplicationType.SERVLET:Web Servlet类型。
    WebApplicationType.deduceFromClasspath()或根据当前应用ClassPath中是否存在相关的实现类来判断应用类型到底是哪个,deduceFromClasspath方法的源码如下所示:
    static WebApplicationType deduceFromClasspath() {
        if (ClassUtils.isPresent("org.springframework.web.reactive.DispatcherHandler", (ClassLoader)null) && !ClassUtils.isPresent("org.springframework.web.servlet.DispatcherServlet", (ClassLoader)null) && !ClassUtils.isPresent("org.glassfish.jersey.servlet.ServletContainer", (ClassLoader)null)) {
            return REACTIVE;
        } else {
            String[] var0 = SERVLET_INDICATOR_CLASSES;
            int var1 = var0.length;

            for(int var2 = 0; var2 < var1; ++var2) {
                String className = var0[var2];
                if (!ClassUtils.isPresent(className, (ClassLoader)null)) {
                    return NONE;
                }
            }

            return SERVLET;
        }
    }

我们也可以直接通过SpringApplication的setWebApplicationType方法或者SpringApplicationBuilder的web方法来指定当前应用的类型。

加载应用上下文初始器

接着下一行代码setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));用于加载应用上下文初始器ApplicationContextInitializer。

getSpringFactoriesInstances方法的源码如下所示:

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
        return this.getSpringFactoriesInstances(type, new Class[0]);
    }

    private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = this.getClassLoader();
        Set<String> names = new LinkedHashSet(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        List<T> instances = this.createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }

上面代码利用Spring工厂加载机制,实例化ApplicationContextInitializer实现类,并进行排序。
所以我们可以通过实现ApplicationContextInitializer接口用于在Spring Boot应用初始化之前执行一些自定义操作。

@Order(Ordered.HIGHEST_PRECEDENCE)
public class HelloApplicationContextInitializer implements ApplicationContextInitializer {
    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println("ConfigurableApplicationContext.id -" + configurableApplicationContext.getId());
    }
}

上面代码中实现了initialize方法,并且使用@Order注解指定优先级。其中Ordered.HIGHEST_PRECEDENCE等于Integer.MIN_VALUE,Ordered.LOWEST_PRECEDENCE等于Integer.MAX_VALUE。所以数值越小,优先级越高。

除了使用@Order注解来指定优先级外,我们也可以通过实现org.springframework.core.Ordered接口的getOrder方法来指定优先级。

public class AfterHelloApplicationContextInitializer implements ApplicationContextInitializer ,Ordered{
    @Override
    public void initialize(ConfigurableApplicationContext configurableApplicationContext) {
        System.out.println("AfterConfigurableApplicationContext.id -" + configurableApplicationContext.getId());
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

上面通过getOrder方法来指定了优先级为最低优先级。

创建好后,我们还需在工厂配置文件里配置这两个实现类。在resources目录下新建META-INF目录,并创建spring.factories文件:

# Initializers
org.springframework.context.ApplicationContextInitializer=\
top.lconcise.config.HelloApplicationContextInitializer,\
top.lconcise.config.AfterHelloApplicationContextInitializer

加载应用事件监听器

在加载完应用上下文初始器后,下一行的setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));代码加载了应用事件监听器。与加载事件上下文初始器类似,Spring Boot也是通过Spring的工厂方法来实例化ApplicationListener的实现类,并进行排序。
既然是事件监听,那么其可以监听什么事件呢?其监听的是ApplicationEvent接口的实现类,我们查看一下都有哪些事件实现了这个接口:

这里我们以ContextClosedEvent为例子来编写自定义的应用事件监听器,监听Spring上下文关闭事件。

@Order(Ordered.HIGHEST_PRECEDENCE)
public class ContextClosedEventListener implements ApplicationListener<ContextClosedEvent> {
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
        System.out.println("ContextClosedEvent: " + contextClosedEvent.getApplicationContext().getId());
    }
}
public class AfterContextClosedEventListener implements ApplicationListener<ContextClosedEvent>, Ordered {
    @Override
    public void onApplicationEvent(ContextClosedEvent contextClosedEvent) {
        System.out.println("AfterContextClosedEvent: " + contextClosedEvent.getApplicationContext().getId());
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}
# Application Listeners
org.springframework.context.ApplicationListener=\
top.lconcise.listener.ContextClosedEventListener,\
top.lconcise.listener.AfterContextClosedEventListener

推断入口类

    private Class<?> deduceMainApplicationClass() {
        try {
            StackTraceElement[] stackTrace = (new RuntimeException()).getStackTrace();
            StackTraceElement[] var2 = stackTrace;
            int var3 = stackTrace.length;

            for(int var4 = 0; var4 < var3; ++var4) {
                StackTraceElement stackTraceElement = var2[var4];
                if ("main".equals(stackTraceElement.getMethodName())) {
                    return Class.forName(stackTraceElement.getClassName());
                }
            }
        } catch (ClassNotFoundException var6) {
            ;
        }

        return null;
    }

SpringApplication运行阶段

SpringApplication的运行阶段对应SpringApplication的run方法,我们查看其源码:

    public ConfigurableApplicationContext run(String... args) {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList();
        this.configureHeadlessProperty();
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();

        Collection exceptionReporters;
        try {
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            this.configureIgnoreBeanInfo(environment);
            Banner printedBanner = this.printBanner(environment);
            context = this.createApplicationContext();
            exceptionReporters = this.getSpringFactoriesInstances(SpringBootExceptionReporter.class, new Class[]{ConfigurableApplicationContext.class}, context);
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            this.refreshContext(context);
            this.afterRefresh(context, applicationArguments);
            stopWatch.stop();
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }

            listeners.started(context);
            this.callRunners(context, applicationArguments);
        } catch (Throwable var10) {
            this.handleRunFailure(context, var10, exceptionReporters, listeners);
            throw new IllegalStateException(var10);
        }

        try {
            listeners.running(context);
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, var9, exceptionReporters, (SpringApplicationRunListeners)null);
            throw new IllegalStateException(var9);
        }
    }

运行阶段大致可以分为下面这几个过程:

开启时间监听

        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

上面代码用于开启Spring Boot应用启动时间监听,配合下面的stopWatch.stop();便可以计算出完整的启动时间。

开启运行监听器

        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        listeners.starting();

getRunListeners方法源码:

    private SpringApplicationRunListeners getRunListeners(String[] args) {
        Class<?>[] types = new Class[]{SpringApplication.class, String[].class};
        return new SpringApplicationRunListeners(logger, this.getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args));
    }

上面代码通过SpringFactoriesLoader检索META-INF/spring.factories找到声明的所有SpringApplicationRunListener的实现类并将其实例化,然后装配到List<SpringApplicationRunListener>运行监听器集合中。

listeners.started();用于遍历运行监听器集合中的所有SpringApplicationRunListener的实现类,并逐一调用它们的starting方法,广播Spring Boot应用要开始启动了。

在Spring Boot中SpringApplicationRunListener接口用于监听整个Spring Boot应用生命周期,其代码如下所示:

public interface SpringApplicationRunListener {
    void starting();

    void environmentPrepared(ConfigurableEnvironment environment);

    void contextPrepared(ConfigurableApplicationContext context);

    void contextLoaded(ConfigurableApplicationContext context);

    void started(ConfigurableApplicationContext context);

    void running(ConfigurableApplicationContext context);

    void failed(ConfigurableApplicationContext context, Throwable exception);
}

创建 Environment

run方法中的这行代码用于创建并配置当前SpringBoot应用将要使用的Environment(包括配置要使用的PropertySource以及Profile):

onfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);

我们已经在准备阶段里推断出了应用类型,这里只要根据相应的应用类型来创建相应的应用环境即可,类型和环境对应关系如下:

  • Web Reactive: StandardReactiveWebEnvironment
  • Web Servlet: StandardServletEnvironment
  • 非 Web: StandardEnvironment

在prepareEnvironment方法中会执行listeners.environmentPrepared(environment);,用于遍历调用所有SpringApplicationRunListener实现类的environmentPrepared()方法,广播Environment准备完毕。

是否打印Banner

run方法中的这行代码会根据我们的配置来决定是否打印Banner:

Banner printedBanner = this.printBanner(environment);

创建Context

run方法中的这行代码用于创建ApplicationContext

context = this.createApplicationContext();

不同的环境对应不同的ApplicationContext:

  • Web Reactive: AnnotationConfigReactiveWebServerApplicationContext
  • Web Servlet: AnnotationConfigServletWebServerApplicationContext
  • 非 Web: AnnotationConfigApplicationContext

装配Context

run方法中的这行代码用于装配Context:

this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);

方法prepareContext的源码如下所示:

    private void prepareContext(ConfigurableApplicationContext context, ConfigurableEnvironment environment, SpringApplicationRunListeners listeners, ApplicationArguments applicationArguments, Banner printedBanner) {
        context.setEnvironment(environment);
        this.postProcessApplicationContext(context);
        this.applyInitializers(context);
        listeners.contextPrepared(context);
        if (this.logStartupInfo) {
            this.logStartupInfo(context.getParent() == null);
            this.logStartupProfileInfo(context);
        }

        ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
        beanFactory.registerSingleton("springApplicationArguments", applicationArguments);
        if (printedBanner != null) {
            beanFactory.registerSingleton("springBootBanner", printedBanner);
        }

        if (beanFactory instanceof DefaultListableBeanFactory) {
            ((DefaultListableBeanFactory)beanFactory).setAllowBeanDefinitionOverriding(this.allowBeanDefinitionOverriding);
        }

        Set<Object> sources = this.getAllSources();
        Assert.notEmpty(sources, "Sources must not be empty");
        this.load(context, sources.toArray(new Object[0]));
        listeners.contextLoaded(context);
    }

prepareContext方法开头为ApplicationContext加载了environment,之后通过applyInitializers方法逐个执行ApplicationContextInitializer的initialize方法来进一步封装ApplicationContext,并调用所有的SpringApplicationRunListener实现类的contextPrepared方法,广播ApplicationContext已经准备完毕了。

之后初始化IOC容器,并调用SpringApplicationRunListener实现类的contextLoaded方法,广播ApplicationContext加载完成,这里就包括通过@EnableAutoConfiguration导入的各种自动配置类。

Refresh Context

run方法中的这行代码用于初始化所有自动配置类,并调用ApplicationContext的refresh方法:

this.refreshContext(context);

广播应用已启动

run方法中的这行代码用于广播Spring Boot应用已启动:

listeners.started(context);

started方法会调用所有的SpringApplicationRunListener的finished方法,广播SpringBoot应用已经成功启动。

执行Runner

run方法中的这行代码callRunners(context, applicationArguments);遍历所有ApplicationRunner和CommandLineRunner的实现类,并执行其run方法。我们可以实现自己的ApplicationRunner或者CommandLineRunner,来对Spring Boot的启动过程进行扩展。

@Component
public class HelloApplicationRunner implements ApplicationRunner {

    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("ApplicationRunner :  Hello SpringBoot ");
    }
}

这里我们需要将HelloApplicationRunner使用@Component注解标注,让其注册到IOC容器中。

@Component
public class HelloCommandLineRunner implements CommandLineRunner {

    @Override
    public void run(String... args) throws Exception {
        System.out.println("CommandLineRunner  Hello SpringBoot ");
    }
}

程序运行输出如下:

ApplicationRunner :  Hello SpringBoot 
CommandLineRunner :  Hello SpringBoot 

广播应用运行中

run方法中的这行代码listeners.running(context);用于调用SpringApplicationRunListener的running方法,广播Spring Boot应用正在运行中。

当run方法运行出现异常时,便会调用handleRunFailure方法来处理异常,该方法里会通过listeners.failed(context, exception);来调用SpringApplicationRunListener的failed方法,广播应用启动失败,并将异常扩散出去。

上面所有的广播事件都是使用Spring的应用事件广播器接口ApplicationEventMulticaster的实现类SimpleApplicationEventMulticaster来进行广播的。

源码地址:https://github.com/lbshold/springboot/tree/master/Spring-Boot-SpringApplication

参考文章

https://mrbird.cc/deepin-springboot-application.html

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342