27. 从零开始学springboot-运行原理

前言

SpringBoot作为目前最流行的 Java 开发框架,秉承“约定优于配置”原则,大大简化了 Spring MVC 繁琐的 XML 文件配置,基本实现零配置启动项目。
本文基于SpringBoot 2.1.4.RELEASE 版本,通过一步步追踪代码,详细探求 SpringBoot的运行原理。

图解

整个springboot运行流程可以由如下图表示(图来自互联网,版权归原作者所有)


1.png

入口类

首先让我们看一下最简单的 SpringBoot入口类

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

入口类的要求是最顶层包下面第一个含有 main 方法的类,使用注解 @SpringBootApplication 来启用SpringBoot特性,使用 SpringApplication.run 方法来启动 Spring Boot 项目。

首先我们来看下@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 {
        //....省略
}

由上可以看出@SpringBootApplication注解实际上是SpringBoot提供的一个复合注解,其中最重要的三个注解分别是:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

事实上,我们可以把入口类上的@SpringBootApplication这个注解替换为以上三个注解,效果是一样的。同学们可以自己试试。

下面我们分别阐述这三个注解都是干什么的。

@SpringBootConfiguration

@SpringBootConfiguration
追踪其实现代码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}

终于看到熟悉的@Configuration注解了,在Spring中,作用是配置Spring容器,也即 JavaConfig 形式的 Spring IoC 容器的配置类所使用。
这说明 @SpringBootConfiguration 也是来源于 @Configuration,二者功能都是将当前类标注为配置类,并将当前类里以 @Bean 注解标记的方法的实例注入到srping容器中,实例名即为方法名。

@EnableAutoConfiguration

@EnableAutoConfiguration
实现代码

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
        //....省略
}

@EnableAutoConfiguration注解作用是启用自动配置,可以帮助 SpringBoot 应用,将所有符合条件的 @Configuration 配置都加载到当前 IoC 容器之中

@AutoConfigurationPackage自动配置包,将SpringBootApplication主配置类所在包以及子包的所有子类扫描到spring容器

@Import(AutoConfigurationImportSelector.class)导入组件开启自动配置类导包的选择器。
我们可以追踪AutoConfigurationImportSelector这个类看看其实现

    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        }
        AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
                .loadMetadata(this.beanClassLoader);
        AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(
                autoConfigurationMetadata, annotationMetadata);
        return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
    }

对应的getAutoConfigurationEntry()实现

protected AutoConfigurationEntry getAutoConfigurationEntry(
            AutoConfigurationMetadata autoConfigurationMetadata,
            AnnotationMetadata annotationMetadata) {
        if (!isEnabled(annotationMetadata)) {
            return EMPTY_ENTRY;
        }
        AnnotationAttributes attributes = getAttributes(annotationMetadata);
        List<String> configurations = getCandidateConfigurations(annotationMetadata,
                attributes);
        configurations = removeDuplicates(configurations);
        Set<String> exclusions = getExclusions(annotationMetadata, attributes);
        checkExcludedClasses(configurations, exclusions);
        configurations.removeAll(exclusions);
        configurations = filter(configurations, autoConfigurationMetadata);
        fireAutoConfigurationImportEvents(configurations, exclusions);
        return new AutoConfigurationEntry(configurations, exclusions);
    }

getCandidateConfigurations()的实现

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata,
            AnnotationAttributes attributes) {
        List<String> configurations = SpringFactoriesLoader.loadFactoryNames(
                getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader());
        Assert.notEmpty(configurations,
                "No auto configuration classes found in META-INF/spring.factories. If you "
                        + "are using a custom packaging, make sure that file is correct.");
        return configurations;
    }

可以看出selectImports() 方法最终是通过调用SpringCore 包里 SpringFactoriesLoader 类的 loadFactoryNames()方法,读取了 ClassPath 下面的 META-INF/spring.factories 文件来获取所有导出类。而spring.factories 文件里关于 EnableAutoConfiguration 的配置其实就是一个键值对结构,我们看下对应的spring.factories文件内容

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
//.......省略

最后,我们概括下,@EnableAutoConfiguration注解实现了从 ClassPath下扫描所有的 META-INF/spring.factories 配置文件,并将spring.factories 文件中的 EnableAutoConfiguration 对应的配置项通过反射机制实例化为对应标注了 @Configuration 的形式的IoC容器配置类,然后注入IoC容器。

@ComponentScan

最后我们看下@ComponentScan注解
@ComponentScan 对应于XML配置形式中的 <context:component-scan>,用于将一些标注了特定注解的bean定义批量采集注册到Spring的IoC容器之中,这些特定的注解大致包括:
@Controller
@Entity
@Component
@Service
@Repository
等等

对于该注解,还可以通过 basePackages 属性来更细粒度的控制该注解的自动扫描范围,比如:

@ComponentScan(basePackages = {"com.mrcoder.controller","com.mrcoder.entity"})

SpringApplication实例初始化过程分析

追踪入口类中的run方法

public static ConfigurableApplicationContext run(Class<?> primarySource,
        String... args) {
    return run(new Class<?>[] { primarySource }, args);
}

/**
  * 第一个参数 primarySource:加载的主要资源类
  * 第二个参数 args:传递给应用的应用参数
  */
public static ConfigurableApplicationContext run(Class<?>[] primarySources,
        String[] args) {
    return new SpringApplication(primarySources).run(args);
}

可以看出,run方法先用主要资源类primarySources 来实例化一个 SpringApplication 对象,再调用这个SpringApplication对象的 run 方法。
进入SpringApplication()构造方法

public SpringApplication(Class<?>... primarySources) {
    this(null, primarySources);
}

然后进入SpringApplication构造函数

@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    //1.资源初始化资源加载器为 null
    this.resourceLoader = resourceLoader;
    //2.断言主要加载资源类不能为 null,否则报错
    Assert.notNull(primarySources, "PrimarySources must not be null");
    //3.初始化主要加载资源类集合并去重
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    //4.推断当前 WEB 应用类型
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    //5.设置应用上线文初始化器
    setInitializers((Collection) getSpringFactoriesInstances(
            ApplicationContextInitializer.class));
    //6.设置监听器            
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    //7.推断主入口应用类
    this.mainApplicationClass = deduceMainApplicationClass();
}

对于1-3没有什么好讲的,大家都能看懂,
我们看看后几步。
第4步
这个就是根据类路径下是否有对应项目类型的类推断出不同的应用类型,进入该类可以看到

package org.springframework.boot;
import org.springframework.util.ClassUtils;
public enum WebApplicationType {
    //非 WEB 项目
    NONE,
    //SERVLET WEB 项目
    SERVLET,
    //响应式 WEB 项目
    REACTIVE;
    
    //省略......

第5步

setInitializers((Collection) getSpringFactoriesInstances(
        ApplicationContextInitializer.class));

ApplicationContextInitializer该类是干啥的呢?进入该类可以看到如下定义

package org.springframework.context;
public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
    void initialize(C applicationContext);
}

用来初始化指定的 Spring 应用上下文,如注册属性资源、激活 Profiles 等。
接下来我们来分析下第5步使用的setInitializers()方法

public void setInitializers(
        Collection<? extends ApplicationContextInitializer<?>> initializers) {
    this.initializers = new ArrayList<>();
    this.initializers.addAll(initializers);
}

分析实现源码,可以看出,其实就是初始化一个 ApplicationContextInitializer 应用上下文初始化器实例的集合。

接下来看下getSpringFactoriesInstances()的方法实现

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
        Class<?>[] parameterTypes, Object... args) {
    //1.获取当前线程上下文类加载器
    ClassLoader classLoader = getClassLoader();
    //2.获取 ApplicationContextInitializer 的实例名称集合并去重
    Set<String> names = new LinkedHashSet<>(
            //3.loadFactoryNames根据类路径下的 META-INF/spring.factories 文件解析并获取 ApplicationContextInitializer 接口的所有配置的类路径名称。
            SpringFactoriesLoader.loadFactoryNames(type, classLoader));
            //4.根据以上类路径创建初始化器实例列表
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
            classLoader, args, names);
    //5.初始化器实例列表排序
    AnnotationAwareOrderComparator.sort(instances);
    //6.返回初始化器实例列表
    return instances;
}

在看第6步,设置监听器,可以看到使用了ApplicationListener这个类,

package org.springframework.context;
import java.util.EventListener;
@FunctionalInterface
public interface ApplicationListener<E extends ApplicationEvent> extends EventListener {
    void onApplicationEvent(E event);
}

以上代码可知,这个接口继承了 JDK 的 java.util.EventListener 接口,实现了观察者模式,它一般用来定义感兴趣的事件类型,事件类型限定于 ApplicationEvent 的子类,这同样继承了 JDK 的 java.util.EventObject 接口。

设置监听器和设置初始化器调用的方法是一样的,只是传入的类型不一样,设置监听器的接口类型为getSpringFactoriesInstances,对应的spring-boot-autoconfigure-2.1.4.RELEASE.jar!/META-INF/spring.factories 文件配置内容请见下方

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer

可以看出目前只有一个 BackgroundPreinitializer 监听器。

第7步推断主入口应用类

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

这个推断入口应用类的方式有点特别,通过构造一个运行时异常,再遍历异常栈中的方法名,获取方法名为 main 的栈帧,从来得到入口类的名字再返回该类。

欢迎关注我

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

推荐阅读更多精彩内容