SpringBoot-SpringSPI机制
SpringBoot-SpringSPI机制
学习核心
- Java的SPI机制(复习)
- SpringbootSPI机制
学习资料
SPI机制
首先对Java的SPI机制进行复盘,在理解的基础上再去拆解Springboot的SPI机制原理(其实在解析Springboot自动装配和启动流程的时候就已经涉及到这块的内容)
SPI(Service Provider Interface)是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要用于框架中开发,例如Dubbo、Spring、Common-Logging,JDBC等采用采用SPI机制,针对同一接口采用不同的实现提供给不同的用户,从而提高了框架的扩展性
Java中的SPI机制实现
Java内置的SPI通过java.util.ServiceLoader类解析classPath和jar包的META-INF/services/目录下的以接口全限定名命名的文件,并加载该文件中指定的接口实现类,以此完成调用。关注其核心实现ServiceLoader源码:
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
private final AccessControlContext acc;
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
private LazyIterator lookupIterator
.........
}
- ServiceLoader类本身实现了Iterable接口并实现了其中的iterator方法,iterator方法的实现中调用了LazyIterator这个内部类中的方法,迭代器创建实例
- 所有服务提供接口的对应文件都是放置在META-INF/services/目录下,final类型决定了PREFIX目录不可变更
Java提供的SPI机制思想很好,但是也存在相应的弊端:
- 无法按需加载:必须依次遍历、实例化所有的实现类,才能获取到想要的方法调用
- 所有的服务接口必须放在META-INF/services/目录下按照指定规则进行配置
针对Java的SPI机制存在的问题,Spring的SPI机制则是沿用SPI的思想,对其进行扩展和优化,采用spring.factories的方式实现SPI机制,可以在不修改Spring源码的前提提供Spring框架的扩展性(可以结合Springboot自动装配和启动流程原理分析,结合自定义扩展的一些案例去理解)
SPI案例分析
1.Java的SPI机制
构建思路
- 创建接口(Video)和对应实现类(TencentVideo、AqyVideo)
- 在resources/META-INF/services下创建一个名为【Video的全限定类名称】的文件,内容为对应的接口实现的全限定类名称(多个实现分行定义)
- 创建JavaSPITest类测试
步骤1
// 定义Video接口
public interface Video {
void play();
}
public class TencentVideo implements Video{
@Override
public void play() {
System.out.println("tencent video play");
}
}
public class AqyVideo implements Video{
@Override
public void play() {
System.out.println("AqyVideo video play");
}
}
步骤2
在resources/META-INF/services下创建一个名为 [com.noob.framework.spi.java.Video]文件,内容定义为对应的接口实现的全限定类名称(多个实现分行定义)
com.noob.framework.spi.java.TencentVideo
com.noob.framework.spi.java.AqyVideo
步骤3
// JavaSPI机制测试
public class JavaSPITest {
public static void main(String[] args) {
// 加载所有的实现并执行
ServiceLoader<Video> serviceLoader = ServiceLoader.load(Video.class);
for (Video video : serviceLoader) {
System.out.println(video);
// 例如此处想要摘选TencentVideo
if(TencentVideo.class.isInstance(video)){
TencentVideo tencentVideo = (TencentVideo) video;
tencentVideo.play();
}
}
}
}
// output
com.noob.framework.spi.java.TencentVideo@42a57993
tencent video play
com.noob.framework.spi.java.AqyVideo@75b84c92
2.Springboot的SPI机制
构建思路
- 创建接口(DataBase)和对应实现类(OracleDataBase、MySQLDataBase)
- 在resources/META-INF下创建一个名为【spring.factories】的文件,内容采用key、value形式存储对应结合和关联实现类定义
- 创建SpringbootSPITest类测试
步骤1
// 定义数据库接口
public interface DataBase {
public void initLink();
}
public class OracleDataBase implements DataBase{
@Override
public void initLink() {
System.out.println("oracle database initLink");
}
}
public class MySQLDataBase implements DataBase{
@Override
public void initLink() {
System.out.println("mysql database initLink");
}
}
步骤2
在resources/META-INF下创建一个名为【spring.factories】的文件,内容采用key、value形式存储对应结合和关联实现类定义
例如此处接口为DataBase,对应多个实现用逗号分隔
com.noob.framework.spi.springboot.DataBase=\
com.noob.framework.spi.springboot.MySQLDataBase,com.noob.framework.spi.springboot.OracleDataBase
步骤3
// SpringbootSPI机制测试
public class SpringbootSPITest {
public static void main(String[] args) {
List<DataBase> dataBaseList = SpringFactoriesLoader.loadFactories(DataBase.class, Thread.currentThread().getContextClassLoader());
for (DataBase dataBase : dataBaseList) {
System.out.println(dataBase);
// 摘选Oracle数据库方法执行
if(OracleDataBase.class.isInstance(dataBase)){
OracleDataBase oracleDataBase = (OracleDataBase) dataBase;
oracleDataBase.initLink();
}
}
}
}
// output
com.noob.framework.spi.springboot.MySQLDataBase@5c0369c4
com.noob.framework.spi.springboot.OracleDataBase@2be94b0f
oracle database initLink
3.案例对比分析
结合上述案例分析可知,两种机制的应用流程非常类似,但是很明显Springboot对Java原生的SPI机制做了优化:
配置文件整合:
- Springboot将所有的配置都定义在一个文件spring.factories中,通过key、value的形式来绑定不同接口及其实现
- Java原生SPi机制则是将所有配置都放在META/services目录下,每一个接口都对应一个配置文件来绑定实现类
Springboot SPI机制源码剖析
// 核心加载代码
SpringFactoriesLoader.loadFactories(DataBase.class, Thread.currentThread().getContextClassLoader());
进入loadFactories
方法拆解源码分析
public static <T> List<T> loadFactories(Class<T> factoryType, @Nullable ClassLoader classLoader) {
Assert.notNull(factoryType, "'factoryType' must not be null");
// 确定类加载器
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
// 核心逻辑1:解析和加载META-INF下的文件(可以进一步跟踪源码追踪到loadSpringFactories方法)
List<String> factoryImplementationNames = loadFactoryNames(factoryType, classLoaderToUse);
if (logger.isTraceEnabled()) {
logger.trace("Loaded [" + factoryType.getName() + "] names: " + factoryImplementationNames);
}
List<T> result = new ArrayList(factoryImplementationNames.size());
Iterator var5 = factoryImplementationNames.iterator();
while(var5.hasNext()) {
String factoryImplementationName = (String)var5.next();
// 核心逻辑2:根据获取到的类限定名称进行实例化操作
result.add(instantiateFactory(factoryImplementationName, factoryType, classLoaderToUse));
}
AnnotationAwareOrderComparator.sort(result);
return result;
}
// --------------------- 核心源码拆解 ---------------------
# 1.跟踪loadFactoryNames方法定义
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return (List)loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
# 2.进一步跟踪loadSpringFactories方法实现
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = (Map)cache.get(classLoader);
if (result != null) {
return result;
} else {
Map<String, List<String>> result = new HashMap();
try {
Enumeration<URL> urls = classLoader.getResources("META-INF/spring.factories");
while(urls.hasMoreElements()) {
URL url = (URL)urls.nextElement();
UrlResource resource = new UrlResource(url);
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
Iterator var6 = properties.entrySet().iterator();
while(var6.hasNext()) {
Map.Entry<?, ?> entry = (Map.Entry)var6.next();
String factoryTypeName = ((String)entry.getKey()).trim();
String[] factoryImplementationNames = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
String[] var10 = factoryImplementationNames;
int var11 = factoryImplementationNames.length;
for(int var12 = 0; var12 < var11; ++var12) {
String factoryImplementationName = var10[var12];
((List)result.computeIfAbsent(factoryTypeName, (key) -> {
return new ArrayList();
})).add(factoryImplementationName.trim());
}
}
}
result.replaceAll((factoryType, implementations) -> {
return (List)implementations.stream().distinct().collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
});
cache.put(classLoader, result);
return result;
} catch (IOException var14) {
IOException ex = var14;
throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", ex);
}
}
}
核心步骤1:拆解loadSpringFactories
核心实现,该方法主要是用于获取所有jar包中META-INF/spring.factories文件路径,以枚举值返回。
核心步骤2:遍历spring.factories文件路径,逐个加载解析,整合factoryClass类型的实现类名称,获取到实现类的全类名称后进行类的实例话操作,拆解instantiateFactory
方法实现,其相关源码如下,其原理是通过反射来进行对象的实例化
private static <T> T instantiateFactory(String factoryImplementationName, Class<T> factoryType, ClassLoader classLoader) {
try {
Class<?> factoryImplementationClass = ClassUtils.forName(factoryImplementationName, classLoader);
if (!factoryType.isAssignableFrom(factoryImplementationClass)) {
throw new IllegalArgumentException("Class [" + factoryImplementationName + "] is not assignable to factory type [" + factoryType.getName() + "]");
} else {
return ReflectionUtils.accessibleConstructor(factoryImplementationClass, new Class[0]).newInstance();
}
} catch (Throwable var4) {
Throwable ex = var4;
throw new IllegalArgumentException("Unable to instantiate factory class [" + factoryImplementationName + "] for factory type [" + factoryType.getName() + "]", ex);
}
}