美文网首页
一图看懂SpringBootApplication启动原理

一图看懂SpringBootApplication启动原理

作者: sknfie | 来源:发表于2021-05-13 11:23 被阅读0次

    概述

    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
    

    相关文章

      网友评论

          本文标题:一图看懂SpringBootApplication启动原理

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