面试回答
Java 中区分 API 和 SPI,通俗的讲:API 和 SPI 都是相对的概念,他们的差别只在语义上,API 直接被应用开发人员使用,SPI 被框架扩展人员使用。
API Application Programming Interface
大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现。
SPI Service Provider Interface
而如果是调用方来制定接口,实现方来针对接口实现不同的实现。调用方来选择自己需要的实现方。
知识扩展
如何定义一个 SPI
步骤1、定义一组接口(假设是 com.chiyi.test.IShout
),并写出接口的一个或多个实现,(假设是 com.chiyi.test.Dog
、com.chiyi.test.Cat
)。
public interface IShout {
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
目录,新增一个以接口命名的文件(com.chiyi.test.IShout
文件),内容是要应用的实现类(这里是 com.chiyi.test.Dog
和com.chiyi.test.Cat
,每行一个类)。
com.chiyi.test.Dog
com.chiyi.test.Cat
步骤3、使用 ServiceLoader 来加载配置文件中指定的实现。
public class Main {
public static void main(String[] args) {
ServiceLoader<IShout> shouts=ServiceLoader.load(IShout.class);
for(IShout s:shouts){
s.shout();
}
}
}
代码输出:
wang wang
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;
// 用于定位,加载和实例化 providers 的类加载器
private final ClassLoader loader;
// 创建 ServiceLoader 时采用的访问控制上下文
private final AccessControlContext acc;
// 缓存 providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private LazyIterator lookupIterator;
······
}
参考具体源码,梳理了一下,实现的流程如下:
- 应用程序调用
ServiceLoader.load
方法,ServiceLoader.load
方法内先创建一个新的ServiceLoader
,并实例化该类中的成员变量,包括:
-
- loader(ClassLoader 类型,类加载器)
- acc(AccessControlContext 类型,访问控制器)
- providers(LinkedHashMap 类型,用于缓存加载成功的类)
- lookupIterator(实现迭代器功能)
- 应用程序通过迭代器接口获取对象实例
-
- ServiceLoader 先判断成员变量 providers 对象中(LinkedHashMap 类型)是否有缓存实例对象,如果有缓存,直接返回。
- 如果没有缓存,执行类的装载:
-
-
- 读取 META-INF/services/ 下的配置文件,获得所有能被实例化的类的名称
- 通过反射方法 Class.forName() 加载类对象,并用 instance() 方法将类实例化
- 把实例化的类缓存到 providers 对象中(LinkedHashMap 类型)
- 然后返回实例对象
-
SPI 的应用场景
概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略。
比如常见的例子:
- 数据库驱动加载接口实现类的加载
- JDBC 加载不同类型数据库的驱动
- 日志门面接口实现类加载
- SLF4J 加载不同提供商的日志实现类
Spring
Spring 中大量使用了 SPI,比如:对 servlet3.0 规范对 ServletContainerInitializer 的实现、自动类型转换 Type Conversion SPI(Converter SPI、Formatter SPI)等
Dubbo
Dubbo 中也大量使用 SPI的方式实现框架的扩展,不过它对 java 提供的原生 SPI 做了封装,允许用户扩展实现 Filter 接口。