SPI - Service Provider Interface

ServiceLoader

ServiceLoader是一种加载类的规范,底层依赖于ClassLoader

  • 一个简单的服务提供商加载设施
  • 服务 是一个熟知的接口和类(通常为抽象类)集合。服务提供者 是服务的特定实现
  • 服务提供者可以以扩展的形式安装在 Java 平台的实现中.也就是将 jar 文件放入任意常用的扩展目录中
  • 也可通过将提供者加入应用程序类路径,或者通过其他某些特定于平台的方式使其可用(所以并不限定你的方式,不在类路径也无所谓哟)

服务提供者必须要提供一个无参的构造器用于类加载实例化

ServiceLoader的使用步骤

  1. 创建一个接口文件
  2. 在resources资源目录下创建META-INF/services文件夹
  3. 在上面services文件夹中创建文件:以接口全类名命名
  4. 在该文件内,写好实现类的全类名们

示例


// 一个服务
public interface IService {

    String sayHello();

    String getScheme();
}

// 一个服务提供者
public class HDFSServiceImpl implements IService {

    @Override
    public String sayHello() {
        return "Hello, HDFSServer";
    }

    @Override
    public String getScheme() {
        return "HDFS";
    }
}

// 另一个服务提供者
public class LocalServiceImpl implements IService {

    @Override
    public String sayHello() {
        return "Hello, LocalServer";
    }

    @Override
    public String getScheme() {
        return "Local";
    }
}

// 测试类
public class ServiceLoaderDemo {

    public static void main(String[] args) {
        final ServiceLoader<IService> serviceLoader = ServiceLoader.load(IService.class);
        serviceLoader.forEach(service -> System.out.println(service.getScheme() + "=" + service.sayHello()));
    }
}

准备META-INF/services/com.daniel.serviceloader.IService文件,并将对应的实现类写入

该文件中只能写实现类,不能写接口

运行上述代码,输出

HDFS=Hello, HDFSServer
Local=Hello, LocalServer

ServiceLoader实现了Iterable接口,可以遍历获取所有实现类

ServiceLoader是有缓存机制的,其策略为

  • ServiceLoader维护到目前为止已经加载的服务提供者缓存
  • 每次调用 iterator 方法返回一个迭代器,它首先按照实例化顺序生成缓存的所有元素
  • 然后以延迟方式查找和实例化所有剩余的服务提供者,并且依次将每个提供者添加到缓存
  • 若清除缓存,可调用ServiceLoader.reload()方法

ServletContainerInitializer

在web容器启动时为提供给第三方组件机会做一些初始化的工作,例如注册servlet或者filtes等,servlet规范中通过ServletContainerInitializer实现此功能。

使用方式同上,需要在resources目录下的META-INF/services中创建一个javax.servlet.ServletContainerInitializer文件,文件中指定其实现类。

Tomcat中的实现

在Tomcat启动时,会执行ContextConfig,它实现了LifeCycleListener,执行ContexConfig#processServletContainerInitializers方法来加载所有的ServletContainerInitializer的实现类

Tomcat调用SCI的时机

  • 解析Web.xml
  • <context-param>参数注入到ServletContext实例中
  • 回调ServletContainerInitializer实现类
  • 触发Listener事件(只会触发ServletContextListener类型的时间),beforeContextInitializedafterContextInitialized
  • 初始化Filter,调用其init方法
  • 加载Servlet

Spring中的实现

SpringFactoriesLoader,是Spring中的SPI实现,支持key-value形式的配置文件,如com.daniel.serviceloader.IService=com.daniel.serviceloader.impl.ServiceImpl,com.daniel.serviceloader.impl.ServiceImpl2

该类暴露了两个方法loadFactoriesloadFactoriesNames,其中loadFactories依赖loadFactoriesNames获取类的全限定名,之后由loadFactories进行实例化

此处特别注意:loadFactories实例化完成所有实例后,会调用AnnotationAwareOrderComparator.sort(result)排序,所以它是支持Ordered接口排序的

SPI能够解决什么问题?

在我们设计一套API供别人调用的时候,如果同一个功能的要求特别多,或者同一个接口要面对很复杂的业务场景的时候,就可以用到SPI