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

你好,我是田哥

上一篇文章:在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 入门

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

我们用代码演示:

@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";
    }
}

测试类:

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());
    }
}

输出:

我正在使用 支付宝 支付
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下的三个目录名称一样。

b8ab76f30702e4c32331179596f25d84.png

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

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机制)。

e2f59c7dd7f0fb849ad68ef91e8652ac.png

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

41947a6db999b20e336ca917ab5d746e.png

com.tian.spi.Pay内容:

wechat=com.tian.spi.WechatPay
alipay=com.tian.spi.AliPay

运行项目:

de134bad338b5e86f5954093051a99d9.png

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

2e0389405b80d22a8d07447e5187e342.png

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

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

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

自适应扩展点

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

关键注解:@Adaptive

  • 如果修饰在类级别,那么直接返回修饰的类

  • 如果修饰在方法界别,动态创建一个代理类(javassist)

ExtensionLoader.getExtensionLoader(Protocol.class).getAdaptiveExtension();
@Adaptive 在类上

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

@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();
    }
}

测试类:

public class PayDemo {
    public static void main(String[] args) {
        Pay pay = ExtensionLoader.getExtensionLoader(Pay.class).getAdaptiveExtension(); 
        System.out.println(pay.way());
    }
}

输出结果:

======进入 AdaptivePay way方法=====
我正在使用 微信 支付
wechat

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

@Adaptive 修饰方法

下面来看案例。

//默认是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

内容:

guangdong=com.tian.spi.GuangdongPersonServiceImpl
beijing=com.tian.spi.BeijingPersonServiceImpl

测试类:

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类,内容如下:

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、获取扩展名称

//通过"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、通过扩展名称获取具体实现实例

com.tian.spi.PersonService extension = (com.tian.spi.PersonService)ExtensionLoader.getExtensionLoader(com.tian.spi.PersonService.class).getExtension(extName);

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

beijing=com.tian.spi.BeijingPersonServiceImpl
guangdong=com.tian.spi.GuangdongPersonServiceImpl

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

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

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

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

后记

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

给大家推荐一个在线刷面试题的网站:

http://woaijava.cc/mianshi/index

月薪不到25K,面试题必须背!!

推荐

疯狂刷题,成功入职 搜狐!

面试官:你知道多少种索引?

坚持一年,成功进入前50名!

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

田哥coder

你的鼓励是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值