Dubbo

Dubbo RPC框架
帅旋
关注
充电
IT宅站长,技术博主,架构师,全网id:arthinking。

JDK中有了SPI,Dubbo为啥又搞一个?

发布于 2022-12-04 | 更新于 2024-05-16
杰克
想要吃饭,做一个菜就可以了?

可选方案有:

  • F1:鲜嫩豆腐鲈鱼煲
  • F2:椒盐鸡翅
  • F3:油焖大虾
  • F4:红烧排骨
  • F5:佛跳墙
  • F6:酿苦瓜
  • F7:水煮蛋
杰克
要不简单点,来一个水煮蛋吧!😎

那么问题来了,如果用的是JDK的SPI,杰克要想吃上水煮蛋,必须把所有的才都得做出来了之后,才能吃,并没有节省做菜的时间,这就是JDK的SPI最大的缺点。

我们还是先来简单点回顾下JDK中的SPI机制。

1. JDK中的SPI机制

在JDK中,提供了SPI机制,全称为:Service Provider Interface,即服务提供者接口,SPI实现了服务提供发现机制,可以用来启用框架扩展和替换组件。在许多开源框架中都用到了SPI。最常见的如JDBC的DriverManager以及Common-Logging框架,都使用到了SPI。

1.1 SPI的使用

SPI的使用很简单,但是得先提供SPI接口,然后有人基于SPI接口去扩展,然后使用方才可以基于接口选择使用哪个扩展。以下是SPI的使用步骤。

1.1.1 定义标准

一般是由关组织或者公司定义标准,所谓标准,映射到代码上,就是接口。比如JDBC中的java.sql.Driver,Sun Microsystems是JDBC标准的制定者。

JDBC(“Java 数据库连接”)允许多个实现存在并由同一个应用程序使用。API 提供了一种机制,用于动态加载正确的 Java 包并将它们注册到 JDBC 驱动程序管理器。Driver Manager 用作创建 JDBC 连接的连接工厂。

Sun Microsystems于 1997 年 2 月 19 日将 JDBC 作为Java 开发工具包 1.1 的一部分发布。

1.1.2 实现标准

一般是由厂商或者具体框架开发者实现。实现的步骤如下:

  • 实现SPI定义的接口,比如MySQL厂商在mysql-connector-java中实现了java.sql.Driver接口,实现类:com.mysql.cj.jdbc.Driver
  • 在META-INF/services目录下定义一个名称为接口全限定名的文件,文件里面保存接口的具体实现全限定名,比如:META-INF/services/java.sql.Driver中保存com.mysql.cj.jdbc.Driver

1.1.3 使用SPI

一般是具体的开发者在项目中使用SPI,使用的方式很简单,对于java.sql.Driver来说,接口的使用者为java.sql.DriverManager,使用方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static void loadInitialDrivers() {
...
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
// 基于Driver接口封装一个类加载器,并初始化一个迭代器
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
// 获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();

try{
// 遍历所有的驱动实现
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
...

在我们的项目中,通过迭代器拿到了具体的实现之后,就可以做具体的业务了。

正是因为DriverManager做了这个封装,我们在项目里面获取连接的时候可以直接这样写,让项目自动加载Driver扩展:

1
2
String url = "jdbc:mysql://itzhai.com:3306/itread";
Connection conn = DriverManager.getConnection(url, username, password);

设计模式应用案例:JDK中的SPI实现使用到了迭代器模式,实现起来还是比较简单易懂的,感兴趣的朋友可以进一步阅读其源码。

1.2 JDK SPI的缺点

阅读了JDK SPI的源码之后,我们可以发现,SPI机制有一些缺点:

  • 加载实现的时候是通过迭代器把所有配置的实现都加在一遍,无法做到按需加载,如果某些不想使用的类实例化很耗时,就会造成资源的浪费了;
  • 第一个点引发的问题:获取某个实现类方式不灵活,不能通过参数控制要加载什么类,每次都只能迭代获取。而在一些框架的运行时通过参数控制加载具体的类的需求是很有必要的;
  • 最后一点,ServiceLoader类的实例用于多个并发线程是不安全的。比如LazyIterator::nextService中的providers.put(cn, p);方法不是线程安全的。

2. Dubbo中的SPI机制

Dubbo为了提供更高的扩展性,于是自己封装了一个SPI:ExtensionLoader,弥补了JDK SPI的缺点,并且做了功能的扩展。

我们到Dubbo项目里面看看,可以发现,大量使用到了ExtensionLoader:

image-20221007113912146

2.1 ExtensionLoader的特性

以下是Dubbo中的ExtensionLoader相比于JDK中的ServicerLoader中的优点:

  1. 根据实现类的标识按需加载
  2. 自动注入关联扩展点
  3. Wrapper机制,实现类似AOP的功能
  4. Adaptive

也不能说Dubbo中实现的都是很完美的,只不过Dubbo为了支持这些功能,按某种方式实现了,下面我们就来详细说明下这些特性以及实现细节。

2.2 ExtensionLoader特性详解

2.2.1 按需加载:想吃什么就煮什么

前面我们讲到,杰克想吃水煮蛋,但是又不想做那么多菜,如果使用的是Dubbo的SPI,那么就可以解决问题问题了。

2.2.1.1 使用Dubbo的SPI吃水煮蛋的代码如下

SPI接口
1
2
3
4
5
6
7
package com.itzhai.dubbo.spi.test.food;
import org.apache.dubbo.common.extension.SPI;

@SPI
public interface Food {
void eat();
}
SPI实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.itzhai.dubbo.spi.test.food;

public class 酿苦瓜 implements Food {

@Override
public void eat() {
System.out.println("吃酿苦瓜");
}
}

public class 水煮蛋 implements Food {

@Override
public void eat() {
System.out.println("吃水煮蛋");
}

}
...
添加META-INF.dubbo/com.itzhai.dubbo.spi.test.Food文件

文件内容:

1
2
3
4
5
6
7
F1=com.itzhai.dubbo.spi.test.food.鲜嫩豆腐鲈鱼煲
F2=com.itzhai.dubbo.spi.test.food.椒盐鸡翅
F3=com.itzhai.dubbo.spi.test.food.油焖大虾
F4=com.itzhai.dubbo.spi.test.food.红烧排骨
F5=com.itzhai.dubbo.spi.test.food.佛跳墙
F6=com.itzhai.dubbo.spi.test.food.酿苦瓜
F7=com.itzhai.dubbo.spi.test.food.水煮蛋
吃水煮蛋
1
2
3
ExtensionLoader<Food> extensionLoader = ExtensionLoader.getExtensionLoader(Food.class);
Food food = extensionLoader.getExtension("F7");
food.eat();

输出结果如下:

1
吃水煮蛋

2.2.2 依赖注入+Adaptive代理机制:再送一个调料

吃饭中…

杰克
添加一点沙茶酱...

快吃完了…

杰克
再来一点酸梅酱...

菜有了,怎么在吃饭过程中添加调料呢?用Dubbo的SPI,可以做到这点。

下面我们来看看是怎么做的。

配料接口

1
2
3
4
5
6
7
@SPI
public interface Seasoning {

@Adaptive
String add(URL url);

}

配料实现类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class 沙茶酱 implements Seasoning {

@Override
public String add(URL url) {
System.out.println("添加沙茶酱");
return "沙茶酱";
}
}


public class 酸梅酱 implements Seasoning {

@Override
public String add(URL url) {
System.out.println("添加酸梅酱");
return "酸梅酱";
}
}

SPI配置文件

com.itzhai.dubbo.spi.test.seasoning.Seasoning

1
2
3
S1=com.itzhai.dubbo.spi.test.seasoning.沙茶酱
S2=com.itzhai.dubbo.spi.test.seasoning.酸梅酱

来一点沙茶酱

1
2
3
URL url = new URL("https", "itzhai.com", 8080);
url = url.addParameter("seasoning", "S1");
food.getSeasoning().add(url);

来一点酸梅酱

1
2
3
url = new URL("https", "itzhai.com", 8080);
url = url.addParameter("seasoning", "S2");
food.getSeasoning().add(url);

输出结果如下:

1
2
3
吃水煮蛋
添加沙茶酱
添加酸梅酱

2.2.3 Wrapper机制:给做好的菜重新摆盘

接口准备吃白灼虾,做好之后…

杰克
这个摆盘不够漂亮,能不能摆的好看点呢...

利用Dubbo SPI的Wrapper机制,可以做到这点!

给食物摆盘的Wrapper类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class FoodWrapper implements Food {

private Food food;

public FoodWrapper(Food food) {
this.food = food;
}

@Override
public void eat() {
System.out.println("先来给食物摆盘");
food.eat();
}

@Override
public Seasoning getSeasoning() {
return food.getSeasoning();
}
}

配置文件:

/META-INF.dubbo/com.itzhai.dubbo.spi.test.food.Food

1
2
3
4
5
6
7
8
F1=com.itzhai.dubbo.spi.test.food.鲜嫩豆腐鲈鱼煲
F2=com.itzhai.dubbo.spi.test.food.椒盐鸡翅
F3=com.itzhai.dubbo.spi.test.food.油焖大虾
F4=com.itzhai.dubbo.spi.test.food.红烧排骨
F5=com.itzhai.dubbo.spi.test.food.佛跳墙
F6=com.itzhai.dubbo.spi.test.food.酿苦瓜
F7=com.itzhai.dubbo.spi.test.food.水煮蛋
wrapper=com.itzhai.dubbo.spi.test.food.FoodWrapper

获取摆好盘的食物

1
2
3
4
5
6
7
8
9
10
11
ExtensionLoader<Food> extensionLoader = ExtensionLoader.getExtensionLoader(Food.class);
Food food = extensionLoader.getExtension("true");
food.eat();

URL url = new URL("https", "itzhai.com", 8080);
url = url.addParameter("seasoning", "S1");
food.getSeasoning().add(url);

url = new URL("https", "itzhai.com", 8080);
url = url.addParameter("seasoning", "S2");
food.getSeasoning().add(url);

输出结果如下:

1
2
3
4
先来给食物摆盘
吃水煮蛋
添加沙茶酱
添加酸梅酱

2.3 Dubbo SPI是如何实现的?

接下来,我们就来看看Dubbo的SPI是如何实现的。

Dubbo的SPI核心的API是这两个:

image-20221009232344304

  1. 根据接口获取到一个ExtensionLoader的实例
  2. 获取扩展实现

下面看看这两个API内部具体逻辑

2.3.1 根据接口获取ExtensionLoader实例

image-20221009232512791

  1. 【11】 创建接口的ExtensionLoader实例,该接口内部又会调用:

    1. 【12】获取到ExtensionFactory接口的ExtensionLoader实例;

    2. 【13】调用ExtensionFactory的getAdaptiveExtension方法获取到AdaptiveExtensionFactory实例,获取过程:

      1. 【15】先加载ExtensionFactory的所有实现类,在【151】方法内部会解析所有META-INF中配置的实现类路径,并加载类,最终得到实现类的map:
        1. image-20221009233651707
        2. 最终得到AdaptiveExtensionFactory类
      2. 【16】调用AdaptiveExtensionFactory的无参构造函数实例化AdaptiveExtensionFactory,构造函数内部又会调用ExtensionFactory的ExtensionLoader实例的getExtension()方法获取所有ExtensionFactory的实现类的实例,放到factories中。这里的getExtension()方法实现我们后边在讲,逐渐有点套娃的感觉。

执行完第一步之后,得到的对象实例结构如下图所示:

image-20221009234250276

有了这个对象实例,我们就可以执行getExtension()方法来获取接口实现类的实例了

2.3.2 获取扩展实现

Wrapper机制,属性注入,代理类的生成,都在这一步实现,下面看看详细的实现。

image-20221009234519164

【22】getExtensionClasses()方法内部会调用loadExtensionClasses()方法,该方法会加载解析文件,得到接口所有实现类的Class的map,相关信息:

  1. image-20221009234713553

image-20221009235132427

  1. 【23】实例化实现类

  2. 【24】依赖注入实现

    1. 通过setter方法注入,解析到setter方法的属性名property,这里会继续调用objectFactory的getExtension方法寻找perperty的实例。objectFactory就是我们AdaptiveExtensionFactory实例了,这里会先从SpringExtensionFactory查找bean,没有找到则继续从SpiExtensionFactory里面找:

      1. image-20221009235856004

      2. 在SpiExtensionFactory里面的查找逻辑:

        1. private Class<?> getAdaptiveExtensionClass() {
              // 加载当前接口的所有扩展类
              getExtensionClasses();
              // 判断是否缓存了@Adaptive注解标记的类
              if (cachedAdaptiveClass != null) {
                  return cachedAdaptiveClass;
              }
              // 如果没有找到@Adaptive注解标记的接口实现类,则自动生成一个Adaptive类
              return cachedAdaptiveClass = createAdaptiveExtensionClass();
          }
          <!--code17-->
          
          
        2. 最后调用这个Adaptive类的无参构造函数实例化这个类,注意,只有贴了@Adaptive注解的方法才会生成具体的代理逻辑。看看上面的代理逻辑,发现里面又是通过被代理的属性类的ExtensionLoader去获取实现类,又继续套娃。

  3. 【25】包装Wrapper类,这一步比较简单,逻辑是:在解析文件获取实现类的时候,已经识别到了Wrapper类,Wrapper类中有一个包含了自己的接口参数的构造函数:

    1. image-20221009235451760
    2. 调用wapperClass的带自己的接口参数的构造函数进行实例化,传入已经实例化好的实现类实例:
      1. image-20221009235604776

Dubbo的SPI实现就这么多了,其实源码都在ExtensionLoader这个类里面,但是逻辑呢就比较绕,套娃了好几层,仔细梳理下逻辑还是比较简单的。

3. 实现思路总结

代码细节不重要,重要的是实现思路。Dubbo SPI的实现思路如下:

1、先构建ExtensionLoader实例,实例里面包含了一个AdaptiveExtensionFactory,该factory可以用于实现依赖注入;

2、调用ExtensionLoader的getExtension方法获取实现类的实例,里面找到配置文件中的所有实现类并装载,实例化需要的类,接着是通过AdaptiveExtensionFactory执行依赖注入,最后是包装成Wrapper切面类;

3、其中依赖注入会先从Spring工厂中找,没找到则从SpiExtensionFactory中找,查找待@Adaptive注解的实现类的实例,如果没找到,则自动生成一个并且实例化。

源码可以跟着流程图一起看。

完整流程如如下:

image-20221204211955458

高清流程图,在Java架构杂谈公众号回复:SPI 获取。

本文作者: 帅旋

本文链接: https://www.itzhai.com/columns/jvm/dubbo-spi.html

版权声明: 版权归作者所有,未经许可不得转载,侵权必究!联系作者请加公众号。

×
IT宅

关注公众号及时获取网站内容更新。

请帅旋喝一杯咖啡

咖啡=电量,给帅旋充杯咖啡,他会满电写代码!

IT宅

关注公众号及时获取网站内容更新。