你好,我是田哥
上一篇文章:在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
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
内容:
wechat=com.tian.spi.WechatPay
alipay=com.tian.spi.AliPay
运行项目:
我们再切换成wechat,运行结果:
到此,我们自定义配置文件目录已经搞定。
尽管我们很少这么用,但是咱们不能说不知道,万一有天面试官抽风问你这个问题,你也能回答上来噻。
面试官问:dubbo3.0版本加入的新功能,你知道哪些?这里不就是新功能了吗?
自适应扩展点
在运行期间,根据上下文来决定当前返回哪个扩展点。
关键注解:@Adaptive
- 如果修饰在类级别,那么直接返回修饰的类
- 如果修饰在方法界别,动态创建一个代理类(javassist)
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
类,内容如下:
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$Adaptive
是com.tian.spi.PersonService
的子类。
上面这个类的内容,可以debug模式到
ExtensionLoader
中的createAdaptiveExtensionClass()
方法里。
Xxx$Adaptive
简要说明
PersonService$Adaptive
的queryCountry()
方法主要内容:
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
去我们的配置文件里找。
beijing=com.tian.spi.BeijingPersonServiceImpl
guangdong=com.tian.spi.GuangdongPersonServiceImpl
最后返回一个实现类实例。
3、调用extension
的queryCountry()
方法,也就是调用我们具体实现类的方法。
搞清楚上面这套规则后,你就再也不用去关心
Xxx$Adaptive
里的内容了。
之前,我也刻意去B站上找了Dubbo
源码分析的视频看看,结果,哎,很多乱七八糟的,Dubbo SPI
机制都没有搞清楚,上来就瞎讲,然后就是各种猜测,我们猜测会调用哪个类?搞清楚上面这些SPI机制后,我们还需要猜吗?那不是一眼就能看出来吗?
后记
想深入学习Dubbo
,Dubbo
的SPI
机制是一定要拿捏住,否则你很快就在源码里晕车了。