SPI
一.JDK SPI 的使用
1.什么是spi:SPI 全称为 Service Provider Interface,一种解耦接口和实现的手段,其实现原理是将接口的实现类全名称配置在配置文件中,程序运行阶段去读取配置文件加载实现类
这个机制为程序带来了很强的扩展性,使得我们可以很方便的基于某接口规范去使用任何第三方的实现
例如:
1.JDBC规范中 java.sql.Driver 只是一个接口,具体实现还得看我们使用什么数据库,导入对应数据库的驱动jar包
2.在将驱动更换之后,该代码仍然能够正常运行,这就是spi带来的好处
代码如下:
public class TestDB {
public static void main(String[] args) throws Exception {
Class.forName("com.mysql.cj.jdbc.Driver");
Connection conn = DriverManager.getConnection("jdbc:mysql://localhost:3306/study_test?serverTimezone=UTC","root","root");
PreparedStatement preparedStatement = conn.prepareStatement("select * from emp");
preparedStatement.execute();
ResultSet resultSet = preparedStatement.getResultSet();
while (resultSet.next()) {
System.out.println(resultSet.getLong("id"));
System.out.println(resultSet.getString("ename"));
}
}
}
pom.xml依赖
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
2.JDK SPI使用
- 在 META-INF/services 目录下创建一个以接口全限定名为名称的文件
- 文件中按行配置该实现类的全限定类名
- 使用 java.util.ServiceLoader#load 去加载获取
3.测试
原则上:由谁提供接口的实现,就由谁提供该配置文件,所以如果是外部第三方提供接口实现,它就要在它的项目 中提供好配置,我们的项目只需引入它的坐标即可
二.Dubbo 为何引入SPI
1,dubbo作为一个rpc框架,在它发送RPC请求时,整个过程会经历很多个关键事件节点,比如集群容错,负载均衡,数据序列化,通信协议编码,网络传输等,每个关键节点都有抽象出对应的接口,而且有多种不同的实现,且用户可自行扩展,那实际运行阶段 dubbo如何根据用户的配置参数来选择具体的实现呢,这就促使dubbo需要一种可插拔的接口实现发现机制
2,dubbo采用微内核架构,将每一个功能接口当作一个可插拔的扩展点接口,内核层面只负责按流程组装并引导执行每个扩展点接口 ,具体的功能和逻辑由具体的扩展点实现来完成,提高了系统的扩展性和灵活性,而spi就是实现微内核的手段
3,dubbo的spi机制是沿用jdk-spi还是需要重新设计?
重新设计
原因如下:
1.jdk-spi 虽然实现了解耦,能够做到接口与实现分离但在使用上只能迭代获取所有实现,无法按需获取
2.jdk-spi 在迭代加载的过程中就创建了所有接口实现的实例对象,也不管当前是否使用
3.jdk-spi 在并发安全处理上并未做任何设计
4.除了想要使用方便,按需获取等特性之外,dubbo还想根据自己的场景添加更多功能特性在里面
4.dubbo的spi在设计上有什么思路?
先解决按需获取的问题
1.将配置文件改成K=V的形式,在程序中通过K来获取V对应的实例
2.将K作为配置参数共使用者选择,用户配置什么实际执行的就是什么
# 配置
AndyLinux=com.andy.spi.jdk.impl.AndyLinux
AndyMac=com.andy.spi.jdk.impl.AndyMac
AndyWindow=com.andy.spi.jdk.impl.AndyWindow
三.Dubbo SPI的基本使用
主方法
public class DubboTest {
public static void main(String[] args) {
ExtensionLoader<AndyJdkApi> extensionLoader = ExtensionLoader.getExtensionLoader(AndyJdkApi.class);
// 基本测试
baseDubbo(extensionLoader);
// wrapper基本测试
baseWrapper(extensionLoader);
// 包装wrapper基本测试
baseWrapper2(extensionLoader);
// protocol方式
protocolTest(extensionLoader);
}
1.入门案例
1.导入dubbo坐标
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>2.7.23</version>
</dependency>
2.在接口上添加 @SPI 注解,告诉dubbo该接口可用其 SPI 机制来加载
3.在META-INF/dubbo,META-INF/dubbo/internal,META-INF/services中任何一个目录下创建一个以接口全限定名称为文件名的文件,在文件中按K=V的形式配置每个扩展点实现的全限定名称,其中K是自定义的
4.使用ExtensionLoader相关API去获取
private static void baseDubbo(ExtensionLoader<AndyJdkApi> extensionLoader) {
AndyJdkApi andyLinux = extensionLoader.getExtension("AndyLinux");
andyLinux.printInfo();
}
2. 自动包装Wrapper
Dubbo SPI 提供了类似Spring AOP的功能,可以对扩展点进行增强,称之为Wrapper
1,提供wrapper类,wrapper类与扩展点实现同一接口,并提供扩展点接口参数类型的构造函数【装饰者模式】
2,配置文件中同样需要把wrapper类配置进去,则所有的扩展点都会用该wrapper进行装饰增强
3,如果有多个wrapper,则会对扩展点进行层层包装增强
4,wrapper的作用是为了去增强其他扩展点实现,本身则不能作为扩展点
2.代码:
public class MyWrapper implements AndyJdkApi {
public final Logger log = LoggerFactory.getLogger(this.getClass().getName());
private final AndyJdkApi api;
public MyWrapper(AndyJdkApi api) {
this.api = api;
}
@Override
public void printInfo() {
log.info("wrapper前置执行");
api.printInfo();
log.info("wrapper后置执行");
}
}
2.配置文件新增:
wrapper=com.andy.spi.dubbo.MyWrapper
3.测试代码
private static void baseWrapper(ExtensionLoader<AndyJdkApi> extensionLoader) {
AndyJdkApi andyLinux = extensionLoader.getExtension("AndyMac");
andyLinux.printInfo();
}
3. 多层包装Wrapper
1.在配置文件中新增如下内容:
wrapper=com.andy.spi.dubbo.MyWrapper2
2.代码:
将MyWrapper代码复制一份命名为MyWraaper2
3.测试代码
private static void baseWrapper2(ExtensionLoader<AndyJdkApi> extensionLoader) {
AndyJdkApi andyLinux = extensionLoader.getExtension("AndyMac");
andyLinux.printInfo();
}
4.自动装配
1.Dubbo SPI 提供了类似Spring IOC 的功能,可以对扩展点进行依赖注入
1.在一个扩展点中通过set方法去注入另外 一个扩展点
2.在wrapper中也同样支持通过set进行依赖注入
2.修改AndyLinux代码
public class AndyLinux implements AndyJdkApi {
public final Logger log = LoggerFactory.getLogger(this.getClass().getName());
private Protocol protocol;
public void setProtocol(Protocol protocol) {
this.protocol = protocol;
}
@Override
public void printInfo() {
log.info("protocol"+protocol);
log.info("Linux系统在实现功能");
}
}
3.测试代码:
private static void protocolTest(ExtensionLoader<AndyJdkApi> extensionLoader) {
AndyJdkApi andyLinux = extensionLoader.getExtension("AndyLinux");
andyLinux.printInfo();
}
四.Dubbo SPI 自适应
1.图示
2.在Dubbo中,自适应扩展点可以由用户编写,也可由Dubbo框架自动生成
3.自适应案例[用户定义]
1.核心要素就是在自适应类上标注@Adaptive注解
2.配置文件中同样需要把自适应类配置进去
3.每个接口的自适应类只能有一个
4.扩展点注入时注入的都是自适应实例
4.自适应案例【框架生成】
1.在扩展点接口方法上标注@Adaptive注解,哪个方法需要在运行时自适应执行则添加
2.方法必须要有URL类型的参数,或者参数有getUrl()方法返回一个URL对象,URL是dubbo存储配置的容器
3.Adaptive注解的value属性指定需要从URL中获取哪个配置信息,dubbo根据此配置值决定执行哪个扩展点
4.可通过日志查看框架生成的自适应扩展点的代码
5.DubboSPI自适应由几种用法
- 开发者自行编写自适应类,自适应类也是接口的一个实现类,并在自适应类上添加@Adaptive注解,然后将自适应类配置到配置文件
- 由框架生成,需要在接口方法上添加@Adaptive注解,方法需要有URL类型的参数或者参数有getUrl方法,并通过Adaptive的value属性指定要从URL中获取哪个配置信息,框架根据此配置信息获取对应的扩展点
五.DubboSPI自动激活
1.DubboSPI自动激活
在前面讨论的所有特性中大都是集中在一个场景:需要从众多扩展点实现中根据name挑出一个,但是有一些场景并不是万里挑一的,而是可以多个扩展点实现共存并且项目初始化后就可以直接使用的,比如:Filter,Listener等.
所谓自动激活其实就是让某些扩展点默认就可以获取并使用,并不需要根据它们的key来逐一获取,这样可以拿到已激活的扩展点集合
- 想要默认激活某扩展点,需要给它添加@Activate注解
- 由于dubbo的主要场景是RPC,分为消费端和提供方,所以可以通过Activate的group属性指定该扩展点在哪一端激活
- 通过Activate的value属性指定更多激活的判断条件
2.DubboSPI自动激活如何使用
- 在扩展点上添加@Activate注解
- 通过group属性指定该扩展点是在消费方还是提供方激活3.通过value属性指定更多激活条件
3.DubboSPI如何指定默认扩展点
- 通过SPI注解的value属性指定