SPI
SPI简介
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。 目前有不少框架用它来做服务的扩展发现,简单来说,它就是一种动态替换发现的机制。使用SPI机制的优势是实现解耦,使得第三方服务模块的装配控制逻辑与调用者的业务代码分离。
JDK中的SPI

Java中要使用JDK自带SPI功能,先提供标准服务接口,然后再提供相关接口实现和调用者。这样就可以通过SPI机制中约定好的信息进行查询相应的接口实现。
使用JDK原生SPI遵循如下约定:
- 1、当服务提供者提供了接口的一种具体实现后,在META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名;
- 2、接口实现类所在的jar包放在主程序的classpath中;
- 3、主程序通过java.util.ServiceLoader动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
- 4、SPI的实现类必须携带一个无参构造方法;
简单实现
- 创建maven父工程
- 创建接口子工程,定义接口
public interface HelloService {
String sayHello();
}
- 创建实现类子工程,定义实现类
public class DogHelloService implements HelloService {
@Override
public String sayHello() {
return "wang wang";
}
}
public class HumanHelloService implements HelloService {
@Override
public String sayHello() {
return "hello human";
}
}
- 实现类子工程resources下创建META-INF/services目录,添加接口的全限定类名的文件

5. 创建主启动类
public class HelloMain {
public static void main(String[] args) {
final ServiceLoader<HelloService> helloServices = ServiceLoader.load(HelloService.class);
for (HelloService service : helloServices) {
System.out.println(service.sayHello());
}
}
}
- 运行测试

注意: 如果编译时提示子工程之间的依赖失败,只需要将父工程clean后重新install
Failed to execute goal on project XXXX-api: Could not resolve dependencies for pro
Dubbo中的SPI
dubbo中大量的使用了SPI来作为扩展点,通过实现同一接口的前提下,可以进行定制自己的实现类。比如比较常见的协议,负载均衡,都可以通过SPI的方式进行定制化,自己扩展。Dubbo中已经存在的所有已经实现好的扩展点。

下图中则是Dubbo中默认提供的负载均衡策略。

Dubbo中扩展点使用方式
使用三个项目来演示Dubbo中扩展点的使用方式,一个主项目main,一个服务接口项目api,一个服务实现项目impl。
api项目创建
(1)导入坐标 dubbo
<dependencies>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-rpc-dubbo</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-remoting-netty4</artifactId>
<version>2.7.8</version>
</dependency>
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-serialization-hessian2</artifactId>
<version>2.7.8</version>
</dependency>
</dependencies>
(2)创建接口
在接口上 使用@SPI
@SPI
public interface HelloService {
String sayHello();
}
impl项目创建
(1)导入 api项目 的依赖
<dependency>
<groupId>con.elvis</groupId>
<artifactId>demo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
(2)建立实现类,为了表达支持多个实现的目的,这里分别创建两个实现。分别为
HumanHelloService 和DogHelloService 。
public class HumanHelloService implements HelloService {
@Override
public String sayHello() {
return "hello human";
}
}
public class DogHelloService implements HelloService {
@Override
public String sayHello() {
return "wang wang";
}
}
(3)SPI进行声明操作,在resources 目录下创建目录META-INF/dubbo 目录,在目录下创建名称为com.elvis.service.HelloService的文件,文件内部配置两个实现类名称和对应的全
限定名:
human=com.elvis.service.impl.HumanHelloService
dog=com.elvis.service.impl.DogHelloService
main项目创建
(1)导入坐标 接口项目 和 实现类项目
<dependencies>
<dependency>
<groupId>con.elvis</groupId>
<artifactId>demo-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>con.elvis</groupId>
<artifactId>demo-api-impl</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
(2)创建DubboSpiMain
和原先调用的方式不太相同, dubbo 有对其进行自我重新实现 需要借助ExtensionLoader,创建新的运行项目。这里demo中的示例和java中的功能相同,查询出所有的已知实现,并且调用
public class DubboSpiMain {
public static void main(String[] args) {
final ExtensionLoader<HelloService> extensionLoader = ExtensionLoader.getExtensionLoader(HelloService.class);
Set<String> supportedExtensions = extensionLoader.getSupportedExtensions();
for (String extension : supportedExtensions) {
HelloService service = extensionLoader.getExtension(extension);
System.out.println(service.sayHello());
}
}
}

(3)dubbo自己做SPI的目的
- JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源
- 如果有扩展点加载失败,则所有扩展点无法使用
- 提供了对扩展点包装的功能(Adaptive),并且还支持通过set的方式对其他的扩展点进行注入
Dubbo SPI中的Adaptive功能
Dubbo中的Adaptive功能,主要解决的问题是如何动态的选择具体的扩展点。通过getAdaptiveExtension 统一对指定接口对应的所有扩展点进行封装,通过URL的方式对扩展点来进行动态选择。 (dubbo中所有的注册信息都是通过URL的形式进行处理的。)这里同样采用相同的方式进行实现。
(1)创建接口
api中的HelloService 扩展如下方法, 与原先类似,在sayHello中增加Adaptive 注解,并且在参数中提供URL参数.注意这里的URL参数的类为org.apache.dubbo.common.URL其中@SP可以指定一个字符串参数,用于指明该SPI的默认实现。
@SPI("human")
public interface HelloService {
String sayHello();
@Adaptive
String sayHello(URL url);
}
(2)创建实现类
与上面Service实现类代码相似,只需增加URL形参即可
public class HumanHelloService implements HelloService {
@Override
public String sayHello() {
return "hello human";
}
@Override
public String sayHello(URL url) {
return "human url " + url;
}
}
(3)编写DubboAdaptiveMain
最后在获取的时候方式有所改变,需要传入URL参数,并且在参数中指定具体的实现类参数
如:
public class DubboUrlMain {
public static void main(String[] args) {
// 匹配的规则为接口名大写字母转换成小写,单词直接使用.分隔,=号后面跟要调用的实例名,就是在META-INF中文件的内容如下:
//human=com.elvis.service.impl.HumanHelloService
// dog=com.elvis.service.impl.DogHelloService
// 调用human 则可以使用下面的URL
// test://localhost/hello?hello.service=human
// 如果不指定实例名则默认使用SPI接口中指定的实例
URL url = URL.valueOf("test://localhost/hello?hello.service=");
HelloService service = ExtensionLoader.getExtensionLoader(HelloService.class).getAdaptiveExtension();
System.out.println(service.sayHello(url));
}
}
测试

注意:
- 因为在这里只是临时测试,所以为了保证URL规范,前面的信息均为测试值即可,关键的点在于hello.service 参数,这个参数的值指定的就是具体的实现方式。关于为什么叫hello.service 是因为这个接口的名称,其中后面的大写部分被dubbo自动转码为. 分割。
- 通过getAdaptiveExtension 来提供一个统一的类来对所有的扩展点提供支持(底层对所有的扩展点进行封装)。
- 调用时通过参数中增加URL 对象来实现动态的扩展点使用。
- 如果URL没有提供该参数,则该方法会使用默认在SPI 注解中声明的实现。
Dubbo调用时拦截操作
与很多框架一样,Dubbo也存在拦截(过滤)机制,可以通过该机制在执行目标程序前后执行我们指定的代码。
Dubbo的Filter机制,是专门为服务提供方和服务消费方调用过程进行拦截设计的,每次远程方法执行,该拦截都会被执行。这样就为开发者提供了非常方便的扩展性,比如为dubbo接口实现ip白名单功能、监控功能 、日志记录等。
步骤如下:
(1)实现org.apache.dubbo.rpc.Filter 接口
@Activate(group = {
CommonConstants.CONSUMER, CommonConstants.PROVIDER})
public class DubboInvokeFilter implements Filter {
public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
long startTime = System.currentTimeMillis();
try {
// 执行方法
return invoker.invoke(invocation);
} finally {
System.out.println("invoke time:"+(System.currentTimeMillis()-startTime) + "毫秒");
}
}
}
(2)使用org.apache.dubbo.common.extension.Activate 接口进行对类进行注册 通过group 可以
指定生产端 消费端 如:
@Activate(group = {
CommonConstants.CONSUMER, CommonConstants.PROVIDER})
(3)在META-INF.dubbo 中新建org.apache.dubbo.rpc.Filter 文件,并将当前类的全名写入
timerFilter=com.elvis.filter.DubboInvokeFilter
(4) 需要使用拦截器的模块引入依赖即可
<dependency>
<groupId>com.elvis</groupId>
<artifactId>dubbo-spi-filter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>


**注意:**一般类似于这样的功能都是单独开发依赖的,所以再使用方的项目中只需要引入依赖,在调用接口时,该方法便会自动拦截。
原文链接: https://blog.csdn.net/Kiven_ch/article/details/117885468