SPI 机制是实现可扩展性的一种方式。上一篇介绍了
JDK SPI
的使用姿势和基本原理,本节来分析Dubbo SPI
的基本使用、适配类使用、AOP 使用、IOC 使用以及激活点的使用(基于 dubbo 2.6.6)。
可参考的 SPI 实现:
一、Dubbo SPI 基本使用
- Dubbo 配置文件名依然是:
SPI 接口的全接口名
- Dubbo SPI 会从以下三个目录读取配置文件:
- META-INF/dubbo/internal/:该目录用于存储 Dubbo 框架本身提供的 SPI 扩展实现,eg.
- META-INF/dubbo/:第三方提供的扩展(包括我们自己写的)
建议
写在这个目录下(实际上写到三个目录的任一目录下都可以,但是不方便管理)- META-INF/services/:JDK SPI 的配置文件目录
SPI 接口
@SPI("logback")
public interface Log {
void execute();
}
SPI 接口实现
public class Logback implements Log {
@Override
public void execute() {
System.out.println("this is logback!");
}
}
public class Log4j implements Log {
@Override
public void execute() {
System.out.println("this is log4j!");
}
}
配置文件
logback=io.study.dubbo.spi.basic.Logback
log4j=io.study.dubbo.spi.basic.Log4j
测试主类
public class TestBasic {
public static void main(String[] args) {
ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
// 1. 指定名称获取具体 SPI 实现类
Log logback = loader.getExtension("logback");
logback.execute(); // this is logback!
Log log4j = loader.getExtension("log4j");
log4j.execute(); // this is log4j!
// 2. 获取默认实现类 @SPI("logback") 中的 logback 就指定了默认的 SPI 实现类的 key
Log defaultExtension = loader.getDefaultExtension();
defaultExtension.execute(); // this is logback!
System.out.println(loader.getDefaultExtensionName()); // logback
// 3. 获取支持哪些 SPI 实现类
Set<String> supportedExtensions = loader.getSupportedExtensions();
supportedExtensions.forEach(System.out::println); // log4j \n logback
// 4. 获取已经加载了哪些 SPI 实现类
Set<String> loadedExtensions = loader.getLoadedExtensions();
loadedExtensions.forEach(System.out::println); // log4j \n logback
// 5. 根据 SPI 实现类实例或者实现类的 Class 信息获取其 key
System.out.println(loader.getExtensionName(logback)); // logback
System.out.println(loader.getExtensionName(Logback.class)); // logback
// 6. 判断是否具有指定 key 的 SPI 实现类
System.out.println(loader.hasExtension("logback")); // true
System.out.println(loader.hasExtension("log4j2")); // false
}
}
二、Dubbo SPI 适配类使用
- Dubbo 适配类:适配类其实就是一个工厂类,根据传递的参数动态的使用相应的 SPI 实现类;
- Dubbo 适配类有两种姿势:(一个 SPI 接口最多只有一个适配类,如果有手动编写的适配类,那么则首先使用手动编写的适配类)
- 手动编写一个适配类(Dubbo 默认只提供了两个手动编写的适配类
AdaptiveExtensionFactory
和AdaptiveCompiler
)- 根据 SPI 接口动态生成一个适配类
2.1 手动编写一个适配类
SPI 接口
@SPI("logback")
public interface Log {
void execute(String name);
}
SPI 实现
public class Log4j implements Log {
@Override
public void execute(String name) {
System.out.println("this is log4j! " + name);
}
}
public class Logback implements Log {
@Override
public void execute(String name) {
System.out.println("this is logback! " + name);
}
}
SPI适配类
/**
* 手动编写 SPI 适配类
* 注意:适配类也需要在配置文件中进行配置
*/
@Adaptive
public class AdaptiveLog implements Log {
private static final ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
@Override
public void execute(String name) {
Log log = null;
if (name == null || name.length() == 0) {
log = loader.getDefaultExtension();
} else {
log = loader.getExtension(name);
}
if (log != null) {
log.execute(name);
}
}
}
适配类要实现 SPI 接口
。(见ExtensionLoader<T>.public T getAdaptiveExtension()
方法定义,类泛型与方法返回泛型相同,都是 SPI 接口)
配置文件
log4j=io.study.dubbo.spi.adaptive.manual.Log4j
logback=io.study.dubbo.spi.adaptive.manual.Logback
adaptive=io.study.dubbo.spi.adaptive.manual.AdaptiveLog
注意:
手动编写的适配类需要在配置文件中进行配置
测试主类
public class TestAdaptiveManual {
public static void main(String[] args) {
ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
System.out.println("======================= 获取 SPI 适配类(自己手写适配类) =======================");
Log adaptiveExtension = loader.getAdaptiveExtension(); // AdaptiveLog 实例
adaptiveExtension.execute("log4j"); // this is log4j! log4j
}
}
2.2 根据 SPI 接口动态生成一个适配类
SPI 接口
/**
* SPI 接口
*/
@SPI("logback")
public interface Log {
/**
* 含有 @Adaptive 注解的方法,生成的动态类会实现该方法,该方法必须直接包含 URL 参数或者方法的参数中要包含 URL 参数
* @Adaptive 注解中的 String[] value() 代表 url 中用于获取 SPI 实现类的 key 的参数名:
*
* eg. 本例的配置生成的代码如下
* String extName = url.getParameter("xxx", url.getParameter("ooo", "logback")); // 其中 logback 是默认值, 即先获取 Url 中key为xxx的值,如果该值存在,则使用该值去 SPI 配置文件中获取对应的实现
* Log extension = ExtensionLoader.getExtensionLoader(Log.class).getExtension(extName);
*/
@Adaptive({"xxx","ooo"})
void execute(URL url);
/**
* 不带有 @Adaptive 注解的方法,生成的动态类中该方法的方法体直接抛异常
*/
void test();
}
注意点
- 含有 @Adaptive 注解的方法,生成的动态类会实现该方法,
该方法必须直接包含 URL 参数或者方法的参数中要包含 URL 参数
(因为要根据 URL 参数来判断具体使用哪一个 SPI 实现,具体见如下“动态生成的适配类”,由此可见,适配类其实就是一个工厂类,根据传递的参数动态的使用相应的 SPI 实现类)- 不带有 @Adaptive 注解的方法,生成的动态类中该方法的方法体直接抛异常
- @Adaptive 注解中的 String[] value() 代表 url 中用于获取 SPI 实现类的 key 的参数名(示例解释见如下代码注释);假设 @Adaptive 没有配置 String[] value(),那么会默认按照类名(大写变小写,且加“.”作为分隔符)作为 key 去查找(eg. interface io.study.TestLog,则 key=“test.log”),即 String extName = url.getParameter("test.log", "logback");
动态生成的适配类
public class Log$Adaptive implements io.study.dubbo.spi.adaptive.auto.Log {
@Override
public void execute(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null) {
throw new IllegalArgumentException("url == null");
}
com.alibaba.dubbo.common.URL url = arg0;
// 首先获取url中的xxx=ppp这个参数的值ppp,假设有,使用该值去获取key为ppp的 SPI 实现类;假设没有,再获取ooo=ppp,假设也没有,使用默认的logback去获取key为logback的SPI实现类
String extName = url.getParameter("xxx", url.getParameter("ooo", "logback"));
if (extName == null) {
throw new IllegalStateException("Fail to get extension(io.study.dubbo.spi.adaptive.auto.Log) name from url(" + url.toString() + ") use keys([xxx, ooo])");
}
io.study.dubbo.spi.adaptive.auto.Log extension = (io.study.dubbo.spi.adaptive.auto.Log) ExtensionLoader.getExtensionLoader(io.study.dubbo.spi.adaptive.auto.Log.class).getExtension(extName);
extension.execute(arg0);
}
@Override
public void test() {
throw new UnsupportedOperationException("method public abstract void io.study.dubbo.spi.adaptive.auto.Log.test() of interface io.study.dubbo.spi.adaptive.auto.Log is not adaptive method!");
}
}
SPI 实现
public class Log4j implements Log {
@Override
public void execute(URL url) {
System.out.println("this is log4j! " + url.getIp());
}
@Override
public void test() {}
}
public class Logback implements Log {
@Override
public void execute(URL url) {
System.out.println("this is logback! " + url.getIp());
}
@Override
public void test() {}
}
配置文件
logback=io.study.dubbo.spi.adaptive.auto.Logback
log4j=io.study.dubbo.spi.adaptive.auto.Log4j
测试主类
public class TestAdaptiveAuto {
public static void main(String[] args) {
ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
Log adaptiveExtension = loader.getAdaptiveExtension();
URL url = new URL("dubbo", "10.211.55.6", 8080);
adaptiveExtension.execute(url.addParameter("xxx", "log4j")); // this is log4j! 10.211.55.6
}
}
三、Dubbo SPI AOP 使用
相较于 JDK SPI 的一个增强点。如果设置了 Wrapper 类,该类会对所有的 SPI 实现类进行包裹。
SPI 接口
@SPI("logback")
public interface Log {
void execute();
}
SPI 实现
public class Log4j implements Log {
@Override
public void execute() {
System.out.println("this is log4j!");
}
}
public class Logback implements Log {
@Override
public void execute() {
System.out.println("this is logback!");
}
}
wrapper 类
/**
* wrapper 类也必须实现 SPI 接口,否则 loadClass() 处报错
*/
public class LogWrapper1 implements Log {
private Log log;
/**
* wrapper 类必须有一个含有单个 Log 参数的构造器
*/
public LogWrapper1(Log log) {
this.log = log;
}
@Override
public void execute() {
System.out.println("LogWrapper1 before");
log.execute();
System.out.println("LogWrapper1 after");
}
}
public class LogWrapper2 implements Log {
private Log log;
public LogWrapper2(Log log) {
this.log = log;
}
@Override
public void execute() {
System.out.println("LogWrapper2 before");
log.execute();
System.out.println("LogWrapper2 after");
}
}
- wrapper 类也必须实现 SPI 接口
- wrapper 类必须有一个含有单个 Log 参数的构造器
配置文件
log4j=io.study.dubbo.spi.aop.Log4j
logback=io.study.dubbo.spi.aop.Logback
io.study.dubbo.spi.aop.LogWrapper1
io.study.dubbo.spi.aop.LogWrapper2
- wrapper 类必须在配置文件中进行配置,不需要配置 key
测试主类
public class TestAOP {
public static void main(String[] args) {
ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
System.out.println("======================= 根据指定名称获取具体的 SPI 实现类(测试 wrapper) =======================");
Log logback = loader.getExtension("logback"); // 最外层的 Wrapper 类实例
/**
* 输出
* LogWrapper2 before
* LogWrapper1 before
* this is logback!
* LogWrapper1 after
* LogWrapper2 after
*/
logback.execute();
}
}
关于 wrapper 类的加载顺序,见 https://github.com/apache/dubbo/issues/4578
四、Dubbo SPI IOC 使用
也是对 JDK SPI 功能的一个增强,
- Dubbo SPI IOC 有两种姿势:
- 注入 Dubbo
适配类
:值得注意的是,Dubbo 只能注入适配类,不能直接注入 SPI 具体实现;- 注入 Spring Bean:需要将包含该 Spring Bean 的 Spring 上下文添加到 Dubbo 的 Spring 上下文管理器(
SpringExtensionFactory
)中,这样后续做 IOC 时,才能获取到该 Bean
- Dubbo 的 SPI 使用的是 set 注入,所以需要提供一个
public void setXxx(单个 SPI 接口参数)
方法,对于注入适配类方式来讲,由于注入的只是适配类,只与 SPI 接口有关,与 setXxx 方法的 Xxx无关;对于注入 Spring Bean 方式来讲,由于注入的是具体的 Bean,Xxx 是 Spring Bean 的名称。
2.1 注入 Dubbo 适配类
SPI 接口
@SPI("logback")
public interface Log {
void execute();
}
SPI 实现
public class Logback implements Log {
/**
* SPI IOC 注入:
* Book 是 SPI 接口,
* 必须存在一个 public ooo setXxx(单个SPI接口) 的方法才可以进行 IOC 注入,
* 且被注入的 SPI 接口必须有适配类(无论是手动还是自动)
*/
private Book book;
/**
* 对于 SPI 注入方式来讲,setXxx 中的 Xxx 没有任何作用,因为注入的都是 SPI 接口的适配类而不是具体的实现类
*/
public void setBookx(Book book) {
this.book = book;
}
@Override
public void execute() {
URL url = new URL("dubbo", "10.211.55.5", 8080);
System.out.println("this is logback! " + book.bookName(url.addParameter("language", "go")));
}
}
public class Log4j implements Log {
/**
* SPI IOC 注入:
* Book 是 SPI 接口,
* 必须存在一个 public ooo setXxx(单个SPI接口) 的方法才可以进行 IOC 注入,
* 且被注入的 SPI 接口必须有适配类(无论是手动还是自动)
*/
private Book book;
// @DisableInject 禁用 IOC 注入
@DisableInject
public void setBook(Book book) {
this.book = book;
}
@Override
public void execute() {
System.out.println("this is log4j!");
}
}
被注入的 SPI 接口及其实现类
/**
* SPI IOC 注入方式:必须有适配类(无论是手动还是自动)
* note:手动编写的 Adaptive 类内也可以实现 IOC 注入
*/
@SPI("java")
public interface Book {
@Adaptive({"language"})
String bookName(URL url);
}
public class JavaBook implements Book {
@Override
public String bookName(URL url) {
return "this is java book!" + url.getIp();
}
}
public class GoBook implements Book {
@Override
public String bookName(URL url) {
return "this is go book!" + url.getIp();
}
}
// 动态生成的 SPI 适配类
public class Book$Adaptive implements io.study.dubbo.spi.ioc.spi.Book {
@Override
public java.lang.String bookName(com.alibaba.dubbo.common.URL arg0) {
if (arg0 == null) {
throw new IllegalArgumentException("url == null");
}
com.alibaba.dubbo.common.URL url = arg0;
String extName = url.getParameter("language", "java");
if (extName == null) {
throw new IllegalStateException("Fail to get extension(io.study.dubbo.spi.ioc.spi.Book) name from url(" + url.toString() + ") use keys([language])");
}
io.study.dubbo.spi.ioc.spi.Book extension = (io.study.dubbo.spi.ioc.spi.Book) ExtensionLoader.getExtensionLoader(io.study.dubbo.spi.ioc.spi.Book.class).getExtension(extName);
return extension.bookName(arg0);
}
}
配置文件
Log SPI 配置文件
log4j=io.study.dubbo.spi.ioc.spi.Log4j
logback=io.study.dubbo.spi.ioc.spi.Logback
Book SPI 配置文件
java=io.study.dubbo.spi.ioc.spi.JavaBook
go=io.study.dubbo.spi.ioc.spi.GoBook
测试主类
public class TestSPIIOC {
public static void main(String[] args) {
ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
// 1. 测试 SPI IOC
Log logback = loader.getExtension("logback");
logback.execute(); // this is logback! this is go book!10.211.55.5
// 2. 测试禁用 SPI IOC
Log log4j = loader.getExtension("log4j");
log4j.execute(); // this is log4j!
}
}
2.2 注入 Spring Bean
SPI 接口
@SPI("logback")
public interface Log {
void execute();
}
SPI 实现
public class Logback implements Log {
/**
* Spring IOC 注入:
* 1. 必须存在一个 public ooo setXxx(其中 Xxx 是 Spring bean 的名称) 的方法才可以进行 IOC 注入,
*/
private Book book;
/**
* 对于 Spring 注入来讲,setXxx 中的 Xxx 代表了注入 Bean 的名称,这里注入的就是 javaBook 这个 Bean
*/
public void setJavaBook(Book book) {
this.book = book;
}
@Override
public void execute() {
System.out.println("this is logback! " + book.bookName());
}
}
public class Log4j implements Log {
/**
* Book 是 SPI 接口,
* 必须存在一个 public ooo setXxx(单个SPI接口) 的方法才可以进行 IOC 注入
*/
private Book book;
public void setGoBook(Book book) {
this.book = book;
}
@Override
public void execute() {
System.out.println("this is log4j!" + book.bookName());
}
}
被注入的 SPI 接口及其实现类
/**
* Spring IOC 注入方式
*/
public interface Book {
String bookName();
}
public class GoBook implements Book {
@Override
public String bookName() {
return "this is go book!";
}
}
public class JavaBook implements Book {
@Override
public String bookName() {
return "this is java book!";
}
}
spring 配置文件
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
<!-- spring bean -->
<bean id="javaBook" class="io.study.dubbo.spi.ioc.spring.JavaBook"/>
<bean id="goBook" class="io.study.dubbo.spi.ioc.spring.GoBook"/>
</beans>
SPI 配置文件
log4j=io.study.dubbo.spi.ioc.spring.Log4j
logback=io.study.dubbo.spi.ioc.spring.Logback
测试主类
public class TestSpringIOC {
public static void main(String[] args) {
// 1. 创建 spring 容器 + 加载 Bean 到该容器中
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext(new String[]{"spring.xml"});
// 2. 将 spring 容器添加到 dubbo 的 SpringExtensionFactory 工厂中
SpringExtensionFactory.addApplicationContext(context);
ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
// 3. 测试 dubbo spring ioc
Log logback = loader.getExtension("logback");
logback.execute(); // this is logback! this is java book!
}
}
五、Dubbo SPI 激活点使用
Dubbo 的激活点机制基于
@Activate
注解完成,可以用于实现根据条件加载多个 SPI 激活点实现类。
@Activate
注解:
String[] group() default {}
:
- 如果
getActivateExtension
接口传入的 group 参数为 null 或者 length==0,表示不限制 group,则允许加载当前 SPI 实现;- 查看当前的 SPI 实现的
@Activate
注解中的参数 groups 是否包含传入的限制参数 group,如果包含,则允许加载当前的 SPI 实现。
String[] value() default {}
:
- 如果当前的 SPI 实现的
@Activate
注解没有 value() 属性,则认为默认是允许当前的 SPI 实现加载的;- 如果 value() 中的任一值出现在当前传入的
URL#getParameters()
中的一个参数名,则认为默认是允许当前的 SPI 实现加载的;
激活点加载流程
(仅列出最常用的主线,其他支线见后续的源码分析及源码注释,整个流程配合下边的例子TestActivate#testValue()
来看):
- 首先获取除了传入的 spiKey 集合(values)指定的 spi 激活点实现类(称为 default 激活点),之后对 default 激活点进行排序
加载 default 激活点的规则:
- 如果
getActivateExtension
接口传入的 group 参数为 null 或者 length==0,表示不限制 group,则允许加载当前 SPI 实现;- 如果 group 有效,则查看当前的 SPI 实现的
@Activate
注解中的参数 groups 是否包含传入的限制参数 group,如果包含,则允许加载当前的 SPI 实现;- 传入的spiKey 集合(values)不包含(-name,name 表示当前处理到的 SPI 激活点的 spiKey):也就是说配置 -name 可以排除掉某个实现类;
- 如果当前的 SPI 实现的
@Activate
注解没有 value() 属性,则认为默认是加载的,直接返回 true;- 如果当前的 SPI 实现的 @Activate 注解有 value() 属性,遍历每一个元素,如果
url.getParameters()
中的参数名包含了其中任意一个元素(也就是说String[] value()
中的任一值出现在当前传入的URL#parameters()
中的一个参数名)
- 之后获取传入的 spiKey 集合(values)指定的 SPI 激活点实现类(称为 usr 激活点)
- 传入的spiKey 集合(values)不包含(-name,name 表示当前处理到的 SPI 激活点的 spiKey):也就是说配置 -name 可以排除掉某个实现类;
- 将 default 激活点集合和 usr 激活点集合放到一个集合中,default 在前,usr 在后
SPI 接口
@SPI
public interface Log {
void execute();
}
SPI 实现
@Activate
public class NoCondition implements Log {
@Override
public void execute() {
System.out.println("this is noCondition!");
}
}
// order 值越小,则排在集合前边,order 默认为 0
@Activate(group = {"provider"}, order = 1)
public class SingleGroup implements Log {
@Override
public void execute() {
System.out.println("this is single group!");
}
}
@Activate(group = {"provider", "consumer"}, order = 2)
public class MultiGroup implements Log {
@Override
public void execute() {
System.out.println("this is multi group!");
}
}
@Activate(value = {"singleValue"})
public class SingleValue implements Log {
@Override
public void execute() {
System.out.println("this is single value!");
}
}
@Activate(value = {"multi"})
public class MultiValue implements Log {
@Override
public void execute() {
System.out.println("this is multi value!");
}
}
@Activate(group = {"provider", "consumer"}, value = {"groupAndValue"})
public class GroupAndValue implements Log {
@Override
public void execute() {
System.out.println("this is GroupAndValue!");
}
}
配置文件
nc=io.study.dubbo.spi.activate.NoCondition
sg=io.study.dubbo.spi.activate.SingleGroup
mg=io.study.dubbo.spi.activate.MultiGroup
sv=io.study.dubbo.spi.activate.SingleValue
mv=io.study.dubbo.spi.activate.MultiValue
gv=io.study.dubbo.spi.activate.GroupAndValue
测试主类
public class TestActivate {
public static void main(String[] args) {
ExtensionLoader<Log> loader = ExtensionLoader.getExtensionLoader(Log.class);
testGroup(loader);
testValue(loader);
}
/**
* 1. 测试 group
* 仅仅过滤出@Activate.groups包含url传入的group=xxx参数
*/
private static void testGroup(ExtensionLoader<Log> loader) {
System.out.println("======================= 测试 group =======================");
URL url = new URL("dubbo", "10.211.55.6", 8080);
String group = "provider";
String[] values = new String[]{};
List<Log> activateExtension = loader.getActivateExtension(url, values, group);
/**
* 输出:
* this is single group!
* this is multi group!
*/
activateExtension.forEach(Log::execute);
}
/**
* 2. 测试 value
*/
private static void testValue(ExtensionLoader<Log> loader) {
System.out.println("======================= 测试 value =======================");
URL url = new URL("dubbo", "10.211.55.6", 8080);
// url = url.addParameter("groupAndValue", "gv");
String[] values = new String[]{"sv", "-mg"};
/**
* NoCondition @Activate no
* SingleGroup @Activate(group = {"provider"}, order = 1) sg
* MultiGroup @Activate(group = {"provider", "consumer"}, order = 2) mg
* SingleValue @Activate(value = {"singleValue"}) sv
* MultiValue @Activate(value = {"multi"}) mv
* GroupAndValue @Activate(group = {"provider", "consumer"}, value = {"groupAndValue"}) gv
*
* 1.首先加载 default 激活点(除了 "sv", "mg"之外的其他激活点),加载条件:
* 我们加了 group 参数,首先会获取具有相关 group 的组,这里获取到 SingleGroup、GroupAndValue,
* 由于 SingleGroup 没有配置 values 属性,所以认为激活,而 GroupAndValue 的 value 值的任一元素(groupAndValue)没有出现在 url.getParameter中,
* 所以 GroupAndValue 不能加载(如果加上该句 url = url.addParameter("groupAndValue", "gv"); 代码,则可以加载)
* 最后对所有的 default 激活点按照 order、before、after 属性进行排序
* 2.之后加载 usr 激活点("sv", "mg" 激活点),sv 正常加载,而 mg 我们配置的 values 是 -mg,也就是说不加载 mg
*/
List<Log> activateExtension = loader.getActivateExtension(url, values, "provider");
/**
* 输出:
* this is single group!
* this is single value!
*/
activateExtension.forEach(Log::execute);
}
}