Dubbo笔记衍生篇①:JDK SPI机制
发布日期:2021-05-10 01:56:46 浏览次数:25 分类:精选文章

本文共 10215 字,大约阅读时间需要 34 分钟。

文章目录

一、前言

本系列为个人Dubbo学习笔记衍生篇,是正文篇之外的衍生内容,内容来源于《深度剖析Apache Dubbo 核心技术内幕》, 过程参考。仅用于个人笔记记录。本文分析基于Dubbo2.7.0版本,由于个人理解的局限性,若文中不免出现错误,感谢指正。


SPI, 全名 service provider interface,是JDK内置的中服务发现机制, 是一种动态替换发现的机制。JDK 中的 SPI 是面向接口编程的,服务规则提供者会在JRE 的核心中提供访问接口,而具体实现则由其他开发商提供。

人话 : JDK 提供了一些功能的接口类,谁想提供这个功能,谁就实现这个接口。


比如对于我们常用的数据库驱动接口,我们在连接不同的数据库时需要使用不同的驱动类,而规范开发者在 rt.jar 中定义了数据库驱动接口 java.sql.Driver,对于不同的厂商(比如Mysql 和 Oracle),他们的驱动实现肯定不同,这时就由他们自己去实现这个接口。开发者只管调用,不管底层如何实现。

但是JDK 如何知道哪个类是java.sql.Driver 的实现类呢?总不能全局扫描判断,费时费力。所以JDK 提供了一个规则:实现了驱动类的厂商在自己Jar包的 META-INF/services 目录下建立名称为SPI 接口类(这里指是 Java.sql.Driver )的文件,文件内容就是SPI 接口类的实现类的全路径名(这里指Mysql 针对java.sql.Driver 接口的实现类)

比如下面的Mysql 和 Oracle :

在这里插入图片描述在这里插入图片描述


在 Spring 中也有类似的SPI 扩展机制,不同的是 Spring是通过 META-INF/spring.factories 文件实现,这个文件很容易让人联想到 Springboot的自动装配机制,个人认为Spring 的自动装配就是在 SPI 机制上的一直延伸的用法。

二、JDK 中的 SPI

上面介绍了SPI 的基本概念,下面我们来写一个简单Demo 来演示:

1 简单使用

  1. 创建需要对外提供的接口类,以及他的两个实现类

    // SPI 接口类public interface SpiDemoService {         String sayName();}// 厂商A 对 SPI  接口的实现public class ASpiDemoServiceImpl implements SpiDemoService {         @Override    public String sayName() {             return "ASpiDemoServiceImpl.sayName";    }}// 厂商B 对 SPI 接口的实现public class BSpiDemoServiceImpl implements SpiDemoService {         @Override    public String sayName() {             return "BSpiDemoServiceImpl.sayName";    }}
  2. 在 META-INF/services 目录下创建文件,文件名为 对外提供的接口类 的全路径名,内容是选择使用的实现类的全路径名。即,我们这里指定了 使用A厂商的实现方式 ASpiDemoServiceImpl

    在这里插入图片描述

  3. 通过 ServiceLoader 加载使用

    public class SpiApplication {     	    public static void main(String[] args) {           	/**          * 输出         * s = ASpiDemoServiceImpl.sayName         */        ServiceLoader
    load = ServiceLoader.load(SpiDemoService.class); for (SpiDemoService spiDemoService : load) { String s = spiDemoService.sayName(); System.out.println("s = " + s); } }}

2 SPI的实现原理

我们以上面的Demo为例,需要关注的是 ServiceLoader<SpiDemoService> load = ServiceLoader.load(SpiDemoService.class);,其代码如下:

public static  ServiceLoader load(Class service) {       	// 1. 获取当前线程的类加载器        ClassLoader cl = Thread.currentThread().getContextClassLoader();      	// 2. 创建ServiceLoader 对象        return ServiceLoader.load(service, cl);    }	    public static  ServiceLoader load(Class service, ClassLoader loader) { return new ServiceLoader<>(service, loader); } private ServiceLoader(Class 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); }

2.1 获取当前线程的类加载器

Java 核心API(比如 rt.jar) 是通过 Bootstrap ClassLoader 类加载器加载的。而ServiceLoader 正是 rt.jar 提供的类,然而一个类由类加载器加载,那么这个类依赖的类也是由相同的类加载器加载的,按照这个道理用户提供的SPI 扩展实现类则应该也是通过 Bootstrap ClassLoader 类加载器加载。然而 用户提供的类都应使用AppClassLoader 进行加载。所以此时采用了一种违反双亲委派模式的方法:JDK通过获取当前当线程上下文类加载器来解决这个问题。并且可以看到的是 cl 随着 ServiceLoader.load(service, cl)传递了下去。具体使用场景,我们下面会说到。

2.2 创建ServiceLoader 对象

上面可以看到 ServiceLoader.load(service, cl) 经过了多层跳转,最终落到了 lookupIterator = new LazyIterator(service, loader); 中。

LazyIterator 看名字就知道是一个懒加载迭代器,猜测就是只有在实际获取迭代器中的对象时才会初始化。我们这里先按下不表

先来看一看 ServiceLoader,SpiApplication 在编译后会变成如下代码(因为迭代器的for循环本质上只是一种语法糖而已,编译后就"原形毕露"),而

public class SpiApplication {       public SpiApplication() {       }    public static void main(String[] args) {           ServiceLoader
load = ServiceLoader.load(SpiDemoService.class); Iterator var2 = load.iterator(); while(var2.hasNext()) { SpiDemoService spiDemoService = (SpiDemoService)var2.next(); String s = spiDemoService.sayName(); System.out.println("s = " + s); } }}

而ServiceLoader 实现了Iterable接口,所以这里的 Iterator 实际实现是 ServiceLoader#iterator 方法的返回,如下,我们这里只看 ServiceLoader#iterator 方法的实现,可以看到ServiceLoader#iterator 方法将逻辑都委托给了 lookupIterator 来处理,而lookupIterator 则是我们一开始初始化的 LazyIterator。

public Iterator iterator() {           return new Iterator() {   			// 将缓存的信息,转换成迭代器。            Iterator
> 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(); // 这里的 lookupIterator 就是上面 初始化时的 LazyIterator return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; }

所以我们这里看一下 ServiceLoader.LazyIterator 的具体实现:

private static final String PREFIX = "META-INF/services/";        private class LazyIterator        implements Iterator    {   		// SPI 接口类        Class service;        // 上面获取到的 当前上下文线程的类加载器。        ClassLoader loader;        Enumeration
configs = null; // 用来保存SPI 文件解析出来的 SPI 实现类的全路径名 Iterator
pending = null; String nextName = null; private LazyIterator(Class
service, ClassLoader loader) { this.service = service; this.loader = loader; } private boolean hasNextService() { if (nextName != null) { return true; } if (configs == null) { try { // 拼接 META-INF/services/ 路径,获取到SPI 接口文件路径,并进行加载获取 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); } } // 如果 pending 还没有加载过,或者不存在元素,则进行加载 while ((pending == null) || !pending.hasNext()) { if (!configs.hasMoreElements()) { return false; } pending = parse(service, configs.nextElement()); } // 保存下一个 SPI 实现类的 全路径名 nextName = pending.next(); return true; } // 返回SPI 实现类 private S nextService() { // 判断是否存在下一个实现类,这里给 nextName 进行赋值 if (!hasNextService()) throw new NoSuchElementException(); // 获取下一个SPI 实现类的全路径类名 String cn = nextName; nextName = null; Class
c = null; try { // 通过反射获取到SPI 实现类的实例,这里需要注意的是,这里指定了类加载器为线程上下文的类加载器,也就是 AppClassLoader 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()); // 将SPI 实现类缓存到providers中,providers 是一个 LinkedHashMap providers.put(cn, p); return p; } catch (Throwable x) { fail(service, "Provider " + cn + " could not be instantiated", x); } throw new Error(); // This cannot happen } public boolean hasNext() { if (acc == null) { return hasNextService(); } else { PrivilegedAction
action = new PrivilegedAction
() { public Boolean run() { return hasNextService(); } }; return AccessController.doPrivileged(action, acc); } } public S next() { if (acc == null) { return nextService(); } else { PrivilegedAction
action = new PrivilegedAction() { public S run() { return nextService(); } }; return AccessController.doPrivileged(action, acc); } } public void remove() { throw new UnsupportedOperationException(); } }

解析完LazyIterator,我们基本就能把整个逻辑猜测的八九不离十了。

即:

  1. ServiceLoader.load 指定SPI 接口类后获取当前线程上下文的类加载器
  2. 依赖于根据SPI 接口类 生成一个 ServiceLoader 返回。此时 ServiceLoader 中初始化了一个懒加载迭代器 LazyIterator
  3. 当我们调用 ServiceLoader 迭代时, ServiceLoader 会调用 LazyIterator 来进行迭代。
  4. LazyIterator 在判断是否有元素时会去加载 META-INF/services 下SPI 接口文件 来获取SPI 实现类,并缓存(第二次判断则不会再重新加载)。
  5. 在通过 next 方法获取 SPI 实现类时才会真正通过反射去创建实现类(这也是为什么叫懒加载迭代器的原因)。

3. Driver 的加载

结合上面的分析,我们再来简单看看 数据库驱动 Driver的加载。


我们依稀记得最原始的驱动加载:

// 加载驱动Class.forName("com.mysql.jdbc.Driver");// 获取连接Connection connection = DriverManager.getConnection("url", "user", "password");

这里我们直接来看 DriverManager,下面代码精简了部分

public class DriverManager {   	.... 	// 静态代码块   	static {          loadInitialDrivers();       println("JDBC DriverManager initialized");   	}		....	    private static void loadInitialDrivers() {       	.....         AccessController.doPrivileged(new PrivilegedAction
() { public Void run() { // 通过SPI 加载 Driver ServiceLoader
loadedDrivers = ServiceLoader.load(Driver.class); Iterator
driversIterator = loadedDrivers.iterator(); // 因为是懒加载迭代器,所以这里需要通过next 将其实例化。 try{ while(driversIterator.hasNext()) { driversIterator.next(); } } catch(Throwable t) { // Do nothing } return null; } }); ..... }

五、Dubbo 的增强SPI

对于SDK 的SPI,Spring 通过 spring.factories 文件实现了增强,但这不是本文重点,所以暂且不表,如有需要,详参: 。


Dubbo 的扩展点机制是基于SDK 中的SPI 增强而来,解决了以下问题:

  1. JDK标准的SPI 会一次性实例化扩展点的所有实现,如果有些扩展点实现初始化很耗时,但又没用上,那么加载就很浪费资源。比如上面所说的Mysql 和Oracle 数据库驱动,当引入这两个包时,即使我们只需要使用其中一个驱动,另一个驱动实现类也会初始化。
  2. 如果扩展点加载失败,是不会友好的向用户通知具体异常,异常提示信息可能并不正确。
  3. 增加了对扩展点 Ioc 和 Aop 的支持,一个扩展点可以直接使用setter() 方法注入其他扩展点,也可以对扩展点使用Wrapper 类进行功能增强。

篇幅所限,详参:


以上:内容部分参考

《深度剖析Apache Dubbo 核心技术内幕》
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

上一篇:Dubbo笔记③ : 服务发布流程 - ServiceConfig#export
下一篇:Dubbo笔记衍生篇②:Dubbo SPI 原理

发表评论

最新留言

第一次来,支持一个
[***.219.124.196]2025年03月31日 03时43分33秒