从源码全面解析 Java SPI 的来龙去脉

一、引言

对于 Java 开发者而言,关于 dubbo ,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。

但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。

本期 dubbo 源码解析系列文章,将带你领略 dubbo 源码的奥秘

本期源码文章吸收了之前 SpringKakfaJUC源码文章的教训,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。

虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!

废话不多说,发车!

二、SPI是什么

SPI 全称 Service Provider Interface ,是 Java 提供的一套用来被第三方实现或者扩展的 API,它可以用来启用框架扩展和替换组件。

Java SPI 实际上是 基于接口的编程+策略模式+配置文件 组合实现的动态加载机制。

Java SPI 就是提供这样的一个机制:为某个接口寻找服务实现的机制。

将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

所以 SPI 的核心思想就是解耦

三、使用介绍

我们定义一个接口 Phone

 
 

java

复制代码

package com.study.spring.spi; public interface Phone { public void getName(); }

实现其三个类:

  • HuaWei
 
 

java

复制代码

package com.study.spring.spi; public class HuaWei implements Phone { @Override public void getName() { System.out.println("我是是华为手机"); } }

  • IPhone
 
 

java

复制代码

package com.study.spring.spi; public class IPhone implements Phone { @Override public void getName() { System.out.println("我是是苹果手机"); } }

  • XiaoMi
 
 

java

复制代码

package com.study.spring.spi; public class XiaoMi implements Phone { @Override public void getName() { System.out.println("我是是小米手机"); } }

重点来了:我们要在 resources 文件夹下面建立一个路径:META-INF/services

然后我们建立一个 txt 名为:com.study.spring.spi.Phone,如下:

我们在这个文件中写上各实现类的路径:

 
 

java

复制代码

com.study.spring.spi.HuaWei com.study.spring.spi.IPhone com.study.spring.spi.XiaoMi

测试类:

 
 

java

复制代码

package com.study.spring.spi; import java.util.Iterator; import java.util.ServiceLoader; public class JavaSPITest { public static void main(String[] args) { // 执行Java SPI的规范 ServiceLoader<Phone> phoneServiceLoader = ServiceLoader.load(Phone.class); // 获取迭代器 Iterator<Phone> it = phoneServiceLoader.iterator(); // 迭代遍历,输出集合中的所有元素 while (it.hasNext()) { System.out.println(it.next()); } } }

结果:

 
 

java

复制代码

我是是华为手机 我是是苹果手机 我是是小米手机

从这里我们可以看到,通过 SPI 的机制,我们可以获取当前接口类的实现

四、生产场景

相信大家在生产上都使用过 JDBC,没错,我们的 JDBC 实际上也使用了 SPI

我们看 DriverManager 的静态方法 loadInitialDrivers

 
 

java

复制代码

static { // 初始化加载 loadInitialDrivers(); println("JDBC DriverManager initialized"); }

我们查看下 loadInitialDrivers 方法的代码:

 
 

java

复制代码

private static void loadInitialDrivers() { String drivers; AccessController.doPrivileged(new PrivilegedAction<Void>() { public Void run() { // SPI的加载机制 ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class); // 迭代 Iterator<Driver> driversIterator = loadedDrivers.iterator(); try{ while(driversIterator.hasNext()) { driversIterator.next(); } } return null; } }); }

当然,这里我们需要引入下面的 MAVEN 依赖,不然 Driver.class 的实现类为空

 
 

xml

复制代码

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.18</version> </dependency>

测试类:

 
 

java

复制代码

package com.study.spring.spi; import java.sql.Driver; import java.util.Iterator; import java.util.ServiceLoader; public class JDBCTest { public static void main(String[] args) { ServiceLoader<Driver> serviceLoader = ServiceLoader.load(Driver.class); for (Iterator<Driver> iterator = serviceLoader.iterator(); iterator.hasNext(); ) { Driver driver = iterator.next(); System.out.println(driver.getClass().getPackage() + " ------> " + driver.getClass().getName()); } } }

启动:

 
 

java

复制代码

package com.mysql.cj.jdbc, JDBC, version 4.2 ------> com.mysql.cj.jdbc.Driver

从这里的输出我们可以得出一个假设:我们引入的 JDBC 包里面,存在上述 SPI 机制的 txt 文件名称为: java.sql.Driver 且内容为:com.mysql.cj.jdbc.Driver

我们直接去引入的包里面搜索一下:

果然没错,接下来我们来一下 SPI 的原理

五、SPI运行原理剖析

从我们上面的示例中可以发现,SPI 的实现一共两行:

 
 

java

复制代码

package com.study.spring.spi; import java.util.Iterator; import java.util.ServiceLoader; public class JavaSPITest { public static void main(String[] args) { // 执行Java SPI的规范 ServiceLoader<Phone> phoneServiceLoader = ServiceLoader.load(Phone.class); // 获取迭代器 Iterator<Phone> it = phoneServiceLoader.iterator(); // 迭代遍历,输出集合中的所有元素 while (it.hasNext()) { System.out.println(it.next()); } } }

我们挨个分析

1、服务初始化加载

 
 

java

复制代码

ServiceLoader<Phone> phoneServiceLoader = ServiceLoader.load(Phone.class); 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) { // svc:com.study.spring.spi.Phone service = Objects.requireNonNull(svc, "Service interface cannot be null"); // AppClassLoader loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl; // 安全控制 acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null; // 重点 reload(); }

这里的重点还是在我们的 reload 中:

 
 

java

复制代码

public void reload() { // 清空我们的服务提供者缓存 providers.clear(); // 延时迭代器,用于延迟加载服务提供者 // 达到用需加载的目的 lookupIterator = new LazyIterator(service, loader); } private LazyIterator(Class<S> service, ClassLoader loader) { this.service = service; this.loader = loader; }

2、获取迭代器

 
 

java

复制代码

Iterator<Phone> it = phoneServiceLoader.iterator(); // 返回我们ServiceLoader自定义实现的迭代器 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(); } return lookupIterator.next(); } public void remove() { throw new UnsupportedOperationException(); } }; }

3、判断是否有数据

 
 

java

复制代码

it.hasNext() public boolean hasNext() { // 刚刚初始化knownProviders是空的 if (knownProviders.hasNext()) return true; // return lookupIterator.hasNext(); } 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); } }

重点还是在 hasNextService 方法中

 
 

java

复制代码

private boolean hasNextService() { // 如果当前的Name不等于空直接返回 if (nextName != null) { return true; } // 配置是否为空(第一次肯定为空) if (configs == null) { try { // 写死的:META-INF/services 加上接口的名字 // META-INF/services/com.study.spring.spi.Phone String fullName = PREFIX + service.getName(); // 加载配置 // configs if (loader == null) configs = ClassLoader.getSystemResources(fullName); else configs = loader.getResources(fullName); } catch (IOException x) { fail(service, "Error locating configuration files", x); } } // 判断当前的集合是否为空 || 是否已经遍历完毕 while ((pending == null) || !pending.hasNext()) { // 校验是否有错误 if (!configs.hasMoreElements()) { return false; } // 从上述配置文件中读取当前的配置信息 // com.study.spring.spi.HuaWei // com.study.spring.spi.IPhone // com.study.spring.spi.XiaoMi pending = parse(service, configs.nextElement()); } // 遍历当前的列表,返回第一次的遍历信息(com.study.spring.spi.HuaWei) nextName = pending.next(); return true; }

4、结果输出

 
 

java

复制代码

System.out.println(it.next()); public S next() { // 刚刚初始化knownProviders是空的 if (knownProviders.hasNext()) return knownProviders.next().getValue(); return lookupIterator.next(); } 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); } }

重点还是在 nextService 方法中

 
 

java

复制代码

private S nextService() { String cn = nextName; nextName = null; Class<?> c = null; // cn:com.study.spring.spi.HuaWei // 根据类加载器+类的名称得出类 c = Class.forName(cn, false, loader); // 创建实例 S p = service.cast(c.newInstance()); // 将实例放到缓存里面 // key:com.study.spring.spi.HuaWei // value:HuaWei@773 providers.put(cn, p); // 返回实例 return p; }

六、SPI流程图

高清图可私聊博主获取

七、总结

鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。

其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。

后面会继续更新 DubboSPI 以及项目真实落地的 SPI 实现

如果你也对 后端架构和中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长

我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。

我们下期再见。

我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。

猜你喜欢

转载自blog.csdn.net/BASK2312/article/details/131574176