JAVA SPI详解
SPI是什么?
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
使用场景
- 数据库驱动加载接口实现类的加载
JDBC加载不同类型数据库的驱动
- 日志门面接口实现类加载
SLF4J加载不同提供商的日志实现类
- Spring
Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
- Dubbo
Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
使用介绍
要使用Java SPI,需要遵循如下约定:
1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
2、接口实现类所在的jar包放在主程序的classpath中;
3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
4、SPI的实现类必须携带一个不带参数的构造方法;
说明
jar包中的META-INF文件有什么作用?
META-INF文件夹存在的话,是用来存储包和扩展的配置数据,包含安全,版本,扩展和服务。
1、MANIFEST.MF
MANIFEST文件用来定义扩展和包相关的数据。
2、INDEX.LIST
如果使用了jar工具的 "-i"选项,这个文件就会自动生成.文件包含了路径信息和应用或扩展的包定义.它是部分jar索 引的实现方式,可用来提高类加载器的加载速度。
3、x.SF
jar包的签名文件,包含清单信息,SF表示signature file, "x" 是文件名。
4、x.DSA
DSA是一种非对称的数字签名算法.可简单理解为"私钥加密生成数字签名,公钥验证数据及签名", x.DSA 是"x.SF"文件关联的同名的"签名块文件",里面存着x.SF的数字签名. SF签名文件和DSA签名块文件可用"jarsigner"命令 生成.其实还支持RSA算法,对应的是.RSA的 后缀名。
5、services/
目录文件,存放 服务提供者 的配置文件。
6、平常还有一种情况,通过maven打包的话,在META-INF 下面默认还会包含maven目录,maven目录下会含有pom相关的配置信息。
示例代码请见:https://download.****.net/download/baidu_28370189/12665506
示例目录
示例代码
步骤1、定义一组接口 (假设是spi.com.spi.IShout),并写出接口的一个或多个实现,(假设是spi.com.spi.Dog、spi.com.spi.Cat)
public interface IShout {
public abstract void shout();
}
public class Dog implements IShout {
@Override
public void shout() {
System.out.println("wang wang");
}
}
public class Cat implements IShout {
@Override
public void shout() {
System.out.println("miao miao");
}
}
步骤2、
在 src/main/resources/ 下建立 /META-INF/services 目录, 新增一个以接口命名的文件 (spi.com.spi.IShout文件),内容是要应用的实现类(这里是spi.com.spi.Dog和spi.com.spi.Cat,每行一个类)
步骤3、使用 ServiceLoader 来加载配置文件中指定的实现
public class SpiMain {
public static void main(String[] args) {
ServiceLoader<IShout> serviceLoader = ServiceLoader.load(IShout.class);
for (IShout shout : serviceLoader) {
shout.shout();
}
}
}
输出:
wang wang
ji ji
miao miao
SPI原理解析
通过上面的代码可以知道最关键的实现就是ServiceLoader这个类,上源码(仅关键代码)
public final class ServiceLoader<S> implements Iterable<S> {
//扫描目录前缀
private static final String PREFIX = "META-INF/services/";
// 被加载的类或接口
private final Class<S> service;
// 用于定位、加载和实例化实现方实现的类的类加载器
private final ClassLoader loader;
// 上下文对象
private final AccessControlContext acc;
// 按照实例化的顺序缓存已经实例化的类
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private java.util.ServiceLoader.LazyIterator lookupIterator;
// 私有内部类,提供对所有的service的类的加载与实例化
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;
//...
private boolean hasNextService() {
if (configs == null) {
try {
//获取目录下所有的类
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
//...
}
//....
}
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射加载类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}
try {
//实例化
S p = service.cast(c.newInstance());
//放进缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
//..
}
//..
}
}
}
实现流程如下:
1、应用程序调用ServiceLoader.load方法。
ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:
-
loader(ClassLoader类型,类加载器)
-
acc(AccessControlContext类型,访问控制器)
-
providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
-
lookupIterator(实现迭代器功能)
2、应用程序通过迭代器接口获取对象实例
ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。
如果没有缓存,执行类的装载,实现如下:
(1)、读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,值得注意的是,ServiceLoader可以跨越jar包获取META-INF下的配置文件,具体加载配置的实现代码如下:
try {
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);
}
(2)、通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
(3)、把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型)
然后返回实例对象。
总结
优点:
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
相比使用提供接口jar包,供第三方服务模块实现接口的方式,SPI的方式使得源框架,不必关心接口的实现类的路径,可以不用通过下面的方式获取接口实现类:
- 代码硬编码import 导入实现类 。
-
指定类全路径反射获取:例如在JDBC4.0之前,JDBC中获取数据库驱动类需要通过Class.forName("com.mysql.jdbc.Driver"),类似语句先动态加载数据库相关的驱动,然后再进行获取连接等的操作。
-
第三方服务模块把接口实现类实例注册到指定地方,源框架从该处访问实例。
通过SPI的方式,第三方服务模块实现接口后,在第三方的项目代码的META-INF/services目录下的配置文件指定实现类的全路径名,源码框架即可找到实现类 。
缺点:
- 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
-
多个并发多线程使用ServiceLoader类的实例是不安全的。
上一篇: 关于kswapd0 CPU占用率高的问题
下一篇: windows环境下运行java的脚本