Dubbo的spi机制分析和实战案例

2022-11-25 14:38:18 浏览数 (1)

你好,我是田哥

上一篇文章:在Dubbo中,模板方法模式 用的真6!

留下来一个问题,想深入学习Dubbo源码,你需要具备哪些技术点。

技术点

  • Spring xml自定义标签 或 通过@DubboComponentScan("con.tian.dubbo.service")扫描@DubboService注解
  • 设计模式:模板方法模式、装饰器模式、责任链模式、代理模式、工厂模式
  • Netty基本知识:创建服务端和客户端,handler,编解码,序列化
  • Dubbo SPI 机制

之前,已经聊过模板方法模式,本文咱们来聊聊Dubbo中的SPI机制,其他相关的我会逐个分享出来的。

为什么把SPI放在前面,主要是这个Dubbo的SPI机制实在是太重要了。

什么是SPI

SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的,它可以用来启用框架扩展和替换组件。

当服务的提供者(provider),提供了一个接口多种实现时, 一般会在jar包的META-INF/services/目录下,创建该接口的同名文件。 该文件里面的内容就是该服务接口的具体实现类的名称。而当外部加载这个模块的时候, 就能通过该jar包META-INF/services/里的配置文件得到具体的实现类名,并加载实例化,完成模块的装配。

如果对JDK自带的SPI机制还不是很熟悉的,请先去熟悉一下,本文就不再赘述了。因为你看到文章标题,就应该有点SPI的基础。

Dubbo SPI 入门

我们平时使用手机支付时,通常都会选择支付宝支付或者微信支付。

我们用代码演示:

代码语言:javascript复制
@SPI("wechat")
public interface Pay {
    String way();
}
public class AliPay implements Pay {
    @Override
    public String way() {
        System.out.println("我正在使用 支付宝 支付");
        return "Alipay";
    }
}
public class WechatPay implements Pay {
    @Override
    public String way() {
        System.out.println("我正在使用 微信 支付");
        return "wechat";
    }
}

测试类:

代码语言:javascript复制
public class PayDemo {
    public static void main(String[] args) {
        // 获取到用于加载Order类型扩展类实例的extensionLoader实例
        ExtensionLoader<Pay> loader = ExtensionLoader.getExtensionLoader(Pay.class);
        //如果loader.getDefaultExtension()返回的是
        //@SPI("wechat")注解中默认值wechat对应的WechatPay
        Pay alipay = loader.getExtension("alipay");
        System.out.println(alipay.way());
    }
}

输出:

代码语言:javascript复制
我正在使用 支付宝 支付
Alipay
实现步骤

1、定义一个接口,然后在接口上加上 @SPI注解

2、写好实现类

3、创建好META-INFO/dubbo文件夹,并在该目录下创建好一个文件,文件名=接口全路径名称

4、把我们的实现类配置在上面的文件里,以key=value形式。key自定义名称,value就是我们对应实现类名全路径名称。

5、通过ExtensionLoader<Pay> loader = ExtensionLoader.getExtensionLoader(Pay.class);加载配置文件

6、通过指定名称loader.getExtension("alipay");获取对应实现类的实例(其实是经过多层包装的实现类,后面再细说)

7、调用实现类的方法

关于配置文件

dubbo在3.0版本之前,我们的配置文件只能在下面三个路径下:

  • META-INF/dubbo/internal :该目录存放 Dubbo 内部使用的 SPI 配置文件
  • META-INF/dubbo :该目录存放用户自定义的 SPI 配置文件
  • META-INF/services:该目录下的 SPI 配置文件是为了用来兼容 Java SPI

并且是按照上面顺序来加载。

dubbo3.0 版本后,我们就可以自定义类配置文件目录了。

自定义扩展点配置文件目录

想自定义目录,需要实现接口:org.apache.dubbo.common.extension.LoadingStrategy

我们通过类关系图,可以看到dubbo有三个实现类,从名字就看出和上面的三个META-INF下的三个目录名称一样。

下面,我们来自定义实现类:com.tian.spi.TianLoadingStrategy

代码语言:javascript复制
public class TianLoadingStrategy implements LoadingStrategy {
    @Override
    public String directory() {
        //我们自定义目录
        return "META-INF/tian/";
    }
    @Override
    public boolean overridden() {
        return true;
    }

    @Override
    public int getPriority() {
        return 100;
    }

    @Override
    public String getName() {
        return "TIAN";
    }
}

上面的三个路径中,META-INF/services目录是用来兼容JavaSPI的,所以我们需要这么干。

在自己的项目中,META-INF/services目录下见一个文件:

org.apache.dubbo.common.extension.LoadingStrategy

然后把我们的实现类全路径名称放进去(这里用的是Java的SPI机制)。

我们在resources目录下建一个目录:META-INF/tian

com.tian.spi.Pay内容:

代码语言:javascript复制
wechat=com.tian.spi.WechatPay
alipay=com.tian.spi.AliPay

运行项目:

我们再切换成wechat,运行结果:

到此,我们自定义配置文件目录已经搞定。

尽管我们很少这么用,但是咱们不能说不知道,万一有天面试官抽风问你这个问题,你也能回答上来噻。

面试官问:dubbo3.0版本加入的新功能,你知道哪些?这里不就是新功能了吗?

自适应扩展点

在运行期间,根据上下文来决定当前返回哪个扩展点。

关键注解:@Adaptive

  • 如果修饰在类级别,那么直接返回修饰的类
  • 如果修饰在方法界别,动态创建一个代理类(javassist)
代码语言:javascript复制
ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
@Adaptive 在类上

我们继续使用上面的案例进行演示,新增一个类:AdaptivePay

代码语言:javascript复制
@Adaptive
public class AdaptivePay implements Pay {
    private String defaultName;

    // 指定要加载扩展类的名称
    public void setDefaultName(String defaultName) {
        this.defaultName = defaultName;
    }

    @Override
    public String way() {
        System.out.println("======进入 AdaptivePay way方法=====");
        ExtensionLoader<Pay> loader = ExtensionLoader.getExtensionLoader(Pay.class);
        Pay pay;
        if (StringUtils.isEmpty(defaultName)) {
            // 加载SPI默认名称的扩展类
            pay = loader.getDefaultExtension();
        } else {
            // 加载指定名称的扩展类
            pay = loader.getExtension(defaultName);
        }
        return pay.way();
    }
}

测试类:

代码语言:javascript复制
public class PayDemo {
    public static void main(String[] args) {
        Pay pay = ExtensionLoader.getExtensionLoader(Pay.class).getAdaptiveExtension(); 
        System.out.println(pay.way());
    }
}

输出结果:

代码语言:javascript复制
======进入 AdaptivePay way方法=====
我正在使用 微信 支付
wechat

这里,证明了返回的就是加有注解@Adaptive的实现类。

@Adaptive 修饰方法

下面来看案例。

代码语言:javascript复制
//默认是guangdong
@SPI("guangdong")
public interface PersonService {
    @Adaptive
    String queryCountry(URL url);
}
//实现类1
public class BeijingPersonServiceImpl implements PersonService {
    @Override
    public String queryCountry(URL url) {
        System.out.println("北京人");
        return "北京人";
    }
}
//实现类2
public class GuangdongPersonServiceImpl implements PersonService {
    @Override
    public String queryCountry(URL url) {
        System.out.println("广东人");
        return "广东人";
    }
}

META-INF/dubbo目录下新建文件:

com.tian.spi.PersonService

内容:

代码语言:javascript复制
guangdong=com.tian.spi.GuangdongPersonServiceImpl
beijing=com.tian.spi.BeijingPersonServiceImpl

测试类:

代码语言:javascript复制
public class PersonTest {
    public static void main(String[] args) {
        URL  url = URL.valueOf("dubbo://192.168.0.101:20880?person.service=guangdong");
        PersonService service = ExtensionLoader.getExtensionLoader(PersonService.class)
                .getAdaptiveExtension();
        service.queryCountry(url);
    }
}

运行结果:广东人

这个过程中会生成一个动态类,比如上面这个案例就会生成一个PersonService$Adaptive类,内容如下:

代码语言:javascript复制
import org.apache.dubbo.common.extension.ExtensionLoader; 
public class PersonService$Adaptive implements com.tian.spi.PersonService {
    public java.lang.String queryCountry(org.apache.dubbo.common.URL arg0)  {
        if (arg0 == null) throw new IllegalArgumentException("url == null");
        
        org.apache.dubbo.common.URL url = arg0;
        
        String extName = url.getParameter("person.service", "guangdong");
        
        if(extName == null) throw new IllegalStateException("Failed to get extension (com.tian.spi.PersonService) name from url ("   url.toString()   ") use keys([person.service])");
        
        com.tian.spi.PersonService extension = (com.tian.spi.PersonService)ExtensionLoader.getExtensionLoader(com.tian.spi.PersonService.class).getExtension(extName);
        return extension.queryCountry(arg0);
    }
} 

从类的定义可以看出:PersonService$Adaptivecom.tian.spi.PersonService的子类。

上面这个类的内容,可以debug模式到ExtensionLoader中的createAdaptiveExtensionClass()方法里。

Xxx$Adaptive简要说明

PersonService$AdaptivequeryCountry()方法主要内容:

1、获取扩展名称

代码语言:javascript复制
//通过"person.service"去URL中找,如果没有就用默认值guangdong。
String extName = url.getParameter("person.service", "guangdong");

这里的getParameter("person.service", "guangdong");中的参数person.service,这个一定要搞清楚是怎么来的,否则,看Dubbo源码基本上都会晕车。

注意: 如果 @Adaptive没有指定默认值,那么此时这个参数就是一个当前接口名称转换来的,比如:PersonService转换来就是person.service。再比如当前接口是Protocol,那么此时参数名称就是protocol。 如果@Adaptive("xxx")给了默认值是xxx,那么此时的参数就是xxx

2、通过扩展名称获取具体实现实例

代码语言:javascript复制
com.tian.spi.PersonService extension = (com.tian.spi.PersonService)ExtensionLoader.getExtensionLoader(com.tian.spi.PersonService.class).getExtension(extName);

拿着这个extName去我们的配置文件里找。

代码语言:javascript复制
beijing=com.tian.spi.BeijingPersonServiceImpl
guangdong=com.tian.spi.GuangdongPersonServiceImpl

最后返回一个实现类实例。

3、调用extensionqueryCountry()方法,也就是调用我们具体实现类的方法。

搞清楚上面这套规则后,你就再也不用去关心Xxx$Adaptive里的内容了。

之前,我也刻意去B站上找了Dubbo源码分析的视频看看,结果,哎,很多乱七八糟的,Dubbo SPI机制都没有搞清楚,上来就瞎讲,然后就是各种猜测,我们猜测会调用哪个类?搞清楚上面这些SPI机制后,我们还需要猜吗?那不是一眼就能看出来吗?

后记

想深入学习DubboDubboSPI机制是一定要拿捏住,否则你很快就在源码里晕车了。

0 人点赞