一图看懂SpringBootApplication启动原理

概述

SpringBoot启动架构图

剖析@SpringBootApplication注解

首先分析springboot的启动注解@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 

@Target(ElementType.TYPE) 注解的目标位置:接口、类、枚举。
@Retention(RetentionPolicy.RUNTIME) 注解会在class字节码文件中存在,在运行时可以通过反射获取到。
@Documented 用于生成javadoc,默认情况下,javadoc是不包括注解的. 但如果声明注解时指定了 @Documented, 则它会被 javadoc 之类的工具处理,所以注解类型信息也会被包括在生成的文档中。
@Inherited 作用:在类继承关系中,如果子类要继承父类的注解,那么要该注解必须被@Inherited修饰的注解。
除了以上常规的几个注解,剩下几个就是springboot的核心注解了。
@SpringBootApplication就是一个复合注解,包括@ComponentScan,和@SpringBootConfiguration,@EnableAutoConfiguration。

@SpringBootApplication注解原理

通过了解@SpringBootApplication,明白了它是一个复合注解。
通过在springboot项目中删除@SpringBootApplication,用下面三个代替,然后启动springboot:

@ComponentScan
@SpringBootConfiguration
@EnableAutoConfiguration

那么以上所有注解就只干一件事:把bean注册到spring ioc容器。
@SpringBootApplication就只干了一件事通过3种方式来实现:

1. @SpringBootConfiguration 通过@Configuration 与@Bean结合,注册到Spring ioc 容器。
2. @ComponentScan  通过范围扫描的方式,扫描特定注解类,将其注册到Spring ioc 容器。
3. @EnableAutoConfiguration 通过spring.factories的配置,来实现bean的注册到Spring ioc 容器。

1. 剖析@SpringBootConfiguration

从以上源码可以看出@SpringBootConfiguration其实就是一个@Configuration,说明了标注当前类是配置类。

(1) 什么是@Configuration注解,它有什么作用?

从Spring3.0开始,@Configuration用于定义配置类,可替换xml配置文件,被注解的类内部包含有一个或多个被@Bean注解的方法,这些方法将会被AnnotationConfigApplicationContext或AnnotationConfigWebApplicationContext类进行扫描,并用于构建bean定义,初始化Spring容器。

(2) 用@Configuration配置spring并加载spring容器

@Configuration标注在类上,@Configuation等价于spring的xml配置文件中的<Beans></Beans>

步骤1:先加入spring的依赖包

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.1.9.RELEASE</version>
</dependency>

步骤2:创建一个Configuration类

@Configuration
public class MyConfiguration {
    public MyConfiguration() {
        System.out.println("MyConfiguration容器启动初始化。。。");
    }

}

以上代码,等价于以下xml配置文件中的<Beans></Beans>

<?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:context="http://www.springframework.org/schema/context" xmlns:jdbc="http://www.springframework.org/schema/jdbc"  
    xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx"
    xmlns:util="http://www.springframework.org/schema/util" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="
        http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd
        http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd
        http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc-4.0.xsd
        http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-4.0.xsd
        http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd
        http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-4.0.xsd
        http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-4.0.xsd" default-lazy-init="false">


</beans>

步骤3:加一个测试类

public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
    }

}

结果:

MyConfiguration容器启动初始化。。。

Process finished with exit code 0

(3)如何把一个对象,注册到Spring IoC 容器中

要把一个对象注册到Spring IoC 容器中,一般是用@Bean 注解来实现:
@Bean的作用:带有 @Bean 的注解方法将返回一个对象,该对象应该被注册为在Spring IoC 容器中。

步骤1:创建一个bean


public class UserBean {

    private String username;

    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "UserBean{" +
                "username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

步骤2:把bean注解在ioc容器里面

@Configuration
public class MyConfiguration {
    public MyConfiguration() {
        System.out.println("MyConfiguration容器启动初始化。。。");
    }

    /**
     * @Bean注解在返回实例的方法上,如果未通过@Bean指定bean的名称,则默认的Bean对象名与标注的方法名相同;
     * 以下创建的对象名,和方法名一样,即userBean
     */
    @Bean
    public UserBean userBean(){
        UserBean userBean= new UserBean();
        userBean.setUsername("test");
        userBean.setPassword("123456");
        return userBean;
    }
}

上面的代码将等同于下面的 XML 配置:

<beans>
   <bean id="userBean" class="com.test.boot.annotation.bean.UserBean" />
</beans>

步骤3:加一个体验类

创建的bean对象,可以通过AnnotationConfigApplicationContext加载进spring ioc 容器中。

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(MyConfiguration.class);
        UserBean userBean=(UserBean)context.getBean("userBean");

        System.out.println(userBean.toString());
    }

结果:

MyConfiguration容器启动初始化。。。
UserBean{username='test', password='123456'}

Process finished with exit code 0

2. 剖析@ComponentScan注解

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),  @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })

excludeFilters:过滤不需要扫描的类型。
@Filter 过滤注解
FilterType.CUSTOM 过滤类型为自定义规则,即指定特定的class
classes :过滤指定的class,即剔除了TypeExcludeFilter.class、AutoConfigurationExcludeFilter.class

从以上源码可以知道:

  1. @SpringBootApplication的源码包含了@ComponentScan。
    因此,只要@SpringBootApplication注解的所在的包及其下级包,都会将class扫描到并装入spring ioc容器。
  2. 如果你自定义的定义一个Spring bean,不在@SpringBootApplication注解的所在的包及其下级包,
    都必须手动加上@ComponentScan注解并指定那个bean所在的包。

(1)为什么要用@ComponentScan?它解決什么问题?

1. 为什么要用@ComponentScan ?

定义一个Spring bean 一般是在类上加上注解 @Service 或@Controller 或 @Component就可以,
但是,spring怎么知道有你这个bean的存在呢?
因此,我们必须告诉spring去哪里找这个bean类。
@ComponentScan就是用来告诉spring去哪里找bean类。

2. @componentscan的作用

作用:告诉Spring去扫描@componentscan指定包下所有的注解类,然后将扫描到的类装入spring bean容器。
例如:@ComponentScan("com.test.boot.scan"),就只能扫描com.test.boot.scan包下的注解类。
如果不写?就像@SpringBootApplication的@ComponentScan没有指定路径名?它去哪里找?
@SpringBootApplication注解的所在的包及其下级包,都会讲class扫描到并装入spring ioc容器。

(2)案例实战:体验@ComponentScan的作用

步骤1:在包名为com.test.boot.scan,新建一个ComponentScan测试类

package com.test.boot.scan;

import org.springframework.stereotype.Service;

@Service
public class TestComponentScan {
}

步骤2:在com.test.boot.app下面建个启动类

package com.test.boot.app;

import com.test.boot.scan.TestComponentScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(Application.class, args);
        TestComponentScan componentScan = run.getBean(TestComponentScan.class);
        System.out.println(componentScan.toString());
    }
}

启动报错:

seconds (JVM running for 3.941)
Exception in thread "main" org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.test.boot.scan.TestComponentScan' available
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:346)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBean(DefaultListableBeanFactory.java:337)
    at org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1123)
    at com.test.boot.app.Application.main(Application.java:14)

以上报错的意思是:找不到com.test.boot.scan.TestComponentScan这个bean,那怎么办呢?
这要加这行代码重新运行即可

  • 手工指定包路径
@ComponentScan("com.test.boot.scan")

整体如下:

@SpringBootApplication
@ComponentScan("com.test.boot.scan")
public class Application {
    public static void main(String[] args) {
        ConfigurableApplicationContext run = SpringApplication.run(Application.class, args);
        TestComponentScan componentScan = run.getBean(TestComponentScan.class);
        System.out.println(componentScan.toString());
    }
}

以上启动正常

3.剖析@EnableAutoConfiguration

@EnableAutoConfiguration是@SpringBootApplication中3大核心注解最重要的一个。
源码如下:

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

    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    /**
     * Exclude specific auto-configuration classes such that they will never be applied.
     * @return the classes to exclude
     */
    Class<?>[] exclude() default {};

    /**
     * Exclude specific auto-configuration class names such that they will never be
     * applied.
     * @return the class names to exclude
     * @since 1.3.0
     */
    String[] excludeName() default {};

}

其中最关键的是@Import(AutoConfigurationImportSelector.class),我们先来讲解@Import

(1)@Import有什么作用?

@Import作用: 将指定的类实例注入到spring IOC容器中。

(2)编码实现@Import例子

步骤1:创建一个bean

创建这个bean的目的是把它注入springioc容器中

public class UserBean {

}

步骤2:新建一个service

采用@Import来,将UserBean注入到spring ioc 容器中

@Component
@Import({UserBean.class})
public class UserService {


}

步骤3:启动类


public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(UserService.class);
        UserService userService = context.getBean(UserService.class);
        UserBean userBean=context.getBean(UserBean.class);

        System.out.println(userService.toString());
        System.out.println(userBean.toString());
    }

}

结果:

com.test.boot.ioc.imports.UserService@52525845
com.test.boot.ioc.imports.UserBean@3b94d659

(3)spring的ImportSelector接口有什么作用?

从AutoConfigurationImportSelector源码,进入后,发现了6个核心接口,如下:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered 

但是最核心的是DeferredImportSelector接口。最核心的!!!

从DeferredImportSelector接口的源码中,看出了它继承了ImportSelector,源码如下:

public interface DeferredImportSelector extends ImportSelector {

再看ImportSelector的源码

public interface ImportSelector {

    /**
     * Select and return the names of which class(es) should be imported based on
     * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
     */
    String[] selectImports(AnnotationMetadata importingClassMetadata);

}

从以上源码可以看出:
ImportSelector接口值定义了一个selectImports方法,它的作用收集需要将class注册到spring ioc容器里面。
ImportSelector接口一般和@Import一起使用,一般用@Import会引入ImportSelector实现类后,会把实现类中得返回class数组都注入到spring ioc 容器中。

(4)案例实战: 模仿@EnableAutoConfiguration注解,写一个@Enable*的开关注解

很多开关注解类,例如:@EnableAsync 、@EnableSwagger2、@EnableAutoConfiguration
@Enable代表的意思就是开启一项功能,起到了开关的作用。
这些开关注解类的原理是什么?
底层是用ImportSelector接口来实现的。

步骤1: 新建2个bean

``
public class UserBean {

}

public class RoleBean {
}
``

步骤2:自定义一个ImportSelector类,记得实现ImportSelector接口

通过ImportSelector的selectImports方法,返回2个calass
"com.test.boot.ioc.selector.UserBean"
"com.test.boot.ioc.selector.RoleBean"
目的:将收集到的2个class注册到spring ioc容器里面

public class UserImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        return new String[]{"com.test.boot.ioc.selector.UserBean",
        "com.test.boot.ioc.selector.RoleBean"};
    }
}

步骤3:自定义一个开关类

一般采用@Import会引入ImportSelector实现类(UserImportSelector.class)后,
会把实现类中得返回class数组
new String[]{"com.test.boot.ioc.selector.UserBean",
"com.test.boot.ioc.selector.RoleBean"};
都注入到spring ioc 容器中。

@Retention(RetentionPolicy.RUNTIME)
@Documented
@Target(ElementType.TYPE)
@Import(UserImportSelector.class)
public @interface EnableUserConfig {
}

步骤4:增加一个配置类,用于设置加入@EnableUserConfig


@EnableUserConfig
public class UserConfig {

}

步骤5:体验测试类


public class Main {

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(UserConfig.class);
        RoleBean roleBean = context.getBean(RoleBean.class);
        UserBean userBean = context.getBean(UserBean.class);

        System.out.println(userBean.toString());
        System.out.println(roleBean.toString());
    }

}

执行结果:

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

推荐阅读更多精彩内容