• 从代码层面看spring boot启动过程


    前言

    我们都知道spring boot项目是通过main方法来启动运行的,但是main方法执行之后,spring boot都替我们完成了哪些操作,最终让我们的服务成功启动呢?今天我们就来从源码层面探讨下这个问题。

    spring boot启动过程

    在开始之前,我们先看这样一段代码:

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

    上面的这段代码就是我们最常见的spring boot启动的main方法,今天我们就从这个main方法开始,进入spring boot的世界。

    springApplication

    首先,在main方法内部执行了SpringApplication.run(DailyNoteApplication.class, args),这个方法有两个入参,一个是项目主类(当前类)的class,另一个就是main方法的args

    这里先说下这个args参数,我们都知道spring boot是支持以命令行的方式注入配置信息的,它的实现就是依赖于这个args参数的。如果你将args删掉,项目也是可以正常启动的,只是你再也没办法通过命令行的方式注入配置了。关于这一块,我们之前在通过k8s启动spring boot项目的时候踩过坑,发现注入的参数不起作用,最后发现就是少了args

    run方法

    run方法内部,首先实例化了一个springApplication对象,然后又调用了另一个run方法:

    springApplication实例化

    我们先看springApplication的实例化过程:

    前两个this都是简单的赋值,这里暂时先不过多研究,第三个this这里的WebApplicationType.deduceFromClasspath()是判断我们的服务器类别,在spring boot中,有两种服务器一种就是传统的sevlet,也就是基于tomcat(其中一种)这种,另一种就是reactive,也就是我们前面分享的webflux这种流式服务器。

    紧接着是初始化ApplicationContextInitializerApplicationListener,这里主要是获取他们的spring工程实例,方便后续创建他们的实例,为了保证主流程的连贯性,我们暂时不看其方法内部实现。

    最后一个赋值操作是找出包含main方法的类的className

    run方法开始执行

    下面我们看下springApplication实例的 run方法内部执行过程:

    • 创建了一个StopWatch对象,并调用它的start方法

      StopWatch stopWatch = new StopWatch();
      stopWatch.start();
      
    • 设置javajava.awt.headless值,如果已经设置过就取系统设置的值,如果没有设置,则设置为true。这个是设置java的无头模式,启用之后,可以用计算能力来处理可视化操作(类似于用算力代替显卡渲染能力)

      configureHeadlessProperty();
      
    • 获取spring boot运行监听器

      SpringApplicationRunListeners listeners = getRunListeners(args);
      listeners.starting();
      
    • 解析控制台参数(args),获取应用参数

      ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
      
    • 配置需要忽略的bean信息,从源码中我们可以看出了,如果我们没有设置spring.beaninfo.ignorespring boot会给他默认true

      configureIgnoreBeanInfo(environment);
      //方法内部实现
      private void configureIgnoreBeanInfo(ConfigurableEnvironment environment) {
      		if (System.getProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME) == null) {
      			Boolean ignore = environment.getProperty("spring.beaninfo.ignore", Boolean.class, Boolean.TRUE);
      			System.setProperty(CachedIntrospectionResults.IGNORE_BEANINFO_PROPERTY_NAME, ignore.toString());
      		}
      	}
      
    • 打印banner信息,这个banner就是spring boot启动的时候打印的哪个logo,那个是支持自定义的

      Banner printedBanner = printBanner(environment);
      
    • 创建spring boot容器,这里创建的时候会根据我们应用的不同,选择不同的容器

      context = createApplicationContext();
      

    • 创建spring工厂实例

      exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
      					new Class[] { ConfigurableApplicationContext.class }, context);
      
    • 准备容器,这一步会进行初始化操作,把环境设置、系统参数、banner注入到容器中,并把容器绑定到监听器上

      prepareContext(context, environment, listeners, applicationArguments, printedBanner);
      
    • 刷新容器,这里其实进行了两步操作,一个是给我们的spring boot绑定SpringContextShutdownHook钩子函数,有了这个函数,我们就可以优雅地关闭spring boot了;另一个是刷新beanFactory,默认情况下spring boot为我们创建的是GenericApplicationContext容器,初始化完后,所有的对象都被初始化在它的beanFactory,为了确保其他组件也能拿到beanFactory中的内容,refreshContext方法内还进行了同步操作(直接copy给他们):

    refreshContext(context);
    

    从源码中可以很明显看出这一点:

    • 刷新完成后会执行afterRefresh方法,但是这个方法默认情况下是空的

      afterRefresh(context, applicationArguments);
      

    • 停止秒表。这个秒表的作用应该就是计时

      stopWatch.stop();
      
    • 调用监听器started方法,这方法修改了容器的状态。和前面starting方法不同的是,这个方法必须在beanfactory刷新后执行:

      listeners.started(context);
      

    • 运行容器中的runner,这里的runner主要有两类,一类是继承ApplicationRunner的,一类是继承CommandLineRunner。我猜测这个应该是为了方便我们实现更复杂的需求实现的,目前还没用到过,后面可以找时间研究下

      callRunners(context, applicationArguments);
      

    • 最后一步还是监听器的操作。这个方法最后将容器的状态改为ACCEPTING_TRAFFIC,表示可以接受请求

      listeners.running(context);
      

      到这里,spring boot就启动成功了。下面是整个run方法的源码,虽然不长,但是我感觉读起来还是有点吃力,想想自己模仿spring boot写的demo,真的是小巫见大巫。

    总结

    spring boot启动过程虽然看起来简单,用起来简单,但是当我一行一行看源码的时候,我觉得不简单,就好比老远看一棵大树,不就是一个直立的杆嘛,但是当你抵近看的时候,你会发现树干有树杈,树杈又有小树杈,总之看起来盘根错节的,总是感觉看不到树真实的样子。不过,随着后面我们不断地将spring boot的树叶、小树杈一一拿掉的时候,我相信我们会越来越清楚地看到spring boot这棵大树真实的样子。

    今天的内容,其实如果有一张时序图,看起来就比较友好了,但是由于时间的关系,今天来不及做了,我们明天争取把时序图搞出来。

    另外,后面我还会把今天一笔带过的方法尽可能详细地研究然后讲解的,我的目标就是由大到小(从树干到树杈,最后到树叶)地剖析spring boot的源码,最后把spring boot的核心技术梳理清楚。好了,今天就先到这里吧!

  • 相关阅读:
    MyBatis3: There is no getter for property named 'code' in 'class java.lang.String'
    jQuery获取Select选择的Text和 Value(转)
    mybatis3 :insert返回插入的主键(selectKey)
    【转】Mybatis/Ibatis,数据库操作的返回值
    Android问题-打开DelphiXE8与DelphiXE10编译空工程提示“[Exec Error] The command exited with code 1.”
    Android问题-打开DelphiXE8与DelphiXE10新建一个空工程提示"out of memory"
    BAT-使用BAT生成快捷方式
    给 TTreeView 添加复选框
    跨进程发送消息数据
    鼠标拖动虚影效果
  • 原文地址:https://www.cnblogs.com/caoleiCoding/p/15232215.html
Copyright © 2020-2023  润新知