Dubbo|基础知识之SPI机制

SPI机制是Dubbo框架的基础知识,学习Dubbo框架之前有必要深入理解SPI机制。下面对SPI的概念,作用,使用以及原理作一个深入的介绍。

1.SPI是什么?SPI有什么用?

SPI的全称是Service Provider Interface,可以翻译为“提供服务接口”;举一个实际场景:模块A提供一个Service接口,模块B和模块C分别实现该Service接口,那么模块A如何在无感知的情况下找到模块B和模块C中Service接口的实现类? SPI机制能解决这个问题。

很明显,如果没有SPI机制,那么模块B和模块C对Service接口的实现类必定需要硬编码在模块A中,这就不符合“开闭原则”。所以SPI机制可以帮助模块A自动寻找Service接口在其他jar包中的实现,简单来说就是寻找接口的服务实现。

2.SPI如何使用?

下面具体用代码演示SPI机制。

第一步:新建Maven项目ModelA。

ModelA项目只提供服务接口SPIService,没有实现。

package com.starry.service;

public interface SPIService {
    String spiService();
}

同时通过maven install命令打包并上传至本地仓库,便于其他模块依赖。

第二步:新建Maven项目ModelB。

ModelB项目依赖ModelA的jar包,并且实现SPIService接口。

package com.starry.service.impl;

import com.starry.service.SPIService;

public class ModelBSPIServiceImpl implements SPIService {
    @Override
    public String spiService(){
        return "model B spiService...";
    }
}

与此同时,在resources目录下新建名为META-INF/services的package,并新建名为“com.starry.service.SPIService”的文件,文件内容则为“com.starry.service.impl.ModelBSPIServiceImpl”,即实现类的类路径名。

项目目录结构如下图所示:


ModelB

通过maven install命令对ModelB项目打包并且上传至本地仓库,供ModelA模块依赖使用。

第三步:新建Maven项目ModelC。

ModelC项目依赖ModelA的jar包,并且实现SPIService接口。

package com.starry.service.impl;

import com.starry.service.SPIService;

public class ModelCSPIServiceImpl implements SPIService {
    @Override
    public String spiService(){
        return "model c spiService...";
    }
}

同样在resources目录下新建名为META-INF/services的package,并新建名为“com.starry.service.SPIService”的文件,文件内容则为“com.starry.service.impl.ModelCSPIServiceImpl”,即实现类的类路径名。

项目的目录结构如下图所示:


ModelC

通过maven install命令对ModelC项目打包并且上传至本地仓库,供ModelA模块依赖使用。

第四步:ModelA模块依赖ModelB和ModelC,并且寻找到SPIService接口的实现。

ModeA模块的pom.xml增加依赖:

<dependencies>
    <dependency>
        <groupId>Model-B</groupId>
        <artifactId>Model-B</artifactId>
        <version>1.0</version>
    </dependency>

    <dependency>
        <groupId>Model-C</groupId>
        <artifactId>Model-C</artifactId>
        <version>1.0</version>
    </dependency>
</dependencies>

测试类:

package com.starry.demo;

import com.starry.service.SPIService;

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

public class Client {
    public static void main(String[] args) {
        ServiceLoader<SPIService> loader = ServiceLoader.load(SPIService.class);
        Iterator<SPIService> serviceIterator = loader.iterator();
        while(serviceIterator.hasNext()) {
            SPIService service = serviceIterator.next();
            System.out.println(service.spiService());
        }
    }
}

很明显,发现服务实现类的重要工具类是 ServiceLoader,表面现象是通过load(SPIService.class)方法获取服务接口的实现类,然后通过iterator()方法枚举服务实现类。

这里我们不禁会想以下两个问题:

  • ServiceLoader类如何发现服务接口的实现类?
  • ServiceLoader类是如何对接口实现类进行加载并且实例化的?

带着这两个问题,深入分析ServiceLoader类的源码。

3.SPI原理分析

针对Client类中的代码深入分析。

步骤1:
ServiceLoader<SPIService> loader = ServiceLoader.load(SPIService.class);

代码表面意思是通过ServiceLoader类的load方法返回SPIService接口的加载器;

1

2

3

4

调用链路
load(Class)-load(Class,ClassLoader)-ServiceLoader(Class,ClassLoader)-reload()
整条链路完成下面三件事:

  • a. 生成SPIService接口的类加载器serviceLoader对象;
  • b. 清理providers的内容;providers是一个有顺序的映射,作用是缓存服务实现的对象;此处重新生成服务类的加载器会清理缓存。
// Cached providers, in instantiation order
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
  • c. 生成LazyIterator类的对象lookupIterator;LazyIterator类是延迟迭代器,顾名思义迭代器的内容只有在迭代过程中才会产生;主要逻辑在这个类里面;
步骤2:
Iterator<SPIService> serviceIterator = loader.iterator();

基于步骤1产生的服务加载器loader对象产生迭代对象serviceIterator。


21

方法内部返回一个iterator对象,主要实现hasNext()和next()方法。注意步骤1中providers对象刚刚被clear掉,所以此刻knownProviders对象也为空。

步骤3:
while(serviceIterator.hasNext())

方法调用链比较长,避免混乱,先给出整体调用链和方法调用结果:
Iterator.hasNext()-LazyIterator.hasNext()-LazyIterator.hasNextService()-lazyIterator.parse(Class,URL)
方法主要完成以下几件事:

  • a. 根据约定(PREFIX常量值)和服务接口的路径名组成文件资源的路径fileName
  • b. 通过loader加载器获取fileName路径下的文件资源configs
  • c. 按行读取configs对象的字符流,并存储在迭代器pending内
  • d. 循环获取迭代器pending的内容,即服务实现类的类路径名

感兴趣的可以看一下下面的具体调用过程,感觉枯燥的可以直接看步骤4。


31

很明显,当providers对象为空时,主要的逻辑都由lookupIterator对象完成。因此调用LazyIterator类的hashNext()方法。


32

此时acc对象为null,调用LazyIterator类的hasNextService();该方法是核心方法,单独拧出来分析一下。
private boolean hasNextService() {
    if (nextName != null) {
        return true;
    }
    if (configs == null) {
        try {
            // PREFIX = “META-INF/services/”
            String fullName = PREFIX + service.getName();
            if (loader == null)
                configs = ClassLoader.getSystemResources(fullName);
            else
                // 类加载器获取jar包指定路径下的文件资源
                configs = loader.getResources(fullName);
        } catch (IOException x) {
            fail(service, "Error locating configuration files", x);
        }
    }
    while ((pending == null) || !pending.hasNext()) {
        if (!configs.hasMoreElements()) {
            return false;
        }
        // 按行读取configs文件资源内的内容,并且作为String放置在迭代器pending中
        pending = parse(service, configs.nextElement());
    }
    // 获取迭代器中内容
    nextName = pending.next();
    return true;
}

看到PREFIX常量的值(META-INF/services/)就很熟悉,方法会把PREFIX和接口的路径名组合成一个文件资源路径fullName,通过类加载的getResources()方法去获取该路径下的资源文件,然后通过parse(Class,URL)获取文件资源内的内容,我们知道其内容为服务实现类路径名;所以既然能够自动获取到实现类的路径名,那么就可以利用反射生成服务实现类的对象;一切都是那么的顺其自然。

步骤4:
SPIService service = serviceIterator.next();

分析这么久,该方法终于获取到服务实现类的对象了。方法调用链:
Iterator.next()-LazyIterator.next()-LazyIterator.nextService()
具体过程如下:

41

目前为止,providers依旧为空,所以调用lookupIterator对象的next()方法。
42

调用nextService()方法,该方法也比较核心,所以单独拧出来分析。

private S nextService() {
    if (!hasNextService())
        throw new NoSuchElementException();
    String cn = nextName;   // nextName为pending迭代器中的第一个内容,即遍历的首个服务实现类名
    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());
        // 以类名为k,以对象为v,放入map;放入本地缓存供后面使用
        providers.put(cn, p);
        return p;  // 返回服务实现类的对象
    } catch (Throwable x) {
        fail(service,
             "Provider " + cn + " could not be instantiated",
             x);
    }
    throw new Error();          // This cannot happen
}

方法实现生成服务实现类对象以及把对象缓存到本地内存两大功能。

步骤5:调用对象的方法。

4.总结

到这里整个调用的过程分析完毕,可以总结下SPI的机制:SPI机制有一个约定,即模块B和模块C实现模块A提供的Service接口,那么模块B和模块C在其jar包的META-INF/services/目录下创建一个以服务接口命名的文件,该文件内容就是实现该接口的实现类的路径名;当模块A依赖模块B和模块C时,就能通过jar包META-INF/services/目录下的文件找到实现类的类名,然后通过反射实现类的装载以及实例化,并完成模块的注入。通过这种寻找接口实现类的机制,可以实现模块间的解耦

SPI是一个很巧妙的设计,模块A在毫无感知的情况下能够获取模块B和模块C对SPIService接口的实现类对象,而这仅仅依靠一个“约定”,即扫描META-INF/services/目录下的文件,文件名则为服务接口的路径名,文件内容放置实现类的类路径名即可。“约定大于配置”的思想,在Spring框架中也有很强的体现。但SPI也有缺点,无法精准获取服务实现类的对象无法控制服务实现类实例化以及SPI的非线程安全等;针对这些问题,Dubbo在SPI的基础上改善后得以解决。

参考资料:

http://www.spring4all.com/article/260
https://www.jianshu.com/p/46b42f7f593c

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,189评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,577评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,857评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,703评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,705评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,620评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,995评论 3 396
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,656评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,898评论 1 298
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,639评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,720评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,395评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,982评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,953评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,195评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,907评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,472评论 2 342

推荐阅读更多精彩内容