SpringBoot第一特性:自动装配

Posted 毛奇志

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot第一特性:自动装配相关的知识,希望对你有一定的参考价值。

文章目录

一、前言

二、Springboot四个特性

springboot和springcloud的关系是什么?
对于单体架构,仅暴露一个端口,比如8080,每个都是一个springboot工程;
对于微服务架构,整个是一个springcloud工程,每个微服务是一个springboot工程,每个springboot暴露一个端口,比如8080,8081,8082.

1、第一特性,EnableAutoConfiguration ,译为自动装配

2、第二特性,Starter启动依赖,依赖于自动装配的请求。

3、第三特性,Actuator监控,提供一些endpoint,这些endpoint可以基于http jmx 等形式去进行访问health信息,metrics信息。

4、第四特性,Spring Boot CLI它是为springcloud提供的springboot命令行操作的功能,它通过groovy脚本快速构建springboot的应用,用得很少,一般还是基于idea应用构建springboot应用,略过。

小结:自动装配和启动依赖是不同的,两者关系是启动依赖依赖于自动装配,监控提供一些访问形式。

三、Springboot自动装配

3.1 Springboot牛刀小试:整合redis,引入自动装配

步骤1:引入redis的starter依赖
步骤2:在application.properties文件中,配置 redis 相关信息
步骤3:在controller类中,使用redisTemplate

问题:既然能够实现@Autowired依赖注入的前提是IOC中存在这个bean的实例,那么是谁装载了这个bean实例?

回答:spring中装载bean实例包括三种方式:xml文件 configuration类 enable注解,现在这三种方式我们都没做,但是为什么springboot可以实现bean装配,这就是springboot的自动装配,完成bean的自动装载,只要写出来@Autowired,自动装配的原理是怎么实现的。

spring静态装配bean实例三种方式:xml文件 Configuration配置类 @Enable模块装配
spring动态装配/自动装配bean实例两种方式:ImportSelector接口 Registator接口

3.2 自动装配的思考

springboot为什么能够完成自动装配?
一句话总结,在pom.xml中引入的依赖(即jar包),都被约定好带有统一的格式,都有XxxConfiguration类,使用时springboot直接对其扫描,就完成了装配,但是这是对开发程序员不可见的,所以被称为自动装配。每个第三方的starter里面都有一个配置类XxxConfiguration类,这是一种约定。

Springboot自动装载问题1:Spring引擎怎么知道第三方组件starter里面具体配置类在哪里?
回答:自动装配,所有服务提供方的starter依赖,都有一个 spring.factories 文件,里面指定了哪些是 Configuration 配置类。

Springboot自动装载问题2:每一个第三方starter里面都有一个配置类XxxConfiguration,Springboot中使用多个第三方组件,如何批量扫描多个配置类XxxConfiguration redis mybatis dubble
回答:批量bean加载机制。spring存在两种动态bean装载机制,一种是ImportSelector接口,一种是Registator接口,都是接口,本文介绍ImportSelector接口。动态装载:根据运行时上下文的具体运行条件来状态来装载配置类XxxConfiguration,不同于@Conditional。

3.3 实践:模拟自动装配

这部分给出一个项目,模拟springboot自动装配,项目结构如下:

3.3.1 两个bean和两个配置类

给出一个 mybatis bean类、mybatis 模拟配置类、redis bean类、redis 模拟配置类,如下:

3.3.2 MyImportSelector类

模拟ImportSelector实现类,实现ImportSelector接口,返回配置类

public class MyImportSelector implements ImportSelector 
    @Override
    public String[] selectImports(AnnotationMetadata annotationMetadata) 
        // 在这里去加载所有的配置类
        // 通过某种机制去完成指定路径的配置类的扫描就行?
        // spring只需要知道 package.class.classname 就行了。
        return new String[]MybatisConfiguration.class.getName(), RedisConfiguration.class.getName();
    

3.3.3 @MyEnableConfiguration注解(定义注解)

如何让SpringbootApplication知道MyImportSelector返回的配置类?只需要声明一个注解就可以完成。

定义一个注解,@Import(ImportSelector接口实现类.class)

这一步是定义注解,下一步是启动类中使用注解

3.3.4 两个启动类(启动类默认会扫描所在包及其子包)使用注解

本项目中,两个启动类

(1) 对于SpringbootdemopageApplication启动类

不要使用@MyEnableConfiguration也可以,因为SpringbootdemopageApplication所在包为package com.example.demo;,而两个bean所在包为package com.example.demo.mybatis 和 package com.example.demo.redis,这样,启动SpringbootdemopageApplication,会扫描所在包及其子包,就扫描到了两个bean。

(2) 对于SpringbootApplication启动类

在com.example.demo.springbootdemo,必须配置@MyEnableConfiguration注解,启动的时候,扫描@MyEnableConfiguration注解,然后知道它@Import(MyImportSelector.class),在MyImportSelector.class中,spring才知道两个配置类的路径,才有了下面的装载bean并打印出来。

3.3.5 附加:既可以是配置类,也可以直接是bean类

貌似MyImportSelector 返回值中直接放bean类会出现重复装配的情况,总之,最好放配置类。

小结:这个实例告诉我们,springboot底层使用 ImportSelector接口+注解动态装载 ,无论第三方组件(项目中com.example.demo.mybatis 和 com.example.demo.redis模拟第三方组件,每一个组件都包括配置类和实体bean)的配置在哪里,都可以用ImportSelector接口找到,并用注解告诉springboot,解决了第一个问题(Spring引擎怎么知道第三方组件starter里面具体配置类在哪里)。对于n个配置类(项目中mybatis和redis模拟两个配置类),可以批量配置,不是问题。所以,两个问题都解决了。

注意1:使用@ComponentScan或base-package不可以,它是静态装配,需要把扫描的路径写死,但是在使用一个第三方组件的时候,你是不知道它的bean或配置类在哪里的,这就是静态装配的局限,只能用动态装配。
注意2:使用@Contional 也不仅,仅仅只是一个给出一个判断,装配还是不装配,当然下文我们的有条件配置。

3.4 SpringBoot源码:解析自动装配的底层原理

解析自动装配的底层原理包括两个方面:一个是SpringBootApplication启动类,一个是RedisConfiguration配置类

3.4.1 源码解析:SpringBootApplication启动类

**可以看到,springboot的动态装载的底层实现的@SpringBootApplication注解和我们的模拟的时候的自定义注解@MyEnableConfiguration底层是一样的,都是集成ImportSelector接口实现其selectImports返回配置类数组。**难怪刚刚开始的redisTemplate可以自动装配。

如下图:SpringBoot完成自动装配的核心是 EnableAutoConfiguration 类。

3.4.2 源码解析:RedisConfiguration类

好了,到这里就结束了,解析自动装配的底层原理包括两个方面:一个是SpringBootApplication启动类,一个是RedisConfiguration配置类

其实RedisConfiguration配置类很简单,就是 @Configuration+@Bean,我们今天还是重点看 使用注解@SpringBootApplication的启动类,且看附录:Springboot自动装载的核心源码。

3.4.3 附录:Springboot自动装载的核心源码

@Autowird
private RedisTemplate redisTemplate;   // 自动装载

步骤1:AutoConfigurationImportSelector自动配置ImportSelector的过程

第一,AutoConfigurationImportSelector 自动配置ImportSelector :getAutoConfigurationEntry 获得自动配置/自动装载实体


步骤2:SpringFactoriesLoader 类处理 spring.factories 加载过程

3.5 springboot自动装配小结(已完成)

开发程序员使用的时候,只要直接使用

@Autowired
private RedisTemplate<string,string> redisTemplate;  // 自动配置好了的,直接用就好

问题:刚才的项目只是在自己的项目中模拟,那么具体实践中,spring引擎怎么知道具体starter配置类在哪里?

回答:第一,所有starter启动器都遵循统一的约定,所有的starter组件,都需要一个META-INF/spring.factories文件,如下:


第二,springboot直接扫描配置文件(org.redis.RedisConfiguration org.mybatis.MybatisConfiguration)就好了,如下图:


第三,自动装载就完成了。

3.6 SPI 设计思想(已完成)

3.6.1 什么是SPI? spring.properties 也是 SPI

SPI 全称 Service provider interface ,服务提供接口,这里以各个不同的数据库驱动为例,如图:

SPI 就是我们提供接口,接口的具体实现由第三方来做(这里,不同的数据库驱动是不同的,由数据库厂商来完成),Java程序只要导入不同的jar包就可以执行,这就是SPI的扩展,就是服务提供接口的扩展。

问题:为什么说spring.factories也是SPI ?
回答: SPI: 我定义,我使用,别人实现。spring.factories 是基于key-value键值对的,其中key就是同一个Configuration,value由第三方具体实现;因为每一个starter组件的jar包都有一个Configuration注解,这个Configuration注解就是一个与springboot沟通的接口,其内部具体实现由第三方来做(redis mybatis dubbo),springboot只要导入不同的jar包(redis依赖 mybatis依赖 dubbo依赖)就好。

3.6.2 实践:SPI设计思想的demo(已完成)

第一个工程,databasedemo,表示数据库驱动

maven install 安装到本地.m2/repository
maven package 将 jar 输出到项目工程 target 目录下

第二个,mysqldemo工程,导入刚刚的 databasedemo 依赖,类似真是场景下导入了一个 redis-starter 依赖

第一个工程dabasedemo和第二个工程 mysqldemo 其实是一体的,都是服务提供者。那么第一个工程存在的意义是什么呢?第一个工程存在的意义在于提供一个了 DatabaseDriver,maven install 放在本地,可以有 mysqldemo oracledemo sqlserverdemo,所以说第二个工程可以有多个,这里仅仅创建了一个mysqldemo。

SPI定义:我定义,我使用,别人实现
SPI实现方式:“基于接口的编程+策略模式+配置文件”

1、服务提供方,就是下面的mysqldemo,需要在classpath目录下创建一个META-INF/services目录,然后在 META-INF/services 目录下创建一个接口的全路径名文件。对于该文件:
(1) 文件中填充这个扩展点的实现
(2) 文件编码格式UTF-8

2、服务使用方,就是下面的appdemo,ServiceLoader去进行加载

解释以下上面三个工程使用到了SPI

对于服务的提供者,就是上面的mysqldemo工程,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。

对于服务使用方,就是上面的appdemo工程,即当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。

对于服务使用方,关键在于  ServiceLoader<Xxx接口> loadedDrivers = ServiceLoader.load(Xxx接口.class);一句, 因为 java.util.ServiceLoader.load(Xxx接口.class) 底层调用nextService()遍历所有pom依赖的 META-INF/services/ 路径下的指定文件,对于每条变量内容,都是得到字节码,得到实例对象,然后put到LinkedHashMap类型的providers,又因为 ServiceLoader implements Iterable,所以程序员可以将其迭代出来,在ServiceLoader 类的 iterator()方法中,先knownProviders= providers.entrySet().iterator();,然后对knownProviders遍历,之前从 META-INF/services/ 路径下加载的文件的东西就都迭代出来了,然后就可以调用具体的方法了,

for (DatabaseDriver databaseDriver:serviceLoader)
        System.out.println(databaseDriver.buildConnect("Test"));   // 第一,迭代出来;第二,调用;

    或者
Iterator<DatabaseDriver > searchs = s.iterator();
if(searchs.hasNext())
  DatabaseDriver databaseDriver= searchs.next();   // 第一,迭代出来;
  System.out.println(databaseDriver.buildConnect("Test"));   // 第二,调用;

所以,以后 在第三方jar包中看到这一句,看到java.util.ServiceLoader.load(Xxx接口.class),就可以知道它是使用SPI了,如commonloging和jdbc4.0之后。

3.6.3 理论:SPI两个应用场景(已完成)

应用1:common-logging

apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现,我定义,我使用,别人实现,这就是SPI.

对于使用者来说,通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件找到日志提供商,通过读取该文件的内容找到日志提供商实现类。

应用2:jdbc

jdbc4.0以前,程序员需要基于Class.forName(“xxx”)的方式来装载驱动,程序员需要在代码中硬编码指定具体的数据库驱动;

jdbc4.0之后,基于spi的机制来找到驱动提供商了,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者,不用再写Class.forName(“xxx”)硬编码。

static 
  loadInitialDrivers();
  println("JDBC DriverManager initialized");

loadInitialDrivers 方法的部分
AccessController.doPrivileged(new PrivilegedAction<Void>() 
public Void run() 
  //这里典型的SPI,Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制
  ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);   // jdbc源码就像我们在上面三工程中的appdemo中一样使用SPI 
  Iterator<Driver> driversIterator = loadedDrivers.iterator();   //  jdbc源码就像我们在上面三工程中的appdemo中一样使用SPI  第一,迭代出来
try
while(driversIterator.hasNext())    //  jdbc源码就像我们在上面三工程中的appdemo中一样使用SPI good 从模拟到实际 第一,迭代出来
  driversIterator.next();   //   jdbc源码就像我们在上面三工程中的appdemo中一样使用SPI good 从模拟到实际 第一,迭代出来

 catch(Throwable t) 
  // Do nothing

  return null;

);

问题:为什么使用SPI,程序员就不用Class.forName(“xxx”)硬编码指定具体的数据库驱动了?
回答:使用方的loadInitialDrivers();中遍历配置文件中所以内容,自动加载。
Class.forName(“xxx”)代码被封装到java.util.ServiceLoader.nextService()方法中了,这个方法中,PREFIX + service.getName();,PREFIX 是写死的路径 META-INF/services/, service.getName()是文件名,遍历该路径下的文件就好了,里面写着实现了,将一个个的实现类都Class.forName(“xxx”)得到字节码,然后
S p = service.cast(c.newInstance()); // 新建实例对象
providers.put(cn, p); // 放到LinkedHashMap类型的providers里面去,在ServiceLoader 类的 iterator()方法中,先knownProviders= providers.entrySet().iterator();,然后对knownProviders遍历,之前从 META-INF/services/ 路径下加载的文件的东西就都出来了

注意:这个ServiceLoader是JavaSE内置类,java.util.ServiceLoader,
全面解析java.util.ServiceLoader类,从 java.util.ServiceLoader.load(Class<>?>) 方法开始

public static <S> ServiceLoader<S> load(Class<S> service) 
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return ServiceLoader.load(service, cl);

public static <S> ServiceLoader<S> load(Class<S> service, ClassLoader loader)

    return new ServiceLoader<>(service, loader);


这里获得线程上下文的ClassLoader,因为双亲委派模式下上层ClassLoader无法加载下层的类,初始化了ServiceLoader对象。

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();
        lookupIterator = new LazyIterator(service, loader);
    

hasNext调用了hasNextService

private static final String PREFIX = "META-INF/services/";

        private boolean hasNextService() 
            if (nextName != null) 
                return true;
            
            if (configs == null) 
                try 
//fullName = META-INF/services/java.sql.Driver
                    String fullName = PREFIX + service.getName();
                    if (loader == null)
                        configs = ClassLoader.getSystemResources(fullName);
                    else
                        configs = loader.getResources(fullName);
                 catch (IOException x) 
                    fail(service, "Error locating configuration files", x);
                
            
            while ((pending == null) || !pending.hasNext()) 
                if (!configs.hasMoreElements()) 
                    return false;
                
                pending = parse(service, configs.nextElement());
            
            nextName = pending.next();
            return true;
        

该方法就是获取META-INF/services/xxx配置文件中你的配置项,也就是实现类的全限定名。这里就是一开始问题的答案。

next方法就是调用Class.forName通过hasNext里得到的nextName(遍历)与传过来的ClassLoader加载接口实现类。

public S next() 
    if (acc == null) 
        return nextService();
     else 
        PrivilegedAction<S> action = new PrivilegedAction<S>() 
            public S run()  return nextService(); 
        ;
        return AccessController.doPrivileged(action, acc);
    


调用nextService,加载并初始化实现类。

private S nextService() 
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try 
        c = Class.forName(cn, false, loader);

	......
    try 
        S p = service.cast(c.newInstance());
        providers.put(cn, p);     

注意事项:META-INF和services目录必须分开创建

maven quickstart 项目,maven clean maven install 之后变成jar包,放在本地 .m2/repository 库,maven clean maven package 之后变成jar包,放在项目中 target 目录下;
maven webapp 项目,maven clean maven install 之后变为war包,放在本地 .m2/repository 库,maven clean maven package 之后变成war包,放在项目中 target 目录下。

四、以自动装配为基础的各种Starter组件

starter组件与自动装配紧密相连,因为所有的第三方的starter组件都是遵循自动装配的约定,提供spring.factories文件,提供配置类给springboot扫描完成自动装配。

4.1 Starter组件的原理:官方包依赖的条件触发

4.1.1 官方依赖包中不存在spring.factories

对于官方的springboot starter,不存在spring.factories,那么为什么不存在spring.factories,它又是如何加载的?
原来,starter包一共包括两种:
(1) 官方包 spring-boot-starter-xxx 不存在 spring.factories 如spring-boot-starter-data-redis spring-boot-starter-thymeleaf
(2) 第三方包 xxx-spring-boot-starter 一定存在spring.factories

以Redis为例,其配置类RedisAutoConfiguration不存在 spring-boot-starter-data-redis 里面 ,而是存在spring-boot-autoconfigure包,

既然如何,springboot又是如何找到配置类RedisAutoConfiguration的呢?
答案是条件注解。

4.1.2 条件注解定义

4.1.3 条件注解用来确保配置类完成配置

小结,官方包的依赖就是条件触发。

好了,最后看一下,starter中的spring.factories 是如何 记录官方包的配置类定位

4.1.4 spring.factories中记录官方包的配置类定位


这里面这些都只需要导入依赖就好,只有第三方包才需要自己写spring.factories。

4.2 实践:spring.factories自定义组件的配置类的装载

服务提供方:@Configuration+@Bean 往 IOC 容器中放置bean对象,然后 spring.factories 文件中指定哪个类是 Configuration 配置类
服务使用方:@Autowired/@Resource 和 getBean(Xxx.class) 使用 IOC 容器中的bean对象

4.2.1 服务提供方

服务提供方:新建maven quickstart项目:autoconfiguredemo

配置类+Bean类,如下:

服务提供方,spring.factories文件中指定配置类路径,如下:

这就是SPI,对于服务使用方:我定义,我使用,别人(服务提供方)实现
对于服务提供方:提供服务的具体实现之后,需要按照服务使用方的要求,提供统一规则的对外接口,给服务使用方调用,spring.factories就是一种SPI,就是在这个文件里面,按照服务使用方的要求,暴露 XxxConfiguration 配置类

服务提供方完成了,maven clean maven install 成为一个jar包,给springboot工程去使用

4.2.2 服务使用方

服务使用方:新建一个maven quickstart 工程 springbootcore2,导入依赖

4.3 实践:@ConditionalOnClass等注解实现条件配置

现在在第一个工程Configuration配置类上面使用条件注解,表示要满足某一个条件才装载,就使用 @ConditionalOnClass(RedisOperations.class) 就好了。

先导入依赖

4.4 实践:spring-autoconfigure-metadata.properties 实现条件配置

在springboot中存在一个spring-autoconfigure-metadata.properties文件,它也是一种配置文件。

这种配置文件只是,使用者需要按要求这样配置,实际上我们自己的工程也可以实现这种配置文件并打成jar包的。

源工程

4.5 两种配置方式一起使用

并不是所有的条件配置都是写在类上面的注解上面的,也可以通过spring-autoconfigure-metadata.properties配置文件去写,且看mybatis

这里是mybatis的条件配置文件 spring-autoconfigure-metadata.properties

这里是mybatis的条件注解

五、小结

本文介绍Springboot自动装配的特性,从使用redis引入了自动装配,然后理解自动装配、模拟实现、阅读springboot源码,最后引入条件触发和demo实现。

一句话说清楚springboot的自动装配和启动依赖:自动装配英文直译EnableAutoConfiguration,这是SpringBoot中的一个注解,这个注解是上面所有的用@Import引入的类,会返回一个Configuration可迭代列表,让SpringBoot知道需要加载哪些配置类,这就是自动装配/动态状态。自动装配就是服务提供方(即各种starter)和服务使用方(即SpringBoot)约定好,关联起来,是使用一个 spring/factories 文件或 spring-autoconfigure-metadata.properties文件来实现的。

天天打码,天天进步!!!

自动装配模拟工程代码:https://download.csdn.net/download/qq_36963950/12555256

SPI设计思想工程代码:https://download.csdn.net/download/qq_36963950/12555258

自动装配+条件注解+条件配置文件工程代码:https://download.csdn.net/download/qq_36963950/12555260

从传统的 spring + mybatis 变成 springboot + mybatis

在传统的spring ioc中,注入bean使用的方式两种,
(1) xml配置 <configuration></configuration><bean></bean>
(2) @Configuration+@Bean
(3) 其他的@Controller @Service @Component注解
使用bean的方式包括两种
(1) context.getBean(Xxx.class)
(2) @Autowired或者@Resource注解

我们现在变成了springboot+mybatis,springboot就是在spring上封装了一层,这个boot就是快速启动的意思,快速通过不断减少配置来实现的。

spring最常见的一种错误,就是没有注入对象
解决:两种解决方式
如果是新增的类:要么添加 xml配置,要么添加 @Configuration+@Bean
如果是架构:缺少@Controller @Service @Component注解 或者 mybatis 没有添加注解,可通过 增加@MapperScan 或者 @Mapper 或者 Mapper.xml

mybatis还有一种错误,就是 mapper.xml里面关联 mapper类和dao类错了(判断方法,ctrl+鼠标左键上去)

spring一个错误:没有注入bean对象到IOC容器(xml或者@Configuration+@Bean)
mybatis两种错误:没有注入bean对象到IOC容器(xml或者@Configuration+@Bean) 或者 Mapper.xml无法和Mapper.java 绑定在一起(ctrl+左键检查)

SPI

api 我定义 我实现 别人调用
spi 我定义 我使用 别人实现
api是上层提供出来的,spi是下层依赖的

以上是关于SpringBoot第一特性:自动装配的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot自动装配流程源码分析

# SpringBoot 自动装配

聊聊什么是SpringBoot 的自动装配原理

传统的Servlet在spring boot中怎么实现的?

Spring Boot 2.0深度实践之核心技术篇

SpringBoot的自动装配(一)