SpringBoot—项目启动时几种初始化操作及SpringApplication类详解

关注:CodingTechWork,一起学习进步。

引言

  在使用Spring Boot搭建项目时,启动项目工程,经常遇到一些需要启动初始化数据或者资源的需求,比如提前加载某个配置文件内容,初始化某个信息、做好安全认证等。这里一起学习总结了几种初始化数据的方式。

@Bean注解配置

使用方式

  编写配置类,使用@Configuration@Bean注解进行初始化。

使用示例

package com.example.andya.demo.conf;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
 * @author Andya
 * @create 2020-09-14 21:37
 */
@Configuration
public class InitConfigTest {

    @Value("${key}")
    private String key;

    @Bean
    public String testInit(){
        System.out.println("init key: " + key);
        return key;
    }
}

ApplicationRunner接口

使用方式

  编写类去实现ApplicationRunner接口,实现run()方法,该方法在工程启动类的XXXApplicationSpringApplication.run(xxxApplication.class, args)方法之前,@Componet会在所有Spring的Beans初始化完成之后,执行完成。

使用示例

package com.example.andya.demo.service.initTest;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

/**
 * @author Andya
 * @date 2020-09-14 21:37
 */
@Component
public class ApplicationRunnerTest implements ApplicationRunner {

    @Value("${key}")
    private String key;

    @Override
    public void run(ApplicationArguments applicationArguments) throws Exception {
        System.out.println("ApplicationRunner test: init key : " + key);
    }
}

CommandLineRunner接口

使用方式

  类似于ApplicationRunner接口,我们同样编写类去实现CommandLineRunner接口,实现run()方法,该方法在工程启动类的XXXApplicationSpringApplication.run(xxxApplication.class, args)方法之前,@Componet会在所有Spring的Beans初始化完成之后,执行完成。
  多个类实现接口后,可以通过@Order注解进行顺序的控制,@Order(n),n越小,启动执行的越早。

使用示例

示例1

package com.example.andya.demo.service.initTest;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @author Andya
 * @create 2020-09-14 21:37
 */
@Component
@Order(1)
public class CommandLineRunner1Test implements CommandLineRunner {

    @Value("${key}")
    private String key;

    @Override
    public void run(String... strings) throws Exception {
        System.out.println("CommandLineRunner first init: 1)init key : " + key);
    }
}

示例2

package com.example.andya.demo.service.initTest;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

/**
 * @author Andya
 * @create 2020-09-14 21:37
 */
@Component
@Order(2)
public class CommandLineRunner2Test implements CommandLineRunner {

    @Value("${key}")
    private String key;

    @Override
    public void run(String... strings) throws Exception {
        System.out.println("CommandLineRunner second init: 2)init key : " + key);
    }
}

两种接口分析

接口对比

  CommandLineRunnerApplicationRunner都是接口,只是内部参数不一样,前者的参数是最原始的参数String类型,无任何处理;后者是ApplicationArguments类型,对原始参数进行了封装处理。

执行结果

上述三种方式的执行结果示例

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.6.RELEASE)

2020-09-14 21:45:28.403  INFO 25408 --- [           main] com.example.andya.demo.DemoApplication   : Starting DemoApplication on DESKTOP-KC40970 with PID 25408 (F:\selfcode\target\classes started by Hugh in F:\selfcode)
2020-09-14 21:45:28.406  INFO 25408 --- [           main] com.example.andya.demo.DemoApplication   : No active profile set, falling back to default profiles: default
2020-09-14 21:45:28.434  INFO 25408 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1542153: startup date [Tue Sep 15 11:28:28 CST 2020]; root of context hierarchy
2020-09-14 21:45:29.322  INFO 25408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 9000 (http)
2020-09-14 21:45:29.327  INFO 25408 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2020-09-14 21:45:29.327  INFO 25408 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.16
2020-09-14 21:45:29.385  INFO 25408 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2020-09-14 21:45:29.385  INFO 25408 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 953 ms
2020-09-14 21:45:29.504  INFO 25408 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2020-09-14 21:45:29.506  INFO 25408 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2020-09-14 21:45:29.506  INFO 25408 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2020-09-14 21:45:29.506  INFO 25408 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2020-09-14 21:45:29.506  INFO 25408 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
init key: value
2020-09-14 21:45:29.818  INFO 25408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@1542153: startup date [Tue Sep 15 11:28:28 CST 2020]; root of context hierarchy
2020-09-14 21:45:29.857  INFO 25408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/aopTest/sayHi/{name}],methods=[GET]}" onto public java.lang.String com.example.andya.demo.controller.AopController.sayHi(java.lang.String)
2020-09-14 21:45:29.857  INFO 25408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/hello],methods=[GET]}" onto public java.lang.String com.example.andya.demo.controller.HelloController.hello()
2020-09-14 21:45:29.859  INFO 25408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2020-09-14 21:45:29.859  INFO 25408 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2020-09-14 21:45:29.923  INFO 25408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2020-09-14 21:45:29.923  INFO 25408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2020-09-14 21:45:29.955  INFO 25408 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2020-09-14 21:45:30.064  INFO 25408 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2020-09-14 21:45:30.069  INFO 25408 --- [           main] s.a.ScheduledAnnotationBeanPostProcessor : No TaskScheduler/ScheduledExecutorService bean found for scheduled processing
2020-09-14 21:45:30.095  INFO 25408 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 9000 (http)
CommandLineRunner first init: 1)init key : value
CommandLineRunner second init: 2)init key : value
ApplicationRunner test: init key : value
2020-09-14 21:45:30.097  INFO 25408 --- [           main] com.example.andya.demo.DemoApplication   : Started DemoApplication in 1.911 seconds (JVM running for 2.601)
2020-09-14 21:46:00.004  INFO 25408 --- [pool-1-thread-1] .s.a.AnnotationAsyncExecutionInterceptor : No task executor bean found for async processing: no bean of type TaskExecutor and no bean named 'taskExecutor' either
2020-09-14 21:46:59.781  INFO 25408 --- [nio-9000-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring FrameworkServlet 'dispatcherServlet'
2020-09-14 21:46:59.781  INFO 25408 --- [nio-9000-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization started
2020-09-14 21:46:59.802  INFO 25408 --- [nio-9000-exec-1] o.s.web.servlet.DispatcherServlet        : FrameworkServlet 'dispatcherServlet': initialization completed in 21 ms

源码分析

XxxApplication启动类

package com.example.andya.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

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

其中,在工程中,点击SpringApplication.run(DemoApplication.class, args);的run方法进行源码追踪。

SpringApplication类源码

    //构造函数1
    public SpringApplication(Object... sources) {
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = new HashSet();
        this.initialize(sources);
    }
    //构造函数2
    public SpringApplication(ResourceLoader resourceLoader, Object... sources) {
        this.bannerMode = Mode.CONSOLE;
        this.logStartupInfo = true;
        this.addCommandLineProperties = true;
        this.headless = true;
        this.registerShutdownHook = true;
        this.additionalProfiles = new HashSet();
        this.resourceLoader = resourceLoader;
        this.initialize(sources);
    }

    //initialize方法
    private void initialize(Object[] sources) {
        //source不为空时,保存配置类
        if (sources != null && sources.length > 0) {
            this.sources.addAll(Arrays.asList(sources));
        }
        //判断该应用是否为web应用
        this.webEnvironment = this.deduceWebEnvironment();
        //获取并保存容器初始化ApplicationContextInitializer类,通过SpringFactoriesLoader.loadFactoryNames方法
        //从META-INF/spring.factories路径中获取ApplicationContextInitializer集合
        this.setInitializers(this.getSpringFactoriesInstances(ApplicationContextInitializer.class));
        //获取并保存监听器 ApplicationListener类,同样的,通过SpringFactoriesLoader.loadFactoryNames方法
        //从META-INF/spring.factories路径中获取ApplicationListener集合  
        this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));
        //通过堆栈追踪名为main方法,从而获取包含main方法的类
        this.mainApplicationClass = this.deduceMainApplicationClass();
    }
    
    //run()方法1
    public static ConfigurableApplicationContext run(Object source, String... args) {
        return run(new Object[]{source}, args);
    }
    //run()方法2
    public static ConfigurableApplicationContext run(Object[] sources, String[] args) {
        return (new SpringApplication(sources)).run(args);
    }
    //run()方法3,最底层
    public ConfigurableApplicationContext run(String... args) {
        //新建计时工具类StopWatch
        StopWatch stopWatch = new StopWatch();
        //启动计时工具类StopWatch
        stopWatch.start();
        ConfigurableApplicationContext context = null;
        FailureAnalyzers analyzers = null;
        //设置java.awt.headless的系统属性,如服务器不需要显示器就需要如此设置
        this.configureHeadlessProperty();
        //获取监听器SpringApplicationRunListeners,调用了getSpringFactoriesInstances()方法
        //该方法又调用了loadFactoryNames()方法从META-INF/spring.factories路径中获取SpringApplicationRunListeners集合
        SpringApplicationRunListeners listeners = this.getRunListeners(args);
        //启动监听器
        listeners.starting();

        try {
            //将args参数封装至ApplicationArguments中
            ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
            //准备环境:内部调用了this.configureEnvironment()和listeners.environmentPrepared()等方法
            ConfigurableEnvironment environment = this.prepareEnvironment(listeners, applicationArguments);
            //从环境中获取Banner进行打印,可以自定义Banner
            Banner printedBanner = this.printBanner(environment);
            //创建Spring的容器
            context = this.createApplicationContext();
            //分析并诊断是项目启动否有问题
            new FailureAnalyzers(context);
            //准备容器上下文:
            //1)设置容器环境:context.setEnvironment(environment);
            //2)设置beanNameGenerator和resourceLoader:this.postProcessApplicationContext(context);
            //3)初始化context并检测是否接受该类型容器:this.applyInitializers(context);
            //4)触发监听事件:listeners.contextPrepared(context);是一个空函数;
            //5)注册bean:通过context.getBeanFactory().registerSingleton()方法向容器注入springApplicationArguments和springBootBanner
            //6)获取sources:Set<Object> sources = this.getSources();
            //7)加载启动类,注入到容器内:this.load(context, sources.toArray(new Object[sources.size()]));
            //给loader设置beanNameGenerator,resourceLoader和environment
            //然后调用load()方法,按照不同的source类型加载class,resource,package,charSequence,超过这些范围的就抛出异常“Invalid source type xxx”
            //8)触发监听事件:listeners.contextLoaded(context);进行
            this.prepareContext(context, environment, listeners, applicationArguments, printedBanner);
            //刷新Spring容器
            this.refreshContext(context);
            //从容器中获取所有的ApplicationRunner和CommandLineRunner进行回调callRunner
            this.afterRefresh(context, applicationArguments);
            //触发监听事件,所有的SpringApplicationRunListener进行callFinishedListener回调
            listeners.finished(context, (Throwable)null);
            //计时器停止
            stopWatch.stop();
            if (this.logStartupInfo) {
                (new StartupInfoLogger(this.mainApplicationClass)).logStarted(this.getApplicationLog(), stopWatch);
            }
            //整个SpringBoot工程启动完毕,返回启动的Ioc容器上下文
            return context;
        } catch (Throwable var9) {
            this.handleRunFailure(context, listeners, (FailureAnalyzers)analyzers, var9);
            throw new IllegalStateException(var9);
        }
    }
    protected void afterRefresh(ConfigurableApplicationContext context, ApplicationArguments args) {
        this.callRunners(context, args);
    }
    //callRunners
    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);
        Iterator var4 = (new LinkedHashSet(runners)).iterator();

        while(var4.hasNext()) {
            Object runner = var4.next();
            if (runner instanceof ApplicationRunner) {
                this.callRunner((ApplicationRunner)runner, args);
            }

            if (runner instanceof CommandLineRunner) {
                this.callRunner((CommandLineRunner)runner, args);
            }
        }

    }

  在上述源码中,我们分析了SpringBoot启动的一个流程,其中,我们可以从源码的run()方法一层一层点击追踪查看到,ApplicationRunnerCommandLineRunner是在callRunners方法中执行的。

参考
jdk1.8

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