作者:zen
API与SPI
API (Application Programming Interface)是我们平常最常用的方式,由服务提供方来制定接口并完成对接口的实现,调用方仅仅依赖接口调用,具体的实现完全由服务提供方决定。
SPI (Service Provider Interface)是调用方来制定接口规范,而由外部来完成具体实现,调用方在调用时则选择自己需要的外部实现。
总体来说,Api是接口和实现都有服务提供者定义,而Spi则是调用方提供接口,服务提供者提供实现,调用方在调用的时候根据特定条件,选择自己需要的实现类。
ServiceLoader类加载说明
ServiceLoader是实现SPI一个重要的类。是在java1.6引入的类,为了解决接口与实现分离的场景。在资源目录META-INF/services中放置提供者配置文件,文件名以接口的类名命名,里面的内容为需要加载的实现类。然后在运行运行时,遇到Serviceloader.load(XxxInterface.class)时,会到META-INF/services的配置文件中寻找这个XxxInterface接口,全路径名命名的文件,文件中,定义类该接口具体的实现类,然后使用Class.forName()(传入设定的类加载器)完成类的加载。
ServiceLoader实现原理
以下是ServiceLoader实现Spi的主要代码
参考具体ServiceLoader具体源码,梳理了一下,实现的流程如下:
-
应用程序调用ServiceLoader.load方法
ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,代码如下this.service = svc; this.serviceName = svc.getName(); this.layer = null; this.loader = cl; this.acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
说明:
- service(load的接口)
- loader(ClassLoader类型,类加载器)
- acc(AccessControlContext类型,访问控制器)
- providers(LinkedHashMap
类型,用于缓存加载成功的类) - lookupIterator(实现迭代器功能)
应用程序通过迭代器接口获取对象实例
ServiceLoader先判断成员变量providers对象中(LinkedHashMap类型)是否有缓存实例对象,如果有缓存,直接返回。
@Override
public S next() {
checkReloadCount();
S next;
if (index < instantiatedProviders.size()) {
next = instantiatedProviders.get(index);
} else {
next = lookupIterator1.next().get();
instantiatedProviders.add(next);
}
index++;
return next;
}
如果没有缓存,执行类的装载,实现如下:
static final String PREFIX = "META-INF/services/";
/**
* Loads and returns the next provider class.
*/
private Class> nextProviderClass() {
if (configs == null) {
try {
// 生成Spi配置文件路径
String fullName = PREFIX + service.getName();
// 通过类加载器加载该配置文件
if (loader == null) {
configs = ClassLoader.getSystemResources(fullName);
} else if (loader == ClassLoaders.platformClassLoader()) {
if (BootLoader.hasClassPath()) {
configs = BootLoader.findResources(fullName);
} else {
configs = Collections.emptyEnumeration();
}
} else {
configs = loader.getResources(fullName);
}
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
// 解析配置文件
pending = parse(configs.nextElement());
}
String cn = pending.next();
try {
// 通过Class.forName的方式加载实现类
return Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service, "Provider " + cn + " not found");
return null;
}
}
说明如下:
- (1) 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件
- (2) 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
- (3) 把实例化后的类缓存到providers对象中,(LinkedHashMap
类型)然后返回实例对象。
Spi应用示例
我们现在手动完成一个Spi案例,来了解一下Spi如何应用,我们案例通过定义一个文件传输接口,然后实现类通过不同的协议来完成文件的传输
定义接口File:
package com.zen.spi;
public interface File {
void read();
}
以下是两个实现类:
package com.zen.spi;
public class HttpFile implements File {
@Override
public void read() {
System.out.println("read file by http");
}
}
package com.zen.spi;
public class FtfFile implements File {
@Override
public void read() {
System.out.println("read file by ftp");
}
}
接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:com.zen.spi.File,里面加上我们需要用到的实现类
META-INF/services/com.zen.spi.File
com.zen.spi.HttpFile
com.zen.spi.FtpFile
然后写一个测试方法
package com.zen.spi;
import java.util.Iterator;
import java.util.ServiceLoader;
public class SpiTest {
public static void main(String[] args) {
ServiceLoader loads = ServiceLoader.load(File.class);
Iterator iterator = loads.iterator();
while (iterator.hasNext()){
File next = iterator.next();
next.read();
}
}
}
输出结果:
read file by http
read file by ftp
Spi在Jdbc中的应用
JDBC4.0之后,JDBC中获取Mysql数据库驱动类不再需要通过Class.forName(“com.mysql.jdbc.Driver”)加载数据库相关的驱动,而是可以通过Spi来直接实现。
我们打开mysql-connector-java.jar包看下:
java.sql.Driver内容如下:
com.mysql.jdbc.Driver
com.mysql.fabric.jdbc.FabricMySQLDriver
定义了Driver的的实现类
我们在获取数据库连接的代码如下:
String url="jdbc:mysql://localhost:3306/spi";
Connection root = DriverManager.getConnection(url, "root", "root");
我们跟到源码中发现有如下代码:
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
// 通过ServiceLoader来加载Driver的实现类
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
try {
while (driversIterator.hasNext()) {
driversIterator.next();
}
} catch (Throwable t) {
// Do nothing
}
return null;
}
});
内部实现也是通过通过ServiceLoader来加载Driver的实现类的方式来实现,具体实现的流程已经在ServiceLoader实现原理中看到了。
总结
优点:
- 使用Java SPI机制的优势是实现解耦,在服务调用方定义好业务接口,而具体的逻辑实现则由服务提供方完成。使用者不用手动引入实现类,也不需要关系实现类的具体位置。提供者只需要按照Spi规范定义好具体实现即可。
缺点:
- 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。