Java Spi以及Dubbo Spi

api跟spi

API:大多数情况下,都是实现方来制定接口并完成对接口的不同实现,即服务方暴露自己的服务供客户调用,调用方仅仅依赖且无权选择不同实现

 SPI:如果是调用方来制定接口,实现方来针对接口来实现不同的实现。调用方来选择自己需要的实现方,再来实现某种功能,例子:JDBC连接数据库在这里插入图片描述

SPI的应用示例

当服务的提供者提供了一种借口的实现后,需要在 classpath 下的 META-INF/services 目录里创建一个以服务接口命名的文件,这个文件内容就是接口的具体实现类,这个实现类就是调用后具体的对应功能服务

JDBC的实现就是SPI,若程序需要连接数据库,则必须通过JDBC,不同的数据库需要用不同的驱动,实现该过程就是SPI

Java的SPI

约定在 Classpath 下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,然后文件里面记录的是此 jar 包提供的具体实现类的全限定名

示例

public interface Person {
    void say();

}

public class XiaoHong implements Person {

    @Override
    public void say() {
        System.out.println("XiaoHong says something");
    }
}

public class XiaoMing implements Person {
    @Override
    public void say() {
        System.out.println("XiaoMing says something");
    }
}

// META-INF/services/下一个叫做spi.Person的文件
spi.XiaoMing
spi.XiaoHong

// 启动类
public static void main(String[] args) {
        // 第一步
        ServiceLoader<Person> serviceLoader = ServiceLoader.load(Person.class);
        // 第二步
        Iterator<Person> iterator = serviceLoader.iterator();
        // 第三步
        while (iterator.hasNext()) {
            // 第四步
            Person person = iterator.next();
            person.say();
        }
}

源码分析

第一步:先通过对应的接口生成相应的ServiceLoader,先找当前线程绑定的 ClassLoader,如果没有就用 SystemClassLoader,然后清除一下缓存,再创建一个 LazyIterator

 

 

第二步:拿到Iterator之后即要进行遍历

第三步:在约定好的地方找到接口对应的文件,然后加载文件并且解析文件里面的内容

 

 第四步:循环加载实现类和创建其实例

 

 总结:Java Spi就是约定一个目录,根据接口名去那个目录找到文件,文件解析得到实现类的全限定名,然后循环加载实现类和创建其实例

 Java Spi的缺点

Java SPI 在查找扩展实现类的时候遍历 SPI 的配置文件并且将实现类全部实例化,假设一个实现类初始化过程比较消耗资源且耗时,但是你的代码里面又用不上它,这就产生了资源的浪费。所以说 Java SPI 无法按需加载实现类

Dubbo Spi

Dubbo Spi按需加载,首先你得给个名字,通过名字去文件里面找到对应的实现类全限定名然后加载实例化即可

并且 Dubbo SPI 除了可以按需加载实现类之外,增加了 IOC 和 AOP 的特性,还有个自适应扩展机制

配置文件里面存放的是键值对

 Dubbo 对配置文件目录

不同于 Java SPI ,Dubbo 分为了三类目录

        -META-INF/services/ 目录:该目录下的 SPI 配置文件是为了用来兼容 Java SPI 

        -META-INF/dubbo/ 目录:该目录存放用户自定义的 SPI 配置文件

        -META-INF/dubbo/internal/ 目录:该目录存放 Dubbo 内部使用的 SPI 配置文件

示例

// 该注解表明要用SPI计值
@SPI(FailoverCluster.NAME)
public interface Robot {

    void action();

}

public class RobotXiaoMing implements Robot{
    @Override
    public void action() {
        System.out.println("RobotXiaoMing does something");
    }
}

public class RobotXiaoHong implements Robot{
    @Override
    public void action() {
        System.out.println("RobotXiaoHong does something");
    }
}

// 在META-INF.dubbo文件里创建一个接口全限定名文件
xiaoMing = dubbo.spi.RobotXiaoMing
xiaoHong = dubbo.spi.RobotXiaoHong

public static void main(String[] args) {
        ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
        Robot xiaoMing = extensionLoader.getExtension("xiaoMing");
        xiaoMing.action();
        Robot xiaoHong = extensionLoader.getExtension("xiaoHong");
        xiaoHong.action();
}

ExtensionLoader是类似ServiceLoader的存在

大致流程就是先通过接口类找到一个 ExtensionLoader ,然后再通过ExtensionLoader.getExtension(name) 得到指定名字的实现类实例

getExtensionLoader():获取ExtensionLoader 

 做了一些判断然后从缓存里面找是否已经存在这个类型的 ExtensionLoader ,如果没有就新建一个塞入缓存。最后返回接口类对应的 ExtensionLoader

getExtension():通过名字从类对应的 ExtensionLoader 中到实例化完的实现类

 重点就是 createExtension()

 先找实现类,判断缓存是否有实例,没有就反射建个实例,然后执行 set 方法依赖注入。如果有找到包装类的话,再包一层

 那么问题来了 getExtensionClasses() 是怎么找的呢?injectExtension() 如何注入的呢(其实我已经说了set方法注入)?为什么需要包装类呢?

getExtensionClasses

 

 而 loadDirectory里面就是根据类名和指定的目录,找到文件先获取所有的资源,然后一个一个去加载类,然后再通过loadResource里的loadClass去做一下缓存操作

 

 可以看到,loadClass 之前已经加载了类,loadClass  只是根据类上面的情况做不同的缓存。分别有 Adaptive 、WrapperClass 和普通类这三种,普通类又将Activate记录了一下。至此对于普通的类来说整个 SPI 过程完结了

Adaptive 注解 - 自适应扩展

首先我们根据配置来进行 SPI 扩展的加载,但是我不想在启动的时候让扩展被加载,我想根据请求时候的参数来动态选择对应的扩展

Dubbo 通过一个代理机制实现了自适应扩展,简单的说就是为你想扩展的接口生成一个代理类,可以通过JDK 或者 javassist 编译你生成的代理类代码,然后通过反射创建实例

@SPI(value = "spi")
public interface SpiTest {

    String getName();

    @Adaptive
    int getAge(URL url);

    @Adaptive(value = "country")
    String getCountry(URL url);

    @Adaptive({"province", "city"})
    String getAddress(URL url);
}

@Test
public void adaptiveTest() {
    SpiTest spiTest = ExtensionLoader.getExtensionLoader(SpiTest.class).getAdaptiveExtension();
    System.out.println(spiTest);
}

原理

首先根据接口获取 ExtensionLoader 对象,Dubbo 会缓存 ExtensionLoader 和接口的 Class 对象的映射关系

接着调用 getAdaptiveExtension() 获取自适应扩展。同样,Dubbo 会缓存自适应扩展对象,但第一次会调用 createAdaptiveExtension() 创建自适应扩展对象。主要逻辑如下:

创建自适应扩展的步骤分为三步:
1.调用 getAdaptiveExtensionClass() 获取自适应扩展类的 Class 对象;
2.调用 Class.newInstance() 反射创建对象;
3.调用 injectExtension 为扩展对象注入依赖这是 Dubbo 实现的 IOC 机制,会为对象自动注入依赖

加载所有扩展类

首先调用 getExtensionClass() 加载所有扩展类,即 Dubbo SPI 的基础功能(上面有)需要注意的是,在加载具体扩展实现类的时候,如果类上有 @Adaptive 注解,会标记为 cachedAdaptiveClass:

如果有 cachedAdaptiveClass,则直接返回;否则自动创建自适应扩展类

创建自适应扩展类

1.根据接口信息和 SPI 信息动态生成代理类 Java 代码;
2.调用 Compiler 接口将编译 Java 代码

动态生成代理类代码

Dubbo 通过调用 AdaptiveClassCodeGenerator#generate() 生成 Java 代码

动态生成的代理类,获取扩展实现类时,根据设置 @Adaptive 的不同分为以下几种情况:

1.未设置 @Adaptive 注解,不支持调用,会抛出 UnsupportedOperationException 异常;

2.只设置 @Adaptive,未设置 value,根据接口名生成扩展名,即 “spi.test”,默认值为 @SPI 定义的 value;

3.设置 @Adaptive(value=“country”), 根据 “country” 从 URL 中获取扩展名,默认值为 @SPI 定义的 value;

4.设置 @Adaptive({“province”, “city”}),先根据 “city” 从 URL 获取配置 value1,默认值为 @SPI 定义的 value;再根据 “province” 从 URL 获取配置 value2,默认值为 value1。

可以看到,@Adaptive 的 value 值如果设置为数组,则按照倒序从 URL 获取配置,每一个配置都是下一个配置的默认值

最后再通过compiler编译代码

总结

Dubbo 通过 @Adaptive 注解定义自适应扩展信息,可定义在类和方法上:如果定义在类上,则直接使用该类获取扩展实现类;如果定义在方法上,则动态生成 Java 代码,并编译加载到类加载器,内部根据 value 的不同,从 URL 获取配置信息,决定使用哪个扩展实现类

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值