SpringBoot第一特性:自动装配
Posted 毛奇志
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了SpringBoot第一特性:自动装配相关的知识,希望对你有一定的参考价值。
文章目录
- 一、前言
- 二、Springboot四个特性
- 三、Springboot自动装配
- 四、以自动装配为基础的各种Starter组件
- 五、小结
一、前言
二、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第一特性:自动装配的主要内容,如果未能解决你的问题,请参考以下文章