面试回答

Java 中区分 API 和 SPI,通俗的讲:API 和 SPI 都是相对的概念,他们的差别只在语义上,API 直接被应用开发人员使用,SPI 被框架扩展人员使用。

API Application Programming Interface

大多数情况下,都是实现方来制定接口并完成对接口的不同实现,调用方仅仅依赖却无权选择不同实现。

SPI Service Provider Interface

而如果是调用方来制定接口,实现方来针对接口实现不同的实现。调用方来选择自己需要的实现方。

知识扩展

如何定义一个 SPI

步骤1、定义一组接口(假设是 com.chiyi.test.IShout),并写出接口的一个或多个实现,(假设是 com.chiyi.test.Dogcom.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.Dogcom.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;
	······
}

参考具体源码,梳理了一下,实现的流程如下:

  1. 应用程序调用 ServiceLoader.load 方法,ServiceLoader.load方法内先创建一个新的 ServiceLoader,并实例化该类中的成员变量,包括:
    1. loader(ClassLoader 类型,类加载器)
    2. acc(AccessControlContext 类型,访问控制器)
    3. providers(LinkedHashMap 类型,用于缓存加载成功的类)
    4. lookupIterator(实现迭代器功能)
  1. 应用程序通过迭代器接口获取对象实例
    1. ServiceLoader 先判断成员变量 providers 对象中(LinkedHashMap 类型)是否有缓存实例对象,如果有缓存,直接返回。
    2. 如果没有缓存,执行类的装载:
      1. 读取 META-INF/services/ 下的配置文件,获得所有能被实例化的类的名称
      2. 通过反射方法 Class.forName() 加载类对象,并用 instance() 方法将类实例化
      3. 把实例化的类缓存到 providers 对象中(LinkedHashMap 类型)
      4. 然后返回实例对象

SPI 的应用场景

概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略。

比如常见的例子:

  1. 数据库驱动加载接口实现类的加载
  2. JDBC 加载不同类型数据库的驱动
  3. 日志门面接口实现类加载
  4. SLF4J 加载不同提供商的日志实现类

Spring

Spring 中大量使用了 SPI,比如:对 servlet3.0 规范对 ServletContainerInitializer 的实现、自动类型转换 Type Conversion SPI(Converter SPI、Formatter SPI)等

Dubbo

Dubbo 中也大量使用 SPI的方式实现框架的扩展,不过它对 java 提供的原生 SPI 做了封装,允许用户扩展实现 Filter 接口。

08-19 05:30