JAVA SPI 机制实现和原理分析

2,694 阅读4分钟

SPI 是 JAVA 提供的一种服务提供发现接口,听起来有点别扭哈!其实就是一种面向接口的编程,为接口去匹配具体服务实现的机制,这一点上与 IOC 的思想类似,都是把装配的控制权放到了程序之外,下面具体看看什么是 SPI

什么是 SPI

SPI 全称为 Service Provider Interface,即服务提供发现接口,这里的服务指的不是我们经常听到的微服务服务发现,这里的一个服务 Service 指的是一个接口或抽象类,服务提供方则是对这个接口或抽象类的实现

SPI 是 ”基于接口的编程 + 策略模式 + 配置文件“ 组合实现的动态加载机制

为什么使用 SPI?

模块化设计中,模块之间基于接口编程,把装配的控制权放到程序之外,实现系统的解耦

使用场景

适用于调用方根据实际需求启用、扩展、替换服务的策略实现

许多开源框架中都使用了 Java 的 SPI 机制,如 JDBC 的 SPI 加载模式、日志框架 SLF4J 加载不同提供商的日志实现、Spring 中也大量适用了 SPI、Dubbo 的扩张机制、ServiceComb Java Chassis (CSE) 的 Filter、异常处理等扩展机制

SPI 的实现

SPI 的实现步骤

  • 在类路径下的 META-INF/services 目录下,创建以服务接口的”全限定名“命名的文件,文件的内容为接口实现类的全限定名
  • 实现类必须在当前程序的 classpath 下
  • 使用 java.util.ServiceLoader 动态加载实现,会扫描 META-INF/services 下的配置文件加载实现类

一个简单的 demo

定义一个接口 SimpleSpiService.java

public interface SimpleSpiService {
    void say();
}

两个简单的实现类

public class SimpleSpiServiceA implements SimpleSpiService {
    @Override
    public void say() {
        System.out.println("Hi! A");
    }
}

public class SimpleSpiServiceB implements SimpleSpiService {
    @Override
    public void say() {
        System.out.println("Hi! B");
    }
}

META_INF/services 目录下创建文件 com.snail.service.SimpleSpiService,里面内容为实现类的全限定名

com.snail.service.prodiver.SimpleSpiServiceA
com.snail.service.prodiver.SimpleSpiServiceB

服务调用

public class ServiceInvoker {
    public static void main(String[] args) {
        ServiceLoader<SimpleSpiService> services = ServiceLoader.load(SimpleSpiService.class);
        for (SimpleSpiService service : services) {
            service.say();
        }
    }
}

运行结果

Hi! A
Hi! B

SPI 实现原理

首先我们可以先来看下 ServiceLoader 的部分成员变量,即内部结构

public final class ServiceLoader<S> implements Iterable<S>{
    private static final String PREFIX = "META-INF/services/";

    // 被加载的类或接口
    private final Class<S> service;

    // 用于定位、加载和实例化 providers 的类加载器
    private final ClassLoader loader;

    // 创建 ServiceLoader 时获取的访问控制上下文
    private final AccessControlContext acc;

    // 缓存 providers, 按照实例化的顺序
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 当前懒加载迭代器
    private LazyIterator lookupIterator;

	......
}

服务 providers 查找的迭代器

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);
        }
    }
}

实现的流程如下

  • 调用方调用 ServiceLoader.load(),会先实例化一个 ServiceLoader 对象,并实例化多个成员变量
    • loader ClassLoader - 用于定位、加载和实例化 providers 的类加载器
    • service Class - 被加载的类或接口
    • acc AccessControlContext - 创建 ServiceLoader 时获取的访问控制上下文
    • providers LinkedHashMap - providers.clear() 清空prividers
    • lookupIterator LazyIterator - 实例化懒加载迭代器
  • 通过迭代器接口获取对象示例,ServiceLoader 会先去 providers 中去判断是否有缓存该实例对象,有就直接返回,没有执行类的加载
    • 读取 META-INF/services/ 下配置文件,获取所有实现类全限定名
    • 通过反射 Class.forName() 获取类并实例化对象
    • 把实例对象添加到 providers (LinkedHashMap ) 缓存中
    • 返回实例对象

使用 Java SPI 最大的优势就是实现模块之间的解耦,第三方的服务模块的装配加载和调用者方的代码分离,这样可以很方便的做到模块服务的扩展和替换

但是 SPI 还是有一定的缺点,虽然 ServiceLoader 是使用的延迟加载,但是每次获取都是通过遍历加载,接口的所有实现类都会被实例化,不能按需加载,如果没有用到其中的某些实现,就会造成资源的浪费