【dubbo系列】 06-java SPI 机制

前言

该篇主要是为了后面讲解 dubbo SPI 机制做个铺垫。要想了解 dubbo SPI 机制,首先需要了解 java SPI 机制。

什么是 SPI ?

SPI 全称为 Service Provider Interface,是一种服务发现机制。其本质是将接口的实现类的全限定名(包名+类名)配置在文件中,并由服务加载器读取配置文件,加载实现类。这样就可以实现在运行时,动态为接口替换实现类。可以理解为一种“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。是 JDK 内置的一种机制。

为什么需要 SPI ?

在面向对象的编程里,一般都是基于接口编程,模块之前不对实现类进行硬编码。因为如果基于实现类进行编码,当需要替换实现类时,需要修改代码。违反了可插拔原则。其有点类似于IOC原理,其主要思想可以说是为了解藕

使用场景

调用者可以根据实际需要,启动、扩展、替换框架实现的策略。

比较常见的例子:

  • JDBC 加载不同类型数据库的驱动类。
  • SLF4J 加载不同提供商的日志实现类。
  • Spring
  • Dubbo

如何使用

新建一个文件夹 META-INF/services ,放在classpath下面。以maven项目为例,放在resources目录下。

定义一个接口和二个实现类

HelloService


package com.joyxj.spi;

/**
 * 定义一个接口,用于spi
 * @author xiaoj
 * @version 1.0
 *
 */
public interface HelloService {

    void sayHello();
}

二个实现类

HelloServiceImpl

package com.joyxj.spi;

/**
 * Hello Service 实现类
 *
 * @author xiaoj
 * @version 1.0
 */
public class HelloServiceImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("hello");
    }
}

HelloServiceSecondImpl

package com.joyxj.spi;

/**
 * Hello Service 实现类
 *
 * @author xiaoj
 * @version 1.0
 */
public class HelloServiceSecondImpl implements HelloService {
    @Override
    public void sayHello() {
        System.out.println("你好");
    }
}

META-INF/services 目录下新建文件com.joyxj.spi.HelloService ,其名称为包名+类名。

其目录结构如下:


- resources
    - META-INF
        - services
            - com.joyxj.spi.HelloService 

其内容为接口实现类的全限定名。如果有多个,每行一个。

com.joyxj.spi.HelloServiceImpl
com.joyxj.spi.HelloServiceSecondImpl

测试

package com.joyxj.spi;

import java.util.Iterator;
import java.util.ServiceLoader;

/**
 * JAVA SPI 测试
 * @author xiaoj
 * @version 1.0
 */
public class HelloServiceSpiTest {

    public static void main(String[] args) {
        ServiceLoader<HelloService> helloServicesLoaders = ServiceLoader.load(HelloService.class);
        Iterator<HelloService> iterator = helloServicesLoaders.iterator();
        while (iterator.hasNext()) {
            iterator.next().sayHello();
        }
    }
}

输出:

hello
你好

源码分析

从上面示例中可以看出 JAVA SPI 的核心类是 ServiceLoader,下面就开始逐步分析这个类的源码。以下分析基于JDK 1.8。

首先看下这个类的类签名和属性定义。


public final class ServiceLoader<S>
    implements Iterable<S>
{
    // 需要加载资源文件的目录。
    private static final String PREFIX = "META-INF/services/";

    // The class or interface representing the service being loaded
    // 表示被加载的类或接口
    private final Class<S> service;

    // The class loader used to locate, load, and instantiate providers
    // 用于定位、加载和实例化 providers 的类加载器
    private final ClassLoader loader;

    // The access control context taken when the ServiceLoader is created
    // 创建ServiceLoader时的访问控制上下文
    private final AccessControlContext acc;

    // Cached providers, in instantiation order
    // 基于实例的顺序缓存类或接口的实现实例,其中Key为实现类的全限定名。
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // The current lazy-lookup iterator
    // 当前的懒查找迭代器
    private LazyIterator lookupIterator;

    // 省略其它
}

  1. 首先应用程序通过调用 ServiceLoader.load()方法创建一个实例。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<>(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();
        lookupIterator = new LazyIterator(service, loader);
}

从上面代码可以看出,实例化操作是要做了一些初始化操作。其并没有去加载META-INF/services/下的文件。

ServiceLoader<S> load(Class<S> service,ClassLoader loader) 是典型的静态工厂方法,通过 ServiceLoader 提供的私有构造方法创建实例。

  1. 回到ServiceLoader类的定义上面,发现其实现了Iterable接口。下面看其iterator()方法。
public Iterator<S> iterator() {
    // 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() {
            if (knownProviders.hasNext())
                return knownProviders.next().getValue();
            return lookupIterator.next();
        }

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

iterator()是iterator接口的匿名实现,其核心主要就在判断先从缓存中读取,如果没有则从lookupIterator中获取。

  1. lookupIterator是一个懒加载迭代器,是一个 LazyIterator 对象。而 LazyIteratorServiceLoader 的一个私有的内部类。该内部类也实现了Iterator接口。
private class LazyIterator
        implements Iterator<S>
{

    Class<S> service;
    ClassLoader loader;
    // 加载资源URL的集合
    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 {
            // 反射获得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 {
            // 获得一个实现类的实例对象
            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
    }
    // Iterable的方法,其主要是调用hasNextService
    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);
        }
    }

    // // Iterable的方法,其主要是调用nextService
    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();
    }

}

LazyIterator 主要是去加载资源文件,然后parse(service, configs.nextElement()) 方法去解析资源文件。

  1. parse 方法解析资源文件。
private Iterator<String> parse(Class<?> service, URL u)
        throws ServiceConfigurationError {
    InputStream in = null;
    BufferedReader r = null;
    ArrayList<String> names = new ArrayList<>();
    try {
        in = u.openStream();
        r = new BufferedReader(new InputStreamReader(in, "utf-8"));
        int lc = 1;
        // 每次读取一行,一直到读取结束。
        while ((lc = parseLine(service, u, r, lc, names)) >= 0);
    } catch (IOException x) {
        fail(service, "Error reading configuration file", x);
    } finally {
        try {
            if (r != null) r.close();
            if (in != null) in.close();
        } catch (IOException y) {
            fail(service, "Error closing configuration file", y);
        }
    }
    return names.iterator();
}

private int parseLine(Class<?> service, URL u, BufferedReader r, int lc,
                          List<String> names)
        throws IOException, ServiceConfigurationError
    {   
        // 读取文件的一行。
        String ln = r.readLine();

        if (ln == null) {
            return -1;
        }
        int ci = ln.indexOf('#');
        if (ci >= 0) ln = ln.substring(0, ci);
        ln = ln.trim();
        int n = ln.length();
        if (n != 0) {
            if ((ln.indexOf(' ') >= 0) || (ln.indexOf('\t') >= 0))
                fail(service, u, lc, "Illegal configuration-file syntax");
            int cp = ln.codePointAt(0);
            if (!Character.isJavaIdentifierStart(cp))
                fail(service, u, lc, "Illegal provider-class name: " + ln);
            for (int i = Character.charCount(cp); i < n; i += Character.charCount(cp)) {
                cp = ln.codePointAt(i);
                if (!Character.isJavaIdentifierPart(cp) && (cp != '.'))
                    fail(service, u, lc, "Illegal provider-class name: " + ln);
            }
            if (!providers.containsKey(ln) && !names.contains(ln))
                names.add(ln);
        }
        return lc + 1;
    }

parse 会一次性读取完资源文件,但是其不会创建相应的实现类的实例对象。只有当调用上面的nextService()方法时才会创建相应的实例。从而实现了一个懒加载。

小结

JAVA SPI 被广泛用于第三方插件式类库的加载,最常见的就是JDBC等。理解JAVA SPI 有助于设计实现和编写可扩展良好的可插拔的第三方类库。

参考

高级开发必须理解的Java中SPI机制
浅析JDK中ServiceLoader的源码


   转载规则


《【dubbo系列】 06-java SPI 机制》 孤独如梦 采用 知识共享署名 4.0 国际许可协议 进行许可。
 上一篇
OpenResty 快速入门 OpenResty 快速入门
简介OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。
2019-06-14
下一篇 
【dubbo系列】 05-dubbo的服务分组(group)和多版本(version)配置 【dubbo系列】 05-dubbo的服务分组(group)和多版本(version)配置
服务分组(group)使用场景当一个接口有多种实现时,可以用group进行区分。 在平时开发时,多个开发者使用同一个注册中心的话,可以使用group进行区分各自的服务。 服务<dubbo:service interface="co
2019-06-10
  目录