天天看点

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”,即实现类的类路径名。

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

Dubbo|基础知识之SPI机制

通过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”,即实现类的类路径名。

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

Dubbo|基础知识之SPI机制

通过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类的load方法返回SPIService接口的加载器;

Dubbo|基础知识之SPI机制
Dubbo|基础知识之SPI机制
Dubbo|基础知识之SPI机制
Dubbo|基础知识之SPI机制

调用链路:

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:

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

Dubbo|基础知识之SPI机制

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

步骤3:

方法调用链比较长,避免混乱,先给出整体调用链和方法调用结果:

Iterator.hasNext()-LazyIterator.hasNext()-LazyIterator.hasNextService()-lazyIterator.parse(Class,URL)

方法主要完成以下几件事:

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

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

Dubbo|基础知识之SPI机制

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

Dubbo|基础知识之SPI机制

此时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:

分析这么久,该方法终于获取到服务实现类的对象了。方法调用链:

Iterator.next()-LazyIterator.next()-LazyIterator.nextService()

具体过程如下:

Dubbo|基础知识之SPI机制

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

Dubbo|基础知识之SPI机制

调用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

继续阅读