一、SPI简介
SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制。SPI常用于框架的功能拓展,最典型的应用就是Spring对数据库驱动的加载、SpringBoot自动配置的实现以及Dubbo插件可拔插实现。
SPI说到底还是接口,但是与传统意义上的接口由有所区别。传统的接口由服务提供者制定并实现接口,供消费者调用,而SPI是自己制定接口自己消费服务,而实现则交给第三方。SPI有点像发布需求招标的过程,我把需求规格、标准发布出来,大家带着实现方案来投标,谁的方案好我就用谁的。
二、原生SPI开发流程
三、SPI的实现案例
项目的结构如下图,是一个Nacos为注册中心的微服务Demo项目,大家测试不需要这么复杂的项目,我这里是为了探索SpringBoot自动装配SPI顺手用了以前的微服务Demo。
common模块就包含很简单一个接口,这个模块会被接口实现方和调用方引入。
package com.wen;
public interface DbType {
String getName();
}
MysqlSPI模块仅包含DbType接口的实现
package com.wen;
public class MysqlDbType implements DbType {
@Override
public String getName() {
return "Mysql";
}
}
discovery-client模块需要引入common模块和MysqlSPI,以及配置SPI的映射文件,映射文件名放在resources/META-INF/services下,文件内容就是MysqlSPI中DbTy实现类的全限定名
测试类
import com.wen.DbType;
import com.wen.client.NacosClientApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.ServiceLoader;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = NacosClientApplication.class)
public class Example {
@Test
public void example01() {
ServiceLoader<DbType> serviceLoader = ServiceLoader.load(DbType.class);
System.out.println("JDK 原生SPI,会缓存实例到Map中");
for (DbType search : serviceLoader) {
System.out.println(search.getName());
}
}
测试结果如下,我们成功拿到了,接口的实例,并执行了方法
四、SPI的实现源码探索
我们就从测试类中使用的ServiceLoader入手。ServiceLoader实现了Iterable,并且声明一个LazyIterator类型的变量lookupIterator,LazyIterator类是ServiceLoader的一个内部类,看名字就不难猜到他也实现了迭代器接口,不过它实现的是Iteratorj接口。注意图中的常量PREFIX,这就是为啥映射要配在这里的原因。下面还有的Map是为了缓存实例。
从ServiceLoader的load方法开始,不太重要的过程我们直接跳过。load走完其实啥也没干,仅仅就完成了ServiceLoader类的初始化。真正的接口实现的实例化是在迭代器遍历的过程中完成的。
我们对load的结果遍历,调用ServiceLoader的iterator方法生成一个迭代器实例,遍历过程会调用该迭代器的hasNext和Next方法,这里都是先从缓存读取,没取到就利用lookupIterator这个迭代器,临时去查找接口实现,并实例化。
LazyIterator的hasNext和next方法最后又调用了他的实例方法hasNextService和nextService
这里要重点关注了,真正干活就在这里
这个service是啥,前面忘了介绍没关系,就是他,也就是接口类的实例。
前面已经拿到了实现类的全限定名,这里最后一步,利用JAVA的反射机制进行实例化,并缓存。
到这里整个过程应该是相当清楚了。这里原生SPI的缺点也是很明显的,每定义一个接口我必须新增一个映射配置文件,另外如果一个接口有多个实现,我无法直接拿到我想要的那个,需要去循环。
五、Spring SPI实现
代码上有一点点改变,映射文件统一为spring.factories,文件内容是键值对,前面是接口全限定名,后面是接口实现的全限定名,多个实现以“,”分隔,多个接口换行符分隔。
测试代码需要调整一下,主要干活的类是SpringFactoriesLoader类
import com.wen.DbType;
import com.wen.client.NacosClientApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.io.support.SpringFactoriesLoader;
import org.springframework.test.context.junit4.SpringRunner;
import java.util.List;
@RunWith(SpringRunner.class)
@SpringBootTest(classes = NacosClientApplication.class)
public class Example {
@Test
public void example01() {
List<DbType> dbTypeList= SpringFactoriesLoader.loadFactories(DbType.class, null);
System.out.println("Spring SPI,非自动配置方式,实例不会缓存,更不会托管给Spring容器");
for (DbType dbType:dbTypeList){
System.out.println(dbType.getName());
}
}
}
看看测试结果
代码逻辑很清晰明了
我们先看下结果,缺点很明显,实例没有缓存,每次调用都要重新解析,实例化,完全没有体现出Spring应有的优势。
六、Spring SPI自动装配实现
前面我们介绍了Spring的SPI很朴素的实现方式,确实有些鸡肋。那么我们更进一步,试试自动配置。项目代码小调整一下,MysqlSPI下新加一个自动配置类
package com.wen;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
@Configuration
public class DbTypeAutoConfiguration {
@Bean(name = "mysql")
@Primary
public DbType dbType(){
return () -> "Mysql";
}
@Bean(name = "mongodb")
public DbType mongodb(){
return () -> "Mongodb";
}
}
映射配置文件需要调整一下,key修改成org.springframework.boot.autoconfigure.EnableAutoConfiguration
值改为刚刚修改的自动配置类的全限定名 com.wen.DbTypeAutoConfiguration
测试类变得更简单了,用例通过,说明自动装配是成功了。这才是Spring该有的样子。
七、既然到SPI的自动配置方式,SpringBoot的自动装配也简单聊下。
1、核心配置
@EnableAutoConfiguration 这是实现自动配置关键注解,通常在启动类要开启,因为启动类一般添加了@SpringBootApplication配置,而@SpringBootApplication注解实现使用了元注解@EnableAutoConfiguration,所以这两个开关并不需要我们手动打开。这两个注解只是开关,实际上干活的缺失AutoConfigurationImportSelector这个类,这个类主要负责处理条件注入、去重、过滤以及被注入类的搜索定位等前置工作,搜索定位还是依靠前面提到过SpringFactoriesLoader类
2、注入过程。我们主要关注三个问题点,入口在哪,如何定位目标,何时初始化
1)入口:依然是我们的启动类,最终到AbstractApplicationContext类的refresh方法,跟过代码就会知道,这里是Spring容器初始化组件的最核心部分。
2)、如何定位目标:定位spring.factories目标,下面是定位SPI映射文件中自动配置类途径的类和方法,过程有点长,不上图了。调用链路在下面表格顺序从上至下。
类名 | 方法 |
AbstractApplicationContext | public void refresh() |
protected void invokeBeanFactoryPostProcessors(ConfigurableListableBeanFactory beanFactory) | |
PostProcessorRegistrationDelegate | public static void invokeBeanFactoryPostProcessors( ConfigurableListableBeanFactory beanFactory, List<BeanFactoryPostProcessor> beanFactoryPostProcessors) |
ConfigurationClassPostProcessor | public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) |
public void processConfigBeanDefinitions(BeanDefinitionRegistry registry) | |
ConfigurationClassParser | public void parse(Set<BeanDefinitionHolder> configCandidates) |
private void processDeferredImportSelectors() | |
内部类DeferredImportSelectorGrouping public Iterable<Group.Entry> getImports() | |
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) | |
AutoConfigurationImportSelector | public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) |
public String[] selectImports(AnnotationMetadata annotationMetadata) | |
SpringFactoriesLoader | public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) |
3)、何时初始化:上一步只是解析并缓存了各种BeanDefinition文件,真正实例化是AbstractApplicationContext的finishBeanFactoryInitialization方法
接着调用DefaultListableBeanFactory的preInstantiateSingletons方法,后面的过程就很简单了。
好了,篇幅有点长,可能略显啰嗦,但是本着把细节讲清楚为目标,我也就不嫌麻烦了,希望大家能有所收获。