Java中SPI机制介绍和源码分析

本文参考资源:

高级开发必须理解的Java中SPI机制 - 简书

什么是SPI

SPI全称为Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过SPI机制为我们的程序提供拓展功能。

Java中SPI机制介绍和源码分析

应用场景:

  • 数据库驱动加载接口实现类的加载
    JDBC加载不同类型数据库的驱动
  • 日志门面接口实现类加载
    SLF4J加载不同提供商的日志实现类
  • Spring
    Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
  • Dubbo
    Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口

Java中的SPI

用法

要使用Java SPI,需要遵循如下约定:

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

示例

创建接口Markable和实现类RedMark,BlackMark,结构如下图所示:

Java中SPI机制介绍和源码分析
public interface Markable {
    void mark();
}
public class RedMark implements Markable {
    public void mark() {
        System.out.println("红色标记");
    }
}
public class BlackMark implements Markable {
    public void mark() {
        System.out.println("黑色标记");
    }
}

在resources下面建立文件夹/META-INF/services/,添加文件com.rhett.service.Markable,内容为

com.rhett.service.impl.BlackMark
com.rhett.service.impl.RedMark

进行测试:

public class SpiTest {
    public static void main(String[] args) {
        ServiceLoader<Markable> serviceLoader = ServiceLoader.load(Markable.class);
        Iterator<Markable> iterator = serviceLoader.iterator();
        while (iterator.hasNext()){
            Markable markable = iterator.next();
            markable.mark();
        }
    }
}

源码分析

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<>(service, loader);
}

ServiceLoader类:

public final class ServiceLoader<S>
    implements Iterable<S>
{
    private static final String PREFIX = "META-INF/services/";
    // 代表传入的接口
    private final Class<S> service;
    // 用于定位、加载和初始化实现类
    private final ClassLoader loader;
    // 访问权限上下文
    private final AccessControlContext acc;
    // 缓存提供器
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
    // 当前的懒查询的迭代器
    private LazyIterator lookupIterator;
    //...
}
private ServiceLoader(Class<S> svc, ClassLoader cl) {
    //传入的接口不为空
    service = Objects.requireNonNull(svc, "Service interface cannot be null");
    //如果类加载器为null默认使用应用程序类加载器
    loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
    //acc:AccessControlContext,访问权限相关
    acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
    //跟进
    reload();
}
public void reload() {
    //清空缓存表
    providers.clear();
    //实例化一个LazyIterator
    lookupIterator = new LazyIterator(service, loader);
}
//将接口、loader赋值给迭代器中的成员变量
private LazyIterator(Class<S> service, ClassLoader loader) {
    this.service = service;
    this.loader = loader;
}

LazyIterator类:

private class LazyIterator
        implements Iterator<S>
{

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

获取iterator方法:

public Iterator<S> iterator() {
    return new Iterator<S>() {

        Iterator<Map.Entry<String,S>> knownProviders
            = providers.entrySet().iterator();

        public boolean hasNext() {=
            if (knownProviders.hasNext())
                return true;
            return lookupIterator.hasNext();
        }

        public S next() {
            //如果缓存表中有,从缓存表(hashmap)中取
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            //否则使用懒查询迭代器的next方法
            return lookupIterator.next();
        }

        public void remove() {
            throw new UnsupportedOperationException();
        }

    };
}

lookupIterator的next:

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);
    }
}
private S nextService() {
    //是否有下一个服务,如果有这里就获取了nextName
    if (!hasNextService())
        throw new NoSuchElementException();
    //cn得到了下一个要加载的实现类全限定名
    String cn = nextName;
    nextName = null;
    Class<?> c = null;
    try {
        //使用Class.forName即通过反射使用类加载器加载类,赋值给了类中的c变量
        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 {
        //将C转换为指定类型的对象
        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
}

获取下一个要加载的实现类全限定名的方法:

private boolean hasNextService() {
    //如果nextName不为空
    if (nextName != null) {
        return true;
    }
    //configs为空,即还没有读取过资源
    if (configs == null) {
        try {
            // PREFIX就是/META-INF/resources,拼接接口的全限定名
            String fullName = PREFIX + service.getName();
            //加载文件资源,会返回一个Enumeration<URL>对象
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    //给pending赋值,pending是一个Iterator<String>,这里就是实现类全限定名的迭代器
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        pending = parse(service, configs.nextElement());
    }
    // 将nextName设置为迭代器中的下一个类全限定名
    nextName = pending.next();
    return true;
}

原生SPI的优缺点

优点:

使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。

并且因为它会加载jar包中资源的特点,可以动态扩充接口的实现类,并使得源框架不必关心接口的实现类的路径。

缺点:

获取机制不够灵活: 虽然ServiceLoader也算是使用的延迟加载,但是基本只能通过Iterator获取,而在迭代器中,每次调用next方法都会对得到的实现类全限定名进行加载,也就是接口的实现类会全部加载并实例化一遍。如果你并不想用某些实现类,它也会被加载并实例化了,这就造成了浪费。

多个并发多线程使用ServiceLoader类的实例是不安全的。

原创文章,作者:彭晨涛,如若转载,请注明出处:https://www.codetool.top/article/java%e4%b8%adspi%e6%9c%ba%e5%88%b6%e4%bb%8b%e7%bb%8d%e5%92%8c%e6%ba%90%e7%a0%81%e5%88%86%e6%9e%90/