Java SPI 详解

127 阅读4分钟

Java SPI

1 什么是Java SPI

SPI(Service Provider Interface),是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。

Java SPI提供了这样的一个机制:为某个接口寻找服务实现的机制。 比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java SPI机制可以为某个接口寻找服务实现。

Java SPI 实际上是 基于接口的编程+策略模式+约定配置文件 组合实现的动态加载机制,在JDK中通过工具类 java.util.ServiceLoader来实现服务的查找。

image.png

2 实现SPI 需要遵循的标准

  • classpath 下创建一个目录,该目录命名必须是:META-INF/service

  • 在该目录下创建一个文件,文件名必须是扩展接口的全路径名称

  • 文件内容是该扩展接口的所有实现类

  • 通过java.util.ServiceLoader的加载机制来发现服务

3 SPI的实际应用

SPI扩展机制应用场景有很多,比如Common-Logging,JDBC,Dubbo等等。 在JDBC场景中,在Java中定义了接口java.sql.Driver,有MySQL驱动、Oracle驱动等,具体的实现都是由不同厂商提供的。

MySQL的实现
以MySQL为例,java.sql.Driver的实现类是com.mysql.jdbc.Driver
在MySQL的jar包mysql-connector-java-{version}.jar中,可以发现:

  • 有META-INF/services目录
  • 目录下有一个名字为java.sql.Driver的文件
  • 文件内容是com.mysql.cj.jdbc.Driver

服务发现
在使用JDBC获取连接时,会调用DriverManager.getConnection()获取连接对象,而在Driver类初始化时会使用ServiceLoader动态获取classpath下注册的驱动实现。

4 SPI Demo

定义一个接口

package com.artemis.spi;

public interface Pet {
    void name();
}

定义不同的实现类
实现一:

public class Dog implements Pet {
    @Override
    public void name() {
        System.out.println("dog");
    }
}

实现二:

public class Cat implements Pet {
    @Override
    public void name() {
        System.out.println("cat");
    }
}

在META-INF/services/目录创建文件

image.png

测试及输出

public static void main(String[] args) {
    ServiceLoader<Pet> pets = ServiceLoader.load(Pet.class);
    for (Pet pet : pets) {
        pet.name();
    }
}

-----------------------------
dog
cat
Process finished with exit code 0

5 源码分析

5.1 调用ServiceLoader.load方法

ServiceLoader.load方法内先创建一个新的ServiceLoader,并实例化该类中的成员变量,包括:

  • loader(ClassLoader类型,类加载器)
  • acc(AccessControlContext类型,访问控制器)
  • providers(LinkedHashMap<String,S>类型,用于缓存加载成功的类)
  • lookupIterator(实现迭代器功能)

load方法

public static <S> ServiceLoader<S> load(Class<S> service) {
    ClassLoader cl = Thread.currentThread().getContextClassLoader();
    return new ServiceLoader(Reflection.getCallerClass(), service, cl);
}

ServiceLoader的成员变量

public final class ServiceLoader<S> implements Iterable<S>
{
    // 查找配置文件的目录
    private static final String PREFIX = "META-INF/services/";
    // 表示要被加载的服务的类或接口
    private final Class<S> service;
    // 这个ClassLoader用来定位,加载,实例化服务提供者
    private final ClassLoader loader;
    // 访问控制上下文
    private final AccessControlContext acc;
    // 缓存已经被实例化的服务提供者,按照实例化的顺序存储
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 迭代器
    private LazyIterator lookupIterator; 
}

5.2 通过迭代器接口获取对象实例

ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者。 如果knownProviders中缓存有实现对象的实例,则返回,否则在LazyIterator lookupIterator中进行查找。


public Iterator<S> iterator() {
    return new Iterator<S>() {
        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();
        // hasNext方法
        public boolean hasNext() {
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext(); // lookupIterator中查找
        }
        // next方法
        public S next() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }
    };
}

5.3 加载对象

  • 读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称
  • 通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
  • 把实例化后的类缓存到providers(LinkedHashMap类型)中, 然后返回实例对象。

LazyIterator类

  • configs 存放服务实现类的资源配置抽象
  • pending 存放服务实现类的全名字符串
  • nextName 是下一个可获取的服务实现类的全名字符串
  • 加载资源使用到 classLoader 的 getSystemResources 和 getResources 方法

Java里的资源抽象使用类 URL 来唯一标识,无论是本地文件 (file:///) 还是网络文件(http(s)://

// 服务提供者查找的迭代器
private class LazyIterator implements Iterator<S> {
  // 服务提供者接口
  Class<S> service;
  // 类加载器
  ClassLoader loader;
  // 保存实现类的url
  Enumeration<URL> configs = null;
  // 保存实现类的全名
  Iterator<String> pending = null;
  // 迭代器中下一个实现类的全名
  String nextName = null;

  public boolean hasNext() {
      if (nextName != null) {
          return true;
      }
      if (configs == null) {
          try {
              String fullName = PREFIX + service.getName();
             //加载资源
              if (loader == null)
                  configs = ClassLoader.getSystemResources(fullName);
              else
                  configs = loader.getResources(fullName);
          } catch (IOException x) {
              fail(service, "Error locating configuration files", x);
          }
      }
      while ((pending == null) || !pending.hasNext()) {
          if (!configs.hasMoreElements()) {
              return false;
          }
          pending = parse(service, configs.nextElement());
      }
      nextName = pending.next();
      return true;
  }

  public S next() {
      if (!hasNext()) {
          throw new NoSuchElementException();
      }
      String cn = nextName;
      nextName = null;
      Class<?> c = null;
      try {
          c = Class.forName(cn, false, loader);
      } catch (ClassNotFoundException x) {
          fail(service,"Provider " + cn + " not found");
      }
      if (!service.isAssignableFrom(c)) {
          fail(service, "Provider " + cn  + " not a subtype");
      }
      try {
          S p = service.cast(c.newInstance());
          providers.put(cn, p);
          return p;
      } catch (Throwable x) {
          fail(service, "Provider " + cn + " could not be instantiated: " + x, x);
      }
      throw new Error();          // This cannot happen
  }
}

6 SPI 的不足

  • 需要遍历所有的实现,并实例化,然后我们在循环中才能找到需要的实现
  • 配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们
  • 扩展如果依赖其他的扩展,做不到自动注入和装配
  • 不提供类似于Spring的IOC和AOP功能
  • 扩展很难和其他的框架集成,比如扩展里面依赖了一个Spring bean,原生的Java SPI不支持