阅读 148

Java SPI 机制解析

1. 什么是SPI

SPI ,全称为 Service Provider Interface,是一种服务发现机制。它通过在 ClassPath 路径下的 META-INF/services 文件夹查找文件,自动加载文件里所定义的类。

比如你有个接口,现在这个接口有多个实现类,在系统运行的时候,如何知道选择哪个实现类呢?通过 SPI 可以指定默认的配置,根据配置加载具体的某一个实现类,然后用这个实现类的实例对象。

SPI 机制一般用在插件扩展的场景,比如说你开发了一个给别人使用的开源框架,如果你想让别人自己写个插件,用到你的开源框架里面,从而扩展某个功能,这个时候 SPI 思想就用上了。这一机制为很多框架扩展提供了可能,比如在 Dubbo、JDBC 中都使用到了 SPI 机制。

2. Java 中 SPI 思想的体现

SPI 经典的思想体现,比如说 jdbc。在 Java 定义了一套 jdbc 的接口,但是 Java 并没有提供 jdbc 的实现类。但是实际上项目跑的时候,要使用 jdbc 接口的哪些实现类呢?一般来说,我们要根据自己使用的数据库,比如 mysql,你就将 mysql-jdbc-connector.jar 引入进来;oracle,你就将 oracle-jdbc-connector.jar 引入进来。所以为了方便管理,需要定制一个统一的接口,使调用者在调用数据库的时候可以方便的面向统一的接口进行编程。但是问题来了,真正使用的时候到底用哪个实现呢?从哪里找到实现类呢?这个时候就需要用到 SPI 机制。

最常见的,在我们在访问数据库时候用到的 java.sql.Driver 接口:

Java SPI 规定在 classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名。这样当我们引用了某个 jar 包的时候就可以去找这个 jar 包的 META-INF/services/ 目录,再根据接口名找到文件,然后读取文件里面的内容去进行实现类的加载与实例化。

3. Java SPI 示例

下面我们自己写一个小例子,来更加深刻的理解 SPI 机制。

首先写一个接口和两个对应的实现类:

public interface SPIService {
	void say();
}

public class Nihao implements SPIService {
	public void say() {
		System.out.println("你好, 逗比");
	}
}

public class Hello implements SPIService{
	public void say() {
		System.out.println("Hello");
	}
}
复制代码

然后我在 META-INF/services/ 目录下建了个以接口全限定名命名的文件,内容是实现类的全限定类名,多个实现类之间用换行符分隔。具体内容如下:

com.demo.spi.Nihao com.demo.spi.Hello 然后我们就可以通过 ServiceLoader.load 或者 Service.providers 方法拿到实现类的实例。写一个 mian 方法来测试下:

public class Main {
	public static void main(String[] args) {
		ServiceLoader<SPIService> serviceLoader = ServiceLoader.load(SPIService);
		Iterator<SPIService> iterator = serviceLoader.iterator();
		while (iterator.hasNext()) {
			SPIService next = iterator.next();
			next.say();
		}
	}
}
复制代码

运行结果:

你好 Hello

Process finished with exit code 0

4. Java SPI 源码分析

我们分析下源码,看具体做了什么,先看下 ServiceLoader 这个类的属性结构:

public final class ServiceLoader<s> implements Iterable<s>
    //配置文件的路径
    private static final String PREFIX = "META-INF/services/";
    //加载的服务类或接口
    private final Class<s> service;
    //已加载的服务类缓存集合(按实例顺序)
    private LinkedHashMap&lt;String,S&gt; providers = new LinkedHashMap&lt;&gt;();
    //类加载器
    private final ClassLoader loader;
    //内部类,真正加载服务类
    private LazyIterator lookupIterator;
}
复制代码

从 ServiceLoader.load() 进去:

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

public static <s> ServiceLoader<s> load(Class<s> service,
                                        ClassLoader loader)
{
    return new ServiceLoader&lt;&gt;(service, loader);
}

private ServiceLoader(Class<s> svc, ClassLoader cl) {
	// 要加载的接口
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    // 类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    // 访问控制器
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    reload();
}

public void reload() {
	// 现清空集合缓存
    providers.clear();
    // 实例化内部类,得到一个 LazyIterator
    lookupIterator = new LazyIterator(service, loader);
}
复制代码

可以看出,上边整个过程是先找当前线程绑定的 ClassLoader,如果没有就用 SystemClassLoader,然后清除一下缓存,重要的是实例化了内部类:LazyIterator。最后返回 ServiceLoader 的实例。而 LazyIterator 其实就是 Iterator 的实现类。先看下它的具体方法:

private class LazyIterator implements Iterator<S> {

    Class<S> service;
    ClassLoader loader;
    Enumeration<URL> configs = null;
    Iterator<String> pending = null;
    String nextName = null;

    private LazyIterator(Class<S> service, ClassLoader loader) {
      this.service = service;
      this.loader = loader;
    }

    private boolean hasNextService() {
      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;
    }

    private S nextService() {
      if (!hasNextService()) 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);
      }
      throw new Error(); // This cannot happen
    }

    public boolean hasNext() {
      if (acc == null) {
        return hasNextService();
      } else {
        PrivilegedAction<Boolean> action =
            new PrivilegedAction<Boolean>() {
              public Boolean run() {
                return hasNextService();
              }
            };
        return AccessController.doPrivileged(action, acc);
      }
    }

    public S next() {
      if (acc == null) {
        return nextService();
      } else {
        PrivilegedAction<S> action =
            new PrivilegedAction<S>() {
              public S run() {
                return nextService();
              }
            };
        return AccessController.doPrivileged(action, acc);
      }
    }

    public void remove() {
      throw new UnsupportedOperationException();
    }
  }
复制代码

可以看出,hasNext() 其实是调用的 hasNextService();next() 其实是调用 nextService()。先看下 hasNextService():

private boolean hasNextService() {
    //第二次调用的时候,已经解析完成了,直接返回
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        //META-INF/services/ 加上接口的全限定类名,就是文件服务类的文件
        String fullName = PREFIX + service.getName();
        //将文件路径转成URL对象
        configs = loader.getResources(fullName);
    }
    while ((pending == null) || !pending.hasNext()) {
        //解析URL文件对象,按行遍历文件内容,最后返回
        pending = parse(service, configs.nextElement());
    }
    //拿到第一个实现类的类名
    nextName = pending.next();
    return true;
}
复制代码

这个方法其实就是根据约定好的路径找到接口对应的文件,并对文件内容进行加载和解析。

再看下 next() 方法:

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    // 全限定类名
    String cn = nextName;
    nextName = null;
    Class&lt;?&gt; c = null;
    try {
        // 创建类的Class对象
        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 {
        // 通过newInstance实例化
        S p = service.cast(c.newInstance());
        // 放入集合,返回实例
        providers.put(cn, p);
        return p;
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}
复制代码

这个方法其实就是通过全限定类名加载类,然后创建实例,把实例放入缓存集合最后返回示例。

以上就是 Java SPI 源码的实现过程,思路其实很简单:
1)首先约定好一个目录
2)根据接口名去那个目录找到文件
3)文件解析得到实现类的全限定名
4)然后循环加载实现类和创建其实例

5. Java SPI 总结

JDK 内置的 SPI 机制本身有它的优点,但由于实现比较简单,也有不少缺点。

优点

使用 Java SPI 机制的优势是实现解耦,使得接口的定义与具体业务实现分离,而不是耦合在一起。应用进程可以根据实际业务情况启用或替换具体组件。

缺点

1)不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。如果你并不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
2)获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
3)多个并发多线程使用 ServiceLoader 类的实例是不安全的。

鉴于 SPI 的诸多缺点,很多系统都是自己实现了一套类加载机制,例如 dubbo。Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制。这部分内容以后再和大家继续分享。

文章分类
后端
文章标签