1.什么是SPI
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。 SPI的作用就是为这些被扩展的API寻找服务实现。
JAVA SPI = 基于接口的编程+策略模式+配置文件 的动态加载机制。
2.SPI和API的使用场景
API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
(1)JDBC加载不同类型的数据库驱动 (2)日志门面接口实现类加载,SLF4J加载不同提供商的日志实现类 (3)Spring中大量使用了SPI。
3.SPI的简单实现
下面我们来简单实现一个jdk的SPI的简单实现。项目地址:https://github.com/hhhhhzj/java-spi-demo.git
首先第一步,定义一组接口:
package org.spi.demo
public interface IShout {
public void shout();
}
这个接口分别有两个实现:
package org.spi.demo.impl
public class Cat implements IShout {
public void shout() {
System.out.println("miaomiao");
}
}
package org.spi.demo.impl
public class Dog implements IShout {
public void shout() {
System.out.println("wangwang");
}
}
然后需要在resources目录下新建META-INF/services目录,并且在这个目录下新建一个与上述接口的全限定名一致的文件,在这个文件中写入接口的实现类的全限定名.
文件位置
resources
-META-INF
-services
-org.spi.demo.IShout
文件内容:
org.spi.demo.impl.Dog
org.spi.demo.impl.Cat
如果觉得写文件的方式比较麻烦,这里推荐引入google的autoService, 导入包和在接口类上添加@AutoService能够自动生成文件, 详细请看官方用例:https://github.com/google/auto/tree/master/service。
4. 原理解析
通过上面简单的demo,可以看到最关键的实现就是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) {
//..
}
//..
}
}
}
下面贴出比较直观的spi加载的主要流程供参考:
5. 总结
优点:
- 能够实现项目解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
缺点:
- 多个并发多线程使用ServiceLoader类的实例是不安全的
- 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。
参考:
[1]. https://www.cnblogs.com/lbys/p/14286874.html
[2]. https://github.com/google/auto/tree/master/service
[3]. https://www.zhihu.com/question/486985113