浅析Java-SPI机制
Posted 风在哪
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了浅析Java-SPI机制相关的知识,希望对你有一定的参考价值。
Java SPI机制
SPI全称为Service Provider Interface(服务提供接口),是JDK内置的一种服务发现机制,是一种将服务接口与具体实现分离以达到解耦,大大提升了程序可扩展性的机制,引入服务提供者就是引入了spi接口的实现者,通过本地的注册发现获取到具体的实现类。
Java SPI实际上就是"基于接口的编程+策略模式+配置文件"组合实现的动态加载机制。
系统设计之初为了各个功能模块之间解耦,一般都是基于接口编程,模块之间不对实现类进行硬编码,一旦代码涉及到具体实现类的耦合,就违反了可插拔、开闭等原则,如果我们希望实现在模块装配的时候能够不在程序编码指定,那就需要一种服务发现机制。Java SPI就是这样一种机制:为某个接口寻找服务实现的机制。SPI的核心思想就是解耦。
这些 SPI 的接口由 Java 核心库来提供(由启动类加载器Bootstrap Classloader负责加载),而这些 SPI 的实现代码则是作为 Java 应用所依赖的 jar 包被包含进类路径(CLASSPATH)里。SPI接口中的代码经常需要加载具体的实现类。那么问题来了,SPI的接口是Java核心库的一部分,是由**启动类加载器(Bootstrap Classloader)来加载的;SPI的实现类是由系统类加载器(System ClassLoader)**来加载的。启动类加载器是无法找到 SPI 的实现类的,因为依照双亲委派模型,BootstrapClassloader无法委派AppClassLoader来加载类。于是加载SPI实现类的重任就落到了线程上下文类加载器(破坏了“双亲委派模型”,可以在执行线程中抛弃双亲委派加载链模式,使程序可以逆向使用类加载器)的身上。
那么Java SPI具体如何使用呢:
- 首先定义一组接口
- 写出接口的一个或者多个实现类
- 在src/main/resources/下新建/META-INF/services目录,并且新增一个以接口的全限定类名命名的文件,文件的内容就是接口的各个实现类的全限定类名
- 通过ServiceLoader加载接口的具体实现类
这就是JDK为我们实现的SPI机制,总体来说就是在配置文件中定义好接口的实现类,然后根据接口从配置文件中加载该接口的所有实现类,供我们的程序使用。
首先来看一个SPI的实际例子。
SPI例子
创建一个简单的maven项目,本例子是在maven项目中实现的
首先定义一组接口,这里定义了一个名为SPIService的接口:
public interface SPIService {
/**
* 方法执行
*/
void execute();
}
然后我们提供这个接口的两个实现类:
public class SpiServiceOne implements SPIService{
@Override
public void execute() {
System.out.println("SpiServiceOne is executed!");
}
}
public class SpiServiceTwo implements SPIService{
@Override
public void execute() {
System.out.println("SpiServiceTwo is executed!");
}
}
通过SPI的机制我们如何获得这两个实现类呢?
首先,我们需要在resources文件夹下添加一个META-INF/services目录,在改目录下添加以SPIService接口的全限定类名为名的文件,在该文件中添加该接口的实现类的全限定类名,具体实现如下:
接下来就是测试用例,来看看JDK的SPI机制如何使用:
import java.util.Iterator;
import java.util.ServiceLoader;
public class Test {
public static void main(String[] args) {
ServiceLoader<SPIService> serviceLoader = ServiceLoader.load(SPIService.class);
Iterator<SPIService> iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
SPIService next = iterator.next();
next.execute();
}
}
}
运行结果如下所示:
原理分析
假如让我们自己实现SPI机制的话,我们会如何实现呢?这里提供一个简单的思路:
- 首先通过指定的文件加载出所有的类名
- 然后通过Java的反射机制构造出这些对象
Java的SPI机制也是类似的思路,它是通过接口的全限定类名作为了文件名,并且指定了文件所处的位置:/resources/META-INF/services,然后加载文件中接口对应的实现类的全限定类名,通过反射机制加载这些类。
那么JDK的SPI机制具体是怎么实现的呢,来看看它的源码。
简单源码分析
了解了SPI的简单使用,可以来看看JDK是如何实现的。
我们首先通过ServiceLoader的静态方法load来获取了ServiceLoader类,那么就从这个方法入手,来看看它的具体实现:
@CallerSensitive
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return new ServiceLoader<>(Reflection.getCallerClass(), service, cl);
}
该方法首先获得了线程上下文类加载器,然后调用了ServiceLoader的构造函数,其中Reflection.getCallerClass()方法是为了获得load方法的调用者,在本例中就是Test对象,可以在调试过程中发现:
然后就会调用ServiceLoader的私有构造函数ServiceLoader(Class<?> caller, Class< S> svc, ClassLoader cl),构造出ServiceLoader对象并返回给调用者。
接下来看看serviceLoader.iterator()方法是如何实现的:
public Iterator<S> iterator() {
// 如果查找服务的迭代器为空,那就新建一个
if (lookupIterator1 == null) {
lookupIterator1 = newLookupIterator();
}
// 创建一个迭代器
return new Iterator<S>() {
// 记录ServiceLoader的加载次数
final int expectedReloadCount = ServiceLoader.this.reloadCount;
// index into the cached providers list
int index;
/**
* 如果缓存的服务提供者因为重新加载而被清空,那就抛出ConcurrentModificationException异常
* 通过比较两者的reloadCount值即可
*/
private void checkReloadCount() {
if (ServiceLoader.this.reloadCount != expectedReloadCount)
throw new ConcurrentModificationException();
}
@Override
public boolean hasNext() {
// 首先判断服务提供者是否已经加载完成,且缓存没有被清空
checkReloadCount();
// 如果下标值小于服务提供者的数量,那么直接返回true
if (index < instantiatedProviders.size())
return true;
// 否则调用lookupIterator1判断是否包含下一个元素
return lookupIterator1.hasNext();
}
// 获得下一个元素
@Override
public S next() {
// 首先判断加载的情况
checkReloadCount();
S next;
/*
instantiatedProviders用于缓存已经加载过的接口实现类
所以我们先从这个缓存中获取资源
如果缓存中没有,再调用迭代器重新进行加载,并将加载后的接口实现类存入缓存中
*/
if (index < instantiatedProviders.size()) {
next = instantiatedProviders.get(index);
} else {
// get()方法真正对接口实现类实例化的地方
next = lookupIterator1.next().get();
instantiatedProviders.add(next);
}
// 下标递增
index++;
return next;
}
};
}
这里采用了一种懒加载的方式,当我们调用iterator.hasNext()方法判断是否存在下一个元素时,ServiceLoader才会去文件中获取全限定类名,并且进行加载,看看调试的调用堆栈:
那么直接来看看nextProviderClass方法:
private Class<?> nextProviderClass() {
if (configs == null) {
try {
// fullName = META-INF/services/com.wyg.spi.SPIService
String fullName = PREFIX + service.getName();
// 如果类加载器为空的话,就调用ClassLoader加载资源
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
// 如果当前类加载器为platformClassLoader,也就是平台类加载器,那就调用启动类加载器加载资源
// 如果类加载器不为空,并且拥有当前的ClassPath,那就调用启动类加载器加载资源
if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
// 如果类加载器即不为空,也不是平台类加载器,就调用它加载资源
// 其实从load()方法我们可以知道,这里的loader类加载器其实是线程上下文类加载器,所以会走到这里
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 解析获取到的资源,其实就是我们的接口全限定类名文件,获取它指定的实现类的全限定类名
// 每次只会获取一个类,也就是说一次只加载一个类,也就是懒加载
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return null;
}
pending = parse(configs.nextElement());
}
// 根据这个全限定类名通过反射加载类
String cn = pending.next();
try {
return Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
return null;
}
}
当我们调用iterator.next()方法时,ServiceLoader才会创造类的实例,也就是调用newInstance()方法:
newInstance()方法是真正产生实现类实例的地方:
private S newInstance() {
S p = null;
Throwable exc = null;
// 如果访问控制为空的话,可以直接对类进行实例化
if (acc == null) {
try {
// 通过接口实现类的构造函数创建新的实例
p = ctor.newInstance();
} catch (Throwable x) {
exc = x;
}
} else {
/*
如果存在访问控制的话,使用PrivilegedExceptionAction相当于有了访问构造函数的特权
通过AccessController.doPrivileged可以在有访问控制的情况下进行对象实例的创建
*/
PrivilegedExceptionAction<S> pa = new PrivilegedExceptionAction<>() {
@Override
public S run() throws Exception {
return ctor.newInstance();
}
};
// invoke constructor with permissions restricted by acc
try {
p = AccessController.doPrivileged(pa, acc);
} catch (Throwable x) {
if (x instanceof PrivilegedActionException)
x = x.getCause();
exc = x;
}
}
// 如果异常不为空,会调用fail方法,返回失败
if (exc != null) {
if (exc instanceof InvocationTargetException)
exc = exc.getCause();
String cn = ctor.getDeclaringClass().getName();
fail(service,
"Provider " + cn + " could not be instantiated", exc);
}
return p;
}
通过上述一系列的函数调用,就可以获得对应的实例了。
实际案例分析
JDBC
在JDBC中就使用到了SPI机制:
我们在使用JDBC时,无需再自己指定驱动,JDBC会自动为我们寻找相关的驱动。
也就是我们在使用JDBC时,即使不写Class.forName(driver),也不会报错,SPI机制会自动帮助我们寻找相关的驱动实现。
Spring Boot
了解Spring Boot的读者应该知道,Spring Boot也有类似的SPI机制,Spring Boot的自动装配机制,就是通过加载META-INF/spring.factories文件中的内容,将对应的类实例化出来,放到Spring IoC容器中,供应用程序使用。
总结
JDK提供的SPI机制还是有一定缺陷的:
- 它不能按需加载,需要遍历所有的实现,并进行实例化,我们只能在循环中找到自己需要的实现,而且在找到目标类之前,会加载其他的类,造成一定的资源浪费
- 只能通过Iterator形式获取某个类的具体实现,不够灵活
如果想要扩展JDK的SPI机制,可以看看Dubbo是如何做的,这个我目前还没有研究,以后研究了再和大家分享!
以上是关于浅析Java-SPI机制的主要内容,如果未能解决你的问题,请参考以下文章