为什么要使用spi
面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。
以上文字从别处复制而来,想必你还是一脸懵逼,但不要慌,去搜一下spi你就会感觉更懵逼,因为你搜出来的只会是这个:
那到底啥是spi思想呢?
spi的概念
首先放个图:我们在“调用方”和“实现方”之间需要引入“接口”,可以思考一下什么情况应该把接口放入调用方,什么时候可以把接口归为实现方。
先来看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的api,它具有以下特征:
- 概念上更接近实现方
- 组织上位于实现方所在的包中
- 实现和接口在一个包中
当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,spi的规则如下:
- 概念上更依赖调用方
- 组织上位于调用方所在的包中
- 实现位于独立的包中(也可认为在提供方中)
如下图所示:
(上图来自:设计原则:小议 SPI 和 API)
开源的案例
接下来从几个案例总结下java spi思想
Jdk
在jdk6里面引进的一个新的特性ServiceLoader,从官方的文档来说,它主要是用来装载一系列的service provider。而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
可能上面讲的有些抽象,下面就结合一个示例来具体讲讲。
jdk spi案例
我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。
先定义好接口
package com.cainiao.ys.spi.learn;
import java.util.List;
public interface Search {
public List<String> searchDoc(String keyword);
}
文件搜索实现
package com.cainiao.ys.spi.learn;
import java.util.List;
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索 "+keyword);
return null;
}
}
数据库搜索实现
package com.cainiao.ys.spi.learn;
import java.util.List;
public class DatabaseSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("数据搜索 "+keyword);
return null;
}
}
接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:com.cainiao.ys.spi.learn.Search,里面加上我们需要用到的实现类
com.cainiao.ys.spi.learn.FileSearch
然后写一个测试方法
package com.cainiao.ys.spi.learn;
import java.util.Iterator;
import java.util.ServiceLoader;
public class TestCase {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("hello world");
}
}
}
可以看到输出结果:文件搜索 hello world
如果在com.cainiao.ys.spi.learn.Search文件里写上两个实现类,那最后的输出结果就是两行了。
这就是因为ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services下找接口的全限定名文件,再根据里面的内容加载相应的实现类。
这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。
那为什么配置文件为什么要放在META-INF/services下面?
可以打开ServiceLoader的代码,里面定义了文件的PREFIX如下:
private static final String PREFIX = "META-INF/services/"
以上是我们自己的实现,接下来可以看下jdk中DriverManager的spi设计思路
DriverManager spi案例
DriverManager是jdbc里管理和注册不同数据库driver的工具类。从它设计的初衷来看,和我们前面讨论的场景有相似之处。针对一个数据库,可能会存在着不同的数据库驱动实现。我们在使用特定的驱动实现时,不希望修改现有的代码,而希望通过一个简单的配置就可以达到效果。
我们在使用mysql驱动的时候,会有一个疑问,DriverManager是怎么获得某确定驱动类的?
我们在运用Class.forName("com.mysql.jdbc.Driver")加载mysql驱动后,就会执行其中的静态代码把driver注册到DriverManager中,以便后续的使用。
代码如下:
package com.mysql.jdbc;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
这里可以看到,不同的驱动实现了相同的接口java.sql.Driver,然后通过registerDriver把当前driver加载到DriverManager中
这就体现了使用方提供规则,提供方根据规则把自己加载到使用方中的spi思想
这里有一个有趣的地方,查看DriverManager的源码,可以看到其内部的静态代码块中有一个loadInitialDrivers方法,在注释中我们看到用到了上文提到的spi工具类ServiceLoader
/**
* Load the initial JDBC drivers by checking the System property
* jdbc.properties and then use the {@code ServiceLoader} mechanism
*/
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
点进方法,看到方法里有如下代码:
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
Iterator<Driver> drivers = loadedDrivers.iterator();
println("DriverManager.initialize: jdbc.drivers = " + loadedDrivers);
可见,DriverManager初始化时也运用了spi的思想,使用ServiceLoader把写到配置文件里的Driver都加载了进来。
我们打开mysql-connector-java的jar包,果然在META-INF/services下发现了上文中提到的接口路径,打开里面的内容,可以看到是com.mysql.jdbc.Driver
其实对符合DriverManager设定规则的驱动,我们并不用去调用class.forname,直接连接就好.因为DriverManager在初始化的时候已经把所有符合的驱动都加载进去了,避免了在程序中频繁加载。
但对于没有符合配置文件规则的驱动,如oracle,它还是需要去显示调用classforname,再执行静态代码块把驱动加载到manager里,因为它不符合配置文件规则:
最后总结一下jdk spi需要遵循的规范
Dubbo SPI普通的使用方式
新建一个接口Robot,并加上@SPI注解来表示这是一个扩展点
@SPI
public interface Robot {
void sayHello();
}
新建Robot的实现类OptimusPrime
public class OptimusPrime implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Optimus Prime.");
}
}
新建Robot的实现类Bumblebee
public class Bumblebee implements Robot {
@Override
public void sayHello() {
System.out.println("Hello, I am Bumblebee.");
}
}
在resources/META-INF/dubbo目录下新建文件com.bxoon.Robot,文件名需要为Robot的全路径名
(请忽略internal和services文件夹)
插件体系
eclipse插件
其实最具spi思想的应该属于插件开发,我们项目中也用到的这种思想,后面再说,这里具体说一下eclipse的插件思想。
Eclipse使用OSGi作为插件系统的基础,动态添加新插件和停止现有插件,以动态的方式管理组件生命周期。
一般来说,插件的文件结构必须在指定目录下包含以下三个文件:
- META-INF/MANIFEST.MF: 项目基本配置信息,版本、名称、启动器等
- build.properties: 项目的编译配置信息,包括,源代码路径、输出路径
- plugin.xml:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等
当eclipse启动时,会遍历plugins文件夹中的目录,扫描每个插件的清单文件MANIFEST.MF,并建立一个内部模型来记录它所找到的每个插件的信息,就实现了动态添加新的插件。
这也意味着是eclipse制定了一系列的规则,像是文件结构、类型、参数等。插件开发者遵循这些规则去开发自己的插件,eclipse并不需要知道插件具体是怎样开发的,只需要在启动的时候根据配置文件解析、加载到系统里就好了,是spi思想的一种体现。
Spring
Spring中运用到spi思想的地方也有很多,下面随便列举几个
scan
我们在spring中可以通过component-scan标签来对指定包路径进行扫描,只要扫到spring制定的@service、@controller等注解,spring自动会把它注入容器。
这就相当于spring制定了注解规范,我们按照这个注解规范开发相应的实现类或controller,spring并不需要感知我们是怎么实现的,他只需要根据注解规范和scan标签注入相应的bean,这正是spi理念的体现。
Scope
spring中有作用域scope的概念。
除了singleton、prototype、request、session等spring为我们提供的域,我们还可以自定义scope。
像是自定义一个 ThreadScope实现Scope接口
再把它注册到beanFactory中
Scope threadScope = new ThreadScope();
beanFactory.registerScope("thread", threadScope);
接着就能在xml中使用了
<bean id=".." class=".." scope="thread"/>
spring作为使用方提供了自定义scope的规则,提供方根据规则进行编码和配置,这样在spring中就能运用我们自定义的scope了,并不需要spring感知我们scope里的实现,这也是平台使用方制定规则,提供方负责实现的思想。
自定义标签
扩展Spring自定义标签配置大致需要以下几个步骤
- 创建一个需要扩展的组件,也就是一个bean
- 定义一个XSD文件描述组件内容,也可以给bean的属性赋值啥的
- 创建一个文件,实现BeanDefinitionParser接口,用来解析XSD文件中的定义和对组件进行初始化,像是为组件bean赋上xsd里设置的值
- 创建一个Handler文件,扩展自NamespaceHandlerSupport,目的是将组件注册到Spring容器,重写其中的的init方法
这样我们就边写出了一个自定义的标签,spring只是为我们定义好了创建标签的流程,不用感知我们是如何实现的,我们通过register就把自定义标签加载到了spring中,实现了spi的思想。
ConfigurableBeanFactory
spring里为我们提供了许多属性编辑器,这时我们如果想把spring配置文件中的字符串转换成相应的对象进行注入,就要自定义属性编辑器,这时我们可以按照spring为我们提供的规则来自定义我们的编辑器
自定义好了属性编辑器后,ConfigurableBeanFactory里面有一个registerCustomEditor方法,此方法的作用就是注册自定义的编辑器,也是spi思想的体现
Hotspot
我们打开hotspot的源码,可以看到,代码分为shared和其他,shared属于引擎层,os属于不同行业的实现
点开os,可以发现里面有不同操作系统的不同实现:
不同的厂商会提供hotspot的不同实现,在hotspot启动的时候,会判断当前是什么系统来启动不同的实现,这也是一种spi的思想。
在hotspot启动时会去执行shared里的代码,除了shared的其他三个包相当于是外部的一些实现,是不同操作系统开发人员加载到hotspot中,这种分层思想已经算是spi的思想
接着我们可以在shared里看到一个createThread接口,这个接口在不同的os下实现肯定是不一样的,这就代表着hotspot制定接口,不同的os开发者去捐献实现,hotspot不用感知是如何实现的,只需要在运行时直接调用接口就好,也是spi的思想。
还有像是Jetty/Tomcat中自定义sessionManager、自定义线程池,dubbo的扩展机制等内容也属于spi
总结
其实在这里就可以发现,只要是能满足用户按照系统规则来自定义,并且可以注册到系统中的功能点,都带有着spi的思想
Dubbo SPI 使用姿势
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);
}
}
作者:原水寒
链接:https://www.jianshu.com/p/de465d70f63f
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。