SPI

SPI:Service Provider Interface,是 JDK 内置的一种服务提供机制。许多开发框架都使用了 Java 的 SPI 机制,如 java.sql.Driver 的 SPI 实现(mysql 驱动、oracle 驱动等)、common-logging 的日志接口实现、dubbo 的扩展实现等等。

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。在实际过程中,API 的实现是封装在 jar 包中的,所以当需要更换一种实现时,要生成新的 jar 包来替换以前的类。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java spi 就是提供这样的一个机制:为某个接口寻找服务实现的机制。通过它就可以实现,不修改原来 jar 的情况下,为 API 新增一种实现。这有点类似 IoC 的思想,将装配的控制权移到了程序之外。

SPI 缺陷

ServiceLoader 缺少一些有用的特性:

  • 缺少实例的维护 ServiceLoader 每次 load 后,都会生成一份实例,也就是我们理解的 prototype;
  • 无法获取指定的实例,在 Spring 中可以通过 beanFactory.getBean("id") 获取一个实例,但是 ServiceLoader 不支持,只能一次获取所有的接口实例;
  • 不支持排序 ServiceLoader 返回的接口实例没有进行排序,随着新的实例加入,会出现排序不稳定的情况;
  • 无法获的所有实现的类型 无法通过接口获取所有的接口实例类型;
  • 作用域缺失,没有定义 singleton 和 prototype 的定义,不利于用户进行自由定制。

HSF SPI

HSF 用自己的方式重新实现了一套 SPI 机制,不使用 Java 原生的 SPI 机制。

  • @Name:用于标注一个实现,相当于给这个拓展实现起了个别名。
  • @Order:当一个接口类有多个拓展时,拓展的先后顺序能够通过该注解进行定义,例如,用于责任链的构造顺序。
  • @Scope:用来描述一个扩展是否多实例,默认为单例,如果使用多例,则会在线程请求时,才创建并存放于 ThreadLocal 当中。
  • @Tag:用来描述一个服务实现的标记,可以使用这个注解形容一个接口的若干服务扩展,例如,当我们需要加载具有某类 tag 的扩展类时,我们可以指定 tag 列表,当指定的扩展实现类含有指定的 tag 时,则说明该扩展类符合条件。
  • @Shared:用来描述一个服务接口类型,被标注的服务是一个共享服务,表示该服务的实例将放置在 Shared Container 中。
  • HSFServiceContainer:该类为 hsf 加载 spi 的一个门面容器类,调用方统一使用该类提供的方法加载扩展服务类。
  • AppServiceContainer:该类为 spi 的具体实现类,你可以把它称为应用服务加载器。
  • ApplicationModel:代表了一个应用实例,持有了应用类所对应的类加载器,而 AppServiceContainer 只要委托该类加载器加载拓展服务实例类即可。

HSFServiceContainer

HSFServiceContainer 作为加载 SPI 的门面容器类,提供了一系列的方法来加载拓展服务类,以下是它的代码,注意观察几点:

  • SHARED_CONTAINER 作为共享的容器,是最顶层的容器,无需指定父类容器以及用户应用模型(而其他的 AppServiceContainer 需要);
  • 方法 createAppServiceContainer()决定了通过 HSFServiceContainer 创建的容器,他们的父容器一定是 SHARED_CONTAINER
  • 一系列重载的 getInstances(...)方法中调用了 AppServiceContainer 中的同名方法,而该方法中内含了委派加载的逻辑。
public class HSFServiceContainer {
  // 共享的容器,它不隶属与任何一个应用
  public static final AppServiceContainer SHARED_CONTAINER = new AppServiceContainer();
  // 创建一个AppServiceContainer
  public static AppServiceContainer createAppServiceContainer(
    ApplicationModel applicationModel
  ) {
    return new AppServiceContainer(applicationModel, SHARED_CONTAINER);
  }
  // 根据一个接口类型,获取容器中的一个服务实例
  public static <T> T getInstance(Class<T> classType) {
    AppServiceContainer appServiceContainer = getAppServiceContainer(classType);
    return appServiceContainer.getInstance(classType);
  }
  // 根据一个接口类型,获取容器中所有的拓展服务实例
  public static <T> List<T> getInstances(Class<T> classType, String... tags) {
    AppServiceContainer appServiceContainer = getAppServiceContainer(classType);
    return appServiceContainer.getInstances(classType, tags);
  }
  /**
   * 根据接口类型,返回所有的扩展实例
   * 可以传入一组名称,如果该名称的类型是可选Optional,通过withDefault可以控制是否加载默认的实现
   * @param classType   接口类型
   * @param names       名称列表,如果传递空表示所有的类型
   * @param withDefault 是否包含默认
   * @param <T>         类型
   * @return 实现列表, 如果不存在返回为空集合
   */
  public static <T> List<T> getInstances(
    Class<T> classType,
    String[] names,
    boolean withDefault
  ) {
    AppServiceContainer appServiceContainer = getAppServiceContainer(classType);
    return appServiceContainer.getInstances(
      classType,
      names,
      new String[] {},
      withDefault
    );
  }
  /**
   * 根据接口类型获取合适的 AppServiceContainer
   * 如果是@Shared,那么直接获取 SHARED_CONTAINER
   * 否则,根据上下文获取当前的 AppServiceContainer
   */
  private static <T> AppServiceContainer getAppServiceContainer(
    Class<T> classType
  ) {
    return AppServiceContainer.isSharedType(classType) ? SHARED_CONTAINER
      : ApplicationModelFactory.getCurrentApplication().getServiceContainer();
  }
}

SPI 使用

Java SPI 的限定如下:

  • 当服务的提供者,提供了接口的一种具体实现后,在 jar 包的 META-INF/services/目录中创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名。
  • SPI 所在的 jar 放在主程序的 classpath 中
  • 外部程序通过 java.util.ServiceLoader 动态装载实现模块,它通过扫描 META-INF/services 目录下的配置文件找到实现类的全限定名,把类加载到 JVM。注意:SPI 的实现类必须带一个不带参数的构造方法

该示例主要为了展示如何使用 SPI,接口是数字操作接口,普通的 API 的实现类是加法操作;两个 SPI 实现类分别是减法操作和乘法操作。INumOperate 接口的代码如下:

package com.demo.api;
/**
 * 数字操作接口
 *
 */
public interface INumOperate {
  public int operator(int a, int b);
}Copy to clipboardErrorCopied

普通的 API 实现,加法操作,代码如下:

package com.demo.api.impl;
import com.demo.api.INumOperate;
/**
 * 数字相加
 *
 */
public class NumPlusOperateImpl implements INumOperate {
  @Override
  public int operator(int a, int b) {
    int r = a + b;
    System.out.println("[实现类机制]加法,结果:" + r);
    return r;
  }
}Copy to clipboardErrorCopied

实现乘法的 SPI,在语法结构上和普通 API 实现一模一样,如下:

package com.demo.spi.impl;
import com.demo.api.INumOperate;
/**
 * 数字相乘
 *
 */
public class NumMutliOperateImpl implements INumOperate {
    @Override
    public int operator(int a, int b) {
        int r = a * b;
        System.out.println("[SPI机制]乘法,结果:" + r);
        return r;
    }
}
package com.demo.spi.impl;
import com.demo.api.INumOperate;
/**
 * 数字相减
 *
 */
public class NumSubtractOperateImpl implements INumOperate {
    @Override
    public int operator(int a, int b) {
        int r = a - b;
        System.out.println("[SPI机制]减法,结果:" + r);
        return r;
    }
}Copy to clipboardErrorCopied

在 META-INFO/services 目录下(如果没有改目录,手工新建即可),新建一个以 com.demo.api.INumOperate 命名的文件,文件内容指明两个 SPI 的实现类的全限定名称,如下:

com.demo.spi.impl.NumMutliOperateImpl
com.demo.spi.impl.NumSubtractOperateImplCopy to clipboardErrorCopied

main 函数如下,主程序中没有显示指明 SPI 的实现,而是通过 ServiceLoader 动态加载实现类:

package com.demo;
import com.demo.api.impl.NumPlusOperateImpl;
import com.demo.api.INumOperate;
import java.util.Iterator;
import java.util.ServiceLoader;
/**
 * 主程序
 *
 */
public class Main {
  public static void main(String[] args) {
    int a = 9;
    int b = 3;
    // 普通的实现类机制,加法
    INumOperate plus = new NumPlusOperateImpl();
    plus.operator(a, b);
    // SPI机制,寻找所有的实现类,顺序执行
    ServiceLoader<INumOperate> loader = ServiceLoader.load(INumOperate.class); // 查找SPI实现类,并加载到jvm
    Iterator<INumOperate> iter = loader.iterator();
    while (iter.hasNext()) {
      INumOperate op = iter.next();
      op.operator(a, b);
    }
  }
}
下一节:网络编程是指编写运行在多个设备(计算机)的程序,这些设备都通过网络连接起来。java.net 包中 J2SE 的 API 包含有类和接口,它们提供低层次的通信细节。你可以直接使用这些类和接口,来专注于解决问题,而不用关注通信细节。