前言
Github:https://github.com/HealerJean
SPI 全称为
Service Provider Interface
,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。在 Dubbo 中,SPI 是一个非常重要的模块。基于 SPI,我们可以很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,我们先来了解一下 Java SPI 与 Dubbo SPI 的用法,然后再来分析 Dubbo SPI 的源码。
1、SPI 实例
1.1、Java SPI
1.1.1、接口**Robot
**
public interface Robot {
void sayHello();
}
1.1.1.1、实现类OptimusPrime
@Slf4j
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
log.info("Hello, I am Optimus Prime.");
}
}
1.1.1.2、实现类Bumblebee
@Slf4j
public class Bumblebee implements Robot {
@Override
public void sayHello() {
log.info("Hello, I am Bumblebee.");
}
}
1.1.2、 接口全限名配置文件
resources -> META-INFO ->services
com.healerjean.proj.study.spi.OptimusPrime
com.healerjean.proj.study.spi.Bumblebee
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MTOIpGAu-1594897494549)(https://raw.githubusercontent.com/HealerJean/HealerJean.github.io/master/blogImages/image-20200627155645060.png)]
1.1.3、Main方法测试
@Slf4j
public class JavaSPITest {
@Test
public void sayHello() {
ServiceLoader<Robot> serviceLoader = ServiceLoader.load(Robot.class);
log.info("Java SPI--------");
serviceLoader.forEach(Robot::sayHello);
}
}
1.1.3.1、控制台日志
2020-06-27 15:58:02 INFO -[ ]- Java SPI-------- com.healerjean.proj.study.spi.java.JavaSPITest.sayHello[21]
2020-06-27 15:58:02 INFO -[ ]- Hello, I am Optimus Prime. com.healerjean.proj.study.spi.OptimusPrime.sayHello[16]
2020-06-27 15:58:02 INFO -[ ]- Hello, I am Bumblebee. com.healerjean.proj.study.spi.Bumblebee.sayHello[17]
1.2、Dubbo SPI
继续使用上面的代码,对其进行改动一点点
1.2.1、接口添加注解@SPI
@SPI
public interface Robot {
void sayHello();
}
1.2.2、配置文件
dubbo配置文件一般在dubbo文件夹中,但是其实通过后面的源码我们也可以知道,不一定非要在这个文件夹中,其他文件夹也是可以的
optimusPrime = com.healerjean.proj.study.spi.OptimusPrime
bumblebee = com.healerjean.proj.study.spi.Bumblebee
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h6jkaSvC-1594897494551)(https://raw.githubusercontent.com/HealerJean/HealerJean.github.io/master/blogImages/image-20200627160249742.png)]
1.2.3、Main方法启测试
@Slf4j
public class DubboSPITest {
@Test
public void sayHello() {
ExtensionLoader<Robot> extensionLoader = ExtensionLoader.getExtensionLoader(Robot.class);
log.info("dubbo SPI--------");
log.info("----------------------------");
log.info("从配置文件中获取--------");
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();
log.info("----------------------------");
log.info("{}", extensionLoader.getSupportedExtensions());
}
1.2.3.1、控制台日志
2020-06-27 16:04:19 INFO -[ ]- using logger: com.alibaba.dubbo.common.logger.slf4j.Slf4jLoggerAdapter com.alibaba.dubbo.common.logger.LoggerFactory.[]
2020-06-27 16:04:19 INFO -[ ]- dubbo SPI-------- com.healerjean.proj.study.spi.dubbo.DubboSPITest.sayHello[21]
2020-06-27 16:04:19 INFO -[ ]- ---------------------------- com.healerjean.proj.study.spi.dubbo.DubboSPITest.sayHello[22]
2020-06-27 16:04:19 INFO -[ ]- 从配置文件中获取-------- com.healerjean.proj.study.spi.dubbo.DubboSPITest.sayHello[23]
2020-06-27 16:04:20 INFO -[ ]- Hello, I am Optimus Prime. com.healerjean.proj.study.spi.OptimusPrime.sayHello[16]
2020-06-27 16:04:20 INFO -[ ]- Hello, I am Bumblebee. com.healerjean.proj.study.spi.Bumblebee.sayHello[17]
2020-06-27 16:04:20 INFO -[ ]- ---------------------------- com.healerjean.proj.study.spi.dubbo.DubboSPITest.sayHello[28]
2020-06-27 16:04:20 INFO -[ ]- ["bumblebee","optimusPrime"] com.healerjean.proj.study.spi.dubbo.DubboSPITest.sayHello[30]
1.3、@Adaptive
注解使用
在类,以及方法上调用。定义了 注解的方法,参数列表中一定要有类型为 URL 的参数。
value 是个字符数组,通过该属性从 URL 中获取扩展名,来决定使用哪个扩展。分为几种情况:
1.设置了 value,且从 URL 中找到了对应的扩展名,则使用该扩展;
2.设置了 value,但从 URL 中找不到扩展名,则使用默认的扩展,即@SPI 中配置的 value,还是找不到则抛出异常;
3.未设置 value,则根据接口名生成 value,比如接口 Animal生成 value = “animal”。
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
ElementType.TYPE, ElementType.METHOD})
public @interface Adaptive {
}
1.3.1、实例说明
1.3.1.1、实例1
@SPI注解中有value值
1.3.1.1.1、示例代码
接口
@SPI(value = "adaptiveAnimal")
public interface Animal {
@Adaptive
void call(String msg, URL url);
}
扩展类
@Slf4j
public class AdaptiveAnimal implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是适配动物,发出叫声:{}", msg);
}
}
@Slf4j
public class Cat implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是猫,发出叫声: {},", msg);
}
}
@Slf4j
public class Dog implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是狗,发出叫声: {},", msg);
}
}
1.3.1.1.2、启动测试
/**
* 1、 @SPI注解中有value值,URL中没有具体的值
* 2、@SPI注解中有value值,URL中也有具体的值
*/
@Test
public void test1() {
ExtensionLoader<Animal> annoimalExtensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
Animal adaptiveExtension = annoimalExtensionLoader.getAdaptiveExtension();
//1、 @SPI注解中有value值,URL中没有具体的值
URL url = URL.valueOf("test://localhost/test");
adaptiveExtension.call("哒哒哒", url); //我是适配动物,发出叫声:哒哒哒
//2、@SPI注解中有value值,URL中也有具体的值
url = URL.valueOf("test://localhost/test?animal=cat");
adaptiveExtension.call("喵喵喵", url); // 我是猫,发出叫声: 喵喵喵
}
控制台日志
我是适配动物,发出叫声:哒哒哒
我是猫,发出叫声: 喵喵喵
1.3.1.1.3、中间dubbo生成的适配扩展类
package com.healerjean.proj.study.spi;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Animal$Adaptive implements com.healerjean.proj.study.spi.Animal {
public void call(java.lang.String arg0, com.alibaba.dubbo.common.URL arg1) {
if (arg1 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;
//获取animal,如果获取不到则选择adaptiveAnimal
String extName = url.getParameter("animal", "adaptiveAnimal");
if (extName == null)
throw new IllegalStateException("Fail to get extension(com.healerjean.proj.study.spi.Animal) name from url(" + url.toString() + ") use keys([animal])");
com.healerjean.proj.study.spi.Animal extension = (com.healerjean.proj.study.spi.Animal)
ExtensionLoader.getExtensionLoader(com.healerjean.proj.study.spi.Animal.class).getExtension(extName);
extension.call(arg0, arg1);
}
}
1.3.1.2、实例2
@SPI注解中有value(dog)值,URL中也有具体的值(cat),实现类AdaptiveAnimal上有@Adaptive注解
1.3.1.2.1、示例代码
接口
@SPI(value = "dog")
public interface Animal {
@Adaptive
void call(String msg, URL url);
}
扩展类
@Adaptive
@Slf4j
public class AdaptiveAnimal implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是适配动物,发出叫声:{}", msg);
}
}
@Slf4j
public class Cat implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是猫,发出叫声: {},", msg);
}
}
@Slf4j
public class Dog implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是狗,发出叫声: {},", msg);
}
}
1.3.1.3.2、启动测试
@Test
public void test3() {
ExtensionLoader<Animal> annoimalExtensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
Animal adaptiveExtension = annoimalExtensionLoader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test?animal=cat");
adaptiveExtension.call("哒哒哒", url); //我是适配动物,发出叫声:哒哒哒
}
控制台日志:
我是适配动物,发出叫声:哒哒哒
1.3.1.2.3、中间dubbo不会生成
不会帮我们生成,因为,这个时候使用了注解
@Adaptive
,则这个类就是适配扩展类,那如果有多个类给注解了呢,则只会选中一个
1.3.1.4、实例4
@SPI注解中有value值,实现类上没有@Adaptive注解,方法上的@Adaptive注解,注解中的value与链接中的参数的key一致,链接中的key对应的value就是spi中的name,获取相应的实现类。
1.3.1.4.1、实例代码
接口:
@SPI(value = "adaptiveAnimal")
public interface Animal {
@Adaptive(value = "name")
void call(String msg, URL url);
}
扩展类
@Slf4j
public class AdaptiveAnimal implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是适配动物,发出叫声:{}", msg);
}
}
@Slf4j
public class Dog implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是狗,发出叫声: {},", msg);
}
}
@Slf4j
public class Cat implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是猫,发出叫声: {},", msg);
}
}
1.3.1.4.2、启动测试
@Test
public void test4() {
ExtensionLoader<Animal> annoimalExtensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
Animal adaptiveExtension = annoimalExtensionLoader.getAdaptiveExtension();
URL url = URL.valueOf("test://localhost/test?name=dog");
adaptiveExtension.call("汪汪汪", url); //我是狗,发出叫声: 汪汪汪
}
控制台日志:
我是狗,发出叫声: 汪汪汪
1.3.1.3.3、中间dubbo生成的适配扩展类
package com.healerjean.proj.study.spi;
import com.alibaba.dubbo.common.extension.ExtensionLoader;
public class Animal$Adaptive implements com.healerjean.proj.study.spi.Animal {
public void call(java.lang.String arg0, com.alibaba.dubbo.common.URL arg1) {
if (arg1 == null) throw new IllegalArgumentException("url == null");
com.alibaba.dubbo.common.URL url = arg1;
//获取url路径中key为name的值,获取不到的话,就选中adaptiveAnimal
String extName = url.getParameter("name", "adaptiveAnimal");
if (extName == null) throw new IllegalStateException("Fail to get extension(com.healerjean.proj.study.spi.Animal) name from url(" + url.toString() + ") use keys([name])");
com.healerjean.proj.study.spi.Animal extension = (com.healerjean.proj.study.spi.Animal) ExtensionLoader.getExtensionLoader(com.healerjean.proj.study.spi.Animal.class).getExtension(extName);
extension.call(arg0, arg1);
}
}
1.3.2、归纳总结
1、在类上加上@Adaptive注解的类,是最为明确的创建对应类型Adaptive类。所以他优先级最高。
2、
@SPI
注解中的value是默认值,如果通过URL获取不到关于取哪个类作为Adaptive类的话,就使用这个默认值,当然如果URL中可以获取到,就用URL中的。3、方法上的
@Adaptive
注解,注解中的value
与链接中的参数的key
一致,链接中的key
对应的value
就是spi
中的name
,获取相应的实现类。
1.4、@Activate
1、根据
loader.getActivateExtension中
的group
和搜索到此类型的实例进行比较,如果group能匹配到,就是我们选择的,也就是在此条件下需要激活的2、
@Activate
中的value
是参数是第二层过滤参数(第一层是通过group
),在group校验通过的前提下,如果URL中的参数(k)与值(v)中的参数名同@Activate
中的value
值一致或者包含,那么才会被选中。相当于加入了value
后,条件更为苛刻点,需要URL中有此参数并且,参数必须有值。3、
@Activate
的order
参数对于同一个类型的多个扩展来说,order值越小,优先级越高。
接着上面的代码
@Activate(group = "default_group", value = "valueAc")
@Slf4j
public class Dog implements Animal {
@Override
public void call(String msg, URL url) {
log.info("我是狗,发出叫声: {},", msg);
}
}
@Test
public void getActive() {
ExtensionLoader<Animal> annoimalExtensionLoader = ExtensionLoader.getExtensionLoader(Animal.class);
URL url = URL.valueOf("test://localhost/test");
List<Animal> list = annoimalExtensionLoader.getActivateExtension(url, new String[]{
}, "default_group");
list.stream().forEach(System.out::println); //null
log.info("-----------------");
url = URL.valueOf("test://localhost/test?valueAc=fasdjafjdklj");
list = annoimalExtensionLoader.getActivateExtension(url, new String[]{
}, "default_group");
list.