JAVA-SPI-2

149 阅读4分钟

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。

3. Java SPI 示例

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

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

public interface SPIService {
	void say();
}

public class SPIImpl1 implements SPIService {
	public void say() {
		System.out.println("你好");
	}
}

public class SPIImpl2 implements SPIService{
	public void say() {
		System.out.println("Hello");
	}
}

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

com.demo.spi.SPIImpl1
com.demo.spi.SPIImpl2

然后我们就可以通过 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 的实现类。我们看下它的具体方法:

img

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

缺点

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

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