- 👏作者简介:大家好,我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,阿里云专家博主
- 📕系列专栏:Java设计模式、Spring源码系列、Netty源码系列、Kafka源码系列、JUC源码系列、duubo源码系列
- 🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦
- 🍂博主正在努力完成2023计划中:以梦为马,扬帆起航,2023追梦人
- 📝联系方式:hls1793929520,加我进群,大家一起学习,一起进步,一起对抗互联网寒冬👀
从源码全面解析 Java SPI 的来龙去脉
一、引言
对于 Java
开发者而言,关于 dubbo
,我们一般当做黑盒来进行使用,不需要去打开这个黑盒。
但随着目前程序员行业的发展,我们有必要打开这个黑盒,去探索其中的奥妙。
本期 dubbo
源码解析系列文章,将带你领略 dubbo
源码的奥秘
本期源码文章吸收了之前 Spring
、Kakfa
、JUC
源码文章的教训,将不再一行一行的带大家分析源码,我们将一些不重要的部分当做黑盒处理,以便我们更快、更有效的阅读源码。
虽然现在是互联网寒冬,但乾坤未定,你我皆是黑马!
废话不多说,发车!
二、SPI是什么
SPI
全称 Service Provider Interface
,是 Java
提供的一套用来被第三方实现或者扩展的 API
,它可以用来启用框架扩展和替换组件。
Java SPI
实际上是 基于接口的编程+策略模式+配置文件 组合实现的动态加载机制。
Java SPI
就是提供这样的一个机制:为某个接口寻找服务实现的机制。
将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
所以 SPI
的核心思想就是解耦。
三、使用介绍
我们定义一个接口 Phone
:
package com.study.spring.spi;
public interface Phone {
public void getName();
}
实现其三个类:
- HuaWei
package com.study.spring.spi;
public class HuaWei implements Phone {
@Override
public void getName() {
System.out.println("我是是华为手机");
}
}
- IPhone
package com.study.spring.spi;
public class IPhone implements Phone {
@Override
public void getName() {
System.out.println("我是是苹果手机");
}
}
- XiaoMi
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
,如下:
我们在这个文件中写上各实现类的路径:
com.study.spring.spi.HuaWei
com.study.spring.spi.IPhone
com.study.spring.spi.XiaoMi
测试类:
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());
}
}
}
结果:
我是是华为手机
我是是苹果手机
我是是小米手机
从这里我们可以看到,通过 SPI
的机制,我们可以获取当前接口类的实现
四、生产场景
相信大家在生产上都使用过 JDBC
,没错,我们的 JDBC
实际上也使用了 SPI
我们看 DriverManager
的静态方法 loadInitialDrivers
:
static {
// 初始化加载
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
我们查看下 loadInitialDrivers
方法的代码:
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
的实现类为空
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
测试类:
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());
}
}
}
启动:
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
的实现一共两行:
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、服务初始化加载
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
中:
public void reload() {
// 清空我们的服务提供者缓存
providers.clear();
// 延时迭代器,用于延迟加载服务提供者
// 达到用需加载的目的
lookupIterator = new LazyIterator(service, loader);
}
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
2、获取迭代器
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、判断是否有数据
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
方法中
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、结果输出
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
方法中
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流程图
高清图可私聊博主获取
七、总结
鲁迅先生曾说:独行难,众行易,和志同道合的人一起进步。彼此毫无保留的分享经验,才是对抗互联网寒冬的最佳选择。
其实很多时候,并不是我们不够努力,很可能就是自己努力的方向不对,如果有一个人能稍微指点你一下,你真的可能会少走几年弯路。
后面会继续更新 Dubbo
的 SPI
以及项目真实落地的 SPI
实现
如果你也对 后端架构 和 中间件源码 有兴趣,欢迎添加博主微信:hls1793929520,一起学习,一起成长
我是爱敲代码的小黄,独角兽企业的Java开发工程师,CSDN博客专家,喜欢后端架构和中间件源码。
我们下期再见。
我从清晨走过,也拥抱夜晚的星辰,人生没有捷径,你我皆平凡,你好,陌生人,一起共勉。文章来源:https://www.toymoban.com/news/detail-531023.html
往期文章推荐:文章来源地址https://www.toymoban.com/news/detail-531023.html
- 美团二面:聊聊ConcurrentHashMap的存储流程
- 从源码全面解析Java 线程池的来龙去脉
- 从源码全面解析LinkedBlockingQueue的来龙去脉
- 从源码全面解析 ArrayBlockingQueue 的来龙去脉
- 从源码全面解析ReentrantLock的来龙去脉
- 阅读完synchronized和ReentrantLock的源码后,我竟发现其完全相似
- 从源码全面解析 ThreadLocal 关键字的来龙去脉
- 从源码全面解析 synchronized 关键字的来龙去脉
- 阿里面试官让我讲讲volatile,我直接从HotSpot开始讲起,一套组合拳拿下面试
到了这里,关于从源码全面解析 Java SPI 的来龙去脉的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!