SpringBoot源码

SpringBoot核心流程

在这里插入图片描述
创建springbootApplication
1.推测应用类型,正常是SERVLET
2.获取所有的初始化器
3.获取所有的监听器
4.推断main主类
run方法
1.运行监听器
2.加载配置文件,运行时环境参数
3.打印banner条
4.加载应用上下文(Tomcat在此创建)
5.加载Spring容器(refreshContext方法,Spring容器)

SpringBoot核心前置内容

1.Spring注解编程的发展过程

image.png

1.1 Spring 1.x

2004年3月24日,Spring1.0 正式发布,提供了IoC,AOP及XML配置的方式。

在Spring1.x版本中提供的是纯XML配置的方式,也就是在该版本中必须要提供xml的配置文件,在该文件中通过 <bean> 标签来配置需要被IoC容器管理的Bean。

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


    <bean class="com.bobo.demo01.UserService" />
</beans>

调试代码

public static void main(String[] args) {
    ApplicationContext ac = new FileSystemXmlApplicationContext("classpath:applicationContext01.xml");
    System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
}

输出结果

image.png

在Spring1.2版本的时候提供了@Transaction (org.springframework.transaction.annotation )注解。简化了事务的操作.

image.png

1.2 Spring 2.x

在2006年10月3日 Spring2.0问世了,在2.x版本中,比较重要的特点是增加了很多注解

Spring 2.5之前

在2.5版本之前新增的有 @Required @Repository @Aspect,同时也扩展了XML的配置能力,提供了第三方的扩展标签,比如 <dubbo>

@Required

如果你在某个java类的某个set方法上使用了该注释,那么该set方法对应的属性在xml配置文件中必须被设置,否则就会报错!!!

public class UserService {


    private String userName;

    public String getUserName() {
        return userName;
    }

    @Required
    public void setUserName(String userName) {
        this.userName = userName;
    }
}

如果在xml文件中不设置对应的属性就会给出错误的提示。

image.png

设置好属性后就没有了错误提示了

image.png

源码中可以看到 @Required从2.0开始提供

image.png

@Repository

@Repository 对应数据访问层Bean.这个注解在Spring2.0版本就提供的有哦,大家可能没有想到。

image.png

@Aspect

@Aspect是AOP相关的一个注解,用来标识配置类。

Spring2.5 之后

在2007年11月19日,Spring更新到了2.5版本,新增了很多常用注解,大大的简化配置操作。

注解说明
@Autowired依赖注入
@Qualifier配置@Autowired注解使用
@Component声明组件
@Service声明业务层组件
@Controller声明控制层组件
@RequestMapping声明请求对应的处理方法

在这些注解的作用下,可以不用在xml文件中去注册没有bean,这时只需要指定扫码路径,然后在对应的Bean头部添加相关的注解即可,这大大的简化了配置及维护工作。案例如下:

在配置文件中只需要配置扫码路径即可:

<?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"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="com.bobo" />

</beans>

持久层代码:

@Repository
public class UserDao {

    public void query(){
        System.out.println("dao query ..." );
    }
}

业务逻辑层代码

@Service
public class UserService {

    @Autowired
    private UserDao dao;

    public void query(){
        dao.query();
    }
}

控制层代码:

@Controller
public class UserController {

    @Autowired
    private UserService service;

    public void query(){
        service.query();
    }
}

测试代码

public class Demo02Main {
    public static void main(String[] args) {
        ApplicationContext ac = new ClassPathXmlApplicationContext("applicationContext02.xml");
        UserController acBean = ac.getBean(UserController.class);
        acBean.query();
    }
}

虽然在Spring的2.5版本提供了很多的注解,也大大的简化了开发,但是任然没有摆脱XML配置驱动。

1.3 Spring 3.x

在2009年12月16日发布了Spring3.0版本,这是一个注解编程发展的里程碑版本,在该版本中全面拥抱Java5。提供了 @Configuration注解,目的就是去xml化。同时通过 @ImportResource来实现Java配置类和XML配置的混合使用来实现平稳过渡。

/**
 * @Configuration 标注的Java类 相当于 application.xml 配置文件
 */
@Configuration
public class JavaConfig {

    /**
     * @Bean 注解 标注的方法就相当于 <bean></bean> 标签
              也是 Spring3.0 提供的注解
     * @return
     */
    @Bean
    public UserService userService(){
        return new UserService();
    }
}

在Spring3.1 版之前配置扫描路径还只能在 XML 配置文件中通过 component-scan 标签来实现,在3.1之前还不能够完全实现去XML配置,在3.1 版本到来的时候,提供了一个 @ComponentScan注解,该注解的作用是替换掉 component-scan标签,是注解编程很大的进步,也是Spring实现无配置话的坚实基础。

@ComponentScan

@ComponentScan的作用是指定扫码路径,用来替代在XML中的 <component-scan>标签,默认的扫码路径是当前注解标注的类所在的包及其子包。

定义UserService

@Service
public class UserService {
}

创建对于的Java配置类

@Configuration
@ComponentScan
public class JavaConfig {

    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
        System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
    }
}

输出的结果

image.png

当然也可以指定特定的扫描路径

@Configuration
// 指定特定的扫描路径
@ComponentScan(value = {"com.bobo.demo04"})
public class JavaConfig {

    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
        System.out.println("ac.getBean(UserService.class) = " + ac.getBean(UserService.class));
    }
}
@Import

@Import注解只能用在类上,作用是快速的将实例导入到Spring的IoC容器中,将实例导入到IoC容器中的方式有很多种,比如 @Bean注解,@Import注解可以用于导入第三方包。具体的使用方式有三种。
用在类上,也能将类导入spring容器

静态导入

静态导入的方式是直接将需要导入到IoC容器中的对象类型直接添加进去即可。

image.png

这种方式的好处是简单,直接,但是缺点是如果要导入的比较多,则不太方便,而且也不灵活。
在这里插入图片描述

ImportSelector

@Import注解中也可以添加一个实现了 ImportSelector接口的类型,这时不会将该类型导入IOC容器中,而是会调用 ImportSelector接口中定义的 selectImports方法,将该方法的返回的字符串数组的类型添加到容器中

定义两个业务类

public class Cache {
}
public class Logger {
}

定义ImportSelector接口的实现,方法返回的是需要添加到ICC容器中的对象对应的类型的全类路径的字符串数组,可以根据不同的业务需求而导入不同的类型,会更加的灵活些

public class MyImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{Logger.class.getName(),Cache.class.getName()};
    }
}

导入测试案例

@Configuration
@Import(MyImportSelector.class)
public class JavaConfig {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
        for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
            System.out.println("beanDefinitionName = " + beanDefinitionName);
        }
    }
}

输出结果:

image.png

ImportBeanDefinitionRegistrar

除了上面所介绍的ImportSelector方式灵活导入以外还提供了 ImportBeanDefinitionRegistrar 接口,也可以实现,相比 ImportSelector 接口的方式,ImportBeanDefinitionRegistrar 的方式是直接在定义的方法中提供了 BeanDefinitionRegistry ,自己在方法中实现注册

public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        // 将需要注册的对象封装为 RootBeanDefinition 对象
        RootBeanDefinition cache = new RootBeanDefinition(Cache.class);
        registry.registerBeanDefinition("cache",cache);

        RootBeanDefinition logger = new RootBeanDefinition(Logger.class);
        registry.registerBeanDefinition("logger",logger);
    }
}

测试代码

@Configuration
@Import(MyImportBeanDefinitionRegistrar.class)
public class JavaConfig {
    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
        for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
            System.out.println("beanDefinitionName = " + beanDefinitionName);
        }
    }
}

输出结果

image.png

@EnableXXX

@Enable模块驱动,其实是在系统中先开发好各个功能独立的模块,比如 Web MVC 模块, AspectJ代理模块,Caching模块等。

image.png

案例说明,先定义好功能模块

/**
 * 定义一个Java配置类
 */
@Configuration
public class HelloWorldConfiguration {

    @Bean
    public String helloWorld(){
        return "Hello World";
    }
}

然后定义@Enable注解

/**
 * 定义@Enable注解
 * 在该注解中通过 @Import 注解导入自定义的模块,使之生效。
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(HelloWorldConfiguration.class)
public @interface EnableHelloWorld {
}

测试代码

@Configuration
// 加载 自定义 模块
@EnableHelloWorld
public class JavaMian {

    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(JavaMian.class);
        String helloWorld = ac.getBean("helloWorld", String.class);
        System.out.println("helloWorld = " + helloWorld);
    }
}

效果

image.png

1.4 Spring 4.x

2013年11月1 日更新的Spring 4.0 ,完全支持Java8.这是一个注解完善的时代,提供的核心注解是@Conditional条件注解。@Conditional 注解的作用是按照一定的条件进行判断,满足条件就给容器注册Bean实例。

@Conditional的定义为:

// 该注解可以在 类和方法中使用
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {

    /**
     * 注解中添加的类型必须是 实现了 Condition 接口的类型
     */
    Class<? extends Condition>[] value();

}

Condition是个接口,需要实现matches方法,返回true则注入bean,false则不注入。

案例讲解:

/**
 * 定义一个 Condition 接口的是实现
 */
public class MyCondition implements Condition {
    @Override
    public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
		@verride
		public boolean matches(ConditionContext context,AnnotatedTypeMetadata metadata) {
		// 根据特定的业务需求来决定是否注入对应的对象
        try {
            boolean flage = context.getRegistry().containsBeanDefinition("userService");
            if (flage) {
                Class.forName("com.bobo.test.test666");
                return flage;
            }
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        return false;// 默认返回false
    }
}

创建Java配置类

@Configuration
public class JavaConfig {

    @Bean
    // 条件注解,添加的类型必须是 实现了 Condition 接口的类型
    // MyCondition的 matches 方法返回true 则注入,返回false 则不注入
    @Conditional(MyCondition.class)
    public StudentService studentService(){
        return new StudentService();
    }

    public static void main(String[] args) {
        ApplicationContext ac = new AnnotationConfigApplicationContext(JavaConfig.class);
        for (String beanDefinitionName : ac.getBeanDefinitionNames()) {
            System.out.println("beanDefinitionName = " + beanDefinitionName);
        }
    }
}

测试:

image.png

但是将 matchs方法的返回结果设置为 true 则效果不同

image.png

所以@Conditional的作用就是提供了对象导入IoC容器的条件机制,这也是SpringBoot中的自动装配的核心关键。当然在4.x还提供一些其他的注解支持,比如 @EventListener,作为ApplicationListener接口编程的第二选择,@AliasFor解除注解派生的时候冲突限制。@CrossOrigin作为浏览器跨域资源的解决方案。

1.5 Spring 5.x

2017年9月28日,Spring来到了5.0版本。5.0同时也是SpringBoot2.0的底层。注解驱动的性能提升方面不是很明显。在Spring Boot应用场景中,大量使用@ComponentScan扫描,导致Spring模式的注解解析时间耗时增大,因此,5.0时代引入**@Indexed**,为Spring模式注解添加索引

当在项目中使用了 @Indexed之后,编译打包的时候会在项目中自动生成 META-INT/spring.components文件。当Spring应用上下文执行 ComponentScan扫描时,META-INT/spring.components将会被 CandidateComponentsIndexLoader 读取并加载,转换为 CandidateComponentsIndex对象,这样的话 @ComponentScan不在扫描指定的package,而是读取 CandidateComponentsIndex对象,从而达到提升性能的目的。

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-indexer</artifactId>
</dependency>

使用@Indexed注解

image.png

image.png
核心条件注解@Conditional控制对象是否注入到容器中,@Import 注解中导入的类型
ImportSelector 接口,不会将该类型注入到容器中而是会将selectImports的返回的类型的全类路径的字符串的数据注入到容器中动态注入(类似于factoryBean)
ImportBeanDefinitionRegistrar 的方式是直接在定义的方法中提供了 BeanDefinitionRegistry ,自己在方法中实现注册。(自己注册了bean定义,接口类似于factoryBean)
@Indexed ,编译打包的时候会在项目中自动生成 META-INT/spring.components文件。当Spring应用上下文执行 ComponentScan扫描时,META-INT/spring.components将会被 CandidateComponentsIndexLoader 读取并加载,从而达到提升spring性能的目的。

2. 什么是SPI

为什么要讲SPI呢?因为在SpringBoot的自动装配中其实有使用到SPI机制,所以掌握了这部分对于SpringBoot的学习还是很有帮助的。

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在ClassPath路径下的META-INF/services文件夹查找文件,自动加载文件里所定义的类。这一机制为很多框架扩展提供了可能,比如在Dubbo、JDBC中都使用到了SPI机制。先通过一个很简单的例子来看下它是怎么用的。

案例介绍

先定义接口项目

在这里插入图片描述

然后创建一个扩展的实现,先导入上面接口项目的依赖

    <dependencies>
        <dependency>
            <groupId>com.bobo</groupId>
            <artifactId>JavaSPIBase</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>
    </dependencies>

然后创建接口的实现

/**
 * SPI:MySQL对于 baseURL 的一种实现
 */
public class MySqlConnection implements BaseData {
    @Override
    public void baseURL() {
        System.out.println("mysql 的扩展实现....");
    }
}

然后在resources目录下创建 META-INF/services 目录,然后在目录中创建一个文件,名称必须是定义的接口的全类路径名称。然后在文件中写上接口的实现类的全类路径名称
在这里插入图片描述

在这里插入图片描述

同样的再创建一个案例

在这里插入图片描述

然后在测试的项目中测试

    public static void main(String[] args) {
        ServiceLoader<BaseData> providers = ServiceLoader.load(BaseData.class);
        Iterator<BaseData> iterator = providers.iterator();
        while(iterator.hasNext()){
            BaseData next = iterator.next();
            next.baseURL();
        }
    }

根据不同的导入,执行的逻辑会有不同

image.png

image.png

源码查看

ServiceLoader

首先来看下ServiceLoader的类结构

   // 配置文件的路径
    private static final String PREFIX = "META-INF/services/";

    // 加载的服务  类或者接口
    private final Class<S> service;

    // 类加载器
    private final ClassLoader loader;

    // 访问权限的上下文对象
    private final AccessControlContext acc;

    // 保存已经加载的服务类
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 内部类,真正加载服务类
    private LazyIterator lookupIterator;

load

load方法创建了一些属性,重要的是实例化了内部类,LazyIterator。

public final class ServiceLoader<S> implements Iterable<S>
    private ServiceLoader(Class<S> svc, ClassLoader cl) {
        //要加载的接口
        service = Objects.requireNonNull(svc, "Service interface cannot be null");
        //类加载器
        loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
        //访问控制器
        acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
         reload();
        
    }
    public void reload() {
        //先清空
        providers.clear();
        //实例化内部类 
        LazyIterator lookupIterator = new LazyIterator(service, loader);
    }
}

查找实现类和创建实现类的过程,都在LazyIterator完成。当调用iterator.hasNext和iterator.next方法的时候,实际上调用的都是LazyIterator的相应方法。

private class LazyIterator implements Iterator<S>{
    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null; 
    private boolean hasNextService() {
        //第二次调用的时候,已经解析完成了,直接返回
        if (nextName != null) {
            return true;
        }
        if (configs == null) {
            //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
            //META-INF/services/com.viewscenes.netsupervisor.spi.SPIService
            String fullName = PREFIX + service.getName();
            //将文件路径转成URL对象
            configs = loader.getResources(fullName);
        }
        while ((pending == null) || !pending.hasNext()) {
            //解析URL文件对象,读取内容,最后返回
            pending = parse(service, configs.nextElement());
        }
        //拿到第一个实现类的类名
        nextName = pending.next();
        return true;
    }
}

创建实例对象,当然,调用next方法的时候,实际调用到的是,lookupIterator.nextService。它通过反射的方式,创建实现类的实例并返回。

private class LazyIterator implements Iterator<S>{
    private S nextService() {
        //全限定类名
        String cn = nextName;
        nextName = null;
        //创建类的Class对象
        Class<?> c = Class.forName(cn, false, loader);
        //通过newInstance实例化
        S p = service.cast(c.newInstance());
        //放入集合,返回实例
        providers.put(cn, p);
        return p; 
    }
}

看到这儿,已经很清楚了。获取到类的实例,自然就可以对它为所欲为了!

SpringBoot自动装配原理分析

自动装配源码分析

在前面的分析中,Spring Framework一直在致力于解决一个问题,就是如何让bean的管理变得更简单,如何让开发者尽可能的少关注一些基础化的bean的配置,从而实现自动装配。所以,所谓的自动装配,实际上就是如何自动将bean装载到Ioc容器中来

实际上在spring 3.x版本中,Enable模块驱动注解的出现,已经有了一定的自动装配的雏形,而真正能够实现这一机制,还是在spirng 4.x版本中,conditional条件注解的出现。看一下spring boot的自动装配是怎么一回事。

ImportSelector接口,调用 ImportSelector接口中定义的 selectImports方法,将该方法的返回的字符串数组的类型添加到容器中
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
过滤,检查候选配置类上的注解@ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载
configurations = this.getConfigurationClassFilter().filter(configurations);
在这里插入图片描述

自动装配的核心

本质上就是Spring容器的初始化
核心是自动配置类实现ImportSelect接口在selectImports方法中通过SPI机制,在自动配置包下,将META-INF下的spring.factory文件中把key为org.springframework.boot.autoconfigure.EnableAutoConfiguration的值获取到,并将value值转换为java类,注入Spring容器当中

自动装配的演示

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency> 
spring:
    redis:
      host: 127.0.0.1 
      port: 6379
 @Autowired
    private RedisTemplate<String,String>redisTemplate;

按照下面的顺序添加starter,然后添加配置,使用RedisTemplate就可以使用了? 那大家想没想过一个问题,为什么RedisTemplate可以被直接注入?它是什么时候加入到Ioc容器的呢? 这就是自动装配。自动装配可以使得classpath下依赖的包相关的bean,被自动装载到Spring Ioc容器中,怎么做到的呢?

深入分析EnableAutoConfiguration

EnableAutoConfiguration的主要作用其实就是帮助springboot应用把所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器中。

再回到EnableAutoConfiguration这个注解中,发现它的import是这样

@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {

但是从EnableAutoCOnfiguration上面的import注解来看,这里面并不是引入另外一个Configuration。而是一个ImportSelector。这个是什么东西呢?

AutoConfigurationImportSelector是什么?

Enable注解不仅仅可以像前面演示的案例一样很简单的实现多个Configuration的整合,还可以实现一些复杂的场景,比如可以根据上下文来激活不同类型的bean,@Import注解可以配置三种不同的class

  1. 第一种就是前面演示过的,基于普通bean或者带有@Configuration的bean进行诸如
  2. 实现ImportSelector接口进行动态注入

实现ImportBeanDefinitionRegistrar接口进行动态注入

CacheService

public class CacheService {
}

LoggerService

public class LoggerService {
}

EnableDefineService

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented 
@Inherited  --允许被继承
@Import({MyDefineImportSelector.class})
public @interface EnableDefineService {

    String[] packages() default "";
}

MyDefineImportSelector

public class MyDefineImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        //获得指定注解的详细信息。可以根据注解中配置的属性来返回不同的class,
        //从而可以达到动态开启不同功能的目的
    
annotationMetadata.getAllAnnotationAttributes(EnableDefineService.class.getName(),true)
            .forEach((k,v) -> {
                log.info(annotationMetadata.getClassName());
                log.info("k:{},v:{}",k,String.valueOf(v));
            });
        return new String[]{CacheService.class.getName()};
    }
}

EnableDemoTest

@SpringBootApplication
@EnableDefineService(name = "mashibing",value = "mashibing")
public class EnableDemoTest {
    public static void main(String[] args) {
        ConfigurableApplicationContext ca=SpringApplication.run(EnableDemoTest.class,args);
        System.out.println(ca.getBean(CacheService.class));
        System.out.println(ca.getBean(LoggerService.class));
    }
}

了解了selector的基本原理之后,后续再去分析AutoConfigurationImportSelector的原理就很简单了,它本质上也是对于bean的动态加载。

@EnableAutoConfiguration注解的实现原理

了解了ImportSelector和ImportBeanDefinitionRegistrar后,对于EnableAutoConfiguration的理解就容易一些了

它会通过import导入第三方提供的bean的配置类:AutoConfigurationImportSelector

@Import(AutoConfigurationImportSelector.class)

从名字来看,可以猜到它是基于ImportSelector来实现基于动态bean的加载功能。Springboot @Enable*注解的工作原理ImportSelector接口selectImports返回的数组(类的全类名)都会被纳入到spring容器中。

那么可以猜想到这里的实现原理也一定是一样的,定位到AutoConfigurationImportSelector这个类中的selectImports方法

selectImports

public String[] selectImports(AnnotationMetadata annotationMetadata) {
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
// 从配置文件(spring-autoconfigure-metadata.properties)中加载 AutoConfigurationMetadata
   AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
         .loadMetadata(this.beanClassLoader);
// 获取所有候选配置类EnableAutoConfiguration
   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);
//使用SpringFactoriesLoader 加载classpath路径下META-INF\spring.factories中,
//key= org.springframework.boot.autoconfigure.EnableAutoConfiguration对应的value
   List<String> configurations = getCandidateConfigurations(annotationMetadata,
         attributes);
//去重
   configurations = removeDuplicates(configurations);
//应用exclusion属性
   Set<String> exclusions = getExclusions(annotationMetadata, attributes);
   checkExcludedClasses(configurations, exclusions);
   configurations.removeAll(exclusions);
//过滤,检查候选配置类上的注解@ConditionalOnClass,如果要求的类不存在,则这个候选类会被过滤不被加载
   configurations = filter(configurations, autoConfigurationMetadata);
   //广播事件
fireAutoConfigurationImportEvents(configurations, exclusions);
   return new AutoConfigurationEntry(configurations, exclusions);
}

本质上来说,其实EnableAutoConfiguration会帮助springboot应用把所有符合@Configuration配置都加载到当前SpringBoot创建的IoC容器,而这里面借助了Spring框架提供的一个工具类SpringFactoriesLoader的支持。以及用到了Spring提供的条件注解@Conditional,选择性的针对需要加载的bean进行条件过滤

SpringFactoriesLoader

为了给大家补一下基础,我在这里简单分析一下SpringFactoriesLoader这个工具类的使用。它其实和java中的SPI机制的原理是一样的,不过它比SPI更好的点在于不会一次性加载所有的类,而是根据key进行加载。

首先,SpringFactoriesLoader的作用是从classpath/META-INF/spring.factories文件中,根据key来加载对应的类到spring IoC容器中。接下来带大家实践一下

创建外部项目jar

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

创建bean以及config

public class mashibingCore {
    public String study(){
        System.out.println("good good study, day day up");
        return "mashibingEdu.com";
    }
}
@Configuration
public class mashibingConfig {
    @Bean
    public mashibingCore mashibingCore(){
        return new mashibingCore();
    }
}

创建另外一个工程(spring-boot)

把前面的工程打包成jar,当前项目依赖该jar包

<dependency>
    <groupId>com.mashibingedu.practice</groupId>
    <artifactId>mashibing-Core</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

通过下面代码获取依赖包中的属性

运行结果会报错,原因是mashibingCore并没有被Spring的IoC容器所加载,也就是没有被EnableAutoConfiguration导入

@SpringBootApplication
public class SpringBootStudyApplication {
    public static void main(String[] args) throws IOException {
        ConfigurableApplicationContext ac=SpringApplication.run(SpringBootStudyApplication.class, args);
        mashibingCore Myc=ac.getBean(mashibingCore.class);
        System.out.println(Myc.study());
    }
}

解决方案

在mashibing-Core项目resources下新建文件夹META-INF,在文件夹下面新建spring.factories文件,文件中配置,key为自定配置类EnableAutoConfiguration的全路径,value是配置类的全路径

org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.mashibingedu.practice.mashibingConfig

重新打包,重新运行SpringBootStudyApplication这个类。

可以发现,编写的那个类,就被加载进来了。

Spring Boot中的条件过滤

在分析AutoConfigurationImportSelector的源码时,会先扫描spring-autoconfiguration-metadata.properties文件,最后在扫描spring.factories对应的类时,会结合前面的元数据进行过滤,为什么要过滤呢? 原因是很多的@Configuration其实是依托于其他的框架来加载的,如果当前的classpath环境下没有相关联的依赖,则意味着这些类没必要进行加载,所以,通过这种条件过滤可以有效的减少@configuration类的数量从而降低SpringBoot的启动时间。

修改mashibing-Core

在META-INF/增加配置文件,spring-autoconfigure-metadata.properties。

com.mashibingedu.practice.mashibingConfig.ConditionalOnClass=com.mashibingedu.TestClass

格式:自动配置的类全名.条件=值

上面这段代码的意思就是,如果当前的classpath下存在TestClass,则会对mashibingConfig这个Configuration进行加载

演示过程(spring-boot)

  1. 沿用前面spring-boot工程的测试案例,直接运行main方法,发现原本能够被加载的mashibingCore,发现在ioc容器中找不到了。

    public static void main(String[] args) throws IOException {
        ConfigurableApplicationContext ac=SpringApplication.run(SpringBootStudyApplication.class, args);
        mashibingCore Myc=ac.getBean(mashibingCore.class);
        System.out.println(Myc.study());
    }
    
  2. 在当前工程中指定的包com.mashibingedu下创建一个TestClass以后,再运行上面这段代码,程序能够正常执行

手写Starter

通过手写Starter来加深对于自动装配的理解

1.创建一个Maven项目,quick-starter

定义相关的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter</artifactId>
    <version>2.1.6.RELEASE</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.56</version>
    <!-- 可选 -->
    <optional>true</optional>
</dependency>

2.定义Formate接口

定义的格式转换的接口,并且定义两个实现类

public interface FormatProcessor {
    /**
     * 定义一个格式化的方法
     * @param obj
     * @param <T>
     * @return
     */
    <T> String formate(T obj);
}

public class JsonFormatProcessor implements FormatProcessor {
    @Override
    public <T> String formate(T obj) {
        return "JsonFormatProcessor:" + JSON.toJSONString(obj);
    }
}
public class StringFormatProcessor implements FormatProcessor {
    @Override
    public <T> String formate(T obj) {
        return "StringFormatProcessor:" + obj.toString();
    }
}

3.定义相关的配置类

首先定义格式化加载的Java配置类

@Configuration
public class FormatAutoConfiguration {

    @ConditionalOnMissingClass("com.alibaba.fastjson.JSON")
    @Bean
    @Primary // 优先加载
    public FormatProcessor stringFormatProcessor(){
        return new StringFormatProcessor();
    }

    @ConditionalOnClass(name="com.alibaba.fastjson.JSON")
    @Bean
    public FormatProcessor jsonFormatProcessor(){
        return new JsonFormatProcessor();
    }
}

定义一个模板工具类

public class HelloFormatTemplate {

    private FormatProcessor formatProcessor;


    public HelloFormatTemplate(FormatProcessor processor){
        this.formatProcessor = processor;
    }

    public <T> String doFormat(T obj){
        StringBuilder builder = new StringBuilder();
        builder.append("Execute format : ").append("<br>");
        builder.append("Object format result:" ).append(formatProcessor.formate(obj));
        return builder.toString();
    }
}

再就是整合到SpringBoot中去的Java配置类

@Configuration
@Import(FormatAutoConfiguration.class)
public class HelloAutoConfiguration {

    @Bean
    public HelloFormatTemplate helloFormatTemplate(FormatProcessor formatProcessor){
        return new HelloFormatTemplate(formatProcessor);
    }
}

4.创建spring.factories文件

在resources下创建META-INF目录,再在其下创建spring.factories文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  org.mashibingedu.autoconfiguration.HelloAutoConfiguration

install 打包,然后就可以在SpringBoot项目中依赖改项目来操作了。

5.测试

在SpringBoot中引入依赖

<dependency>
    <groupId>org.example</groupId>
    <artifactId>format-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

在controller中使用

@RestController
public class UserController {

    @Autowired
    private HelloFormatTemplate helloFormatTemplate;

    @GetMapping("/format")
    public String format(){
        User user = new User();
        user.setName("BoBo");
        user.setAge(18);
        return helloFormatTemplate.doFormat(user);
    }
}

6.自定义Starter关联配置信息

有些情况下可以需要用户在使用的时候动态的传递相关的配置信息,比如Redis的Ip,端口等等,这些信息显然是不能直接写到代码中的,这时就可以通过SpringBoot的配置类来实现。

首先引入依赖支持

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <version>2.2.6.RELEASE</version>
    <optional>true</optional>
</dependency>

然后创建对应的属性类

@ConfigurationProperties(prefix = HelloProperties.HELLO_FORMAT_PREFIX)
public class HelloProperties {

    public static final String HELLO_FORMAT_PREFIX="yuyang.hello.format";

    private String name;

    private Integer age;

    private Map<String,Object> info;

    //set,get方法
}

然后再Java配置类中关联

@Configuration
@Import(FormatAutoConfiguration.class)
@EnableConfigurationProperties(HelloProperties.class)
public class HelloAutoConfiguration {

    @Bean
    public HelloFormatTemplate helloFormatTemplate(HelloProperties helloProperties,FormatProcessor formatProcessor){
        return new HelloFormatTemplate(helloProperties,formatProcessor);
    }
}

调整模板方法

public class HelloFormatTemplate {

    private FormatProcessor formatProcessor;

    private HelloProperties helloProperties;

    public HelloFormatTemplate(HelloProperties helloProperties,FormatProcessor processor){
        this.helloProperties = helloProperties;
        this.formatProcessor = processor;
    }

    public <T> String doFormat(T obj){
        StringBuilder builder = new StringBuilder();
        builder.append("Execute format : ").append("<br>");
        builder.append("HelloProperties:").append(formatProcessor.formate(helloProperties.getInfo())).append("<br>");
        builder.append("Object format result:" ).append(formatProcessor.formate(obj));
        return builder.toString();
    }
}
增加提示

在这个工程的META-INF/下创建一个additional-spring-configuration-metadata.json,这个是设置属性的提示类型

{
  "properties": [
    {
      "name": "yuyang.hello.format.name",
      "type": "java.lang.String",
      "description": "账号信息",
      "defaultValue": "root"
    },{
      "name": "yuyang.hello.format.age",
      "type": "java.lang.Integer",
      "description": "年龄",
      "defaultValue": 18
    }
  ]
}
	protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
        // 加载当前系统下 META-INF/spring.factories 文件中声明的配置类
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
        // 移除掉重复的
		configurations = removeDuplicates(configurations);
        // 移除掉显示排除的
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
        // 过滤掉不需要载入的配置类
		configurations = getConfigurationClassFilter().filter(configurations);
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}

SpringBoot初始化核心流程源码

自动装配的时候为什么没有走selectImports方法?

分析SpringBoot自动装配源码的时候讲过在 @EnableAutoConfiguration注解上通过 @Import注解导入了一个 ImportSelector接口的实现类 AutoConfigurationImportSelector。按照之前对 @Import 注解的理解,应该会执行重写的 selectImports 方法,但调试的时候,执行的流程好像和期待的不一样哦,没有走 selectImports方法。

  通过Debug模式,端点定位能够发现进入到了getAutoConfigurationEntry方法中。

image.png

  但是没有进入selectImports方法。

image.png

  这是什么原因呢?他不是实现了ImportSelector接口吗?怎么和理解的不一样呢?这就需要再来细说下@Import注解了。

@Import

  前面介绍过@Import注解可以根据添加的不同类型做出不同的操作

导入类型注入方式
实现了ImportSelector接口不注入该类型的对象,调用selectImports方法,将返回的数据注入到容器中
实现了ImportBeanDefinitionRegistrar接口不注入该类型的对象,调用registerBeanDefinitions方法,通过注册器注入
普通类型直接注入该类型的对象

  而在自动装配中导入的AutoConfigurationImportSelector这个类型有点特殊。具体看下类图结构

image.png
  那这个DeferredImportSelector这个接口的作用是什么呢?字面含义是延迟导入的意思。具体怎么实现的后面再说,先来说下他的作用。

DeferredImportSelector接口

  DeferredImportSelector接口本身也有ImportSelector接口的功能,如果仅仅是实现了DeferredImportSelector接口,重写了selectImports方法,那么selectImports方法还是会被执行的,来看代码。

public class MyDeferredImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("selectImports方法执行了---->");
        return new String[0];
    }


}

对应的配置启动类

@Configuration
@Import(MyDeferredImportSelector.class)
public class JavaConfig {

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

启动效果:

image.png

但是如果重写了DeferredImportSelector中的Group接口,并重写了getImportGroup,那么容器在启动的时候就不会执行selectImports方法了,而是执行getImportGroup方法。进而执行Group中重写的方法。

public class MyDeferredImportSelector implements DeferredImportSelector {

    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        System.out.println("selectImports方法执行了---->");
        return new String[0];
    }

    @Override
    public Class<? extends Group> getImportGroup() {
        System.out.println("getImportGroup");
        return MyDeferredImportSelectorGroup.class;
    }

    public static class MyDeferredImportSelectorGroup implements Group{
        private final List<Entry> imports = new ArrayList<>();
        @Override
        public void process(AnnotationMetadata metadata, DeferredImportSelector selector) {
            System.out.println("MyDeferredImportSelectorGroup.Group");
        }

        @Override
        public Iterable<Entry> selectImports() {
            System.out.println("Group中的:selectImports方法");
            return imports;
        }
    }
}

执行效果:

image.png

通过上面的效果解释了为什么在SpringBoot自动装配的时候没有走selectImports方法。那么DeferredImportSelector接口的作用是什么呢?为什么要这么设计呢?接下来继续分析

DeferredImportSelector的作用

  通过前面的类图结构知道DeferredImportSelector是ImportSelector接口的一个扩展。

image.png

ImportSelector实例的selectImports方法的执行时机,是在@Configguration注解中的其他逻辑被处理之前,所谓的其他逻辑,包括对@ImportResource、@Bean这些注解的处理(注意,这里只是对@Bean修饰的方法的处理,并不是立即调用@Bean修饰的方法,这个区别很重要!)

DeferredImportSelector实例的selectImports方法的执行时机,是在@Configguration注解中的其他逻辑被处理完毕之后,所谓的其他逻辑,包括对@ImportResource、@Bean这些注解的处理.

上面的结论可以直接在源码中看到对应的答案。首先定位到ConfigurationClassParser中的parse方法。

image.png

上面代码有两个非常重要的分支,在下面逐一的介绍

1.parse方法

先看parse方法,也就是解析注解类的方法。进入

image.png

看到调用的是processConfigurationClass,翻译过来就比较好理解了,处理配置类

image.png

再进入到循环的方法中。

image.png

继续往下看

image.png

image.png

逻辑处理还是非常清楚的。然后需要回到上面的处理@Import注解的方法中。在这个方法中可以看到@Import注解的实现逻辑

image.png

也就是前面给大家回顾的@Import注解的作用

image.png

然后来看下导入的类型是ImportSelector接口的逻辑。

image.png

上面的代码重点解决了ImportSelector接口的不同类型的实现。

image.png

对应的实例存储了起来

image.png

2.process方法

  好了上面的代码分析清楚了,然后再回到process方法中来看下DeferredImportSelectorHandler是如何处理的。

image.png

进入process方法

image.png

先看register方法

image.png

然后再看processGroupImports方法。

image.png

进去后需要进入getImports方法中。

image.png

然后进入到process方法中,可以看到自动装配的方法被执行了!

image.png

到这儿是不是帮助大家解决了自动装配为什么没有走 AutoConfigurationImportSelector中的 selectImports 方法了!!!

同时也介绍清楚了ImportSelector与DeferredImportSelector的区别,就是selectImports方法执行时机有差别,这个差别期间,spring容器对此Configguration类做了些其他的逻辑:包括对@ImportResource、@Bean这些注解的处理

SpringBoot源码

对于想要研究SpringBoot源码的小伙伴来说,在本地编译源码环境,然后在研究源码的时候可以添加对应的注释是必须的,本文就给大家来介绍下如何来搭建我们的源码环境。

1.官方源码下载

  首先大家要注意SpringBoot项目在2.3.0之前是使用Maven构建项目的,在2.3.0之后是使用Gradle构建项目的。后面分析的源码以SpringBoot2.2.5为案例,所以本文就介绍下SpringBoot2.2.5的编译过程。

  官网地址:https://github.com/spring-projects/spring-boot

image.png

直接下载对于的压缩文件即可

image.png

  下载后直接解压缩即可

2.本地源码编译

  把解压缩的源码直接导入到IDEA中,修改pom文件中的版本号。

image.png

pom文件中提示 disable.checks属性找不到,我们添加一个即可。

	<properties>
		<revision>2.2.5.snapshot</revision>
		<main.basedir>${basedir}</main.basedir>
		<!-- 添加属性 -->
		<disable.checks>true</disable.checks>
	</properties>

然后执行编译命令

mvn clean install -DskipTests

image.png

然后控制台出现如下错误

image.png

按照提示,执行下面的 命令 就好了:

image.png

image.png

在执行编译命令就可以了

mvn clean install -DskipTests

image.png

3.源码环境使用

  既然源码已经编译好之后我们就可以在这个项目中来创建我们自己的SpringBoot项目了,我们在 spring-boot-project项目下创建 module,

image.png

  然后在我们的module中添加对应的start依赖

image.png

然后添加我们的启动类

image.png

项目能够正常启动

image.png

同时点击run方法进去,我们可以添加注释了:

image.png

  在其他项目使用我们编译的源码,这个可能是大家比较感兴趣的一个点了,我们也来介绍下,依赖我们还是可以使用官方的依赖即可,不过最好还是和我们编译的版本保持一致。

  主要是关联上我们编译的源码。

image.png

image.png

image.png

修改代码

image.png

image.png

好了到此就可以开启SpringBoot的源码探索之旅了哦。

1.SpringBoot启动的入口

  当我们启动一个SpringBoot项目的时候,入口程序就是main方法,而在main方法中就执行了一个run方法。

@SpringBootApplication
public class StartApp {

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

2.run方法

  然后我们进入run()方法中看。代码比较简单
image.png

	public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
		// 调用重载的run方法,将传递的Class对象封装为了一个数组
		return run(new Class<?>[] { primarySource }, args);
	}

  调用了重载的一个run()方法,将我们传递进来的类对象封装为了一个数组,仅此而已。我们再进入run()方法。

	public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
		// 创建了一个SpringApplication对象,并调用其run方法
		// 1.先看下构造方法中的逻辑
		// 2.然后再看run方法的逻辑
		return new SpringApplication(primarySources).run(args);
	}

	public SpringApplication(Class<?>... primarySources) {
		// 调用其他的构造方法
		this(null, primarySources);
	}

  在该方法中创建了一个SpringApplication对象。同时调用了SpringApplication对象的run方法。这里的逻辑有分支,先看下SpringApplication的构造方法中的逻辑

3.SpringApplication构造器

  SpringApplication的构造方法,看的核心代码为

	public SpringApplication(Class<?>... primarySources) {
		// 调用其他的构造方法
		this(null, primarySources);
	}
	
	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		// 传递的resourceLoader为null无需关注
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		// 记录主方法的配置类名称
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		// 记录当前项目的类型,    
		//正常情况下 是SERVLET
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		// 加载配置在spring.factories文件中的ApplicationContextInitializer对应的类型并实例化
		// 并将加载的数据存储在了 initializers 成员变量中。
		
		//实例化化并保存spring.factories文件中ApplicationContextInitializer的类型bean到initializers中
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		// 初始化监听器 并将加载的监听器实例对象存储在了listeners成员变量中
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 反推main方法所在的Class对象 并记录在了mainApplicationClass对象中
		// 反推启动类
		this.mainApplicationClass = deduceMainApplicationClass();
	}
//实例化化并保存spring.factories文件中ApplicationContextInitializer的类型bean到initializers中
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

	/**
	 * 扩展点的加载
	 * @param type
	 * @param <T>
	 * @return
	 */
	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
		return getSpringFactoriesInstances(type, new Class<?>[] {});
	}
	/**
	 * 初始化Initializer
	 * SpringFactoriesLoader.loadFactoryNames(type, classLoader)
	 *    根据对应的类型加载 spring.factories 文件中的配置信息
	 * @param type
	 * @param parameterTypes
	 * @param args
	 * @param <T>
	 * @return
	 */
	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
		// 获取当前上下文类加载器
		ClassLoader classLoader = getClassLoader();
		// 获取到的扩展类名存入set集合中防止重复
		Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		// 创建扩展点实例
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}

//SpringFactoriesLoader.loadFactoryNames(type, classLoader)
	public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
        //加载spring.factories中所有的信息到内存中
        //(List)loadSpringFactories(classLoader)
        //根据类型获取相关信息
        //getOrDefault(factoryTypeName, Collections.emptyList())
    }

在这里插入图片描述

		// 反推启动类
		this.mainApplicationClass = deduceMainApplicationClass();
	/**
	 * StackTrace:
	 *    我们在学习函数调用时,都知道每个函数都拥有自己的栈空间。
	 *    一个函数被调用时,就创建一个新的栈空间。那么通过函数的嵌套调用最后就形成了一个函数调用堆栈
	 * @return
	 */
	private Class<?> deduceMainApplicationClass() {
		try {
			// 获取当前run方法执行的堆栈信息
			StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
			// 遍历堆栈信息
			for (StackTraceElement stackTraceElement : stackTrace) {
				// 如果调用的是main方法说明就找到了
				if ("main".equals(stackTraceElement.getMethodName())) {
					return Class.forName(stackTraceElement.getClassName());
				}
			}
		}
		catch (ClassNotFoundException ex) {
			// Swallow and continue
		}
		return null;
	}

  在本方法中完成了几个核心操作

  1. 推断当前项目的类型(正常为SERVLET)
  2. 加载配置在spring.factories文件中的ApplicationContextInitializer中的类型并实例化后存储在了initializers中。(加载spring.factories文件中所有的applicationcontextinitializer(初始化器)并保存)
  3. 和2的步骤差不多,完成监听器的初始化操作,并将实例化的监听器对象存储在了listeners成员变量中(加载spring.factories文件中所有的listeners(监听器)并保存)
  4. 通过StackTrace反推main方法所在的Class对象

  上面的核心操作具体的实现细节我们在后面的详细文章会给大家剖析

4.run方法

  接下来再回到SpringApplication.run()方法中。

	public ConfigurableApplicationContext run(String... args) {
		// 创建一个任务执行观察器
		StopWatch stopWatch = new StopWatch();
		// 开始执行记录执行时间
		stopWatch.start();
		// 声明 ConfigurableApplicationContext 对象
		ConfigurableApplicationContext context = null;
		// 声明集合容器用来存储 SpringBootExceptionReporter 启动错误的回调接口
		Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
		// 设置了一个名为java.awt.headless的系统属性
		// 其实是想设置该应用程序,即使没有检测到显示器,也允许其启动.
		//对于服务器来说,是不需要显示器的,所以要这样设置.
		configureHeadlessProperty();
		// 获取 SpringApplicationRunListener 加载的是 EventPublishingRunListener
		// 获取启动时的监听器
		SpringApplicationRunListeners listeners = getRunListeners(args);
		// 触发启动事件
		listeners.starting();
		try {
			// 构造一个应用程序的参数持有类
			ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
			// 创建并配置环境
			ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
			// 配置需要忽略的BeanInfo信息
			configureIgnoreBeanInfo(environment);
			// 输出的Banner信息
			Banner printedBanner = printBanner(environment);
			// 创建应用上下文对象
			context = createApplicationContext();
			// 加载配置的启动异常处理器
			exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
					new Class[] { ConfigurableApplicationContext.class }, context);
			// 刷新前操作
			prepareContext(context, environment, listeners, applicationArguments, printedBanner);
			// 刷新应用上下文 完成Spring容器的初始化
			refreshContext(context);
			// 刷新后操作
			afterRefresh(context, applicationArguments);
			// 结束记录启动时间
			stopWatch.stop();
			if (this.logStartupInfo) {
				new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
			}
			// 事件广播 启动完成了
			listeners.started(context);
			callRunners(context, applicationArguments);
		}
		catch (Throwable ex) {
			// 事件广播启动出错了
			handleRunFailure(context, ex, exceptionReporters, listeners);
			throw new IllegalStateException(ex);
		}
		try {
			// 监听器运行中
			listeners.running(context);
		}
		catch (Throwable ex) {
			handleRunFailure(context, ex, exceptionReporters, null);
			throw new IllegalStateException(ex);
		}
		// 返回上下文对象--> Spring容器对象
		return context;
	}

  在这个方法中完成了SpringBoot项目启动的很多核心的操作,总结下上面的步骤

1. 创建了一个任务执行的观察器,统计启动的时间
2. 声明ConfigurableApplicationContext对象
3. 声明集合容器来存储SpringBootExceptionReporter即启动错误的回调接口
4. 设置java.awt.headless的系统属性
5. 获取我们之间初始化的监听器(EventPublishingRunListener),并触发starting事件
6. 创建ApplicationArguments这是一个应用程序的参数持有类
7. 创建ConfigurableEnvironment这时一个配置环境的对象
8. 配置需要忽略的BeanInfo信息
9. 配置Banner信息对象
10. 创建对象的上下文对象
11. 加载配置的启动异常的回调异常处理器
12. 刷新应用上下文,本质就是完成Spring容器的初始化操作
13. 启动结束记录启动耗时
14. 完成对应的事件广播
15. 返回应用上下文对象。

 到此SpringBoot项目的启动初始化的代码的主要流程就介绍完成了。细节部分后面再说。

SpringApplication构造器

前面给大家介绍了SpringBoot启动的核心流程,本文开始给大家详细的来介绍SpringBoot启动中的具体实现的相关细节。 https://www.processon.com/view/link/61eab8f47d9c085d604e614d

SpringBoot2.png

  首先来看下在SpringApplication的构造方法中是如何帮我们完成这4个核心操作的。

image.png

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
		// 传递的resourceLoader为null
		this.resourceLoader = resourceLoader;
		Assert.notNull(primarySources, "PrimarySources must not be null");
		// 记录主方法的配置类名称
		this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
		// 记录当前项目的类型
		this.webApplicationType = WebApplicationType.deduceFromClasspath();
		// 加载配置在spring.factories文件中的ApplicationContextInitializer对应的类型并实例化
		// 并将加载的数据存储在了 initializers 成员变量中。
		setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
		// 初始化监听器 并将加载的监听器实例对象存储在了listeners成员变量中
		setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
		// 反推main方法所在的Class对象 并记录在了mainApplicationClass对象中
		this.mainApplicationClass = deduceMainApplicationClass();
	}

1.webApplicationType

  首先来看下webApplicationType是如何来推导出当前启动的项目的类型。通过代码可以看到是通过deduceFromClassPath()方法根据ClassPath来推导出来的。

this.webApplicationType = 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;
	}

  在看整体的实现逻辑之前,我们先分别看两个内容,第一就是在上面的代码中使用到了相关的静态变量。

image.png

  这些静态变量其实就是一些绑定的Java类的全类路径。第二个就是 ClassUtils.isPresent()方法,该方法的逻辑也非常简单,就是通过反射的方式获取对应的类型的Class对象,如果存在返回true,否则返回false

image.png

  所以到此推导的逻辑就非常清楚了

image.png

2.setInitializers

  然后我们再来看下如何实现加载初始化器的。

// 加载配置在spring.factories文件中的ApplicationContextInitializer对应的类型并实例化
// 并将加载的数据存储在了 initializers 成员变量中。
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));

  首先所有的初始化器都实现了 ApplicationContextInitializer接口,也就是根据这个类型来加载相关的实现类。

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {
    void initialize(C var1);
}

  然后加载的关键方法是 getSpringFactoriesInstances()方法。该方法会加载 spring.factories文件中的key为 org.springframework.context.ApplicationContextInitializer 的值。

spring-boot项目下

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.rsocket.context.RSocketPortInfoApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

spring-boot-autoconfigure项目下

# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener

image.png

  具体的加载方法为 getSpringFacotiesInstance()方法,我们进入查看

	private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
		// 获取当前上下文类加载器
		ClassLoader classLoader = getClassLoader();
		// 获取到的扩展类名存入set集合中防止重复
		Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
		// 创建扩展点实例
		List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
		AnnotationAwareOrderComparator.sort(instances);
		return instances;
	}

  先进入 SpringFactoriesLoader.loadFactoryNames(type, classLoader)中具体查看加载文件的过程.

image.png

  然后我们来看下 loadSpringFactories方法

image.png

  通过Debug的方式查看会更清楚哦
在这里插入图片描述
  通过 loadSpringFactories 方法我们看到把 spring.factories文件中的所有信息都加载到了内存中了,但是我们现在只需要加载 ApplicationContextInitializer类型的数据。这时我们再通过 getOrDefault()方法来查看。

image.png

  进入方法中查看

image.png

  然后会根据反射获取对应的实例对象。

image.png

image.png

  好了到这其实我们就清楚了 getSpringFactoriesInstances方法的作用就是帮我们获取定义在 META-INF/spring.factories文件中的可以为 ApplicationContextInitializer 的值。并通过反射的方式获取实例对象。然后把实例的对象信息存储在了SpringApplication的 initializers属性中。

image.png

3.setListeners

  清楚了 setInitializers()方法的作用后,再看 setListeners()方法就非常简单了,都是调用了 getSpringFactoriesInstances方法,只是传入的类型不同。也就是要获取的 META-INF/spring.factories文件中定义的不同信息罢了。

image.png

  即加载定义在 META-INF/spring.factories文件中声明的所有的监听器,并将获取后的监听器存储在了 SpringApplicationlisteners属性中。

image.png

  默认加载的监听器为:

image.png

4.mainApplicationClass

  最后我们来看下 duduceMainApplicaitonClass()方法是如何反推导出main方法所在的Class对象的。通过源码我们可以看到是通过 StackTrace来实现的。

StackTrace:
我们在学习函数调用时,都知道每个函数都拥有自己的栈空间。
一个函数被调用时,就创建一个新的栈空间。那么通过函数的嵌套调用最后就形成了一个函数调用堆栈

  StackTrace其实就是记录了程序方法执行的链路。通过Debug方式可以更直观的来呈现。

image.png

  那么相关的调用链路我们都可以获取到,剩下的就只需要获取每链路判断执行的方法名称是否是 main就可以了。

image.png

SpringBoot中的监听机制详解

1.观察者模式

  监听器的设计会使用到Java设计模式中的观察者模式,所以在搞清楚SpringBoot中的监听器的设计之前我们还是非常有必要把观察者模式先弄清楚。

  观察者模式又称为发布/订阅(Publish/Subscribe)模式,在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新.

  在java.util包中包含有基本的Observer接口和Observable抽象类.功能上和Subject接口和Observer接口类似.不过在使用上,就方便多了,因为许多功能比如说注册,删除,通知观察者的那些功能已经内置好了.

1.1 定义具体被观察者

package com.dpb.observer2;

import java.util.Observable;

/**
 * 目标对象
 * 继承 Observable
 * @author dengp
 *
 */
public class ConcreteSubject extends Observable {

	private int state; 

	public void set(int s){
		state = s;  //目标对象的状态发生了改变
		setChanged();  //表示目标对象已经做了更改
		notifyObservers(state);  //通知所有的观察者
	}

	public int getState() {
		return state;
	}

	public void setState(int state) {
		this.state = state;
	}
}

观察者只需要继承Observable父类。发送消息的方式执行如下两行代码即可

setChanged();  //表示目标对象已经做了更改
notifyObservers(state);  //通知所有的观察者

Observable源码对应的是:
在这里插入图片描述
在这里插入图片描述

1.2 定义具体观察者

package com.dpb.observer2;

import java.util.Observable;
import java.util.Observer;
/**
 * 观察者模式:观察者(消息订阅者)
 * 实现Observer接口
 * @author dengp
 *
 */
public class ObserverA implements Observer {

	private int myState;

	@Override
	public void update(Observable o, Object arg) {
		myState = ((ConcreteSubject)o).getState();
	}
	public int getMyState() {
		return myState;
	}
	public void setMyState(int myState) {
		this.myState = myState;
	}
}

观察者也就是订阅者只需要实现Observer接口并重写相关update方法即可,在目标实现中我们发现触发的时候执行的就是观察者的update方法。

1.3 测试

package com.dpb.observer2;

public class Client {
	public static void main(String[] args) {
		//创建目标对象Obserable
		ConcreteSubject subject = new ConcreteSubject();

		//创建观察者
		ObserverA obs1 = new ObserverA();
		ObserverA obs2 = new ObserverA();
		ObserverA obs3 = new ObserverA();

		//将上面三个观察者对象添加到目标对象subject的观察者容器中
		subject.addObserver(obs1);
		subject.addObserver(obs2);
		subject.addObserver(obs3);

		//改变subject对象的状态
		subject.set(3000);
		System.out.println("===============状态修改了!");
		//观察者的状态发生了变化
		System.out.println(obs1.getMyState());
		System.out.println(obs2.getMyState());
		System.out.println(obs3.getMyState());

		subject.set(600);
		System.out.println("===============状态修改了!");
		//观察者的状态发生了变化
		System.out.println(obs1.getMyState());
		System.out.println(obs2.getMyState());
		System.out.println(obs3.getMyState());

		//移除一个订阅者
		subject.deleteObserver(obs2);
		subject.set(100);
		System.out.println("===============状态修改了!");
		//观察者的状态发生了变化
		System.out.println(obs1.getMyState());
		System.out.println(obs2.getMyState());
		System.out.println(obs3.getMyState());
	}
}

在这里插入图片描述
  这样就实现了官方提供观察者模式.

2.SpringBoot中监听器的设计

1.观察者模式

  监听器的设计会使用到Java设计模式中的观察者模式,所以在搞清楚SpringBoot中的监听器的设计之前我们还是非常有必要把观察者模式先弄清楚。

  观察者模式又称为发布/订阅(Publish/Subscribe)模式,在对象之间定义了一对多的依赖,这样一来,当一个对象改变状态,依赖它的对象会收到通知并自动更新.

  在java.util包中包含有基本的Observer接口和Observable抽象类.功能上和Subject接口和Observer接口类似.不过在使用上,就方便多了,因为许多功能比如说注册,删除,通知观察者的那些功能已经内置好了.

1.1 定义具体被观察者

package com.dpb.observer2;

import java.util.Observable;

/**
 * 目标对象
 * 继承 Observable
 * @author dengp
 *
 */
public class ConcreteSubject extends Observable {

	private int state; 

	public void set(int s){
		state = s;  //目标对象的状态发生了改变
		setChanged();  //表示目标对象已经做了更改
		notifyObservers(state);  //通知所有的观察者
	}

	public int getState() {
		return state;
	}

	public void setState(int state) {
		this.state = state;
	}
}

观察者只需要继承Observable父类。发送消息的方式执行如下两行代码即可

setChanged();  //表示目标对象已经做了更改
notifyObservers(state);  //通知所有的观察者

Observable源码对应的是:
在这里插入图片描述
在这里插入图片描述

1.2 定义具体观察者

package com.dpb.observer2;

import java.util.Observable;
import java.util.Observer;
/**
 * 观察者模式:观察者(消息订阅者)
 * 实现Observer接口
 * @author dengp
 *
 */
public class ObserverA implements Observer {

	private int myState;

	@Override
	public void update(Observable o, Object arg) {
		myState = ((ConcreteSubject)o).getState();
	}
	public int getMyState() {
		return myState;
	}
	public void setMyState(int myState) {
		this.myState = myState;
	}
}

观察者也就是订阅者只需要实现Observer接口并重写相关update方法即可,在目标实现中我们发现触发的时候执行的就是观察者的update方法。

1.3 测试

package com.dpb.observer2;

public class Client {
	public static void main(String[] args) {
		//创建目标对象Obserable
		ConcreteSubject subject = new ConcreteSubject();

		//创建观察者
		ObserverA obs1 = new ObserverA();
		ObserverA obs2 = new ObserverA();
		ObserverA obs3 = new ObserverA();

		//将上面三个观察者对象添加到目标对象subject的观察者容器中
		subject.addObserver(obs1);
		subject.addObserver(obs2);
		subject.addObserver(obs3);

		//改变subject对象的状态
		subject.set(3000);
		System.out.println("===============状态修改了!");
		//观察者的状态发生了变化
		System.out.println(obs1.getMyState());
		System.out.println(obs2.getMyState());
		System.out.println(obs3.getMyState());

		subject.set(600);
		System.out.println("===============状态修改了!");
		//观察者的状态发生了变化
		System.out.println(obs1.getMyState());
		System.out.println(obs2.getMyState());
		System.out.println(obs3.getMyState());

		//移除一个订阅者
		subject.deleteObserver(obs2);
		subject.set(100);
		System.out.println("===============状态修改了!");
		//观察者的状态发生了变化
		System.out.println(obs1.getMyState());
		System.out.println(obs2.getMyState());
		System.out.println(obs3.getMyState());
	}
}

在这里插入图片描述
  这样就实现了官方提供观察者模式.

SpringBoot中默认的监听器

  首先来回顾下SpringBoot中给我们提供的默认的监听器,这些都定义在spring.factories文件中。

监听器监听事件说明
ClearCachesApplicationListenerContextRefreshedEvent当触发ContextRefreshedEvent事件会清空应用的缓存
ParentContextCloserApplicationListenerParentContextAvailableEvent触发ParentContextAvailableEvent事件会完成父容器关闭的监听器
CloudFoundryVcapEnvironmentPostProcessorApplicationPreparedEvent判断环境中是否存在VCAP_APPLICATION或者VCAP_SERVICES。如果有就添加Cloud Foundry的配置;没有就不执行任何操作。
FileEncodingApplicationListenerApplicationEnvironmentPreparedEvent文件编码的监听器
AnsiOutputApplicationListenerApplicationEnvironmentPreparedEvent根据 spring.output.ansi.enabled参数配置 AnsiOutput
ConfigFileApplicationListenerApplicationEnvironmentPreparedEvent <br>ApplicationPreparedEvent完成相关属性文件的加载,application.properties
application.yml
前面源码内容详细讲解过
DelegatingApplicationListenerApplicationEnvironmentPreparedEvent监听到事件后转发给环境变量 context.listener.classes指定的那些事件监听器
ClasspathLoggingApplicationListenerApplicationEnvironmentPreparedEvent <br>ApplicationFailedEvent一个SmartApplicationListener,对环境就绪事件ApplicationEnvironmentPreparedEvent/应用失败事件ApplicationFailedEvent做出响应,往日志DEBUG级别输出TCCL(thread context class loader)的classpath。
LoggingApplicationListenerApplicationStartingEvent <br>ApplicationEnvironmentPreparedEvent <br>ApplicationPreparedEvent <br>ContextClosedEvent <br>ApplicationFailedEvent配置 LoggingSystem。使用 logging.config环境变量指定的配置或者缺省配置
LiquibaseServiceLocatorApplicationListenerApplicationStartingEvent使用一个可以和Spring Boot可执行jar包配合工作的版本替换liquibase ServiceLocator
BackgroundPreinitializerApplicationStartingEvent <br>ApplicationReadyEvent <br>ApplicationFailedEvent尽早触发一些耗时的初始化任务,使用一个后台线程

SpringBoot中的事件类型

  然后我们来看下对应的事件类型,SpringBoot中的所有的事件都是继承于 ApplicationEvent这个抽象类,在SpringBoot启动的时候会发布如下的相关事件,而这些事件其实都实现了 SpringApplicationContext接口。

事件说明
ApplicationStartingEvent容器启动的事件
ApplicationEnvironmentPreparedEvent应用处理环境变量相关的事件
ApplicationContextInitializedEvent容器初始化的事件
ApplicationPreparedEvent应用准备的事件
ApplicationFailedEvent应用启动出错的事件
ApplicationStartedEvent应用Started状态事件
ApplicationReadyEvent应用准备就绪的事件

也就是这些事件都是属于SpringBoot启动过程中涉及到的相关的事件

image.png

  当然在启动过程中还会发布其他的相关事件,可以自行查阅相关源码哦

自定义事件

  接下来通过几个自定义事件来加深下对事件监听机制的理解

监听所有事件

emsp; 我们先创建一个自定义监听器,来监听所有的事件。创建一个Java类,实现ApplicationListener接口在泛型中指定要监听的事件类型即可,如果要监听所有的事件,那么泛型就写ApplicationEvent。

image.png

  之后为了在容器启动中能够发下我们的监听器并且添加到SimpleApplicationEventMulticaster中,我们需要在spring.factories中注册自定义的监听器

image.png

  这样当我们启动服务的时候就可以看到相关事件发布的时候,我们的监听器被触发了。

image.png

监听特定事件

  那如果是监听特定的事件呢,我们只需要在泛型出制定即可。

image.png

启动服务查看

image.png

自定义事件

  那如果我们想要通过自定义的监听器来监听自定义的事件呢?首先创建自定义的事件类,非常简单,只需要继承ApplicationEvent即可

image.png

然后在自定义的监听器中监听自定义的事件。

image.png

同样的别忘了在spring.factories中注册哦

image.png

之后我们就可以在我们特定的业务场景中类发布对应的事件了

image.png

然后当我们提交请求后

image.png

可以看到对应的监听器触发了

image.png

这样一来不光搞清楚了SpringBoot中的监听机制,而且也可以扩展应用到我们业务开发中了。好了本文就给大家介绍到这里,希望对你有所帮助。

  然后来看下SpringBoot启动这涉及到的监听器这块是如何实现的。

2.1 初始化操作

  通过前面的介绍我们知道在SpringApplication的构造方法中会加载所有声明在spring.factories中的监听器。

image.png

  通过Debug模式我们可以看到加载的监听器有哪些。

image.png

  其实就是加载的spring.factories文件中的key为ApplicationListener的value

image.png

image.png

  通过对这些内置监听器的源码查看我们发现这些监听器都实现了 ApplicationEvent接口。也就是都会监听 ApplicationEvent发布的相关的事件。ApplicationContext事件机制是观察者设计模式的实现,通过ApplicationEvent类和ApplicationListener接口,可以实现ApplicationContext事件处理。

image.png

2.2 run方法

  然后我们来看下在SpringApplication.run()方法中是如何发布对应的事件的。

image.png

  首先会通过getRunListeners方法来获取我们在spring.factories中定义的SpringApplicationRunListener类型的实例。也就是EventPublishingRunListener.

image.png

image.png

image.png

  加载这个类型的时候会同步的完成实例化。

image.png

image.png

  实例化操作就会执行EventPublishingRunListener.

image.png

  在这个构造方法中会绑定我们前面加载的11个过滤器。

image.png

  到这其实我们就已经清楚了EventPublishingRunListener和我们前面加载的11个监听器的关系了。然后在看事件发布的方法。

image.png

查看starting()方法。

image.png
再进入
image.png

进入到multicastEvent中方法中我们可以看到具体的触发逻辑

image.png

在这儿以ConfigFileApplicationListener为例。

image.png

触发会进入ConfigFileApplicationListener对象的onApplicationEvent方法中,

image.png

通过代码我们可以发现当前的事件是ApplicationStartingEvent事件,都不满足,所以ConfigFileApplicationListener在SpringBoot项目开始启动的时候就不会做任何的操作。而当我们在配置环境信息的时候,会发布对应的事件来触发

image.png

image.png

继续进入

image.png

继续进入

image.png

然后再触发ConfigFileApplicationListener监听器的时候就会触发如下方法了

image.png

  其实到这儿,后面的事件发布与监听器的处理逻辑就差不多是一致了。到这儿对应SpringBoot中的监听器这块就分析的差不错了。像SpringBoot的属性文件中的信息什么时候加载的就是在这些内置的监听器中完成的。

image.png

官方内置的事件有:

image.png

  好了本文就给大家介绍到这里,希望能对你有所帮助哦。

  运行监听器,发布事件

SpringBoot中的属性文件加载原理

在创建SpringBoot项目的时候会在对应的application.properties或者application.yml文件中添加对应的属性信息,这些属性文件是什么时候被加载的?如果要实现自定义的属性文件怎么来实现呢?本文来给大家揭晓答案:

image.png

1.找到入口

  结合前面介绍的SpringBoot中的监听事件机制,我们首先看下SpringApplication.run()方法,在该方法中会针对SpringBoot项目启动的不同的阶段来发布对应的事件。

在这里插入图片描述

  处理属性文件加载解析的监听器是 ConfigFileApplicationListener ,这个监听器监听的事件有两个。

image.png

  而我们进入SpringApplication.prepareEnvironment()方法中发布的事件其实就是ApplicationEnvironmentPreparedEvent事件。进入代码查看。
在这里插入图片描述

进行进入

image.png

继续进入会看到对应的发布事件:ApplicationEnvironmentPreparedEvent

image.png

  结合上篇文件的内容,得知在initialMulticaster中是有ConfigFileApplicationListener这个监听器的。

image.png

  那么在此处触发了配置环境的监听器,后续的逻辑就应该进入对应的

ConfigFileApplicationListener

主要流程分析

  接下来我们看下ConfigFileApplicationListener中具体的如何来处理配置文件的加载解析的。

image.png

  根据逻辑我们直接进入onApplicationEnvironmentPreparedEvent()方法中。

image.png

  系统提供那4个不是重点,重点是 ConfigFileApplicationListener 中的这个方法处理.

image.png

  直接进入ConfigFileApplicationListener.postProcessEnvironment()方法。

image.png

  在进入addPropertySources()方法中会完成两个核心操作,1。创建Loader对象,2。调用Loader对象的load方法,

image.png

Loader构造器

  现在我们来看下在Loader构造器中执行了什么操作。

image.png

  通过源码我们可以发现在其中获取到了属性文件的加载器、从spring.factories文件中获取,对应的类型是 PropertySourceLoader类型。

image.png

  而且在loadFactories方法中会完成对象的实例化。

image.png

  到这Loader的构造方法执行完成了,然后来看下load()方法的执行。先把代码贴上

void load() {
			FilteredPropertySource.apply(this.environment, DEFAULT_PROPERTIES, LOAD_FILTERED_PROPERTY,
					(defaultProperties) -> {
						// 创建默认的profile 链表
						this.profiles = new LinkedList<>();
						// 创建已经处理过的profile 类别
						this.processedProfiles = new LinkedList<>();
						// 默认设置为未激活
						this.activatedProfiles = false;
						// 创建loaded对象
						this.loaded = new LinkedHashMap<>();
						// 加载配置 profile 的信息,默认为 default
						initializeProfiles();
						// 遍历 Profiles,并加载解析
						while (!this.profiles.isEmpty()) {
							// 从双向链表中获取一个profile对象
							Profile profile = this.profiles.poll();
							// 非默认的就加入,进去看源码即可清楚
							if (isDefaultProfile(profile)) {
								addProfileToEnvironment(profile.getName());
							}
							load(profile, this::getPositiveProfileFilter,
									addToLoaded(MutablePropertySources::addLast, false));
							this.processedProfiles.add(profile);
						}
						// 解析 profile
						load(null, this::getNegativeProfileFilter, addToLoaded(MutablePropertySources::addFirst, true));
						// 加载默认的属性文件 application.properties
						addLoadedPropertySources();
						applyActiveProfiles(defaultProperties);
					});
		}

  然后我们进入具体的apply()方法中来查看。

image.png

  中间的代码都有注释,主要是处理profile的内容。

image.png

  首先是getSearchLocations()方法,在该方法中会查询默认的会存放对应的配置文件的位置,如果没有自定义的话,路径就是 file:./config/ file:./ classpath:/config/ classpath:/ 这4个

image.png

image.png

  然后回到load方法中,遍历4个路径,然后加载对应的属性文件。

image.png

  getSearchNames()获取的是属性文件的名称。如果自定义了就加载自定义的

image.png

  否则加载默认的application文件。

image.png

再回到前面的方法

image.png

进入load方法,会通过前面的两个加载器来分别加载application.properties和application.yml的文件。

image.png

loader.getFileExtensions()获取对应的加载的文件的后缀。

image.png

image.png

image.png

进入loadForFileExtension()方法,对profile和普通配置分别加载

image.png

继续进入load方法

image.png

image.png

image.png

image.png

image.png

开始加载我们存在的application.properties文件。

properties加载

  在找到了要加载的文件的名称和路径后,我们来看下资源加载器是如何来加载具体的文件信息的。

image.png

进入loadDocuments方法中,我们会发现会先从缓存中查找,如果缓存中没有则会通过对应的资源加载器来加载了。

image.png

此处是PropertiesPropertySourceLoader来加载的。

image.png

image.png

进入loadProperties方法

image.png

之后进入load()方法看到的就是具体的加载解析properties文件中的内容了。感兴趣的可以看下具体的逻辑,本文就给大家介绍到这里了。

image.png

SpringBoot源码分析之bootstrap.properties文件加载的原理

emsp; 对于SpringBoot中的属性文件相信大家在工作中用的是比较多的,对于application.properties和application.yml文件应该非常熟悉,但是对于bootstrap.properties文件和bootstrap.yml这个两个文件用的估计就比较少了,用过的应该清楚bootstrap.properties中定义的文件信息会先与application.properties中的信息加载。而且大家在使用的时候还经常碰到获取不到bootstrap.properties中定义的信息的困扰,本文就来给大家揭开这些谜团。

GitEE源码地址:https://gitee.com/dengpbs/spring-boot-2.2.5.snapshot.git

1.bootstrap的使用

  首先在SpringBoot中默认是不支持bootstrap.properties属性文件的。需要映入SpringCloud的依赖才可以。

image.png

相关的版本环境

image.png

image.png

然后创建对应的bootstrap.properties文件,当然也可以创建bootstrap.yml文件

image.png

同步的我们也会创建application.properties文件,其中会覆盖一个属性

image.png

然后我们在controller中获取测试

image.png

访问测试:http://localhost:8080/query

image.png

通过访问看到bootstrap.properties中的信息获取到了,同时age也被application.properties中的属性覆盖掉了。加载顺序到底是什么?为什么会覆盖呢?接下来分析。

2.bootstrap加载原理分析

  看本文之前最好看下前面讲解的SpringBoot中的监听机制。

2.1 BootstrapApplicationListener

  在使用bootstrap.properties文件时我们需要映入相关的依赖

image.png

  其实在这个依赖中会在对应的spring.factories文件中给我们提供新的监听器,也就是BootstrapApplicationListener监听器。

image.png

  而BootstrapApplicationListener监听触发的事件是ApplicationEnvironmentPreparedEvent事件,这个事件其实和我们前面介绍监听application.properties的时候的监听器ConfigFileApplicationListener监听的是同一个事件。

image.png

image.png

  如果你看了前面的文章,那么此处会觉得有点眉目了。也就是当启动的时候发布对应的事件,该监听器会触发相关的解析行为。

2.2 启动流程梳理

  搞清楚了监听器的关系后,来看下启动的流程代码具体是怎么执行的。

image.png

image.png

直接进入

image.png

在SpringApplication的构造方法中我们要注意两点,1.监听器的加载 2.main方法的主类记录

image.png

然后回来进入run方法

image.png

image.png

Debug到第一个端点。

image.png

然后我们放过。

GIF.gif

通过上面的动图可以看到又进入了一次这个run方法。先看处理的结果。

image.png

然后我们再放过,继续

image.png

分两次加载,有先右后哦。那么这里面的第一个加载的原理到底是什么呢?继续来分析。

2.3 bootstrap.properties的加载原理

  接下来看看是如果出现的一个父context来优先加载我们的bootstrap.properteis文件的,还是从这个图开始

image.png

image.png

链路如上面一步步跟踪即可。

image.png

跳过非关键的,直接进入到BootstrapApplicationListener中来看。

image.png

然后进入到 bootstrapServiceContext方法中。

image.png

这儿我们看到有创建了一个SpringApplication对象。这个其实就是父Context对象了。

image.png

进入run方法你会发现,回到了前面

image.png

image.png

到这应该就清楚了执行的核心流程了

SpringBoot中的Tomcat容器加载

在创建应用上下文这一步创建Tomcat容器

Tomcat基础

   想要搞清楚在SpringBoot启动中到的是如何集成的Tomcat容器,这个就需要先对Tomcat本身要有所了解,不然这个就没办法分析了。Tomcat版本是8.5.73

1.目录结构

  先简单的回顾下一个Tomcat文件的目录结构

image.png

  这个非常基础和简单就快速过掉。

2.启动流程

  Tomcat的架构相关的内容在本文中就不再赘述,可以查阅Tomcat源码专题的内容,我们来看下当我们要启动一个Tomcat服务,我们其实是执行的bin目录下的脚本程序,startup.batstartup.sh.一个是windows的脚本,一个是Linux下的脚本,同样还可以看到两个停止的脚本 shutdown.batshutdown.sh.

image.png

  为了比较直观的来查看脚本的内容,我们通过VCCode来查看吧。

image.png

查看 startup.bat

image.png

可以看到在这个脚本中调用了 catalina.bat这个脚本文件,继续进入,配置信息很多,找核心的脚本

image.png

对应的我们进入到doStart方法中

image.png

最后会执行的程序是

image.png

image.png

而这个MAINCLASS变量是前面定义的有的

image.png

其实前面看了这么一堆的脚本文件,都是在做一些环境的检测和运行时的参数,最终执行的是Bootstrap中的main方法。

3.Bootstrap类

3.1 架构图

  在分析具体的源码流程之前还是需要对Tomcat的架构图要有所了解的

image.png

3.2 流程分析

  接下来我们需要查看下Bootstrap中的main方法了,这时我们需要下载对应的源码文件了。可以官网自行下载,也可以在课件资料中找到。

image.png

  本文不详解介绍,只为SpringBoot中内容做铺垫。

bootstrap.init(); // 初始化类加载器
bootstrap.load(); // 间接调用Catalina,创建对象树,然后调用生命周期的init方法初始化整个对象树
bootstrap.start(); // 间接调用Catalina的start方法,然后调用生命周期的start方法启动整个对象树

SpringBoot中详解

1.自动装配

  首先我们来看下在spring.factories中注入了哪些和Web容器相关的配置类。

1.1 EmbeddedWebServerFactoryCustomizerAutoConfiguration

  第一个是EmbeddedWebServerFactoryCustomizerAutoConfiguration。

image.png

  查看代码,比较容易

image.png

image.png

在这个配置类里面就是根据我们的配置来内嵌对应的Web容器,比如Tomcat或者Jetty等。

1.2 ServletWebServerFactoryAutoConfiguration

  然后来看下ServletWebServerFactoryAutoConfiguration这个配置类。

image.png

  首先来看下在类的头部引入和一些核心的信息

image.png

  重点我们需要看下EmbeddedTomcat这个内部类。

image.png

  看到的核心其实是创建了一个TomcatServletWebServerFactory对象并注入到了Spring容器中。这块的内容非常重要,是我们后面串联的时候的一个切入点。

2.启动流程

  有了上面的自动配置类的支持我们就可以看看在SpringBoot的run方法中是在哪个位置帮我们内嵌了Tomcat容器呢?首先我们从SpringBoot的run方法的刷新上下文的方法进入。

image.png

  这部分其实就是Spring的核心代码了,我们进入到refresh()方法。

image.png

继续进入:

image.png

  然后我们进入ServletWebServerApplicationContext对象的onRefresh方法中。

image.png

核心方法 createWebServer() 创建我们的Tomcat容器。

image.png

可以看到,从容器中获取的工厂对象其实就我们上面注入的对象,然后根据工厂对象获取到了一个TomcatWebServer实例,也就是Tomcat服务对象。关键点我们需要看下getWebServer方法的逻辑

image.png

image.png

然后继续进入到 getTomcatWebServer方法中。

image.png

进入构造方法查看

image.png

进入Tomcat初始化的方法initialize方法

image.png

进入start方法

image.png

image.png

到这儿后面的逻辑其实就是Tomcat自身启动的逻辑了。

SpringBoot中的Acuator监控

  • 18
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值