美文网首页
SpringBoot启动过程

SpringBoot启动过程

作者: AC编程 | 来源:发表于2022-12-11 23:03 被阅读0次

    一、SpringBoot是什么

    SpringBoot是依赖于Spring的,比起Spring,除了拥有Spring的全部功能以外,SpringBoot无需繁琐的xml配置,这取决于它自身强大的自动装配功能。

    并且SpringBoot自身已嵌入Tomcat、Jetty等web容器,集成了SpringMVC,使得SpringBoot可以直接运行,不需要额外的容器。

    SpringBoot提供了一些大型项目中常见的非功能性特性,如嵌入式服务器、安全、指标,健康检测、外部配置等。

    其实Spring大家都知道,Boot是启动的意思。所以,SpringBoot其实就是一个启动Spring项目的一个工具而已,总而言之,SpringBoot 是一个服务于框架的框架。也可以说SpringBoot是一个工具,这个工具简化了Spring的配置。

    二、Spring Boot的核心功能

    • 可独立运行的Spring项目:Spring Boot可以以jar包的形式独立运行。

    • 内嵌的Servlet容器:Spring Boot可以选择内嵌Tomcat、Jetty或者Undertow,无须以war包形式部署项目。

    • 简化的Maven配置:Spring提供推荐的基础POM文件来简化Maven配置。

    • 自动配置Spring:Spring Boot会根据项目依赖来自动配置Spring框架,极大地减少项目要使用的配置。

    • 提供生产就绪型功能:提供可以直接在生产环境中使用的功能,如性能指标、应用信息和应用健康检查。

    • 无代码生成和xml配置:Spring Boot不生成代码。完全不需要任何xml配置即可实现Spring的所有配置。

    三、SpringBoot Project Demo

    新建一个SpringBoot项目,mavan依赖包如下:

    <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
    SpringBoot Project Demo

    四、SpringBoot启动过程

    SpringBoot的启动经过了一些一系列的处理,我们先看看整体过程的流程图

    SpringBoot启动过程

    org.springframework.boot.SpringApplication源码:

    public class SpringApplication {
    
        // 运行 spring 应用程序
        public ConfigurableApplicationContext run(String... args) {
            // 记录启动时间
            long startTime = System.nanoTime();
            // spring应用上下文,也就是我们所说的spring根容器
            DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    
            ConfigurableApplicationContext context = null;
            //java.awt.headless是J2SE的一种模式用于在缺少显示屏、键盘或者鼠标时的系统配置,很多监控工具如jconsole 需要将该值设置为true,系统变量默认为true
            configureHeadlessProperty();
            //获取spring.factories中的监听器变量,args为指定的参数数组,默认为当前类SpringApplication
            
            //第一步:获取并启动监听器
            SpringApplicationRunListeners listeners = getRunListeners(args);
            // 触发ApplicationStartingEvent事件,启动监听器会被调用,一共5个监听器被调用,但只有两个监听器在此时做了事
            listeners.starting(bootstrapContext, this.mainApplicationClass);
            try {
                // 参数封装,也就是在命令行下启动应用带的参数,如--server.port=9000
                ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
                //第二步:构造容器环境
                ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
                // 配置spring.beaninfo.ignore,并添加到名叫systemProperties的PropertySource中;默认为true即开启
                configureIgnoreBeanInfo(environment);
                //打印banner
                Banner printedBanner = printBanner(environment);
    
                //第三步:创建容器
                context = createApplicationContext();
                context.setApplicationStartup(this.applicationStartup);
                //第四步:准备容器
                prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
                //第五步:刷新容器 :解析配置文件,加载业务 `bean`,启动 `tomcat` 等
                refreshContext(context);
                //第六步:刷新容器后的扩展接口
                afterRefresh(context, applicationArguments);
                Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
                if (this.logStartupInfo) {
                    new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
                }
                listeners.started(context, timeTakenToStartup);
                callRunners(context, applicationArguments);
            } catch (Throwable ex) {
                handleRunFailure(context, ex, listeners);
                throw new IllegalStateException(ex);
            }
            try {
                Duration timeTakenToReady = Duration.ofNanos(System.nanoTime() - startTime);
                listeners.ready(context, timeTakenToReady);
            } catch (Throwable ex) {
                handleRunFailure(context, ex, null);
                throw new IllegalStateException(ex);
            }
            return context;
        }
    }
    
    4.1 运行SpringApplication.run()方法

    可以肯定的是,所有的标准的SpringBoot的应用程序都是从run方法开始的

    package com.alanchen.demo;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    public class SpringbootTestApplication {
    
        public static void main(String[] args) {
            SpringApplication.run(SpringbootTestApplication.class, args);
        }
    }
    
    4.1.1 源码分析

    进入run方法后,会 new 一个SpringApplication对象,创建这个对象的构造函数做了一些准备工作,编号第2~5步就是构造函数里面所做的事情。

    SpringApplication源码
    4.1.2 SpringBoot的三种启动方式
    4.1.2.1 @SpringBootApplication (最常用方式)

    @SpringBootApplication注解的作用是标注这是一个SpringBoot的应用,被标注的类是一个主程序, SpringApplication.run(App.class, args);传入的类App.class必须是被@SpringBootApplication标注的类。

    @SpringBootApplication是一个组合注解,组合了其他相关的注解,点进去注解后我们可以看到,这个注解集成了@EnableAutoConfiguration@ComponentScan。在这里的@ComponentScan()注解有一堆东西,它的作用是将主配置类所在包及其下面所有后代包的所有注解扫描。

    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 {
        @AliasFor(
            annotation = EnableAutoConfiguration.class
        )
        Class<?>[] exclude() default {};
    
        //其他代码省略
    }
    

    SpringBootApplication使用起来更加简单,只需要一个注解即可完成

    package com.spring.controller;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    
    @SpringBootApplication
    public class App {
    
        public static void main(String[] args) {
            // 启动springboot
            SpringApplication.run(App.class,args);
        }
    }
    
    4.1.2.2 @EnableAutoConfiguration

    @EnableAutoConfiguration的作用是开启自动装配,帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot,并创建对应配置类的Bean,并把该Bean实体交给IOC容器进行管理。

    @EnableAutoConfiguration
    @RestController
    public class IndexController {
    
    
         // 访问路径   http://localhost:8080/index
        @RequestMapping("/index")
        public String index(){
            System.out.println("我进来了");
            return "index controller";
        }
    
        public static void main(String[] args) {
            // 启动springboot
            SpringApplication.run(IndexController.class,args);
        }
    }
    
    4.1.2.3 @ComponentScan

    @ComponentScan()注解的作用是根据定义的扫描路径,将符合规则的类加载到spring容器中,比如在类中加入了以下注解 @Controller、@Service、@Mapper 、@Component、@Configuration 等等。

    @EnableAutoConfiguration  // 开启自动装配
    @ComponentScan("com.spring.controller")
    public class App {
    
        public static void main(String[] args) {
            // 启动springboot
            SpringApplication.run(App.class,args);
        }
    }
    
    4.2 确定应用程序类型

    在SpringApplication的构造方法内,首先会通过 WebApplicationType.deduceFromClasspath(); 方法判断当前应用程序的容器,默认使用的是Servlet 容器,除了servlet之外,还有NONE 和 REACTIVE (响应式编程)。

    deduceFromClasspath

    WebApplicationType.deduceFromClasspath()源码:

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

    这里主要是通过判断REACTIVE相关的字节码是否存在,如果不存在,则web环境即为SERVLET类型。这里设置好web环境类型,在后面会根据类型初始化对应环境。

    4.3 加载所有的初始化器

    这里加载的初始化器是SpringBoot自带初始化器,从META-INF/spring.factories配置文件中加载的,那么这个文件在哪呢?自带有2个,分别在源码的jar包的 spring-boot-autoconfigure 项目和 spring-boot项目里面各有一个

    spring.factories

    spring.factories文件里面,看到开头是 org.springframework.context.ApplicationContextInitializer接口就是初始化器了

    ApplicationContextInitializer

    当然,我们也可以自己实现一个自定义的初始化器:实现 ApplicationContextInitializer接口既可。

    4.3.1 自定义的初始化器

    实现ApplicationContextInitializer接口

    public class MyApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
        @Override
        public void initialize(ConfigurableApplicationContext applicationContext) {
            System.out.println("我是初始化的 MyApplicationContextInitializer...");
        }
    }
    

    在resources目录下添加META-INF/spring.factories配置文件,内容如下,将自定义的初始化器注册进去。spring.factories配置文件:

    org.springframework.context.ApplicationContextInitializer=\
    com.alanchen.demo.MyApplicationContextInitializer
    

    启动SpringBoot后,就可以看到控制台打印的内容了,在这里我们可以很直观的看到它的执行顺序,是在打印banner的后面执行的。

    自定义的初始化器
    4.4 加载所有的监听器

    加载监听器也是从META-INF/spring.factories配置文件中加载的,与初始化不同的是,监听器加载的是实现了ApplicationListener 接口的类。

    监听器
    4.5 设置程序运行的主类

    deduceMainApplicationClass();这个方法仅仅是找到main方法所在的类,为后面的扫包作准备,deduce是推断的意思,所以准确地说,这个方法作用是推断出主方法所在的类。

    deduceMainApplicationClass

    deduceMainApplicationClass()源码:

    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;
        }
    
    4.6 开启计时器

    程序运行到这里,就已经进入了run方法的主体了,第一步调用的run方法是静态方法,那个时候还没实例化SpringApplication对象,现在调用的run方法是非静态的,是需要实例化后才可以调用的,进来后首先会开启计时器,这个计时器有什么作用呢?顾名思义就使用来计时的嘛,计算SpringBoot启动花了多长时间。关键代码如下:

    new SpringApplication对象 计时器
    4.7 将java.awt.headless设置为true

    这里将java.awt.headless设置为true,表示运行在服务器端,在没有显示器器和鼠标键盘的模式下照样可以工作,模拟输入输出设备功能。

    做了这样的操作后SpringBoot想干什么呢?其实是想设置该应用程序,即使没有检测到显示器,也允许其启动。对于服务器来说是不需要显示器的,所以要这样设置。

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

    通过方法可以看到,setProperty()方法里面又有个getProperty();这不多此一举吗?其实getProperty()方法里面有2个参数, 第一个key值,第二个是默认值,意思是通过key值查找属性值,如果属性值为空,则返回默认值 true,保证了一定有值的情况。

    4.8 获取并启用监听器

    这一步 通过监听器来实现初始化的的基本操作,这一步做了2件事情:
    1、创建所有 Spring 运行监听器并发布应用启动事件。
    2、启用监听器。

    获取并启用监听器
    4.9 设置应用程序参数

    将执行run方法时传入的参数封装成一个对象

    设置应用程序参数

    仅仅是将参数封装成对象,对象的构造函数如下:

    public class DefaultApplicationArguments implements ApplicationArguments {
    
        private final Source source;
    
        private final String[] args;
    
        public DefaultApplicationArguments(String... args) {
            Assert.notNull(args, "Args must not be null");
            this.source = new Source(args);
            this.args = args;
        }
    
    // 其他代码省略
    }
    

    那么问题来了,这个参数是从哪来的呢?其实就是main方法里面执行静态run方法传入的参数。

    参数
    4.10 准备环境变量

    准备环境变量,包含系统属性和用户配置的属性,执行的代码块在 prepareEnvironment方法内。

    prepareEnvironment

    打了断点之后可以看到,它将maven和系统的环境变量都加载进来了。


    加载变量
    4.11 忽略bean信息

    这个方法configureIgnoreBeanInfo()这个方法是将spring.beaninfo.ignore的默认值值设为true,意思是跳过beanInfo的搜索,其设置默认值的原理和第7步一样。

    configureIgnoreBeanInfo

    当然也可以在配置文件中添加以下配置来设为false

    spring.beaninfo.ignore=false
    
    4.12 打印banner信息

    显而易见,这个流程就是用来打印控制台那个很大的Spring的banner的

    banner信息

    那他在哪里打印的呢?他在SpringBootBanner.java 里面打印的,这个类实现了Banner 接口,而且banner信息是直接在代码里面写死的。

    SpringBootBanner printBanner源码
    private Banner printBanner(ConfigurableEnvironment environment) {
            if (this.bannerMode == Banner.Mode.OFF) {
                return null;
            }
            ResourceLoader resourceLoader = (this.resourceLoader != null) ? this.resourceLoader
                    : new DefaultResourceLoader(null);
            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);
        }
    
    4.12.1 Banner图在哪里加载

    既然我们想要更换Spring启动的默认logo,首先我们就的知道,这logo是怎么出现的,只有搞明白了这个问题,我们才能去修改它。

    其实Spring Boot启动打印默认logo的类是SpringApplicationBannerPrinter类,SpringBoot 默认寻找 Banner的顺序是:

    • 首先依次在Classpath下找文件banner.gif,banner.jpg和 banner.png,使用优先找到的。
    • 若没找到上面文件的话,继续Classpath下找banner.txt。
    • 若上面都没有找到的话, 用默认的 SpringBootBanner,也就是上面输出的 SpringBoot logo。
    4.12.2 自定义Banner图

    一般是把banner.txt文件放在src/main/resources/目录下。既然找到了关键的问题,我们就可以自己创建一个banner.txt文件,让他来覆盖SpringBoot默认的logo,实现我们自定义的logo。在resources目录下添加一个banner.txt 的文件即可,txt文件内容如下:

    自定义banner

    只需要加一个文件即可,其他什么都不用做,然后直接启动SpringBoot,就可以看到效果了。

    4.12.3 第三方Banner生成工具

    在项目中更改图像很简单,无非是添加一个banner.txt文件而已,但是文件的中图咱么搞啊,难道要自己手敲吗,这可不是一般人能搞的出来的啊。所以这里给大家介绍几个网站,可以生成一些图形。

    4.12.3.1 Text toASCII Art Generator

    字母转换为ASCII 艺术字,推荐 Text toASCII Art Generator ,优点:

    • 它支持的字体效果(艺术字)最多。

    • 并且可以通过点击 Test All 同时生成所有效果(共314种)来供你选择,而无需一个一个去选择,这样可以大大减少挑选时间。

    • 还可以通过 More Opts 来设置以编程注释或回显输出的形式格式化输出。

    Text toASCII Art Generator地址:http://patorjk.com/software/taag/

    效果图
    4.12.3.2 ASCII艺术字(图)集

    Ascii艺术字,可以在这里寻找现成的一些图集(也可以生成ASCII艺术字),可以直接搜索你想要的图形,搜索出来的结果可以直接下载或者复制都可以(截图右上角)。

    ascii-art地址:https://www.bootschool.net/ascii-art/search

    搜索美女 图片
    4.13 创建应用程序的上下文

    实例化应用程序的上下文, 调用createApplicationContext()方法,这里就是用反射创建对象。

    createApplicationContext
    4.14 实例化异常报告器

    异常报告器是用来捕捉全局异常使用的,当SpringBoot应用程序在发生异常时,异常报告器会将其捕捉并做相应处理,在spring.factories文件里配置了默认的异常报告器。

    spring.factories

    需要注意的是,这个异常报告器只会捕获启动过程抛出的异常,如果是在启动完成后,在用户请求时报错,异常报告器不会捕获请求中出现的异常。

    异常
    4.14.1 自定义异常报告器

    MyExceptionReporter实现SpringBootExceptionReporter接口,并提供一个带参的构造函数

    package com.alanchen.demo;
    
    import org.springframework.boot.SpringBootExceptionReporter;
    import org.springframework.context.ConfigurableApplicationContext;
    
    public class MyExceptionReporter implements SpringBootExceptionReporter {
    
        private ConfigurableApplicationContext context;
    
        //必须要有一个有参的构造函数,否则自定义异常报告器无效
        public MyExceptionReporter(ConfigurableApplicationContext context){
            this.context = context;
        }
    
        @Override
        public boolean reportException(Throwable failure) {
            System.out.println("进入自定义异常报告器");
            failure.printStackTrace();
            //返回false会打印详细的springboot信息,返回true只打印异常信息
            return false;
        }
    }
    

    spring.factories文件中注册异常报告器

    org.springframework.context.ApplicationContextInitializer=\
    com.alanchen.demo.MyApplicationContextInitializer
    
    org.springframework.boot.SpringBootExceptionReporter=\
    com.alanchen.demo.MyExceptionReporter
    
    spring.factories

    接着我们在application.yml 中把端口号设置为一个很大的值,这样肯定会报错

    设置端口 进入自定义异常报告器
    4.15 准备上下文环境
    准备上下文环境
    4.15.1 实例化单例的beanName生成器

    postProcessApplicationContext(context);方法里面。使用单例模式创建 了BeanNameGenerator对象,其实就是beanName生成器,用来生成bean对象的名称。

    4.15.2 执行初始化方法

    初始化方法有哪些呢?还记得第3步里面加载的初始化器嘛?其实是执行第3步加载出来的所有初始化器,实现了ApplicationContextInitializer接口的类。

    4.15.3 将启动参数注册到容器中

    这里将启动参数以单例的模式注册到容器中,是为了以后方便拿来使用,参数的beanName 为 :springApplicationArguments

    4.16 刷新上下文

    刷新上下文已经是spring的范畴了,自动装配和启动 tomcat就是在这个方法里面完成的。

    刷新上下文
    4.17 刷新上下文后置处理

    afterRefresh方法是启动后的一些处理,留给用户扩展使用,目前这个方法里面是空的。

    afterRefresh
    4.18 结束计时器

    到这一步,SpringBoot其实就已经完成了,计时器会打印启动SpringBoot的时长。

    结束计时器
    4.19 发布上下文准备就绪事件
    发布上下文准备就绪事件
    4.20 执行自定义的run方法

    这是一个扩展功能,callRunners(context, applicationArguments)可以在启动完成后执行自定义的run方法;有2中方式可以实现:

    • 实现ApplicationRunner接口
    • 实现CommandLineRunner接口

    下来我们验证一把,为了方便代码可读性,我把这2种方式都放在同一个类里面

    package com.alanchen.demo;
    
    import org.springframework.boot.ApplicationArguments;
    import org.springframework.boot.ApplicationRunner;
    import org.springframework.boot.CommandLineRunner;
    import org.springframework.stereotype.Component;
    
    @Component
    public class MyRunner implements ApplicationRunner, CommandLineRunner {
    
        @Override
        public void run(ApplicationArguments args) throws Exception {
            System.out.println("自定义ApplicationRunner运行");
        }
    
        @Override
        public void run(String... args) throws Exception {
            System.out.println("自定义CommandLineRunner运行");
        }
    }
    
    启动结果

    相关文章

      网友评论

          本文标题:SpringBoot启动过程

          本文链接:https://www.haomeiwen.com/subject/gakxqdtx.html