Dubbo中SPI机制及自适应扩展机制一之理论篇

一.SPI机制的概念

SPI的全称是Service Provider Interface,是一种服务发现机制。它的主要目的是将某个接口的实现类的全限定名配置在以当前接口命名的文件中,在服务启动时通过动态加载的方式实现自动注入。大概的意思是假如现在有一个接口,而有两个类实现了该接口,当我们在使用接口调用方法时,通过SPI机制我们能动态的感知到底是使用该接口下的哪个实现类的实例,而不需要我们通过硬编码的方式创建具体的实例,便于程序功能的扩展。

二.SPI机制的应用

早在Java中,就已经存在了SPI机制。简称Java SPI。而这种机制在我们平常使用的框架中应用的也是相当广泛。

我们熟知的SpingBoot,为何能实现自动装配呢?其中就基于SPI机制的优点,在服务启动时,扫描自动装配包下的spring.factories文件,获取所有的自动装配的类名,然后根据条件进行自动装配。

另一个也就是我今天要重点讲的就是Dubbo SPI,他也是借鉴了Java SPI的思想,对其进行了增强,为了能满足更好的需求。因为Dubbo中是支持多协议,多注册中心的,非常的灵活,这也是完全归功于SPI机制的实现。今天就简单介绍下Java SPI和Dubbo SPI,SpringBoot中的扩展机制可以参考我其他的博文。

三.Java SPI

Java SPI大概流程是,首先需要再resource目录下创建/META-INF/services的目录。然后将需要动态扩展的接口以接口全限定名为文件名的形式创建文件放置在此目录下,文件中的内容是接口实现类的全限定名。下面我简单演示一下,让大家更好的理解。

1.创建接口类和实现类。

public interface Robot {
    void sayHello();
}
public class OptimusPrime implements Robot {
    
    @Override
    public void sayHello() {
        System.out.println("Hello, I am Optimus Prime.");
    }
}

public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}

2.在resource目录下创建/META-INF/services文件夹,并创建Robot类全类名的文件,例如xxx.xxx.xxx.Robot。然后将其实现类的全类名配置其中,以换行符分隔。

org.apache.spi.OptimusPrime
org.apache.spi.Bumblebee

3.创建Test类,进行测试。通过ServiceLoader.load方法进行加载,然后通过迭代器分别调用两个实现类中的sayHello方法。

public class Test {

    public static void main(String[] args) {
        ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
        System.out.println("Java SPI");
        serviceLoader.forEach(Robot::sayHello);
    }
}
Java SPI
Hello, I am Bumblebee.
Hello, I am Optimus Prime.

4.验证原理,此处配置了扫描的路径前缀。特别提醒下,文件路径一定要配对,不然检索不到。

private static final String PREFIX = "META-INF/services/";

load方法只是加载生产ClassLoader初始化类加载器,当迭代器迭代时,此时会进行配置文件加载,进行反射生成实例。

从配置文件中获取到信息,存入迭代器中。

Java SPI最常用的一个例子,便是数据库驱动Driver,jdk中仅仅只是定义了驱动的具体规范,但是实现交由各个数据库厂商。如MySQL,DB2,Oracle等,便于使用者更灵活的进行对接。

四.Dubbo SPI

看了上面的Java SPI,大家都能看出来。为了进行接口的注入,需要将所有的实现类都进行初始化一遍,然后迭代调用。倘若我仅仅只想使用其中的某一个对应的实现类呢?岂不是白白的浪费了这么多的系统资源。而在Dubbo的SPI中,它采用了键值对的形式key=value,这样就能通过静态的通过key去加载到对应的value,然后通过反射生成实例。但是这种方式也需要通过硬编码的方式明确传入key。Dubbo还提供了另外一种更高级的扩展机制,叫做自适应扩展点,

  • 在接口类上的@SPI注解上配置默认实现的key,如@SPI("opt"),来获取一个默认的实现。
  • 在接口对应的某个实现类上标注@Adaptive注解。
  • 还有一种更高级的使用方式,也是最重要的一种方式,Dubbo在通过协议发布等地方大量使用到了这种扩展机制。即在接口的对应方法上加入,如果既没有@SPI("opt")指定默认实现,又没有在实现类上标注@Adaptive注解。此时Dubbo会动态帮我们生成一个Robot$Adaptive类,类中实现接口中标注了注解的方法,当进行调用时,实则是进行调用动态生成的类中的方法,可以说是非常的灵活,下面一一举例演示。

1.接口和实现类仍以上面为例,但是需要修改两个地方,第一在接口上标注@SPI注解,因为Dubbo在获取扩展点时会识别该注解,没有则会报错;第二就是将文件内容改为键值对的形式,Dubbo扫描的文件路径如下。

@SPI
public interface Robot {
    void sayHello();
}
private static final String SERVICES_DIRECTORY = "META-INF/services/";

private static final String DUBBO_DIRECTORY = "META-INF/dubbo/";

private static final String DUBBO_INTERNAL_DIRECTORY = DUBBO_DIRECTORY + "internal/";
opt=org.apache.spi.OptimusPrime
bum=org.apache.spi.Bumblebee

2.先演示最简单的静态扩展,先构建ExtensionLoader,然后通过getExtension("name")获取到对应的实例。

ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot opt = extensionLoader.getExtension("opt");
opt.sayHello();

----------------------------------------------
结果如下:Hello, I am Optimus Prime.

3.演示自适应扩展点的第一种,在接口上的@SPI注解上指定默认需要实现的key。

@SPI(value = "opt")
public interface Robot {
    void sayHello();
}

测试代码和结果如下:

ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
//获取默认的实现
Robot opt = extensionLoader.getDefaultExtension();
//默认实现的方法
opt.sayHello();

-----------------------------------
执行结果如下:Hello, I am Optimus Prime.

4.演示在某个实现类上标注@Adaptive注解,测试代码和结果如下:

@Adaptive
public class Bumblebee implements Robot {

    @Override
    public void sayHello() {
        System.out.println("Hello, I am Bumblebee.");
    }
}
//测试代码
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
Robot opt = extensionLoader.getAdaptiveExtension();
opt.sayHello();

----------------------------------
结果如下:Hello, I am Bumblebee.

5.演示在接口的方法上标注@Adaptive注解,测此时需要提前声明一下,如果通过方法的自适应扩展,需要在方法的参数中指定URL或者形参带有URL属性,因为在动态生产的实现类中,会进行判断URL是否存在,下面是报错提示。

Failed to create adaptive class for interface com.gupao.vip.spi.Robot: not found url parameter or url attribute in parameters of method sayHello

为什么要配置URL参数呢?在Dubbo中,我们发布服务的时候,实则是将一串URL地址暴露在netty服务上,地址上带有我们配置好的接口名,引用,协议名等等参数。而传入URL实则是为了动态的获取到其中的参数,进行解析然后实现动态调用,这也正是自适应扩展点的一个精髓所在。上面的例子可能已经不适应了,我重新举例来说明,大家一看便会明白。

@SPI
public interface MakeCar {

    void makeCar(URL url);
}

public class MakeCarImpl implements MakeCar {

    private MakeWheel makeWheel;

    //通过自适应扩展机制完成setter注入
    public void setMakeWheel(MakeWheel makeWheel) {
        this.makeWheel = makeWheel;
    }

    @Override
    public void makeCar(URL url) {
        //通过传入的url,获取对应的key,生产具体的轮子
        makeWheel.makeWheel(url);
        //有了轮子才有车
        System.out.println("车子造好了");
    }
}

这是一个造车的接口,里面有makeWheel属性,该属性通过SPI机制会动态注入,后面再展开讲细节。

@SPI
public interface MakeWheel {

    void makeWheel(URL url);
}

@Adaptive
public class MakeWheelAdaptive implements MakeWheel {

    @Override
    public void makeWheel(URL url) {
        if (url == null) {
            throw new IllegalArgumentException("url为空");
        }
        //获取url传入的key
        String name = url.getParameter("key");
        if (name == null || "".equals(name)) {
            throw new IllegalArgumentException("key为空");
        }
        //通过静态扩展点生成对应实例
        MakeWheel makeWheel = ExtensionLoader.getExtensionLoader(MakeWheel.class).getExtension(name);

        //调用具体的造轮子的方法,造具体的轮子
        makeWheel.makeWheel(url);
    }
}

public class MakeBikeWheel implements MakeWheel{
    @Override
    public void makeWheel(URL url) {

        System.out.println("制造自行车的轮子");
    }
}

这是一个造轮子的接口,有2个实现类,其中MakeWheelAdaptive类实现了自适应扩展点,上面造车实现类中的属性也就是此类的实例。此类中的makeWheel主要做了3件事。

  1. 获取url中的key属性,key为具体需要实现的类实例的key值;
  2. 通过key值进行静态扩展,获取对应的实例;
  3. 调用具体的造轮子的方法。

测试类和结果:

public class Test {

    public static void main(String[] args) {
        MakeCar makeCar = ExtensionLoader.getExtensionLoader(MakeCar.class).getExtension("car");
        Map<String, String> parameters = new HashMap<>();
        parameters.put("key", "bike");
        URL url = new URL("dubbo", "127.0.0.1", 2181, parameters);
        makeCar.makeCar(url);
    }
}
-------------------------------------------------
结果如下:
制造自行车的轮子
车子造好了

当然,MakeWheelAdaptive类实际并不存在,Dubbo会动态生成。但是我们可以看出,在这次的转发过程中却起到了至关重要的作用。

Dubbo SPI的几种演示先介绍到这,这种SPI机制在Dubbo中的被广泛的使用,尤其时自适应扩展机制,由于篇幅的原因,源码分析放到了下一篇文章,感兴趣的可以看看。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值