Java的SPI机制

SPI是什么

我们在面向对象的设计中,一般推荐模块之间基于接口进行编程,模块之间不通过直接依赖实现类进行硬编码。在调用方与实现方中间,嵌入一层接口类,符合可插拔的原则。
SPI全称为Service Provider Interface,直译过来就是服务提供接口,是Java提供的一套用来方便于第三方扩展实现的一种“API”,它可以用来实现插件化扩展功能。简单的理解,就是SPI是Java提供的一套原生的“寻找服务实现”的机制。

SPI和API的区别是啥

API与SPI.jpg

API(Application Programing Interface),是为调用方提供服务。而SPI强调的是为第三方扩展功能的一种方法。简而言之,API强调接口给调用方提供服务,SPI强调接口给实现方留下的扩展方法。API是接口对外部客户端交互的窗口,SPI是内部给第三方留下的扩展后门。

那么如何区分一个接口,究竟是API还是SPI呢,其实上图已经很清晰的描述了。如果接口类和调用方在同一侧(同属于一个模块),那么这个接口就是SPI,因为实现类不在当前模块中,也就是说当前模块里定义的接口是需要第三方扩展实现的。如果接口类和实现方在同一侧,那么这个接口就称为API。

SPI示例讲解

直接来看一个简单的DEMO

1.定义一个SPI接口

/**
* @author Herman on 2020/12/1
*/
public interface DemoSpiService {
void test();
}

2.编写接口实现类


/**
* @author Herman on 2020/12/1
*/
public class SpiServiceImpl implements DemoSpiService {
@Override
public void test() {
System.out.println("[SPI]调用方法-SpiServiceImpl");
}
}


/**
* @author Herman on 2020/12/1
*/
public class SpiServiceTwoImpl implements DemoSpiService {
@Override
public void test() {
System.out.println("[SPI]调用方法-SpiServiceTwoImpl");
}
}

3.在工程下resource目录下,创建”META-INF”文件夹,以及”services”文件夹

目录.jpg

4.创建一个SPI接口的全路径名称的文件。比如我上面给出的接口,在我们工程中的路径为com.lhh.study.spi.service.DemoSpiService
则我对应需要创建这样一个文件。如下图示

文件路径.jpg

5.修改文件内容,把需要“被发现”的具体实现类写上去

如何发现实现者.jpg

6.启动运行

/**
* @author Herman on 2020/12/1
*/
public class DemoMain {
public static void main(String[] args) {
ServiceLoader<DemoSpiService> loader=ServiceLoader.load(DemoSpiService.class);
for (DemoSpiService spiService : loader) {
spiService.test();
}
}
}

输出结果如下

“C:\Program Files\Java\jdk1.8.0_221\bin\java.exe”…
[SPI]调用方法-SpiServiceImpl
[SPI]调用方法-SpiServiceTwoImpl

Process finished with exit code 0

刚才才说接口类和实现类在一侧,那这里应该是API才对啊!话是这么说,其实我这里只是为了演示一下Java的SPI整个流程。
严格来说,应该把实现类独立成一个模块去实现才对,否则怎么能叫第三方扩展呢!

现在来举一个,严格意义上的SPI示例。比如我们经常使用的“mysql-connector-java”。
我们可以在Maven工程下的External Libraries下找到它

mysql的demo.jpg

它扩展的方法是java.sql.Driver。打开源码,可以知道这个Driver就是一个典型的SPI!它是Java自身定义的留给第三方“数据库”厂商去实现的一个接口类,jdk本身不做数据库连接相关的实现,而是提供一套接口规范/发现机制给第三方,从而实现了插拔式扩展。

java.sql.DriverManager的加载过程:

public class DriverManager {

//省略代码...

/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}

private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction<String>() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
// If the driver is packaged as a Service Provider, load it.
// Get all the drivers through the classloader
// exposed as a java.sql.Driver.class service.
// ServiceLoader.load() replaces the sun.misc.Providers()

AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
//spi机制加载第三方扩展插件
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator = loadedDrivers.iterator();

/* Load these drivers, so that they can be instantiated.
* It may be the case that the driver class may not be there
* i.e. there may be a packaged driver with the service class
* as implementation of java.sql.Driver but the actual class
* may be missing. In that case a java.util.ServiceConfigurationError
* will be thrown at runtime by the VM trying to locate
* and load the service.
*
* Adding a try catch block to catch those runtime errors
* if driver not available in classpath but it's
* packaged as service and that service is there in classpath.
*/
try{
while(driversIterator.hasNext()) {
//上面的英文注释已经描述清楚了,这步骤实际上是一个隐式初始化的过程,目的是为了保证创建Driver实例之前,,类加载器执行了隐式初始化
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
//省略代码...
}
ServiceLoader源码解析

来,开始从源码上去理解整个过程的关键点!
1.创建ServiceLoader,调用ServiceLoader.load静态方法

 public static <S> ServiceLoader<S> load(Class<S> service) {
//获取当前线程的上下文中的ClassLoader
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);
}

//私有构造器
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();
}

// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

public void reload() {
//重置的是会将已有的服务提供者进行清空,理解成初始化行为即可
providers.clear();
//创建迭代器-懒加载迭代(按需载入)
lookupIterator = new LazyIterator(service, loader);
}

2.ServiceLoader中的懒加载迭代器,源码分析

//ServiceLoader 实现了Iterable接口,可以看出ServiceLoader有一个自己的内部迭代器
public final class ServiceLoader<S>
implements Iterable<S>
{
// Cached providers, in instantiation order。用于记录已经被发现的服务提供者
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

//实现Iterable接口方法,在匿名类中实现迭代器的hashNext(),next()方法
public Iterator<S> iterator() {
return new Iterator<S>() {
//优先从这个迭代器中查询已经发现的服务提供者。
Iterator<Map.Entry<String,S>> knownProviders
= providers.entrySet().iterator();

public boolean hasNext() {
if (knownProviders.hasNext())
//若已经发现过该服务则直接返回true
return true;
//首次发现则需要通过懒加载迭代器进行按需载入
return lookupIterator.hasNext();
}

public S next() {
//尝试优先从已发现的服务列表中的迭代器获取
if (knownProviders.hasNext())
return knownProviders.next().getValue();
//首次发现,则通过懒加载迭代器按需取用
return lookupIterator.next();
}

public void remove() {
throw new UnsupportedOperationException();
}

};
}

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

// The current lazy-lookup iterator
private LazyIterator lookupIterator;

//懒加载迭代器
private class LazyIterator
implements Iterator<S>
{

Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
Iterator<String> pending = null;
String nextName = null;

private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}

private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//配置文件:META-INF/services/服务接口的全限定名,这就是为什么SPI要求固定路径的原因
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循环中读入并解析文件中的信息,并要求文件是UTF-8编码,配置信息读入之后获得一个载有全部信息的迭代器pending
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
//通过pending迭代器获得当前类全限定名称
nextName = pending.next();
return true;
}

private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//上一个步骤hasNextService中获取的nextName,通过反射创建类实例
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
//转换成具体子类,并计入缓存(已发现服务提供者集合,providers)
S p = service.cast(c.newInstance());//c.newInstance()意味着服务提供类需要有一个无参构造器
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}

public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<Boolean>() {
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}

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);
}
}

public void remove() {
throw new UnsupportedOperationException();
}

}
}

//parse方法
private Iterator<String> parse(Class<?> service, URL u)
throws ServiceConfigurationError
{
InputStream in = null;
BufferedReader r = null;
ArrayList<String> names = new ArrayList<>();
try {
in = u.openStream();
//此处要求文件必须是UTF-8
r = new BufferedReader(new InputStreamReader(in, "utf-8"));
int lc = 1;
while ((lc = parseLine(service, u, r, lc, names)) >= 0);
} catch (IOException x) {
fail(service, "Error reading configuration file", x);
} finally {
try {
if (r != null) r.close();
if (in != null) in.close();
} catch (IOException y) {
fail(service, "Error closing configuration file", y);
}
}
return names.iterator();
}
SPI的使用约定规范

通过对上述源代码的分析,我们可以知道SPI机制中几个重要的约定:
1.固定的文件夹(/META-INF/services),详见:private static final String PREFIX = “META-INF/services/“;
2.需要SPI接口全限定名称的文件,且要求文件编码UTF-8,详见:parse方法中的r = new BufferedReader(new InputStreamReader(in, “utf-8”));
3.S p = service.cast(c.newInstance());

我学到了什么

通过对ServiceLoader的一番通读,我总结了几个要点。
1.ServiceLoader实现了Iertable接口,内部实现了一个懒加载迭代器(懒加载体现在,按需载入,仅仅首次发现的时候去通过反射获取类实例)。后续再次使用服务的时候,将通过已发现的服务集合providers中去寻找可用服务
2.ServiceLoader使用LinkedHashMap缓存创建的服务类实例,在后续迭代的时候如果需要找到服务实例,可以通过get方法快速获取
3.ServiceLoader的一个缺点就是会对所有实现类全部加载并进行一次实例化,如果我只是想使用其中某些服务则造成了浪费。即获取方式不灵活!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值