美文网首页
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