前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Dubbo的spi机制分析和实战案例

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

作者头像
田维常
发布2022-11-25 14:38:18
2550
发布2022-11-25 14:38:18
举报

你好,我是田哥

上一篇文章:在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机制是一定要拿捏住,否则你很快就在源码里晕车了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-09-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java后端技术全栈 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 技术点
    • 什么是SPI
      • Dubbo SPI 入门
        • 实现步骤
          • 关于配置文件
            • 自定义扩展点配置文件目录
              • 自适应扩展点
              • 后记
              领券
              问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档