SPI是什么
SPI全称Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。
系统设计的各个抽象,往往有很多不同的实现方案,在面向的对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。
使用场景
概括地说,适用于:调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
比较常见的例子:
- 数据库驱动加载接口实现类的加载
JDBC加载不同类型数据库的驱动
- 日志门面接口实现类加载
SLF4J加载不同提供商的日志实现类
- Spring
Spring中大量使用了SPI,比如:对servlet3.0规范对ServletContainerInitializer的实现、自动类型转换Type Conversion SPI(Converter SPI、Formatter SPI)等
- Dubbo
Dubbo中也大量使用SPI的方式实现框架的扩展, 不过它对Java提供的原生SPI做了封装,允许用户扩展实现Filter接口
实际案例
- 定义一个接口IPerson
package com.alioo.spi;
public interface IPerson {
void sayHello();
}
- 编写一个程序入口类SPIDemo
在这个程序入口类里获取该接口的实现类,并进行调用
package com.alioo.spi;
import java.util.Iterator;
import java.util.ServiceLoader;
public class SPIDemo {
public static void main(String[] args) {
ServiceLoader<IPerson> shouts = ServiceLoader.load(IPerson.class);
System.out.println("shouts:" + shouts);
Iterator<IPerson> it = shouts.iterator();
while (it.hasNext()) {
IPerson s = it.next();
System.out.println("s:" + s);
s.sayHello();
}
}
}
备注:
事实上到目前为止我们还没有这个接口的任何实现类,然而上面的代码不影响我们编写、编译打包的
- 编写接口的实现类Man
package com.alioo.spi;
public class Man implements IPerson {
@Override
public void sayHello() {
System.out.println("我是男人");
}
}
还需要创建一个META-INF/services目录,并在该目录下创建一个文件,文件名为接口IPerson的包路径,即com.alioo.spi.IPerson,文件内容为实现类的包路径
│ ├── pom.xml
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── alioo
│ │ └── spi
│ │ └── Man.java
│ └── resources
│ └── META-INF
│ └── services
│ └── com.alioo.spi.IPerson
# more src/main/resources/META-INF/services/com.alioo.spi.IPerson
com.alioo.spi.Man
备注:
这个实现类Man可以独立于上面的程序入口类SPIDemo而存在,比如将Man.class和META-INF目录单独打到一个jar里,只需要SPIDemo指定classpath包含这个jar可以
- 运行效果
shouts:java.util.ServiceLoader[com.alioo.spi.IPerson]
s:com.alioo.spi.Man@610455d6
我是男人
- 引入更多的实现类
比如再增加一个实现类Woman
package com.alioo.spi;
public class Woman implements IPerson {
@Override
public void sayHello() {
System.out.println("我是女人");
}
}
同步骤4,增加META-INF/services目录及文件com.alioo.spi.IPerson,文件内容为当前实现类的包路径
│ ├── pom.xml
│ └── src
│ └── main
│ ├── java
│ │ └── com
│ │ └── alioo
│ │ └── spi
│ │ └── Woman.java
│ └── resources
│ └── META-INF
│ └── services
│ └── com.alioo.spi.IPerson
more src/main/resources/META-INF/services/com.alioo.spi.IPerson
com.alioo.spi.Woman
这个时候的运行效果如下:
shouts:java.util.ServiceLoader[com.alioo.spi.IPerson]
s:com.alioo.spi.Man@610455d6
我是男人
s:com.alioo.spi.Woman@60e53b93
我是女人
总结
优点
使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。
相比使用提供接口jar包,供第三方服务模块实现接口的方式,SPI的方式使得源框架,不必关心接口的实现类的路径,可以不用通过下面的方式获取接口实现类:
- 代码硬编码import导入实现类
- 指定类全路径反射获取:例如在JDBC4.0之前,JDBC中获取数据库驱动类需要通过Class.forName(“com.mysql.jdbc.Driver”),类似语句先动态加载数据库相关的驱动,然后再进行获取连接等的操作
- 第三方服务模块把接口实现类实例注册到指定地方,源框架从该处访问实例
- 通过SPI的方式,第三方服务模块实现接口后,在第三方的项目代码的META-INF/services目录下的配置文件指定实现类的全路径名,源码框架即可找到实现类
缺点
-
虽然ServiceLoader也算是使用的延迟加载 ,但是基本只能通过遍历全部获取,也就是接口的实现类全部加载并实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。获取某个实现类的方式不够灵活,只能通过Iterator形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用ServiceLoader类的实例是不安全的。
使用场景附加说明
java.sql.DriverManager 含有static代码块,当该类被加载时,会触发调用loadInitialDrivers()方法
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
在loadInitialDrivers()方法中含有jdk spi的调用
ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
通过这种方式再也不用向以前那样调用这句话了```Class.forName(“com.mysql.jdbc.Driver”)``
几个问题
既然这么方便,为啥之前就得加Class.forName(…)了呢
因为JDK SPI技术是从jdk6才开始有的(阅读下ServiceLoader类的注释就可以看到了)
参考文章
作者:分布式系统架构
链接:https://www.jianshu.com/p/46b42f7f593c
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。