设计思想之 【Java spi 扩展机制 +概念+源码 +demo】
目录
1、背景
最近在研究某某网关的设计和实现,发现其架构采用可拔插式微内核架构,感觉其架构的扩展性非常好,了解到其底层主要基于Java spi机制实现,于是想要彻底的了解Java spi是什么?它能干什么?其他开源框架是怎么用它的?它有什么缺点?适用什么场景?
2、概念
Java SPI
(Service Provider Interface)是Java官方提供的一种代码级别的服务发现机制
,它允许在运行时动态地加载实现
特定接口的类,而不需要在代码中显式地指定该类,从而实现灵活性和高扩展
看到这个“运行时动态加载”,笔者脑子里蹦出的第一个想法就是**“反射”** ,至于对不对,后面看看源码就清楚了
3、常见应用场景
应用框架、组件 | |
---|---|
日志框架 | 流行的开源日志框架 ,如Log4j、SLF4J和Logback 等,都采用了SPI机制。用户可以根据自己的需求选择合适的日志实现,而不需要修改代码 |
Spring框架 | Spring框架 中的Bean加载机制就使用了SPI思想,通过读取classpath下的META-INF/spring.factories文件来加载各种自定义的Bean 。 |
Dubbo框架、JOYRPC、SOFARPC | Dubbo框架 也使用了SPI思想,通过接口注解@SPI 声明扩展点接口,并在classpath下的META-INF/dubbo目录中提供实现类的配置文件,来实现扩展点的动态加载;其中JOYRPC、SOFARPC类似 |
MyBatis框架 | MyBatis框架 中的插件机制也使用了SPI思想,通过在classpath下的META-INF/services目录中存放插件接口的实现类路径,来实现插件的加载和执行 。 |
Netty框架 | Netty框架 也使用了SPI机制,让用户可以根据自己的需求选择合适的网络协议实现方式 。 |
数据库驱动程序加载 | JDBC 为了实现可插拔 的数据库驱动,在Java.sql.Driver接口中定义了一组标准的API规范,而具体的数据库厂商则需要实现这个接口,以提供自己的数据库驱动程序。在Java中,JDBC驱动程序的加载就是通过SPI机制实现的。 |
某某内部网关 | 插件式组装限流、路由等 |
以上提到的框架或者组件都是基于原生Java spi的增强,除了上面提到的,比如还有skywalking也用到了类似的插件式四项进行设计
4、实现Java spi的基本步骤
- 第一步:
定义接口
,首先需要定义一个接口(即定义这个接口可以提供什么服务),所有实现该接口的类都将被注册为服务提供者 - 第二步:
创建实现类
,创建一个或多个实现第一步定义接口的类,这些类将作为服务提供者。 - 第三步:
配置文件
,在 META-INF/services 目录下创建一个以接口全限定名命名的文件,文件内容为实现该接口的类的全限定名,每个类名占一行。 - 第四步:加载使用服务:使用 java.util.ServiceLoader 类的静态方法 load(Class service) 加载服务,默认情况下会加载 classpath 中所有符合条件的提供者。调用 ServiceLoader 实例的 iterator() 方法获取迭代器,遍历迭代器即可获取所有实现了该接口的类的实例。
看百遍,不如自己实操一遍
基于Java spi的基本步骤 自己手写感受一下:
4.1 定义接口
首先定义一个接口:该接口主要提供统一适配第三方请求的能力
public interface ServiceApiAdapter {
/**
* 处理 第三方请求
*/
void request(Object obj);
}
4.2 实现接口
RestServiceApiAdapter
public class RestServiceApiAdapter implements ServiceApiAdapter{
@Override
public void request(Object obj) {
System.out.println("正在解析 [rest] 请求");
}
}
SelfRpcServiceApiAdapter
public class SelfRpcServiceApiAdapter implements ServiceApiAdapter{
@Override
public void request(Object obj) {
System.out.println("正在解析 [RPC] 请求");
}
}
4.3 定义全限定类文件
4.4 测试demo
public class JAVASpiTest {
public static void main(String[] args) {
ServiceLoader<ServiceApiAdapter> serviceApiAdapters = ServiceLoader.load(ServiceApiAdapter.class);
for (ServiceApiAdapter serviceApiAdapter : serviceApiAdapters) {
serviceApiAdapter.request(null);
}
}
}
相信到这里大家也跟笔者一样已经知道如何使用Java spi了
5 、为什么按照上面的4个步骤就可使用Java spi呢?
不知道,大家有不有跟笔者一样的疑惑:
- 为什么按照上面的4个步骤就可使用Java spi呢?
- 为什么 ServiceLoader serviceApiAdapters = ServiceLoader.load(ServiceApiAdapter.class); 可以拿到文件里配置的实现类呢?
前面笔者猜测,ServiceLoader肯定用到了反射,那接下来就跟着源码去验证一下自己的猜测:
5.1 Java spi 实现原理(源码阅读)
笔者简单debug了一下,源码比较简单,底层确实是通过反射去动态加载实现类的,感兴趣的小伙伴可以自己试一下
简单回答一下上面我在没看源码之前的2个问题:
- 问题1:为什么按照上面的4个步骤就可使用Java spi呢?
因为jdk1.8已经封装好了,如果我们要使用Java spi特性,就得遵守他的规则,就比如我们用springboot或者其他框架,我们如果想要用人家的特性,那么就得遵守使用规范
-
问题2:为什么 ServiceLoader serviceApiAdapters = ServiceLoader.load(ServiceApiAdapter.class); 可以拿到文件里配置的实现类呢?
扫描 META-INF 下我们定义的文件 通过反射 + 上面文件读取类的全路径类名 获取对象
以下是笔者debug看源码时的截图:
以上三个截图的内容,都是我们尝试获取对应实现类的时候才会触发的,所以有一种懒加载的思想在里面
到这里相信大家对 Java spi的原理有一个大概的了解了
6、Java spi 有哪些优缺点
优点
- 松耦合性:SPI具有很好的松耦合性,应用程序可以在运行时动态加载实现类,而无需在编译时将实现类硬编码到代码中。
- 扩展性:通过SPI,应用程序可以为同一个接口定义多个实现类。这使得应用程序更容易扩展和适应变化。
- 易于使用:使用SPI,应用程序只需要定义接口并指定实现类的类名,即可轻松地使用新的服务提供者。
缺点
配置较麻烦
:SPI需要在META-INF/services目录下创建配置文件,并将实现类的类名写入其中。这使得配置相对较为繁琐。安全性不足
:SPI提供者必须将其实现类名称写入到配置文件中,因此如果未正确配置,则可能存在安全风险。- 每次加载,需要全部都进行迭代(即使我们只需要其中一个),且每次查找都需要遍历整个集合
笔者在搜索资料的时候发现 有一篇博文关于Java spi缺点描述中有以下结论:
性能损失
:每次查找服务提供者都需要重新读取配置文件,这可能会增加启动时间和内存开销。
“每次查找服务提供者都需要重新读取配置文件” 这个结论是错的
笔者通过debug调试发现,应用启动后,在不使用serviceApiAdapters.reload();方法时,读取配置文件只会加载一次
如果我们使用了serviceApiAdapters.reload();方法,那么在下一次遍历获取实现类的时候会从新加载配置文件
验证代码:
public class JAVASpiTest {
public static void main(String[] args) {
ServiceLoader<ServiceApiAdapter> serviceApiAdapters = ServiceLoader.load(ServiceApiAdapter.class);
for (ServiceApiAdapter serviceApiAdapter : serviceApiAdapters) {
serviceApiAdapter.request(null);
}
serviceApiAdapters.reload(); // 如果调用这个方法,那么下面遍历的时候会重新加载配置文件,否则不会
for (ServiceApiAdapter serviceApiAdapter : serviceApiAdapters) {
serviceApiAdapter.request(null);
}
}
}
断点:
大家可以通过上面的代码和断点去验证笔者的正确性,所以大家在网上看到的资料一定要理性对待
7、java spi 学习和收获
通过spi概念以及源码的阅读,笔者大概了解他的一个优缺点以及原理实现,整体来说java spi机制还是比较弱的,虽然java spi比较弱,但是它的思想比较强大,后面有时间,笔者准备研究一下微内核插件式架构以及其他组件扩展的功能更强大的spi机制