前言
之前简单涉猎过 Dubbo 的核心源码,时间一长,难免有些内容就忘了。因此,这次细读 Dubbo 源码时,决定把相关知识点记录下来,毕竟写下来和自己想想差别挺大的,虽然会花费比较长的时间,这样也会加深印象。本系列文章仅供自己学习使用,如果有任何侵权,欢迎告知。
简介
Dubbo 作为一款优秀的 RPC 开源框架,其具备良好的可拓展性,我们可以拓展负载均衡、注册中心等实现,这都基于 Dubbo SPI 加载机制,而且 Dubbo 源码中也是大量使用 SPI 机制。因此,阅读 Dubbo RPC 核心之前,搞懂 Dubbo SPI 是有一定的必要。
SPI 全称为 (Service Provider Interface) ,是一种服务发现机制。我们可以在配置文件中配置接口实现类,由服务加载器去读取配置并加载实现类,这样在运行时可以动态为接口替换实现类。Java 本身支持 SPI 方式,比如我们常用的 java.sql.Driver
就是基于这一原理,不同数据库厂商可以通过 SPI 方式提供不同的接口实现。但 Dubbo 并未使用 Java 原生的 SPI 机制,而是自己实现了一套 SPI 机制,进行功能增强。
Java SPI 示例
- 定义接口
public interface Person {
void sayHello();
}
- 两个实现类
public class Teacher implements Person {
@Override
public void sayHello() {
System.out.println("hello, I am a teacher");
}
}
public class Student implements Person {
@Override
public void sayHello() {
System.out.println("hello, I am a student");
}
}
- 创建配置文件
在 META-INF/services 文件夹下创建配置文件,文件名为接口全限定名com.java.example.Person 文件内容为实现类的全限定的类名。
com.java.example.Teacher
com.java.example.Student
- 测试类
public class JavaSPITest {
@Test
public void sayHello() {
ServiceLoader<Person> serviceLoader = ServiceLoader.load(Person.class);
System.out.println("Java SPI");
serviceLoader.forEach(Person::sayHello);
}
}
运行结果:
Java SPI
hello, I am a teacher
hello, I am a student
Dubbo SPI 示例
- 在 META-INF/dubbo 路径新建配置文件,文件名依旧是接口全限定名
com.java.example.Person
,只不过内容和 Java SPI 不同
teacher=com.java.example.Teacher
student=com.java.example.Student
- 在 Person 接口上加上
@SPI
注解
@SPI
public interface Person {
void sayHello();
}
- 创建测试类
public class DubboSPITest {
@Test
public void sayHello() {
System.out.println("Dubbo SPI");
ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class);
Person teacher = extensionLoader.getExtension("teacher");
teacher.sayHello();
Person student = extensionLoader.getExtension("student");
student.sayHello();
}
}
运行结果:
Dubbo SPI
hello, I am a teacher
hello, I am a student
Java SPI 和 Dubbo SPI 对比
看到这里你是不是觉得 Dubbo SPI 和 Java SPI 好像没有太大的不同之处,为啥 Dubbo 还需要自己实现一套呢,下面我们就开始介绍 Dubbo SPI 对比 Java SPI 功能增强之处。
- Java SPI 不能通过指定名称使用具体的实现类,只能通过遍历的方式拿到所有实现。Dubbo SPI 可以通过
getExtension("name")
的方式获取指定实现类。 - Java SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
- Java SPI 很单纯,Dubbo SPI 增加了对拓展点 IOC 和 AOP 的支持。一个扩展点可以直接 setter 注入其它扩展点,并且可以和 Spring 容器进行集成,注入Spring bean。也可以通过 Warpper 类的构造方法,对一个拓展点进行 AOP 前后增强。
Dubbo SPI 进阶使用
下面就对这些 Dubbo SPI 的改进功能进行一一介绍。
- 自动包装( AOP 功能)
ExtensionLoader
在加载拓展时,如果发现这个拓展类包含其他拓展点作为构造函数的参数,则这个拓展类就会被认为是Wrapper
类。
1). 定义Wrapper
类
2). 修改配置文件,添加如下内容public class PersonWrapper implements Person { private Person person; public PersonWrapper(Person person) { this.person = person; } @Override public void sayHello() { System.out.println("before...."); person.sayHello(); System.out.println("after...."); } }
3). 执行DubboSPITest. sayHello()personWrapper=com.java.example.PersonWrapper
运行结果: Dubbo SPI before.... hello, I am a teacher after.... before.... hello, I am a student after....
Dubbo 通过这种包装类方式,自动完成了 AOP 的前后增强。
- 自动装配(依赖注入功能)
如果某个拓展类是另一个拓展类的成员属性,并且拥有setter方法,就会自动装配对应的拓展点实例,具体装配哪个实例,可以根据@Adaptive
类自适应,通过调用getAdaptiveExtension()
方法装配。
1). 添加Dao接口和DaoImpl
2). 添加 Dao 配置文件在 META-INF/dubbo ,文件名com.java.example.Dao@SPI public interface Dao { @Adaptive void insert(URL url); } public class DaoImpl implements Dao { @Override public void insert(URL url) { System.out.println("dao insert()"); } }
3). 修改 Student 类daoImpl=com.java.example.DaoImpl
4). 测试方法public class Student implements Person { private Dao dao; public void setDao(Dao dao) { this.dao = dao; } @Override public void sayHello(URL url) { System.out.println("hello, I am a student"); dao.insert(url); } }
@Test public void testAdaptive() { ExtensionLoader<Person> extensionLoader = ExtensionLoader.getExtensionLoader(Person.class); Person person = extensionLoader.getExtension("student"); URL url = new URL("p1", "1.2.3.4", 1010, "path1"); url = url.addParameters("dao", "daoImpl"); person.sayHello(url); } 运行结果: hello, I am a student dao insert()
Dubbo SPI 中会为 Dao 接口自动生成一个 Dao$Adaptive
代理类,根据 URL 参数动态获取具体的实现。
- 拓展点自适应
动态获取实现类,Dubbo 中主要通过@Adaptive
注解,@Adaptive
可以标记在类和方法上,调用ExtensionLoader#getAdaptiveExtension
方法获取动态的实现类,每次只会获得一个实现类。
1). 标记在某个实现类上时,该实现类会被 cache 到cachedAdaptiveClasses
中,getAdaptiveExtension
会获取该实现类,这种方式优先级最高。
2). 标记在接口的方法上,Dubbo 会为接口的方法生成代理类,并且封装了 URL 参数的逻辑,根据 URL 传参调用对应的实现类,可参考2中的@Adaptive
使用方式。
自动生成Dao$Adaptive
代理类代码如下,并且@Adaptive
注解未定义 value,默认参数为接口名的驼峰规则,自动根据接口名大小写分开,也可以在注解上自定义参数。
package com.java.example;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Dao$Adaptive implements com.java.example.Dao {
public void insert(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("dao");
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.java.example.Dao) name from url(" + url.toString() + ") use keys([dao])");
com.java.example.Dao extension = (com.java.example.Dao) ExtensionLoader.getExtensionLoader(com.java.example.Dao.class).getExtension(extName);
extension.insert(arg0);
}
}
自适应优先级
- 实现类标记
@Adaptive
注解 - 方法上标记
@Adaptive
时,调用时 URL 传参数 - 方法上标记
@Adaptive
时,调用时未传URL参数,则根据@SPI
上定义的 value
- 自动激活
@Adaptive
动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要多个实现类同时被激活的话,如 Filter 过滤器等,那么就需要用到自动激活。可以在实现类上添加@Activate(group = {"default_group"})
,之后可以通过调用getActivateExtension(URL url, String[] values, String group)
方法,获得List<T>
,这里就不再展现示例代码了。
至此,我们清楚了 Dubbo SPI 有了哪些改进功能,下一篇我们分析 Dubbo SPI 的实现原理。