Spring Boot启动原理

一、前言说明

启动一个springboot项目,最简单的就是配置一个springboot启动类,然后运行即可

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

通过上面的代码,我们可以看出springboot启动的关键主要有两个地方,第一个就是@SpringBootApplication注解,第二个就是 SpringApplication.run(SpringBoot.class, args);这个方法,那么他们内部究竟是如何运作的呢?下面我们就一起来看看。

二、@SpringBootApplication原理解析

2.1. @SpringBootApplication组合注解剖析

直接追踪@SpringBootApplication的源码,可以看到其实@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 {}

上面的注解看起来虽然很多,但是当你除去元注解,真正起作用的注解只有以下三个注解:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan

那这三个注解是有什么作用呢?是这样的在Spring Boot 1.2版之前,或者我们初学者刚开始接触springboot时,都还没开始使用@SpringBootApplication这个注解,而是使用以上三个注解启动项目。可以手动敲敲代码,就会发现这样也可以正常启动项目:

@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public class Springboot03Application {

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

}

所以这三个注解才是背后的真正其作用的,@SpringBootApplication只是个空壳子花架子。下面就说明下这三个注解各自的作用。

2.2. @SpringBootConfiguration注解

跟踪下@SpringBootConfiguration的源代码,看下他由哪些注解组成:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed

除去元注解之后,剩下的@Configuration注解应该都很熟了!springboot为什么可以去除xml配置,靠的就是@Configuration这个注解。所以,它的作用就是将当前类申明为配置类,同时还可以使用@bean注解将类以方法的形式实例化到spring容器,而方法名就是实例名,看下代码你就懂了!

@Configuration
public @interface SpringBootConfiguration {
    @AliasFor(annotation = Configuration.class)
    boolean proxyBeanMethods() default true;
}

@AliasFor(annotation = Configuration.class) 表示该注解是用来替代@Configuration注解的,具有相同的作用。

boolean proxyBeanMethods() default true; 是@Configuration注解中的一个属性,用于指定是否使用CGLIB代理来增强@Bean方法。当proxyBeanMethods为true时,Spring会使用CGLIB代理来创建@Bean方法返回的对象,以便实现AOP等功能。当proxyBeanMethods为false时,Spring会直接调用@Bean方法返回的对象,不会进行代理增强。

默认情况下,proxyBeanMethods属性为true,即使用CGLIB代理来增强@Bean方法。如果不需要AOP等功能,可以将proxyBeanMethods设置为false,以提高性能。作用等同于xml配置文件实现

2.3. @ComponentScan

@ComponentScan作用就是扫描当前包以及子包,将有@Component,@Controller,@Service,@Repository等注解的类注册到容器中,以便调用。
注:大家第一眼见到@ComponentScan这个注解的时候是否有点眼熟?之前,一些传统框架用xml配置文件配置的时候,一般都会使用<context:component-scan>来扫描包。以下两中写法的效果是相同的:

配置类的方式:

@Configuration
@ComponentScan(basePackages="XXX")
public class SpringBoot {

}

xml文件的方式

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:cache="http://www.springframework.org/schema/cache" xmlns:mvc="http://www.springframework.org/schema/mvc"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd 
                         http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd 
                         http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd
                         http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd
                         http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.0.xsd ">

    <!-- 扫描需要被调用的注解文件包 -->
    <context:component-scan base-package="XXX"></context:component-scan>
</beans>

注意:如果@ComponentScan不指定basePackages,那么默认扫描当前包以及其子包,而@SpringBootApplication里的@ComponentScan就是默认扫描,所以我们一般都是把springboot启动类放在最外层,以便扫描所有的类。

2.4. @EnableAutoConfiguration

@EnableAutoConfiguration的工作原理,它主要就是通过内部的方法,扫描classpath的META-INF/spring.factories配置文件(key-value),将其中的org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的配置项实例化并且注册到spring容器。

我们同样打开@EnableAutoConfiguration源码,可以发现他是由以下几个注解组成的

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)

除去元注解,主要注解就是@AutoConfigurationPackage@Import(AutoConfigurationImportSelector.class),springboot项目为什么可以自动载入应用程序所需的bean?就是因为注解@Import。那么这个@Import怎么为什么会有这个功能呢?没关系!我们一步一步的看下去!

首先我们先进入AutoConfigurationImportSelector类,可以看到他有一个方法selectImports(),

继续跟踪,进入getAutoConfigurationEntry()方法

List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

可以看到这里有个List集合,那这个List集合又是干嘛的?没事,我们继续跟踪getCandidateConfigurations()方法!

可以看到这里有个方法

SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));

这个方法的作用就是读取classpath下的META-INF/spring.factories文件的配置,将key为 org.springframework.boot.autoconfigure.EnableAutoConfiguration 对应的配置项读取出来,通过反射机制实例化为配置文件,然后注入spring容器。

注:假如你想要实例化一堆bean,可以通过配置文件先将这些bean实例化到容器,等其他项目调用时,在spring.factories中写入这个配置文件的路径即可!

三、 SpringApplication.run()原理解析

首先我们点击查看run方法的源码:

 可以看出,其实SpringApplication.run()包括两个部分,一部分就是创建SpringApplicaiton实例,另一部分就是调用run()方法,那他们又是怎么运行的?

3.1. 创建SpringApplicaiton

继续跟踪SpringApplication实例的源码:

继续跟踪进入,到如下这个方法中

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    //获取应用类型
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    this.bootstrapRegistryInitializers = new ArrayList<>(
            getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
    //获取所有初始化器
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    //获取所有监听器
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    //定位main方法
    this.mainApplicationClass = deduceMainApplicationClass();
}

springboot在创建SpringApplicaiton实例的时候,主要是做了以上四个操作。

3.1.1 获取应用类型

跟踪deduceFromClasspath方法源码如下:

从返回结果我们可以看出应用类型一共有三种,分别是

  • NONE: 非web应用,即不会启动服务器
  • SERVLET: 基于servlet的web应用
  • REACTIVE: 响应式web应用

判断一共涉及四个常量:

  • WEBFLUX_INDICATOR_CLASS
  • WEBMVC_INDICATOR_CLASS
  • JERSEY_INDICATOR_CLASS
  • SERVLET_INDICATOR_CLASSES

springboot在初始化容器的时候,会对以上四个常量所对应的class进行判断,看看他们是否存在,从而返回应用类型!至于常量代表哪些class,大家可以自己跟踪看看,也在当前类中!

3.1.2 获取初始化器

跟踪进入getSpringFactoriesInstances方法

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    //获取所有初始化器的名称集合
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    //根据名称集合实例化这些初始化器
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    //排序
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

从代码可以看出是在META-INF/spring.factories配置文件里获取初始化器,然后实例化、排序后再设置到initializers属性中。

3.1.3 获取初监听器

跟踪源码,发现监听器和初始化的操作是基本一样的

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
        ClassLoader classLoader = getClassLoader();
        // Use names and ensure unique to protect against duplicates
        //获取所有监听器的名称集合
        Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
        //根据名称集合实例化这些监听器
        List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
        //排序
        AnnotationAwareOrderComparator.sort(instances);
        return instances;
    }

3.1.4 定位main方法

跟踪源码进入deduceMainApplicationClass方法

    private Class<?> deduceMainApplicationClass() {
        try {
            //通过创建运行时异常的方式获取栈
            StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
            //遍历获取main方法所在的类并且返回
            for (StackTraceElement stackTraceElement : stackTrace) {
                if ("main".equals(stackTraceElement.getMethodName())) {
                    return Class.forName(stackTraceElement.getClassName());
                }
            }
        }
        catch (ClassNotFoundException ex) {
            // Swallow and continue
        }
        return null;
    }

3.2. 调用run方法

3.2.1 run方法

public ConfigurableApplicationContext run(String... args) {
    //作用是获取当前系统时间的纳秒级别的精确时间,用于记录开始时间
    long startTime = System.nanoTime();
    //作用是创建一个默认的引导上下文(BootstrapContext)对象。在Spring Boot应用程序的启动过程中,引导上下文是一个重要的组件。它负责管理和协调应用程序的启动过程,并提供一些必要的功能和资源。createBootstrapContext() 方法会创建一个DefaultBootstrapContext对象,它是BootstrapContext接口的默认实现。这个引导上下文对象包含了一些关键的信息和资源,比如应用程序的启动类、类加载器、环境配置等。
    //通过创建引导上下文对象,可以为应用程序的启动过程提供一些必要的上下文环境和资源。这样可以确保应用程序在启动过程中能够正常加载和初始化所需的组件和配置。
    DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    //声明应用上下文
    ConfigurableApplicationContext context = null;
    configureHeadlessProperty();
    // 获取监听器,作用是为后期一些环境参数进行赋值,就是加载配置文件
    SpringApplicationRunListeners listeners = getRunListeners(args);
    //遍历调用监听器,表示监听器已经开始初始化容器
    listeners.starting(bootstrapContext, this.mainApplicationClass);
    try {
        // 将args包装厂ApplicationArguments类
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        // ********监听器开始对对环境参数进行赋值***********
        ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
        configureIgnoreBeanInfo(environment);
        //打印输出banner图,就是springboot启动时,前面几行图形
        Banner printedBanner = printBanner(environment);
        // 初始化上下文对象AnnotationConfigServletWebServerApplicationContext
        context = createApplicationContext();
        context.setApplicationStartup(this.applicationStartup);
        // 部署上下文
        prepareContext(bootstrapContext, context, environment, listeners, applicationArguments, printedBanner);
        // 刷新上下文
        refreshContext(context);
        //刷新后的方法,空方法,给用户自定义重写
        afterRefresh(context, applicationArguments);
        //作用是计算程序启动所花费的时间。它使用System.nanoTime()方法获取当前时间的纳秒数,并与startTime进行减法运算,得到程序启动所经过的纳秒数。然后,使用Duration.ofNanos()方法将纳秒数转换为Duration对象,以便后续使用和处理。
        Duration timeTakenToStartup = Duration.ofNanos(System.nanoTime() - startTime);
        //输出日志记录执行主类名、时间信息
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), timeTakenToStartup);
        }
        //********* 使用广播和回调机制告诉监听者springboot容器已经启动化成功**********
        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;
}

3.2.2 监听器

跟踪监听器:EventPublishingRunListener
run方法代码总览在这里面,listeners出现了很多次,调用了starting,started等方法,那他们又有什么区别呢?首先,我们先跟踪源码看看这个listeners到底是什么,进入getRunListeners方法,可以看到

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

getSpringFactoriesInstances方法大家看了前面的现在应该知道了,这段代码的意思就是要去所有的META-INF文件下的spring.factorie寻找关于key为SpringApplicationRunListener的value配置,可以发现在这里存在:

这个方法最后返回的是org.springframework.boot.context.event.EventPublishingRunListener这个类,那我们就打开这个类看看里面的内容:

这个方法它实现了SpringApplicationRunListener接口,这个接口就是用来加载我们配置文件用的。

3.2.3 引入注解

springboot的启动分为两部分,一部分是注解,一部分是SpringApplication.run(Springboot.class, args),那么注解又是如何嵌入到程序中呢?依靠的就是refreshContext方法,同理,我们跟踪源码进入从SpringApplication中进入refreshContext方法:

private void refreshContext(ConfigurableApplicationContext context) {
    if (this.registerShutdownHook) {
        shutdownHook.registerApplicationContext(context);
    }
    refresh(context);
}

refresh方法源码如下:

/**
 * Refresh the underlying {@link ApplicationContext}.
 * @param applicationContext the application context to refresh
 */
protected void refresh(ConfigurableApplicationContext applicationContext) {
    applicationContext.refresh();
}

protected void refresh(ConfigurableApplicationContext appliationContext)这个方法的作用是刷新一个可配置的应用程序上下文(ConfigurableApplicationContext)。在方法内部,调用了应用程序上下文的refresh()方法来执行刷新操作。

刷新应用程序上下文是指重新加载配置文件、重新实例化Bean对象、重新注入依赖等操作,以确保应用程序上下文中的所有组件都处于最新的状态。这通常在应用程序启动时或者在需要重新加载配置的情况下使用。

通过调用refresh()方法,应用程序上下文会重新加载配置文件,并根据配置文件中的定义重新实例化Bean对象。同时,它还会重新解析依赖关系,并将依赖注入到相应的Bean中。

总之,这个方法的作用是刷新应用程序上下文,以确保其中的所有组件都处于最新的状态。

3.2.4.内置tomcat

内置tomcat是在注解引入的类中生成的,而refreshContext可以引入注解。前面说了,我们refreshContext是刷新上下文,那如果想要知道上下文中是否存在生成tomcat的类,我们直接去最后返回的上下文中找对应的类即可!在启动类的main方法写获取上下文的代码,并且打印出对应的name:

@SpringBootApplication
public class Springboot03Application {

    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(Springboot03Application.class, args);

        String[] beanDefinitionNames = run.getBeanDefinitionNames();
        for (String beanDefinitionName : beanDefinitionNames) {
            System.out.println(beanDefinitionName);
        }
    }
}

直接启动,可以看到有org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration这个类

我们点开这个类,跟踪源码:

我们知道,springboot其实有三种内容服务器,分别是Tomcat,Jetty,Undertow。默认内置tomcat。继续跟踪EmbeddedTomcat.class

可以看到,其实这里的tomcat服务器是内部通过java代码实现的。到这里,run()方法就算结束了。run()方法总结起来并不多,大多无非是配置环境参数,引入注解刷新上下文。其他的一些捕获异常、计时操作都是非重点操作。

四、springboot总结

4.1 springboot原理

springboot包装spring核心注解,使用springmvc无xml进行启动,通过自定义starter和maven依赖简化开发代码,开发者能够快速整合第三方框架,通过java语言内嵌入tomcat

4.2 springboot启动流程

--------------------------------创建springbootApplication对象---------------------------------------------
1. 创建springbootApplication对象springboot容器初始化操作
2. 获取当前应用的启动类型。
    注1:通过判断当前classpath是否加载servlet类,返回servlet web启动方式。
    注2:webApplicationType三种类型:
        1.reactive:响应式启动(spring5新特性)
        2.none:即不嵌入web容器启动(springboot放在外部服务器运行 )
        3.servlet:基于web容器进行启动
3. 读取springboot下的META-INFO/spring.factories文件,获取对应的ApplicationContextInitializer装配到集合
4. 读取springboot下的META-INFO/spring.factories文件,获取对应的ApplicationListener装配到集合
5. mainApplicationClass,获取当前运行的主函数
6. 
------------------调用springbootApplication对象的run方法,实现启动,返回当前容器的上下文----------------------------------------------
7. 调用run方法启动
8. StopWatch stopWatch = new StopWatch(),记录项目启动时间
9. getRunListeners,读取META-INF/spring.factores,将SpringApplicationRunListeners类型存到集合中
10. listeners.starting();循环调用starting方法
11. prepareEnvironment(listeners, applicationArguments);将配置文件读取到容器中
        读取多数据源:classpath:/,classpath:/config/,file:./,file:./config/底下。其中classpath是读取编译后的,file是读取编译前的
        支持yml,yaml,xml,properties
12. Banner printedBanner = printBanner(environment);开始打印banner图,就是sprongboot启动最开头的图案
13. 初始化AnnotationConfigServletWebServerApplicationContext对象
14. 刷新上下文,调用注解,refreshContext(context);
15. 创建tomcat
16. 加载springmvc
17. 刷新后的方法,空方法,给用户自定义重写afterRefresh()
18. stopWatch.stop();结束计时
19. 使用广播和回调机制告诉监听者springboot容器已经启动化成功,listeners.started(context);
20. 使用广播和回调机制告诉监听者springboot容器已经启动化成功,listeners.started(context);
21. 返回上下文

五、SpringBoot常用注解简介

注解 含义
@Configuration 作用于类,用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法会被AnnotationConfigApplicationContextAnncationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化spring容器。
@ComponentScan 该注解会扫描@Controller,@Service,@Repository,@component注解到类到spring容器中。
@Conditional 该注解作用于类,它可以根据代码中的条件装载不同的bean,在设置注解之前类需要实现Condition接口,然后对该实现接口的类设置是否装载条件。
@Import 通过导入的方式实现吧实例加入spring容器中,可以在需要时间没有被spring管理的类导入至Spring容器中。
@ImportResource @Import类似,区别就是该注解导入的是配置文件。
@Component 该注解是一个元注解,意思是可以注解其它类注解,如@Controller @Service @Repository。带此注解的类被看作组件,当使用基于注解的配置和类路径扫描的时候,这些类就会被实例化。
@SpringBootApplication 这个注解是Spring Boot最核心的注解,用在SpringBoot的主类上,标识这是一个Spring Boot应用。用来开启Spring Boot的各项能力,实际上这个注解@Configuration,@EnableAutoConfiguration,@ComponentScan三个注解的组合.由于这些注解一般都是一起使用的。
@EnableAutoConfiguration 允许Spring Boot自动配置注解,开启这个注解之后,Spring Boot就能根据当前类路径下的包或者类来配置Spring Bean。配置信息是从META-INF/spring.factories加载的。
posted @ 2022-11-02 11:50  酒剑仙*  阅读(143)  评论(0)    收藏  举报