https://blog.csdn.net/mubu520/article/details/104209608
https://blog.csdn.net/yangbaggio/article/details/97617750
https://www.jianshu.com/p/96917c6c90fb
https://www.jianshu.com/p/dc616814ce98
https://blog.csdn.net/weixin_33967071/article/details/92608993
SPI
dubbo
SPI(Service Provider Interface)
- 本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。
- 在Java中SPI是被用来设计给服务提供商做插件使用的。基于策略模式 来实现动态加载的机制 。我们在程序只定义一个接口,具体的实现交个不同的服务提供者;在程序启动的时候,读取配置文件,由配置确定要调用哪一个实现;
- 通过 SPI 机制为我们的程序提供拓展功能,在dubbo中,基于 SPI,我们可以很容易的对 Dubbo 进行拓展。例如dubbo当中的protocol,LoadBalance等都是通过SPI机制扩展。
想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们了解下JAVA SPI与dubbo SPI的用法,再分析DUBBO SPI的源码,本文的dubbo源码是基于2.7.5版本。
JAVA 原生SPI 示例
- 先简单介绍JAVA SPI的应用。首先,我们定义一个Car接口
public interface Car {
String getBrand();
}
- 定义该接口的两个实现类。
public class BM implements Car {
public String getBrand() {
System.out.println("BM car");
return "BM";
}
}
public class Benz implements Car {
public String getBrand() {
System.out.println("benz car");
return "Benz";
}
}
- 再在resources下创建META-INF/services 文件夹,并创建一个文件,文件名称为Car接口的全限定名com.dubbo.dp.spi.Car。内容为接口实现类的全限定类名。
com.dubbo.dp.spi.Benz
com.dubbo.dp.spi.BM
- 使用如下,调用Car接口在配置文件中的所有实现类。
public class JavaSPITest {
@Test
public void sayHello() throws Exception {
ServiceLoader<Car> serviceLoader = ServiceLoader.load(Car.class);
serviceLoader.forEach(car -> System.out.println(car.getBrand()));
}
}
JAVA SPI实现了接口的定义与具体业务实现解耦,应用进程可以根据实际业务情况启用或替换具体组件。
举例:JAVA的java.sql包中就定义一个接口Driver,各个服务提供商实现该接口。当我们需要使用某个数据库时就导入相应的jar包。
- 不能按需加载。Java SPI在加载扩展点的时候,会一次性加载所有可用的扩展点,很多是不需要的,会浪费系统资源;
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 不支持AOP与依赖注入。
- JAVA SPI可能会丢失加载扩展点异常信息,导致追踪问题很困难;
dubbo SPI示例
- dubbo重新实现了一套功能更强的 SPI 机制,支持了AOP与依赖注入,并且利用缓存提高加载实现类的性能,同时支持实现类的灵活获取,文中接下来将讲述SPI的应用与原理。
Dubbo的SPI接口都会使用@SPI注解标识,该注解的主要作用就是标记这个接口是一个SPI接口。源码如下:
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface SPI {
/**
* default extension name
* 设置默认拓展类
*/
String value() default "";
}
该注解只作用在接口上,value用来设置默认拓展类
- 首先讲解下dubbo SPI的使用。在上述Car接口加上@SPI注解,它的实现类暂时不变,配置文件的路径与文件名也暂时不变,文件内容调整如下:
@SPI
public interface Car {
String getBrand();
}
benz=com.dubbo.dp.spi.Benz
bm=com.dubbo.dp.spi.BM
配置文件通过键值对的方式进行配置,这样我们可以按需加载指定的实现类。使用如下:
public class JavaSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Car> carExtensionLoader = ExtensionLoader.getExtensionLoader(Car.class);
//按需获取实现类对象
Car car = carExtensionLoader.getExtension("benz");
System.out.println(car.getBrand());
}
}
输出结果为
benz car
Benz
----------------------------------------------------------------------------------------------------------------------------------------------------------
adapative-介绍
Dubbo提供了一种SPI的机制用于动态的加载扩展类,但是如何在运行时动态的选用哪一种扩展类来提供服务,这就需要一种机制来进行动态的匹配。Dubbo SPI中提供的Adaptive机制就为解决这个问题提供了一种良好的解决方案,本文首先会通过一个示例来讲解Adaptive机制的用法,然后会从源码的角度对其实现原理进行讲解。
1. 用法示例
对应于Adaptive机制,Dubbo提供了一个注解@Adaptive
,该注解可以用于接口的某个子类上,也可以用于接口方法上。如果用在接口的子类上,则表示Adaptive机制的实现会按照该子类的方式进行自定义实现;如果用在方法上,则表示Dubbo会为该接口自动生成一个子类,并且按照一定的格式重写该方法,而其余没有标注@Adaptive
注解的方法将会默认抛出异常。对于第一种Adaptive的使用方式,Dubbo里只有ExtensionFactory
接口使用了,其有一个子类AdaptiveExtensionFactory
就使用了@Adaptive
注解进行了标注,主要作用就是在获取目标对象时,分别通过ExtensionLoader
和Spring容器
两种方式获取,该类的实现原理比较简单,读者可自行阅读其源码,本文主要讲解将@Adaptive
注解标注在接口方法上以实现Adaptive机制的使用原理。这里我们以一个"水果种植者"的示例来进行讲解,水果种植者可以种植诸如苹果和香蕉等水果。这里我们首先定义一个苹果种植者的接口:
@SPI("apple")
public interface FruitGranter {
Fruit grant();
@Adaptive
String watering(URL url);
}
这里需要注意的是,如果要使用Dubbo的SPI的支持,必须在目标接口上使用@SPI
注解进行标注,后面的值提供了一个默认值,也就是说如果没有自定义的指定使用哪个子类,那么就使用该值所指定的子类。在该接口中,我们将watering()
方法使用@Adaptive
注解进行了标注,表示该方法在自动生成的子类中是需要动态实现的方法。下面是我们为FruitGranter
提供的两个实现类:
// 苹果种植者
public class AppleGranter implements FruitGranter {
@Override
public Fruit grant() {
return new Apple();
}
@Override
public String watering(URL url) {
System.out.println("watering apple");
return "watering finished";
}
}
// 香蕉种植者
public class BananaGranter implements FruitGranter {
@Override
public Fruit grant() {
return new Banana();
}
@Override
public String watering(URL url) {
System.out.println("watering banana");
return "watering success";
}
}
这里提供了AppleGranter
和BananaGranter
表示的其实是两种基础服务类,本质上它们三者的关系是FruitGranter
用于对外提供一个规范,而AppleGranter
和BananaGranter
则是实现了这种规范的两种基础服务。至于调用方需要使用哪种基础服务来实现其功能,这就需要根据调用方指定的参数来动态选取的,而@Adaptive
机制就是提供了这样一种选取功能。
在Dubbo的SPI中,我们指定了上述两种服务类之后,需要在META-INF/dubbo
下创建一个文件,该文件的名称是目标接口的全限定名,这里是org.apache.dubbo.demo.example.eg19.FruitGranter
,在该文件中需要指定该接口所有可提供服务的子类,形式如:
apple=org.apache.dubbo.demo.example.eg19.AppleGranter
banana=org.apache.dubbo.demo.example.eg19.BananaGranter
文件中每一个子类都有一个key与之对应,这个key也就是前面@SPI
注解后面所指定的值,也就是说这里如果调用方没有自定义指定使用哪个子类,那么默认就会使用AppleGranter
来提供服务。下面我们来看一下调用方代码如何实现:
public class ExtensionLoaderTest {
@Test
public void testGetExtensionLoader() {
// 首先创建一个模拟用的URL对象
URL url = URL.valueOf("dubbo://192.168.0.101:20880?fruit.granter=apple");
// 通过ExtensionLoader获取一个FruitGranter对象
FruitGranter granter = ExtensionLoader.getExtensionLoader(FruitGranter.class)
.getAdaptiveExtension();
// 使用该FruitGranter调用其"自适应标注的"方法,获取调用结果
String result = granter.watering(url);
System.out.println(result);
}
}
上述代码中,我们首先模拟构造了一个URL对象,这个URL对象是Dubbo中进行参数传递所使用的一个基础类,在配置文件中配置的属性都会被封装到该对象中。这里我们主要要注意该对象是通过一个url构造的,并且url的最后我们有一个参数fruit.granter=apple
,这里其实就是我们所指定的使用哪种基础服务类的参数。比如这里指定的就是使用apple
对应的AppleGranter
。
在构造一个URL对象之后,我们通过ExtensionLoader.getExtensionLoader(FruitGranter.class)
方法获取了一个FruitGranter
对应的ExtensionLoader
对象,然后调用其getAdaptiveExtension()
方法获取其为FruitGranter
接口构造的子类实例,这里的子类实际上就是ExtensionLoader
通过一定的规则为FruitGranter
接口编写的子类代码,然后通过javassist
或jdk
编译加载这段代码,加载完成之后通过反射构造其实例,最后将其实例返回。在上面我们调用该实例,也就是granter对象的watering()
方法时,该方法内部就会通过url对象指定的参数来选择具体的实例,从而将真正的工作交给该实例进行。通过这种方式,Dubbo SPI就实现了根据传入参数动态的选用具体的实例来提供服务的功能。如下是该ExtensionLoader
为FruitGranter
动态生成的子类代码:
package org.apache.dubbo.demo.example.eg19;
import org.apache.dubbo.common.extension.ExtensionLoader;
public class FruitGranter$Adaptive implements org.apache.dubbo.demo.example.eg19.FruitGranter {
public org.apache.dubbo.demo.example.eg19.Fruit grant() {
throw new UnsupportedOperationException(
"The method public abstract org.apache.dubbo.demo.example.eg19.Fruit "
+ "org.apache.dubbo.demo.example.eg19.FruitGranter.grant() of interface "
+ "org.apache.dubbo.demo.example.eg19.FruitGranter is not adaptive method!");
}
public java.lang.String watering(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("fruit.granter", "apple");
if (extName == null) {
throw new IllegalStateException(
"Failed to get extension (org.apache.dubbo.demo.example.eg19.FruitGranter) name "
+ "from url (" + url.toString() + ") use keys([fruit.granter])");
}
org.apache.dubbo.demo.example.eg19.FruitGranter extension =
(org.apache.dubbo.demo.example.eg19.FruitGranter) ExtensionLoader
.getExtensionLoader(org.apache.dubbo.demo.example.eg19.FruitGranter.class)
.getExtension(extName);
return extension.watering(arg0);
}
}
关于该生成的代码,我们主要要注意如下几个问题:
- 所有未使用
@Adaptive
注解标注的接口方法,默认都会抛出异常; - 在使用
@Adaptive
注解标注的方法中,其参数中必须有一个参数类型为URL,或者其某个参数提供了某个方法,该方法可以返回一个URL对象; - 在方法的实现中会通过URL对象获取某个参数对应的参数值,如果在接口的
@SPI
注解中指定了默认值,那么在使用URL对象获取参数值时,如果没有取到,就会使用该默认值; - 最后根据获取到的参数值,在
ExtensionLoader
中获取该参数值对应的服务提供类对象,然后将真正的调用委托给该服务提供类对象进行; - 在通过URL对象获取参数时,参数key获取的对应规则是,首先会从
@Adaptive
注解的参数值中获取,如果该注解没有指定参数名,那么就会默认将目标接口的类名转换为点分形式作为参数名,比如这里FruitGranter
转换为点分形式就是fruit.granter
。
adapative-案例:
现在我们再来回顾一下dubbo SPI的简单使用,代码如下:
// 1、先获取 extensionLoader
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
// 2、根据名称获取对应的扩展点instance
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
// 3、调用对应扩展点instance的方法
optimusPrime.sayHello();
这段代码演示了dubbo SPI的使用,但是有一个问题,扩展点对应的实现类不能在程序运行时动态指定,就是extensionLoader.getExtension
方法写死了扩展点对应的实现类,不能在程序运行期间根据运行时参数进行动态改变。
对于这个矛盾的问题,就有引入我们今天要将的Adaptive
,就是dubbo 的自适应机制。Dubbo 通过自适应拓展机制很好的解决了。我们首先看一下如果是我们,怎么解决这个矛盾,下面给出示例:
- 这里先定义一个接口,取名叫
AdaptiveExt
, 这里需要加一个com.alibaba.dubbo.common.extension.SPI
的注解
@SPI
public interface AdaptiveExt {
String echo(String msg, URL url);
}
这里的URL
类为dubbo里面的URL
- 接着创建一个实现类
SpringAdaptiveExtImpl
,按照dubbo SPI 配置,配置好
public class SpringAdaptiveExtImpl implements AdaptiveExt {
@Override
public String echo(String msg, URL url) {
return "spring";
}
}
- 重点来了,这一步我们解决扩展点对应的实现类不能在程序运行时动态指定的问题,这里我们自己编写一个
AdaptiveExt
自适应实现类,如下:
public class AdaptiveExtProxy implements AdaptiveExt{
@Override
public String echo(String msg, URL url) {
// 非空检查
if (url == null) {
throw new IllegalArgumentException("url == null");
}
// 1、从 URL 中获取 AdaptiveExt 名称
String name = url.getParameter("adaptive.ext");
if (name == null ) {
throw new IllegalArgumentException("name == null");
}
// 2、调用 SPI 动态加载具体的 AdaptiveExt
AdaptiveExt adaptiveExt = ExtensionLoader
.getExtensionLoader(AdaptiveExt.class)
.getExtension(name);
// 3、调用具体的方法
return adaptiveExt.echo(msg, url);
}
}
AdaptiveExtProxy
其实是一个代理类,AdaptiveExtProxy
所代理的对象是在echo
方法中通过SPI加载得到的,echo
方法主要做了三件事情:
- 从 URL 中获取
AdaptiveExt
名称 - 通过 SPI 加载具体的
AdaptiveExt
实现类 - 调用目标方法
- 进行测试,测试代码如下:
@Test
public void test() {
URL url = URL.valueOf("test://localhost/test?adaptive.ext=spring");
AdaptiveExt adaptiveExt = new AdaptiveExtProxy();
System.out.println(adaptiveExt.echo("e", url));
}
输出结果:
spring
Process finished with exit code 0
到这里,我们自己基本上解决了上面所说的矛盾。大家大致知道 dubbo SPI 扩展点对应的实现类,在程序运行期间根据运行时参数进行动态改变的解决方案了。就是设置一个代理类,代理类根据程序运行期间URL的参数,动态加载响应的实现类并代理。
dubbo Adaptive的使用
刚才我们简单讲了dubbo的自适应拓展机制是啥,现在我们开学习dubbo 自适应机制的学习。dubbo 自适应机制的使用是通过注解的方式,注解为com.alibaba.dubbo.common.extension.Adaptive
。直接上代码示例:
- 首先定义一个接口,名称为
AdaptiveExt
,使用SPI
和Adaptive
修饰:
@SPI
public interface AdaptiveExt {
@Adaptive
String echo(String msg, URL url);
}
- 创建三个实现类,分别为
DubboAdaptiveExtImpl
、SpringAdaptiveExtImpl
和SpringBootAdaptiveExtImpl
public class DubboAdaptiveExtImpl implements AdaptiveExt {
@Override
public String echo(String msg, URL url) {
return "dubbo";
}
}
public class SpringAdaptiveExtImpl implements AdaptiveExt {
@Override
public String echo(String msg, URL url) {
return "spring";
}
}
public class SpringBootAdaptiveExtImpl implements AdaptiveExt{
@Override
public String echo(String msg, URL url) {
return "spring boot";
}
}
- 接着在META-INF/dubbo文件夹下创建一个文件,名称为
Robot
的全限定名com.hui.wang.dubbo.learn.dubbo.adaptive.AdaptiveExt
,内容如下:
dubbo=com.hui.wang.dubbo.learn.dubbo.adaptive.DubboAdaptiveExtImpl
spring=com.hui.wang.dubbo.learn.dubbo.adaptive.SpringAdaptiveExtImpl
springboot=com.hui.wang.dubbo.learn.dubbo.adaptive.SpringBootAdaptiveExtImpl
到这里,示例的基本代码搭建完成,现在开始进入使用和测试
- 在URL里面指定参数,代码为:
@Test
public void test1() {
ExtensionLoader<AdaptiveExt> extExtensionLoader = ExtensionLoader.getExtensionLoader(AdaptiveExt.class);
AdaptiveExt adaptiveExt = extExtensionLoader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test?adaptive.ext=spring");
System.out.println(adaptiveExt.echo("d", url));
}
打印结果为:
spring
可以看出,当URL中参数指定相应的扩展点实现类名称后,就可以根据名称动态的调用响应名称的实现类。
- 配置
Adaptive
注解的value
值,代码如下:
@SPI
public interface AdaptiveExt {
@Adaptive(value = {"myAdaptiveName"})
String echo(String msg, URL url);
}
测试代码:
@Test
public void test4() {
ExtensionLoader<AdaptiveExt> extExtensionLoader = ExtensionLoader.getExtensionLoader(AdaptiveExt.class);
AdaptiveExt adaptiveExt = extExtensionLoader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test?myAdaptiveName=spring");
System.out.println(adaptiveExt.echo("d", url));
}
打印结果为:
spring
在方法上打上@Adaptive
注解,注解中的value
与链接中的参数的key
一致,链接中的key
对应的value
就是spi中的name
,获取相应的实现类。
3.@Adaptive
注解使用在实现类上,代码如下:
@Adaptive
public class DubboAdaptiveExtImpl implements AdaptiveExt {
@Override
public String echo(String msg, URL url) {
return "dubbo";
}
}
测试代码:
@Test
public void test4() {
ExtensionLoader<AdaptiveExt> extExtensionLoader = ExtensionLoader.getExtensionLoader(AdaptiveExt.class);
AdaptiveExt adaptiveExt = extExtensionLoader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test?myAdaptiveName=spring");
System.out.println(adaptiveExt.echo("d", url));
}
打印结果为:
dubbo
这里可以看到实现类上有@Adaptive
注解后,默认调用该类进行调用。