Dubbo 源码分析(一) 初识 SPI 机制

2020-01-04  本文已影响0人  吃西瓜的龙猫

前言

之前简单涉猎过 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 示例

  1. 定义接口
public interface Person {
    void sayHello();
}
  1. 两个实现类
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");
    }
}
  1. 创建配置文件
    在 META-INF/services 文件夹下创建配置文件,文件名为接口全限定名com.java.example.Person 文件内容为实现类的全限定的类名。
com.java.example.Teacher
com.java.example.Student
  1. 测试类
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 示例

  1. 在 META-INF/dubbo 路径新建配置文件,文件名依旧是接口全限定名com.java.example.Person,只不过内容和 Java SPI 不同
teacher=com.java.example.Teacher
student=com.java.example.Student
  1. 在 Person 接口上加上 @SPI 注解
@SPI
public interface Person {
    void sayHello();
}
  1. 创建测试类
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 功能增强之处。

  1. Java SPI 不能通过指定名称使用具体的实现类,只能通过遍历的方式拿到所有实现。Dubbo SPI 可以通过 getExtension("name") 的方式获取指定实现类。
  2. Java SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。
  3. Java SPI 很单纯,Dubbo SPI 增加了对拓展点 IOC 和 AOP 的支持。一个扩展点可以直接 setter 注入其它扩展点,并且可以和 Spring 容器进行集成,注入Spring bean。也可以通过 Warpper 类的构造方法,对一个拓展点进行 AOP 前后增强。

Dubbo SPI 进阶使用

下面就对这些 Dubbo SPI 的改进功能进行一一介绍。

  1. 自动包装( AOP 功能)
    ExtensionLoader 在加载拓展时,如果发现这个拓展类包含其他拓展点作为构造函数的参数,则这个拓展类就会被认为是 Wrapper 类。
    1). 定义 Wrapper
    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....");
       }
    }
    
    2). 修改配置文件,添加如下内容
    personWrapper=com.java.example.PersonWrapper
    
    3). 执行DubboSPITest. sayHello()
    运行结果:
    Dubbo SPI
    before....
    hello, I am a teacher
    after....
    before....
    hello, I am a student
    after....
    

Dubbo 通过这种包装类方式,自动完成了 AOP 的前后增强。

  1. 自动装配(依赖注入功能)
    如果某个拓展类是另一个拓展类的成员属性,并且拥有setter方法,就会自动装配对应的拓展点实例,具体装配哪个实例,可以根据 @Adaptive 类自适应,通过调用 getAdaptiveExtension() 方法装配。
    1). 添加Dao接口和DaoImpl
    @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()");
        }
    }
    
    2). 添加 Dao 配置文件在 META-INF/dubbo ,文件名com.java.example.Dao
    daoImpl=com.java.example.DaoImpl
    
    3). 修改 Student 类
    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);
        }
    }
    
    4). 测试方法
    @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 参数动态获取具体的实现。

  1. 拓展点自适应
    动态获取实现类,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);
    }
}

自适应优先级

  1. 自动激活
    @Adaptive 动态寻找实现类的方式比较灵活,但只能激活一个具体的实现类,如果需要多个实现类同时被激活的话,如 Filter 过滤器等,那么就需要用到自动激活。可以在实现类上添加 @Activate(group = {"default_group"}),之后可以通过调用 getActivateExtension(URL url, String[] values, String group) 方法,获得 List<T>,这里就不再展现示例代码了。

至此,我们清楚了 Dubbo SPI 有了哪些改进功能,下一篇我们分析 Dubbo SPI 的实现原理。

上一篇 下一篇

猜你喜欢

热点阅读