系列目录
spi 05-dubbo adaptive extension 自适应拓展
问题引入
以前一直想指定一套标准,让别人按照这个标准来实现,并编写好对应的容器。
然后我在代码中动态获取这些实现,让代码运行起来。
困难
如何获取某个接口的实现?
初步方案
和同事讨论,是通过扫描包的 class 的方式。然后判断是否为定制标准的子类。
- 缺点
觉得很别扭,需要限定死实现类的包名称,而且性能也较差。
SPI 的解决方式
今天在阅读 hibernate-validator 源码时受到了启发。
可以通过 SPI 的方式,更加自然的解决这个问题。
SPI 是 Service Provider Interfaces 的缩写。
本文简单介绍下如何使用,具体原理,暂时不做深究。
SPI 是什么
SPI 是 Java 提供的一种服务加载方式,全名为 Service Provider Interface。
根据 Java 的 SPI 规范,我们可以定义一个服务接口,具体的实现由对应的实现者去提供,即服务提供者。
然后在使用的时候再根据 SPI 的规范去获取对应的服务提供者的服务实现。
通过 SPI 服务加载机制进行服务的注册和发现,可以有效的避免在代码中将具体的服务提供者写死。从而可以基于接口编程,实现模块间的解耦。
SPI 机制的约定
-
在 META-INF/services/ 目录中创建以接口全限定名命名的文件,该文件内容为API具体实现类的全限定名
-
使用 ServiceLoader 类动态加载 META-INF 中的实现类
-
如 SPI 的实现类为 Jar 则需要放在主程序 ClassPath 中
-
API 具体实现类必须有一个不带参数的构造方法
应用场景举例
SPI 应用场景举例
- JDBC
jdbc4.0以前, 开发人员还需要基于Class.forName(“xxx”)的方式来装载驱动,jdbc4也基于spi的机制来发现驱动提供商了,可以通过METAINF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者.
- COMMON-LOGGING
apache最早提供的日志的门面接口。只有接口,没有实现。
具体方案由各提供商实现,发现日志提供商是通过扫描METAINF/services/org.apache.commons.logging.LogFactory配置文件,通过读取该文件的内容找到日志提工商实现类。
只要我们的日志实现里包含了这个文件,并在文件里制定 LogFactory 工厂接口的实现类即可。
好处
Java中的SPI(Service Provider Interface)是一种用于实现解耦和扩展的机制。它提供了一种在运行时动态加载和替换实现类的方式,使得代码具有更高的可扩展性和灵活性。以下是Java使用SPI的一些好处:
-
松耦合:使用SPI可以将服务的提供者和使用者分离开来,提供者可以独立开发、发布和更新,而不需要修改使用者的代码。这种松耦合的设计可以降低代码的维护成本,同时也更容易实现模块化和组件化。
-
可插拔性:SPI使得应用程序可以在运行时动态地加载和替换实现类。这意味着可以轻松地扩展应用程序的功能,添加新的功能模块或者替换现有的实现,而无需修改核心代码。这种可插拔性使得应用程序更容易适应不同的需求和变化的环境。
-
易于扩展:SPI提供了一种机制,允许第三方开发者为已有的接口提供新的实现。这样,应用程序的功能可以通过扩展现有接口的实现来快速增加,而无需修改原有的代码。这种扩展性使得应用程序具有更高的灵活性和可扩展性。
-
多样性:SPI允许多个实现类同时存在,应用程序可以根据需要选择适合的实现。这种多样性使得应用程序可以根据不同的需求选择最合适的实现方式,提高了应用程序的可定制性和适应性。
-
增强可维护性:使用SPI可以将应用程序的不同功能模块分开开发和维护,每个模块专注于自己的实现,降低了代码的复杂性。这样,当需要修改或者更新某个功能模块时,只需要关注该模块的实现,而不会对其他模块造成影响。
总的来说,Java中的SPI机制通过实现解耦和动态加载的方式,提供了一种灵活、可扩展的编程模型。它可以使得代码更具有可维护性和可扩展性,并且能够快速适应不同的需求和变化的环境。
简单实现
文件目录
.
├── java
│ └── com
│ └── github
│ └── houbb
│ └── forname
│ ├── Say.java
│ ├── Sing.java
│ └── impl
│ ├── DefaultSay.java
│ └── DefaultSing.java
└── resources
└── META-INF
└── services
└── com.github.houbb.forname.Say
定义接口和实现
- Say.java
public interface Say {
/**
* 说
*/
void say();
}
- DefaultSay.java
import com.github.houbb.forname.Say;
public class DefaultSay implements Say {
@Override
public void say() {
System.out.println("Default say");
}
}
编写 services 实现指定
在 resources 目录下,创建 META-INF/services 文件夹,以接口全路径名 com.github.houbb.forname.Say 为文件名称,内容为对应的实现类全路径。
如果是多个,就直接换行隔开。
- com.github.houbb.forname.Say
com.github.houbb.forname.impl.DefaultSay
测试
- SayTest.java
public class SayTest {
@Test
public void spiTest() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ServiceLoader<Say> loader = ServiceLoader.load(Say.class, classLoader);
for (Say say : loader) {
say.say();
}
}
}
- 测试结果
Default say
简单总结
Java 中,可以通过 ServiceLoader 类比较方便的找到该类的所有子类实现。
META-INF/services 下的实现指定和实现子类实现完全可以和接口定义完全分开。
麻烦的地方
每次都要手动创建实现指定文件,比较繁琐。
Auto 就为解决这个问题而生。
Auto 版本演示
maven 引入
<dependencies>
<dependency>
<groupId>com.google.auto.service</groupId>
<artifactId>auto-service</artifactId>
<version>1.0-rc4</version>
<optional>true</optional>
</dependency>
</dependencies>
接口和定义
- Sing.java
public interface Sing {
/**
* 唱歌
*/
void sing();
}
- DefaultSing.java
@AutoService(Sing.class)
public class DefaultSing implements Sing {
@Override
public void sing() {
System.out.println("Sing a song...");
}
}
测试
- SingTest.java
public class SingTest {
@Test
public void spiTest() {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
ServiceLoader<Sing> loader = ServiceLoader.load(Sing.class, classLoader);
for (Sing sing : loader) {
sing.sing();
}
}
}
- 结果
Sing a song...
简单总结
通过 google 的 auto,可以在编译时自动为我们生成对应的接口实现指定文件。
在 target 对应的文件下可以看到。
实现原理,也相对简单。通过 java 的编译时注解,生成对应的文件即可。
实际上诸如 dubbo 等框架,会利用 SPI 机制来提升项目整体的灵活性。
java 自带的 SPI 有很多不足的地方,本系列就是要学习使用,并且实现自己增强的 SPI 框架。