欢迎关注公众号(通过文章导读关注:【11来了】),及时收到 AI 前沿项目工具及新技术的推送!
在我后台回复 「资料」 可领取
编程高频电子书
!
在我后台回复「面试」可领取硬核面试笔记
!文章导读地址:点击查看文章导读!
感谢你的关注!
最新 Dubbo3 深入理解原理系列
Dubbo 的 SPI 机制
SPI 机制原理介绍
在 Dubbo 中 SPI 是一个非常重要的模块,基于 SPI 可以很容易的进行扩展,可以 很灵活的替换接口的实现类
,通过 SPI 可以在运行期间动态的寻找具体的实现类!
并且 Dubbo 的 SPI 还实现了自己的 IOC 和 AOP!
其实 SPI 的原理很简单,就是我们定义一个接口 UserService,在定义一个配置文件(假设为文件 a),此时假设 UserService 有两个实现类:UserServiceImpl1、UserServiceImpl2,用户根据自己的需求在文件 a 中指定需要加载哪一个实现类,如下:
# 指定接口对应实现类的全限定类名
com.example.hello.UserService=com.example.hello.impl.UserServiceImpl1
像 Java 中也提供了 SPI 机制,但是 Dubbo 中并没有使用 Java 提供的 SPI ,而是 基于 Java 提供的 SPI 实现了一套功能更强的 SPI 机制!
Dubbo 中通过 SPI 指定实现类的配置文件放在 META-INF/dubbo 路径下(一般 SPI 机制的配置文件都在 META-INF 目录下)
Dubbo 为什么不用 JDK 中的 SPI 而是自己实现一套呢?
其实很容易想到,为什么不用呢,就是因为太弱了!
JDK 提供的 SPI 机制不满足 Dubbo 的需求,因此 Dubbo 才要开发自己的 SPI 机制
回答的思路就是,先说 JDK 的 SPI 哪里不满足呢?那就是列出 JDK 的 SPI 缺点
之后再说在 Dubbo 中的针对它的哪些需求做了哪些的改进
这些 JDK 的 SPI 缺点、Dubbo SPI 优点,网上一查一大堆,这里我也给列一下:
- JDK SPI 的缺点:
JDK 的 SPI 机制在查找实现类的时候,由于配置文件根根据接口的全限定类名命名的,需要先遍历 META-INF/services/
目录下的所有配置文件,找到对应的配置文件,再将配置文件中的全部实现类都取出来,进行实例化操作
因此呢,它的缺点就是无法按需加载实现类,导致出现资源浪费,并且指定了配置目录 META-INF/services/
,不是很灵活
- Dubbo SPI 的优点:
Dubbo 的 SPI 对配置文件的目录规定了多个,各自的职责不同:
- META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 。
- META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件。
- META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件。
Dubbo 的 SPI 代码中还实现了 IOC 和 AOP,可以对扩展的实现类进行依赖注入,以及 AOP 拦截,也就是方法增强
并且 Dubbo 中的 SPI 是通过 K-V
方式配置的,因此可以 按需加载实现类
,优化了 JDK SPI 的缺点
从这几个点呢,可以看出 Dubbo 的 SPI 机制是非常灵活的,可以针对实现类做出拦截扩展操作,并且性能也不错,按需加载,不会出现资源浪费
Dubbo 中 SPI 使用
先说一下 Dubbo 中的 SPI 使用:
- 第一步:配置文件如下(配置文件在 META-INF/dubbo 目录下,Dubbo 会自动去扫描该目录中的配置文件):
userServiceImpl1 = com.example.hello.impl.UserServiceImpl1
userServiceImpl2 = com.example.hello.impl.UserServiceImpl2
- 第二步:SPI 接口:
@SPI
public interface UserService {
void sayHello();
}
- 第三步:加载实现类
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(UserService.class);
UserService userServiceImpl1 = extensionLoader.getExtension("userServiceImpl1");
userServiceImpl1.sayHello();
UserService userServiceImpl2 = extensionLoader.getExtension("userServiceImpl2");
userServiceImpl2.sayHello();
}
}
Dubbo 的 SPI 实现中,包含了 IOC 和 AOP,接下来说一下 Dubbo 如何实现了 IOC 和 AOP
Dubbo 的 IOC?
Dubbo 通过 SPI 来创建接口的扩展实现类时,那么如果这个实现类中有其他扩展点的依赖的话,Dubbo 会自动将这些依赖注入到这个扩展实现类中
Dubbo 中的 IOC 和 AOP 的代码都是在 ExtensionLoader # createExtension()
方法中(为了代码简洁性,省略一些无关代码):
@SuppressWarnings("unchecked")
private T createExtension(String name, boolean wrap) {
Class<?> clazz = getExtensionClasses().get(name);
try {
T instance = (T) extensionInstances.get(clazz);
if (instance == null) {
extensionInstances.putIfAbsent(clazz, createExtensionInstance(clazz));
instance = (T) extensionInstances.get(clazz);
instance = postProcessBeforeInitialization(instance, name);
// IOC 代码
injectExtension(instance);
instance = postProcessAfterInitialization(instance, name);
}
}
}
SPI 中 IOC 的核心方法就是 injectExtension()
:
private T injectExtension(T instance) {
try {
// 使用反射遍历所有的方法
for (Method method : instance.getClass().getMethods()) {
// 如果不是 setter 方法就跳过
if (!isSetter(method)) {
continue;
}
// 获取 setter 方法的参数
Class<?> pt = method.getParameterTypes()[0];
if (ReflectUtils.isPrimitives(pt)) {
continue;
}
try {
// 获取 setter 中需要设置的属性,比如 setUserName,该方法就是取出来 set 后边的名称 String property = "UserName"
String property = getSetterProperty(method);
// 寻找需要注入的属性
Object object = injector.getInstance(pt, property);
if (object != null) {
// 通过反射进行注入
method.invoke(instance, object);
}
}
}
}
return instance;
}
Dubbo 的 IOC 是 通过 setter 方法注入依赖
的:
- 第一步:通过反射获取实例的所有方法,找到 setter 方法
- 第二步:通过 ObjectFactory(这里的 ObjectFactory 其实是 AdaptiveExtensionFactory 实例,这个实例就是 Dubbo 中的扩展工厂) 获取依赖对象(也就是需要注入的对象),来进行 setter 属性注入的!
Dubbo 的 AOP?
Dubbo 的 AOP 其实就是通过 装饰者模式
来实现的,在包装类上进行增强
Dubbo 的 IOC 和 AOP 都在 org.apache.dubbo.common.extension.ExtensionLoader # createExtension()
这个方法中,AOP 相关的源码如下:
private T createExtension(String name, boolean wrap) {
try {
if (wrap) {
List<Class<?>> wrapperClassesList = new ArrayList<>();
// 拿到缓存中的包装类 WrapperClass
if (cachedWrapperClasses != null) {
wrapperClassesList.addAll(cachedWrapperClasses);
// 将所有的包装类按照 order 进行排序,order 比较小的包装类在较外层
wrapperClassesList.sort(WrapperComparator.COMPARATOR);
Collections.reverse(wrapperClassesList);
}
if (CollectionUtils.isNotEmpty(wrapperClassesList)) {
// 通过 for 循环,进行 Wrapper 的包装,进行包装类的层层嵌套
// 比如有三个 Wrapper 类,AWrapper、BWrapper、CWrapper
// 那么经过包装之后也就是:AWrapper(BWrapper(CWrapper(被包装类)))
// 执行流程:先执行 AWrapper 包装的方法,再执行 BWrapper 包装的方法,再执行 CWrapper 包装的方法,再执行被包装类的方法
for (Class<?> wrapperClass : wrapperClassesList) {
Wrapper wrapper = wrapperClass.getAnnotation(Wrapper.class);
boolean match = (wrapper == null)
|| ((ArrayUtils.isEmpty(wrapper.matches())
|| ArrayUtils.contains(wrapper.matches(), name))
&& !ArrayUtils.contains(wrapper.mismatches(), name));
if (match) {
// 先调用包装类的构造方法创建包装类,有 3 个包装类,因此是 3 次 for 循环,外层包装类包裹了里边的包装类
// 比如第一次就是 instance = CWrapper(被包装类)
// 第二次就是 instance = BWrapper(CWrapper(被包装类))
// 第三次就是 instance = AWrapper(BWrapper(CWrapper(被包装类)))
instance = injectExtension(
(T) wrapperClass.getConstructor(type).newInstance(instance));
}
}
}
}
return instance;
}
}
}
上边的方法主要是扫描 wrapperClassesList(包装类),而这个包装类集合其实就是 cachedWrapperClasses
cachedWrapperClasses 是 Dubbo 在扫描类(执行 loadClass)的时候,会去判断这个类是不是包装类,如果是包装类,就加入到 cachedWrapperClasses 中
通过 for 循环进行包装类的包装,下边举一个 SPI AOP 的例子,也就是通过 Wrapper 包装实现 Dubbo 中的 AOP 机制 :
// Person 接口
@SPI("person")
public interface Person {
void hello();
}
// SPI 接口实现类
public class Student implements Person {
public void hello() {
System.out.println("I am student");
}
}
// Wrapper 包装类
public class StudentWrapper implements Person {
private Person person;
public StudentWrapper(Person person) {
this.person = person;
}
public void hello() {
System.out.println("before");
person.hello();
System.out.println("after");
}
}
// Dubbo 配置文件(配置文件名与 Person 接口保持一致):resources/META-INF/dubbo/com.zqy.hello.Person
student=com.zqy.hello.impl.Student
filter=com.zqy.hello.wrapper.StudentWrapper
// 运行测试类即可看到包装类输出效果
public static void main(String[] args) {
ExtensionLoader<Person> loader = ExtensionLoader.getExtensionLoader(Person.class);
Person studesnt = loader.getExtension("student");
studesnt.hello();
}