第2章 JDK SPI 的设计与实现

SPI 机制是实现可扩展性的一种方式。Dubbo SPI 是从 JDK SPI(Service Provider Interface)扩展点发现机制加强而来。本文首先分析 JDK SPI 的使用姿势,再分析其基本原理,在下一章再分析 Dubbo SPI 的相关使用和基本原理。

实例代码地址:https://github.com/zhaojigang/dubbo-demo

一、作用

  • 为接口自动寻找实现类。

二、实现方式

  1. 标准制定者制定接口(eg. JDK 制定 SPI 接口:java.sql.Driver
  2. 不同厂商编写针对于该接口的实现类,并在jar的 classpath:META-INF/services/全接口名称 文件中指定相应的实现类全类名(eg. mysql-connector-java 实现了 java.sql.Driver 这个 SPI 接口,实现类是 com.mysql.cj.jdbc.Driver
image.png
  1. 开发者直接引入相应的 jar,就可以实现为接口自动寻找实现类的功能

三、使用方法

image.png

标准接口 io.study.jdk.spi.Log

package io.study.jdk.spi;

/**
 * SPI 接口
 */
public interface Log {
    void execute();
}

具体实现1 io.study.jdk.spi.Log4j

package io.study.jdk.spi;

/**
 * Log4j 实现类
 */
public class Log4j implements Log {
    @Override
    public void execute() {
        System.out.println("this is log4j!");
    }
}

具体实现2 io.study.jdk.spi.Logback

package io.study.jdk.spi;

/**
 * Logback 实现类
 */
public class Logback implements Log {
    @Override
    public void execute() {
        System.out.println("this is logback!");
    }
}

配置文件 classpath:META-INF/services/io.study.jdk.spi.Log

io.study.jdk.spi.Logback
  • 这里指定了实现类 Logback,那么加载的时候就会自动为 Log 接口指定实现类为 Logback。
  • 这里也可以指定两个实现类,那么在实际中使用哪一个实现类,就需要使用额外的手段来控制。

加载实现主类 io.study.jdk.Test

public class Test {
    public static void main(String[] args) {
        // 1. 获取 ServiceLoader
        ServiceLoader<Log> loader = ServiceLoader.load(Log.class);
        // 2. 获取迭代器
        Iterator<Log> iterator = loader.iterator();
        // 3. 迭代读取实现类 调用 hasNext 方法的时候会去加载配置文件进行解析
        while (iterator.hasNext()) {
            // 调用 next 方法的时候进行实例化并缓存
            Log log = iterator.next();
            // 4. 执行实现类
            log.execute();
        }
    }
}

注意

不是实例化 ServiceLoader 以后,就去读取配置文件中的具体实现,并进行实例化的。而是等到使用迭代器去遍历的时候,调用 hasNext 方法的时候会去加载配置文件进行解析调用 next 方法的时候进行实例化并缓存 - 即 懒加载。具体见“源码分析”。

四、源码解析

1、获取ServiceLoader

ServiceLoader<Log> serviceLoader = ServiceLoader.load(Log.class);

首先来看一下 ServiceLoader 的重要属性:

// 定义实现类的接口文件所在的目录
private static final String PREFIX = "META-INF/services/";
// SPI 接口
private final Class<S> service;
// 定位、加载、实例化实现类
private final ClassLoader loader;
// 以初始化的顺序缓存 <接口全名称, 实现类实例>
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// 真正进行迭代的迭代器
private LazyIterator lookupIterator;

其中 LazyIterator 是 ServiceLoader 的一个内部类,在迭代部分会说。

    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;
        ...
        reload();
    }

    public void reload() {
        // 清空 SPI 接口和实现类缓存
        providers.clear();
        // 创建 LazyIterator 实例
        lookupIterator = new LazyIterator(service, loader);
    }

这样一个 ServiceLoader 实例就创建成功了。在创建的过程中,我们看到还实例化了一个LazyIterator,该类下边会说。

2、获取迭代器并迭代

        // 2. 获取迭代器
        Iterator<Log> iterator = loader.iterator();
        // 3. 迭代读取实现类 调用 hasNext 方法的时候会去加载配置文件进行解析 + 存储实现类名
        while (iterator.hasNext()) {
            // 调用 next 方法的时候进行实例化并缓存
            Log log = iterator.next();
            // 4. 执行实现类
            log.execute();
        }

外层迭代器:

    public Iterator<S> iterator() {
        // 创建返回 Iterator 实例
        return new Iterator<S>() {
            // 获取 providers 缓存的迭代器
            Iterator<Map.Entry<String,S>> knownProviders = providers.entrySet().iterator();
            
            // 获取是否有下一个 SPI 实现类
            public boolean hasNext() {
                // 首先从 providers 缓存中获取,如果缓存中存在,则直接返回 true
                if (knownProviders.hasNext())
                    return true;
                // 如果缓存中不存在,则需要使用在创建 ServiceLoader 时创建的 LazyIterator 来进行查找
                return lookupIterator.hasNext();
            }

            // 获取下一个 SPI 实现类
            public S next() {
                // 如果 providers 缓存中有下一个 SPI 实现类的元素,则直接返回下一个 SPI 实现类
                if (knownProviders.hasNext())
                    return knownProviders.next().getValue();
                // 如果缓存中不存在,则需要使用在创建 ServiceLoader 时创建的 LazyIterator 来进行查找
                return lookupIterator.next();
            }

            ...
        };
    }

从查找过程hasNext()和迭代过程next()来看。

  • hasNext():先从 provider(缓存)中查找,如果有,直接返回 true;如果没有,通过 LazyIterator 来进行查找。
  • next():先从 provider(缓存)中直接获取,如果有,直接返回实现类对象实例;如果没有,通过 LazyIterator 来进行获取。

下面来看一下,LazyIterator 这个类。首先看一下他的属性:

Class<S> service; // SPI 接口
ClassLoader loader; // 类加载器
Enumeration<URL> configs = null; // 存放配置文件(可能会有多个)
Iterator<String> pending = null; // 存放配置文件中的内容,并存储为 ArrayList,即存储多个实现类名称
String nextName = null; // 当前处理的实现类名称

其中,service 和 loader 在上述实例化 ServiceLoader 的时候就已经实例化好了。

下面看一下 hasNext():

        public boolean hasNext() {
            ...
            return hasNextService();
            ...
        }

        private boolean hasNextService() {
           // 如果当前正在处理的实现类存在,直接返回 true
            if (nextName != null) {
                return true;
            }
            // 如果配置文件为 null,加载配置文件
            if (configs == null) {
                ...
                // 加载配置文件 META-INF/services/io.study.jdk.spi.Log 
                String fullName = PREFIX + service.getName();
                ...
                configs = loader.getResources(fullName);
                ...
            }
            // 如果 pending 为 null 或者 pending 中没有下一个元素了
            while ((pending == null) || !pending.hasNext()) {
                // 则检测加载的多个配置文件 configs 中是否还有元素,如果没有了,直接返回false;如果有,获取下一个元素(即下一个文件 URL),执行解析操作
                if (!configs.hasMoreElements()) {
                    return false;
                }
                // 将 configs.nextElement() 这个文件中的每一行(即每一个 SPI 实现类的全类名添加到 ArrayList 中,并返回该 ArrayList 的迭代器)
                pending = parse(service, configs.nextElement());
            }
            // 设置当前获取的元素名
            nextName = pending.next();
            return true;
        }

hasNextService()中,核心实现 如下:

  1. 首先使用 loader 加载 SPI 配置文件,此时找到了 META-INF/services/io.study.jdk.spi.Log 文件;
  2. 然后解析这个配置文件,并将各个实现类名称存储在 pending 这个迭代器所代表的 ArrayList 中;此时列表为 [ io.study.jdk.spi.Logback ]
  3. 最后指定当前获取的 nextName;此时 String nextName = "io.study.jdk.spi.Logback"

下面看一下next():

        public S next() {
            ...
            return nextService();
            ...
        }

        private S nextService() {
            // 如果已经没有元素了,直接抛错
            if (!hasNextService())
                throw new NoSuchElementException();
            // 获取要获取的 SPI 实现全类名 nextName(该属性在 hasNextService() 方法中进行了初始化)
            String cn = nextName;
            nextName = null;
            ...
            // 加载 SPI 实现类为 Class
            Class<?> c = Class.forName(cn, false, loader);
            ....
            // 检测 c(SPI 实现类)是不是 service(SPI 接口)的实现类,如果不是,直接抛错
            if (!service.isAssignableFrom(c)) {
                fail(service,
                     "Provider " + cn  + " not a subtype");
            }
            ...
            // 创建 SPI 实现类实例,之后向上转型为 SPI 接口类型
            S p = service.cast(c.newInstance());
            // 将 <SPI 接口名,SPI 实现类实例> 存储在 providers 缓存中
            providers.put(cn, p);
            // 最后返回 SPI 实现类实例
            return p;
            ...
        }

nextService()中,核心实现 如下:

  1. 首先加载 nextName(要获取的 SPI 实现全类名,该属性在 hasNextService() 方法中进行了初始化) 代表的类 Class,这里为io.study.jdk.spi.Logback;
  2. 之后创建该类的实例,并转型为所需的接口类型
  3. 最后存储在 provider 缓存中,供后续查找,最后返回转型后的实现类实例。

在 next() 之后,拿到实现类实例后,就可以执行其具体的方法了。

五、JDK SPI 的缺点

dubbo 官网:扩展点配置

  • JDK 标准的 SPI 会一次性 实例化 扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。(Dubbo 使用 key = value 的形式,可以方便的实现按需实例化,eg. 根据配置的 key="dubbo" 直接获取到 value=DubboProtocol 实例)
  • 如果扩展点加载失败,连扩展点的名称都拿不到了。比如:JDK 标准的 ScriptEngine,通过 getName() 获取脚本类型的名称,但如果 RubyScriptEngine 因为所依赖的 jruby.jar 不存在,导致 RubyScriptEngine 类加载失败,这个失败原因被吃掉了,和 ruby 对应不起来,当用户执行 ruby 脚本时,会报不支持 ruby,而不是真正失败的原因。(疑问
  • 缺乏对扩展点 IOC 和 AOP 的支持,一个扩展点无法直接 setter 注入其它扩展点。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,047评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,807评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,501评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,839评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,951评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,117评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,188评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,929评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,372评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,679评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,837评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,536评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,168评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,886评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,129评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,665评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,739评论 2 351