本文对应源码地址:https://github.com/nieandsun/dubbo-study
注意:dubbo 要求SPI扩展点的实现类必须要有一个无参构造,除了Wrapper实现类之外



上篇文章《【SPI】 — java spi 机制简介》中, 可以看到,java spi 机制非常简单, 就是读取指定的配置文件, 将所有的类都加载到程序中。 而这种机制, 存在很多缺陷, 比如:

  • 所有实现类无论是否使用, 直接被加载, 可能存在浪费
  • 不能够灵活控制什么时候什么时机, 匹配什么实现, 功能太弱

Dubbo 基于自己的需要,对SPI 机制进行了增强, 本篇文章将简单介绍一下 Dubbo 中的 SPI用法。


1 @SPI 标签 及其使用简介

与 Java SPI 实现类配置不同, Dubbo SPI 是通过键值对的方式进行配置, 这样我们就可以按需加载指定的实现类。
@SPI注解的玩法如下:

  • (1)在某个接口上标注@SPI注解,表明此接口是 SPI 的扩展点
@SPI("b") //指定缺省实现类的别名
public interface InfoService {
    Object sayHello(String name);
}
  • (2)在META-INF下的dubbo文件夹下指定接口与实现类之间的映射关系,具体配置如下:

【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo
-(3)简单看一下某一个实现类的源代码:

public class InfoServiceAImpl implements InfoService {
    @Override
    public Object sayHello(String name) {
        System.out.println(name + ",你好,调通了A实现!");
        return name + ",你好,调通了A实现!";
    }
}
  • (4)通过(2)中指定的实现类的key获取具体的实现类
 /**
  * dubbo spi类加载验证
  * extensionLoader.getExtension("a") ---------- 取到对应的扩展类
  * extensionLoader.getDefaultExtension() ------ 取得SPI("b")指定的实现类(默认实现类)
  */
 @Test
 public void dubboSPI() {
     //获取InfoService的 Loader 实例
     ExtensionLoader<InfoService> extensionLoader = ExtensionLoader.getExtensionLoader(InfoService.class);
     //取得a拓展类
     InfoService infoServiceA = extensionLoader.getExtension("a");
     infoServiceA.sayHello("yoyo");
     //取得b拓展类
     InfoService infoServiceB = extensionLoader.getDefaultExtension();
     infoServiceB.sayHello("nrsc");
 }
  • (5)测试结果如下

【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_02


2 @Adaptive标签 及其使用简介

2.1 @Adaptive标注在类上的具体含义详解 — 将该类作为最佳适配类


2.1.1 场景介绍

试想假如我写了一个接口,并想将其作为dubbo的SPI扩展点,但却并没在该接口上标注一个【缺省实现类的别名】----> 也就是说没有指定默认的实现类。
但是我又想让该扩展点有一个【不得不走的默认实现】,并且在该默认实现里我可以拿到其他非默认实现,这该怎么搞呢???

有了以上需求后,回顾一下1中的用法,做个简图如下:
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_03
回顾完之后你应该会有这样的思路:

我可以先通过extensionLoader.getExtension(【不得不走的默认实现】的别名),获取到该默认实现类,然后在该默认实现里再遍历获取到所有的非默认实现!!!

是的,可以做到,但是有点不够优雅:

  • 首先,按照这种思路这个【不得不走的默认实现】的别名应该得写死在调用的代码里
  • 其次,在这个不得不走的默认实现类里,找其他非默认实现类的的方法还得自己去写

而@Adaptive注解标注在类上的使用姿势就优雅地解决了上诉问题。


2.1.2 @Adaptive标注在类上使用姿势及其意义详解

使用姿势介绍
(1)在某个接口上标注@SPI注解,表明此接口是 SPI 的扩展点
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_04
(2)在某个具体实现类上标注上@Adaptive 表明该实现类是【不得不走的默认实现类】,如下:

@Adaptive
public class InfoServiceAImpl implements InfoService {

    @Override
    public Object sayHello(String name) {
        System.out.println(name + ",你好,调通了A实现!");
        return name + ",你好,调通了A实现!";
    }
}

(3)当然要像1中的(2)一样,在META-INF下的dubbo文件夹下指定接口与实现类之间的映射关系

(4)测试类如下:

    @Test
    public void test1() {
        //获取InfoService的ExtensionLoader
        ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
        InfoService adaptiveExtension = loader.getAdaptiveExtension(); //获取到@Adaptive注解标注的实现类
        System.out.println(adaptiveExtension.sayHello("yoyo"));

        InfoService defaultExtension = loader.getDefaultExtension();//获取接口上@SPI("别名")中别名指定的实现类
        System.out.println(defaultExtension.sayHello("nrsc"));
    }

(5)通过打断点的方式来看一下,@Adaptive注解标注在类上如何优雅的解决了我在上面说的问题
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_05
由上图可知,其解决思路如下:
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_06
这里需要注意的是:

  • (1)生成的InfoService的ExtensionLoader会加入到ExtensionLoader中的一个ConcurrentMap缓存起来,因此我们假若在标有的@Adapter注解的实现类里再获取InfoService的ExtensionLoader,可以直接从ConcurrentMap缓存里取
  • (2)遍历并执行所有非@Adapter注解标注的实现类的姿势如下:
 //获取所有未标注@Adapter注解的实现类的别名,并通过别名获取到实现类
 Set<String> supportedExtensions = loader.getSupportedExtensions();
 for (String name : supportedExtensions) {
     System.out.println(loader.getExtension(name).sayHello("MM"));
 }

2.2 @Adaptive标注在方法上具体含义详解 — 静态代理该方法


2.2.1 场景介绍

现在有这样一个需求,一个接口下里有多个方法,但是我只想将某个方法暴漏给别人,该怎么搞???

相信大家肯定都知道,解决方式主要有两种:

  • 动态代理
  • 静态代理

而将@Adaptive注解标注在方法上就可以完成对该方法进行静态代理的工作 —》 据说因为静态代理比动态代理效率更高,所以dubbo采用了静态代理 —> 有兴趣的可以进行考证!!!


2.2.2 @Adaptive标注在方法上的使用姿势

(1)
在某个接口上标注@SPI注解,表明此接口是 SPI 的扩展点
在该接口的某个方法上标注@Adapter注解,表明该方法需要被静态代理
并且注意这个方法必须要有一个参数为URL
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_07
(2)如1中的(2)一样,在META-INF下的dubbo文件夹下指定接口与实现类之间的映射关系

(3)某一个实现类的源码:

public class InfoServiceAImpl implements InfoService {
    @Override
    public Object sayHello(String name) {
        System.out.println(name + ",你好,调通了A实现!");
        return name + ",你好,调通了A实现!";
    }
    @Override
    public Object passInfo(String msg, URL url) {
        System.out.println("恭喜你,调通了A实现:" + url);
        return msg;
    }
}

(4)测试

【情况1】各个实现类上都没有标注@Adaptive注解, 接口的SPI注解为@SPI(“b”),url无参数的情况

/**
 * 各个实现类上都没有@Adaptive
 * SPI上有注解@SPI("b"),
 * url无参数
 * 则以@SPI("b")为准,选择InfoServiceBImpl 实现中的方法进行静态代理
 */
@Test
public void test1() {
    ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
    InfoService adaptiveExtension = loader.getAdaptiveExtension();
    URL url = URL.valueOf("test://localhost/test");
    System.out.println(adaptiveExtension.passInfo("yoyo", url));
}

测试结果如下:
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_08


【情况2】各个实现类上都没有标注@Adaptive注解, 接口的SPI注解为@SPI(“b”),URL中有具体的值info.service=a
注意:url中的info.service是按照接口名称InfoService拆解的结果,dubbo会按此规则找到InfoService接口别名为a的实现类,并对此实现类中的方法进行静态代理

/**
 * 各个实现类上都没有@Adaptive
 * SPI上有注解@SPI("b")
 * URL中有具体的值info.service=a,
 * 则以URL为准,选择InfoServiceAImpl 实现中的方法进行静态代理
 */
@Test
public void test2() {
    ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
    InfoService adaptiveExtension = loader.getAdaptiveExtension();
    URL url = URL.valueOf("test://localhost/test?info.service=a");
    System.out.println(adaptiveExtension.passInfo("james", url));
}

测试结果如下:
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_09


【情况3】各个实现类上都没有标注@Adaptive注解, 接口方法中加上的注解为@Adaptive({“NRSC”}),URL中有具体的值为NRSC=c

  • 此时InfoService接口应为:
@SPI("b") //指定缺省实现类的别名
public interface InfoService {
    Object sayHello(String name);

    @Adaptive({"NRSC"})
    //@Adaptive
    Object passInfo(String msg, URL url);
}
  • 测试类为:
/**
 * 各个实现类上面没有@Adaptive
 * 接口方法中加上的注解为@Adaptive({"NRSC"}),
 * URL中有具体的值NRSC=c,
 * 则以URL中的NRSC参数为准,选择InfoServiceCImpl 实现中的方法进行静态代理
 */
@Test
public void test3() {
    ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
    InfoService adaptiveExtension = loader.getAdaptiveExtension();
    URL url = URL.valueOf("test://localhost/test?info.service=a&NRSC=c");
    System.out.println(adaptiveExtension.passInfo("james", url));
}
  • 测试结果如下:

【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_10
由此可知这种方式其实是对 【情况2】的一种更优雅的实现!!!


【情况4】InfoServiceCImpl实现类上有@Adaptive, 接口方法中加上的注解为@Adaptive({“NRSC”}),URL随意传值

/**
 * InfoServiceCImpl实现类上有@Adaptive,URL随意传值
 * 直接会拿到InfoServiceCImpl的实现类 ---》 并未做静态代理
 */
@Test
public void test4() {
    ExtensionLoader<InfoService> loader = ExtensionLoader.getExtensionLoader(InfoService.class);
    InfoService adaptiveExtension = loader.getAdaptiveExtension();
    URL url = URL.valueOf("test://localhost/test");
    System.out.println(adaptiveExtension.passInfo("james", url));
}

测试结果:
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_11


2.3 简单撸一下源码,看看为啥 @Adaptive标注在方法上会静态代理该方法,标注在类上却不会走静态代理

打着断点可以看到源码中ExtensionLoader中创建具体实现类的关键代码如下:
【dubbo源码解析】 --- dubbo spi 机制(@SPI、@Adaptive)详解_dubbo_12
接下来我们看一下接口的任何实现类都没有标注@Adapter注解时,dubbo为我们拼接的代理类,即上图中code对应的字符串,将其格式化后如下:

package com.nrsc.service;

import org.apache.dubbo.common.extension.ExtensionLoader;


public class InfoService$Adaptive implements com.nrsc.service.InfoService {
    public java.lang.Object passInfo(java.lang.String arg0,
        org.apache.dubbo.common.URL arg1) {
        if (arg1 == null) {
            throw new IllegalArgumentException("url == null");
        }

        org.apache.dubbo.common.URL url = arg1;
        String extName = url.getParameter("NRSC", "b");

        if (extName == null) {
            throw new IllegalStateException(
                "Failed to get extension (com.nrsc.service.InfoService) name from url (" +
                url.toString() + ") use keys([NRSC])");
        }
		//通过别名找到具体的实现类
        com.nrsc.service.InfoService extension = (com.nrsc.service.InfoService) ExtensionLoader.getExtensionLoader(com.nrsc.service.InfoService.class)
                                                                                               .getExtension(extName);
		//拿着实现类调用方法
        return extension.passInfo(arg0, arg1);
    }
	
	//未标注@Adapter注解的方法无法被调用
    public java.lang.Object sayHello(java.lang.String arg0) {
        throw new UnsupportedOperationException(
            "The method public abstract java.lang.Object com.nrsc.service.InfoService.sayHello(java.lang.String) of interface com.nrsc.service.InfoService is not adaptive method!");
    }
}

看到这段字符串,再联系一下上篇文章《【dubbo源码解析】— 通过javassist/JavassistCompiler动态生成一个实例对象》,是不是就可以很清楚的明白下面的结论了:

  • @Adapter标签标注在方法上可以对该方法进行静态代理
  • 如果接口的某个实现类上标注了@Adapter注解,将直接调用该实现类,而不会进行静态代理

end!!!