springBoot自动配置原理源码分析+自定义starter启动器+可视化监控+mybatisPlus使用

Posted ahcfl

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springBoot自动配置原理源码分析+自定义starter启动器+可视化监控+mybatisPlus使用相关的知识,希望对你有一定的参考价值。

一、springBoot自动化配置原理

1. starter管理机制

通过依赖了解SpringBoot管理了哪些starter

  1. 通过依赖 spring-boot-dependencies 搜索 starter- 发现非常多的官方starter,并且已经帮助我们管理好了版本。
  2. 项目中使用直接引入对应的 starter 即可,这个场景下需要的依赖就会自动导入到项目中,简化了繁琐的依赖。

所有的场景启动器都依赖于spring-boot-starter

例如druid:

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.24</version>
    <scope>compile</scope>
</dependency>

这个启动器本身没有代码,通过依赖,构建了springBoot的基础运行环境,

包括spring基础环境,自动化配置基本环境。

org\\springframework\\boot\\spring-boot-starter\\spring-boot-starter-2.5.0.pom 里面的默认依赖

自动化配置依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.5.0</version>
    <scope>compile</scope>
</dependency>

spring的IOC容器
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-core</artifactId>
    <version>5.3.7</version>
    <scope>compile</scope>
</dependency>

。。。。。等等

小结:

  • 引入的官方starter依赖默认都可以不写版本
  • 如果配置满足您当前开发需要,则默认配置即可
  • 自定义的starter,需要引入spring-boot-starter

2. springmvc的自动化配置原理

以web MVC自动化配置原理为例,理解web MVC自动化配置加入了哪些依赖,做了哪些默认配置。

回忆一下:SpringMVC学习时候,我们在 SSM整合时,添加spring及spring web mvc相关依赖

springmvc.xml 配置文件配置了:
1. 扫描controller 所在包
2. 配置annotation-driven支持mvc功能(HandlerMapping, HandlerAdapter)
3. 视图解析器
4. 静态资源
5. 拦截器
6. ……

web.xml 配置:
1. 初始化spring容器
2. 初始化springmvc DispatcherServlet
3. post请求乱码过滤器

部署还需要单独的tomcat
-----------------------------------------------------
也就是说:我们现在需要在开发业务代码前,就必须要准备好这些环境,否则无法完成业务代码,
这就是我们现在的问题。

让这些问题成为过去,现在我们就探索一下SpringBoot是如何帮助我们完成强大而又简单自动化配置的。

以引入web启动器为列:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
image-20210612214843402 image-20210612220418462 image-20210612215231369

小结: 有了SpringBoot以后,让开发人员重点关注业务本身,而不是环境上,提升了开发效率。

3. 底层原理之@Configuration

理解@Configuration的作用和新特性

@Configuration : 标注当前类是一个配置类,spring会加载改配置类
   属性 proxyBeanMethods:  
         true:  @Bean标注的方式创建 对象会使用代理方式创建,并且放到spring容器中,单例。
         false: @Bean标注的方法执行调用来创建对象,不会进spring容器(多例)。
         默认为true
步骤:
1.创建MyConfig配置类,提供方法创建对象,使用@Bean标注
2.创建User实体类,
3.通过spring容器获取配置类对象,调用方法获取对象。

代码演示:

package com.ahcfl.demo2.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration(proxyBeanMethods = false) // 标注当前类是一个配置类,spring会加载改配置类,且不会进spring容器(多例模式)。
public class MyConfig {
    @Bean
    public User user(){
        return new User();
    }

}
package com.ahcfl.demo2.pojo;

public class User {
    public User() {
        System.out.println("对象被创建了");
    }
}
package com.ahcfl.demo2;

import com.ahcfl.demo2.config.MyConfig;
import com.ahcfl.demo2.pojo.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;

@SpringBootApplication
public class Demo2Application {

   public static void main(String[] args) {
      ConfigurableApplicationContext ac = SpringApplication.run(Demo2Application.class, args);
      MyConfig myConfig = ac.getBean(MyConfig.class);
      User user = myConfig.user();
      User user2 = myConfig.user();
      System.out.println(user);
      System.out.println(user2);
   }

}

小结:不常用的bean设置为false,不加入IOC容器中,可以提升springBoot启动速度。

4. 底层原理之@Import

【1】@Import的基础用法

1.导入Bean,会自动执行当前类的构造方法创建对象,存到IOC容器, bean名称为:类的全路径
2.导入配置类,并且类中有 带有@Bean注解方法,创建对象存到IOC容器,bean名称为:默认方法名称

代码演示

package com.ahcfl.demo2;

import com.ahcfl.demo2.config.MyConfig;
import com.ahcfl.demo2.pojo.User;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Import;

@SpringBootApplication
@Import(User.class)
@Import(MyConfig.class)
public class Demo2Application {

   public static void main(String[] args) {
      ConfigurableApplicationContext ac = SpringApplication.run(Demo2Application.class, args);
      User user = ac.getBean(User.class);
      System.out.println(user); 
   }
}

【2】@Import另外两种实现

为讲解源码做铺垫

  1. 导入 ImportSelector 实现类。会调用接口的selectImports()方法来加载资源。

    image-20210613000128805
  2. 导入 ImportBeanDefinitionRegistrar 实现类,会调用接口的registerBeanDefination()

    来向spring注册bean的信息。(将对象放到spring容器中)

    image-20210613000535321

5. 底层原理之@Conditional衍生条件装配

作用:条件装配,满足Conditional指定的条件,则进行组件注入,初始化Bean对象到IOC容器

image-20210613000732168
package com.ahcfl.demo2.config;

import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class MyConfig {
    @ConditionalOnMissingBean(Dog.class) //在没有dog类对象的情况下创建bean
    //@ConditionalOnClass(Dog.class) //存在当前类的情况下创建bean
    @Bean
    public User user(){
        return new User();
    }

}

小结:@ConditionalOnXXX 注解存在的意义是:满足条件当前类或者Bean才有效,按需导入。

6. 底层原理之@ConfigurationProperties配置绑定

在springBoot基础中有演示,用于配置文件的自动依赖注入。

7. 自动化配置原理@SpringBootApplication入口分析

理解SpringBoot自动化配置流程中@SpringBootApplication是一个组合注解,及每一个注解的作用

@SpringBootApplication组合注解

image-20210612234803630

【1】@SpringBootConfiguration注解作用

  • @SpringBootConfiguration是对@Configuration注解的包装,

    proxyBeanMethods 默认配置 true, full模式(单例模式创建Bean),反之,false为 多例模式

  • 标识是一个配置类所以 引导类也是配置类

image-20210612234815878

【2】@ComponentScan注解作用

  • 组件扫描,默认扫描的规则 ( 引导类所在的包及其子包所有带注解的类 )

问题:

  1. 在引导类中配置 @Bean 注解可以吗?
  2. 为什么Controller、service类添加完注解后,不需要添加扫描包?

【3】@EnableAutoConfiguration自动配置注解

理解@EnableAutoConfiguration自动化配置核心实现注解

@EnableAutoConfiguration也是一个组合注解

image-20210612234927175

【1】@AutoConfigurationPackage

作用:利用Registrar给容器中导入一系列组件 ,将引导类的所有包及其子包的组件导入进来

image-20210612234951082

点击 Registrar 进入到源码的 register 方法,添加 断点,测试

image-20210613084407124

通过 debug 程序发现,默认情况下 将引导类的所有包及其子包的组件导入进来

【2】@Import(AutoConfigurationImportSelector.class)注解作用

作用:利用selectImports方法中的 getAutoConfigurationEntry 方法给容器中批量导入工厂配置相关组件

  1. 调用AutoConfigurationImportSelector类中的selectImports方法
  2. 调用List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes)获取到所有需要导入到容器中的配置类
image-20210613085434132
  1. 利用工厂加载 Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader)得到所有的组件
  2. 从META-INF/spring.factories位置来加载一个文件。

默认扫描我们当前系统里面所有META-INF/spring.factories位置的文件

image-20210613094041615 image-20210613094400722

小结: 自动化配置默认加载的配置文件在哪?

META-INF/spring.factories

8. 自动化配置原理-按条件开启自动配置类和配置项

  • 理解所有的自动化配置虽然会全部加载,但由于底层有大量的@ConditionalOnXXX注解进行判断,所以有很多自动配置类并不能完全开启
  • 如果配置生效了,则会加载默认的属性配置类,实现默认的对应场景的自动化配置

以上通过 META-INF/spring.factories 配置文件找到所有的自动化配置类,但 是不是全部的生效的呢?很显然是不可能全部都生效的。

以webmvc自动化配置为例

image-20210613095800261

问题: 这些不用的 starter 的依赖,能不能导入到我们工程里面? 为什么?

导入相关的starter 依赖,才会进行自动配置加载。不用的不必要导入,会降低springBoot启动速度。

9. 自动化配置原理-springBoot源码分析(理解)

理解整个SpringBoot启动的完成自动化配置及属性加载的全过程

【1】 启动类分析

package com.ahcfl;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan("com.ahcfl.mapper")
public class WebApplication {

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

这里跟SpringBoot有关联的部分有两个

一个是SpringApplication.run(WebApplication.class, args);

一个就是启动类上的注解:@SpringBootApplication

分别跟踪两部分内容。

【2】 springBoot启动过程

main函数中的SpringApplication.run(BankApplication.class, args);就是项目的入口,

也是Spring加载的完整过程,我们从这里开始。

首先跟入run方法,流程如图:

image-20210613111015475

因此,接下来要看的是两部分:

  • new SpringApplication(primarySources):构造函数初始化
  • run(args):成员的run方法
[1] SpringApplication构造函数

我们把跟构造函数有关的几个变量和方法提取出来,方便查看:

// SpringApplication.java

/**
 * 资源加载器,读取classpath下的文件
 */
private ResourceLoader resourceLoader;
/**
 * SpringBoot核心配置类的集合,这里只有一个元素,是我们传入的主函数
 */
private Set<Class<?>> primarySources;
/**
 * 当前项目的应用类型
 */
private WebApplicationType webApplicationType;

/**
 * ApplicationContextInitializer 数组
 */
private List<ApplicationContextInitializer<?>> initializers;
/**
 * ApplicationListener 数组
 */
private List<ApplicationListener<?>> listeners;

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

// 核心构造函数
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    // 1.记录资源加载器
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // 2.将传入的启动类装入集合
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 3.判断当前项目的类型,可以是SERVLET、REACTIVE、NONE
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 4.初始化 initializers 数组
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 5.初始化 listeners 数组
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

分析说明:

  • ResourceLoader resourceLoader:Spring中用来加载资源的加载器

  • Class<?>... primarySources:这里是启动类,本例中就是WebApplication.class

  • WebApplicationType.deduceFromClasspath():判断当前项目的类型,

    可以是SERVLET、REACTIVE、NONE,根据当前classpath中包含的class来判断,

    影响后续创建的ApplicationContext的类型 【3】

  • getSpringFactoriesInstances(ApplicationContextInitializer.class):获取ApplicationContextInitializer类型的实现类对象数组 【4】

  • getSpringFactoriesInstances(ApplicationListener.class):获取ApplicationListener类型的实现类对象数组 【5】

  • deduceMainApplicationClass():没有实际用途,打印日志,输出当前启动类名称

我们只看难点部分,也就是步骤3、4、5

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

可以看到判断结果包含3种:

  • REACTIVE:要求classpath中包含org.springframework.web.reactive.DispatcherHandler,这个是WebFlux中的核心处理器,我们并没有。
  • SERVLET:要求classpath中包含org.springframework.web.servlet.DispatcherServlet,这是SpringMVC的核心控制器,在classpath中肯定可以找到
  • NONE:以上都不满足,就是NONE
2)getSpringFactoriesInstances()方法

在构造函数中被调用了两次,分别加载ApplicationContextInitializerApplicationListener

1612285452094

getSpringFactoriesInstances(Class<T> type) 方法的作用是获得指定接口的实现类的实例集合。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    // 调用下面的一个重载方法,参数type就是接口的类型
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

// 真正的处理逻辑
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // 1.先加载指定接口的实现类的名称集合
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 2.根据类的名称,创建实例对象
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    // 3.排序
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

这里关键是第1步中,调用SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法,

是用来获取指定接口的实现类的名称字符串,而后就可以根据名称创建实例了。

例如我们传递的参数是:ApplicationContextInitializer.class,那么获取的就是ApplicationContextInitializer下面的实现类的名称字符串集合。

那么这里是如何根据接口找到对应的实现类名称呢?

3)loadFactoryNames加载类名称

那么loadFactoryNames是如何根据接口找到对应的实现类名称呢,继续跟入:

SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法:

// SpringFactoriesLoader
/**
  * 使用指定的类加载器,加载{@value #FACTORIES_RESOURCE_LOCATION}中记录的,指定factoryClass
  * 类型的实现类的全路径名。
  * @param factoryClass 需要加载的接口或抽象类
  * @param classLoader 用来加载资源的类加载器
  */
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    // 获取接口名称
    String factoryClassName = factoryClass.getName();
    // 从loadSpringFactories(classLoader)方法返回的是一个Map:key是接口名称字符串,值是实现类的名称集合
    // 然后就可以调用map的get方法,根据factoryClass名称获取对应的实现类名称数组
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

注意到这里是先调用loadSpringFactories(classLoader)方法,

此方法方法返回的是一个Map:key是接口名称字符串,值是实现类的名称集合。

那么,loadSpringFactories方法是如何读取到这样的map呢?上面有截图分析这一部分,代码如下:

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    // 尝试从缓存中获取结果
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        // 从默认路径加载资源文件,地址是:"META-INF/spring.factories"
        Enumeration<URL> urls = (classLoader != null ?
                                 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        // 创建空map
        result = new LinkedMultiValueMap<>();
        // 遍历资源路径
        while (urls.hasMoreElements()) {
            // 获取某个路径
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            // 加载文件内容,文件中是properties格式,key是接口名,value是实现类的名称以,隔开
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                // 获取key的 名称
                String factoryClassName = ((String) entry.getKey()).trim();
                // 将实现类字符串变成数组并遍历,然后添加到结果result中
                for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
                    result.add(factoryClassName, factoryName.trim());
                }
            }
        }
        // 缓存中放一份,下次再加载可以从缓存中读取
        cache.put(classLoader, result);
        return result;
    }
    catch (IOException ex) {
        throw new IllegalArgumentException("Unable to load factories from location [" +
                                           FACTORIES_RESOURCE_LOCATION + "]", ex);
    }
}

这个方法是利用ClassLoader加载classpath下的所有的/META-INF/spring.factories文件。

注意:所有jar包都会被扫描和查找

例如,在spring-boot的jar包中,就有这样的文件

1562132554776

spring.factories内容类似这样:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\\
org.springframework.boot.env.PropertiesPropertySourceLoader,\\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\\
org.springframework.boot.context.event.EventPublishingRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\\
org.springframework.boot.diagnostics.FailureAnalyzers

# 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.web.context.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\\
org.springframework.boot.ClearCachesApplicationListener,\\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\\
org.springframework.boot.context.FileEncodingApplicationListener,\\

以上是关于springBoot自动配置原理源码分析+自定义starter启动器+可视化监控+mybatisPlus使用的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot源码分析----SpringBoot自动配置

SpringBoot的自动装配原理(含例子和源码分析)

SpringBoot源码分析----SpringBoot自动配置原理

210630:springBoot自动配置-自定义start

SpringBoot:自动配置源码底层原理分析

SpringBoot编写自定义的starter