Dubbo系列|四、Dubbo 扩展机制—基础应用

1,136 阅读5分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

JAVA SPI

SPI是英文Service Provider Interface 的缩写,是一种服务发现机制。SPI是一种可插拔机制,需要定义一个接口,然后针对不同的场景进行实现,体现了我们常说的面向接口编程思想。

JAVA自带的SPI机制是JDK1.6引入的,下面来看一个示例,了解下SPI的使用方式:

首先定义一个接口

public interface Mascot {
    String getName();
}

针对这个接口创建两个实现类

public class YoYo implements Mascot{
    @Override
    public String getName() {
        return "YoYo";
    }
}

public class Click implements Mascot {
    @Override
    public String getName() {
        return "Click";
    }
}

resources目录创建文件夹META-INF/services,然后创建一个名字问接口名的文件cn.juejin.spi.Mascot,内容为两个类的全名。

cn.juejin.spi.Click
cn.juejin.spi.YoYo

好了,创建一个启动类来启动测试下吧

public class Main {
    public static void main(String[] args) {
        ServiceLoader<Mascot> mascots = ServiceLoader.load(Mascot.class);
        for (Mascot mascot : mascots) {
            System.out.println(mascot.getName());
        }
    }
}

右键运行,结果如下:

image.png

JAVA SPI是通过ServiceLoader来加载接口对应的实现类,但是他会将文件里的所有类都进行加载进来实例化,并不能按需加载。

可能我们并没有这样使用过JDK的SPI,但是我们间接的使用过,比如JDK中大名鼎鼎的java.sql.Driver接口,当我们引入MySQL的jar包时,JDK就会加载MySQL的实现类,当我们引入Oracle的jar包时,他就会加载Oracle的实现类。这就是通过SPI类实现的。

Dubbo SPI

JAVA SPI有个很明确的缺点是,不能按需加载,接口对应的文件里有多少个类,就会实例化多少。通常情况下,我们更希望的是加载一个指定的类。在配置文件里,为每一个类定义一个key,需要加载的时候,指定这个key就好了。

click=cn.juejin.spi.Click
yoyo=cn.juejin.spi.YoYo

JAVA SPI不能满足这样的功能,所以Dubbo自己实现类一套SPI机制,即Dubbo SPI

我们都知道Dubbo支持多种协议:dubbohttpredisrmirestthrift等。我们需要哪些协议,只需要在配置文件里指定就好了,这就是通过Dubbo SPI来实现的。

下面我们来看下一段获取http协议的代码:

ExtensionLoader<Protocol> extensionLoader = ExtensionLoader.getExtensionLoader(Protocol.class);
Protocol protocol = extensionLoader.getExtension("dubbo");
System.out.println(protocol);

其中Protocol是一个协议接口,dubbo支持的协议都实现了这个接口。ExtensionLoader是一个扩展点加载器,上面例子第一行是说,获取一个Protocol的扩展点的扩展点加载器,dubbohttpredis这些协议就是Protocol的扩展点。第二行是说,要获取dubbo这个具体的扩展点。运行之后会获取dubbo对应的一个扩展点实现类。

好了,这是Dubbo SPI的一个简单用法,知道了怎么用之后,那我们把文中第一个示例改造一下,使用Dubbo SPI来实现吧。

首先,在接口上加上@SPI注解。

@SPI
public interface Mascot {
    String getName();
}

resources目录创建文件夹META-INF/dubbo,然后创建一个名字问接口名的文件cn.juejin.spi.Mascot,内容如下。

click=cn.juejin.spi.Click
yoyo=cn.juejin.spi.YoYo

改造一下Main函数,并运行看一下吧。

image.png

我们指定了,要获取yoyo这个扩展点,然后输出了YoYo这个类的实例化对象。

SPI AOP

Dubbo SPI只是做了这些功能吗?可以指定一个key进行加载?当然不是啦,Dubbo SPI还支持了类似AOP的功能,可以实现在一个类中注入另一个对象。

我们为Mascot接口创建一个Wrapper

public class MascotWrapper implements Mascot {

    private final Mascot mascot;

    public MascotWrapper(Mascot mascot) {
        this.mascot = mascot;
    }

    @Override
    public String getName() {
        System.out.println("MascotWrapper.....");
        return mascot.getName();
    }
}

修改META-INF/dubbo/cn.juejin.spi.Mascot文件内容

click=cn.juejin.spi.Click
yoyo=cn.juejin.spi.YoYo
cn.juejin.spi.MascotWrapper

现在我们再次运行刚才的Main函数,会输出什么呢?还是YoYo吗?来看一下:

image.png 我们可以看到,虽然我们获取的是yoyo,但实际上输出的是MascotWrapper,但MascotWrapper里面的mascot属性值却是yoyo,这就是Dubbo SPI提供的类似AOP的功能,或者叫做依赖注入。当然,如果你愿意,也可以创建多个Wrapper,比如MascotWrapperWrapperMascotWrapperWrapperWrapper。。。

另外,这个命名也不一定飞叫Wrapper,名字是可以随便起的,你也可以叫MascotRap,dubbo是根据这个类的构造器来确定的。

源码分析

ExtensionLoader<Mascot> extensionLoader 
    = ExtensionLoader.getExtensionLoader(Mascot.class);

这一行代码比较简单,获取Mascot接口对应的扩展点加载器。

image.png

getExtensionLoader方法,首先是一堆判断,可以先忽略,先从map缓存里获取接口类对应的扩展点加载器,如果缓存里没有,那就创建一个,然后放到缓存里,再返回。

Mascot yoyo = extensionLoader.getExtension("yoyo");

这一行代码就是要获取一个具体的扩展点了,看下具体的实现。这段代码也比较简单,先判断是否为默认扩展点,如果是返回默认的扩展点,否则,判断缓存中是否已经创建了扩展点,如果没有创建过则创建对应的扩展点并放入缓存中,然后返回。

image.png

如果name的值是true,将得到一个默认的扩展点,那什么是默认的扩展点呢?我们来试一下。

首先对接口进行改造:

@SPI("yoyo")
public interface Mascot {
    String getName();
}

把那name值改为true,运行一下,我们可以看到依然获取到了YoYo这个默认的对象。 image.png

继续看源码,getOrCreateHolder获取class对应的holder对象,这个对象作为一个锁,跟并发有关系,防止重复生成。

createExtension方法是创建扩展点。

image.png

getExtensionClasses方法是获取解析配置文件,查找所有的扩展点并缓存起来。 查找配置文件的路径为META-INF/dubbo/internal/META-INF/dubbo/META-INF/services/。查找到之后,把对应的key和类class文件放到map中。再根据传进来的name获取对应的class文件,并进行实例化。

injectExtension(instance);

对实例化后的对象,看里面有哪些属性并进行赋值。

image.png

这里就是我们刚才写的Wrapper类,这里遍历拿到的所有包装类,根据传过来的type寻找对应的构造器,并进行实例化。实例化后再对包装类的属性进行注入。

这里就是对Dubbo SPI获取扩展点的简单源码解析。