Dubbo笔记衍生篇①:JDK SPI机制

一、前言

本系列为个人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<SpiDemoService> 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 <S> ServiceLoader<S> load(Class<S> service) {
    
    
    	// 1. 获取当前线程的类加载器
        ClassLoader cl = Thread.currentThread().getContextClassLoader();
      	// 2. 创建ServiceLoader 对象
        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);
    }

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<SpiDemoService> 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<S> 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();
                // 这里的 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<S>
    {
    
    
		// SPI 接口类
        Class<S> service;
        // 上面获取到的 当前上下文线程的类加载器。
        ClassLoader loader;
        Enumeration<URL> configs = null;
        // 用来保存SPI 文件解析出来的 SPI 实现类的全路径名
        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 {
    
    
                	// 拼接 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<Boolean> action = new PrivilegedAction<Boolean>() {
    
    
                    public Boolean run() {
    
     return hasNextService(); }
                };
                return AccessController.doPrivileged(action, acc);
            }
        }

        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,我们基本就能把整个逻辑猜测的八九不离十了。
即:

  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<Void>() {
    
    
            public Void run() {
    
    
				// 通过SPI 加载 Driver 
                ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
                Iterator<Driver> driversIterator = loadedDrivers.iterator();

             	// 因为是懒加载迭代器,所以这里需要通过next 将其实例化。
                try{
    
    
                    while(driversIterator.hasNext()) {
    
    
                        driversIterator.next();
                    }
                } catch(Throwable t) {
    
    
                // Do nothing
                }
                return null;
            }
        });

     .....
    }

五、Dubbo 的增强SPI

对于SDK 的SPI,Spring 通过 spring.factories 文件实现了增强,但这不是本文重点,所以暂且不表,如有需要,详参: Spring源码分析十一:Springboot 自动装配


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

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

篇幅所限,详参: Dubbo笔记衍生篇②:Dubbo SPI 原理


以上:内容部分参考
《深度剖析Apache Dubbo 核心技术内幕》
https://my.oschina.net/kipeng/blog/1789849
https://www.cnblogs.com/helloz/p/10961026.html
https://www.cnblogs.com/LUA123/p/12460869.html
如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

猜你喜欢

转载自blog.csdn.net/qq_36882793/article/details/114639136