JAVA-SPI让框架更优雅
架构师是做什么的:
- 负责公司软件系统的技术路线、架构设计、研发工作;
- 承担从产品需求像技术实现转换的桥梁作用,根据产品规划更新技术架构的研发方向;
- 参与项目计划评审;
- 参与需求分析、建模、软件设计评审;
- 负责组织技术研究和攻坚工作;
- 负责组织及带领公司内部员工研究与项目相关的新技术;
- 管理技术架构团队并给项目、产品开发实施团队提供技术支持;
- 理解项目的业务需求,给出软件系统整体解决方案;
- 对技术基础架构的相关技术和业务进行培训,指导开发人员开发。
What?
**SPI(Service Provider Interface)机制:**一种服务发现机制,其实源自服务提供者框架(Service Provider Framework,参考【EffectiveJava】page6),是一种将服务接口与服务实现分离以达到解耦、大大提升了程序可扩展性的机制。引入服务提供者就是引入了SPI接口的实现者,通过本地的注册发现获取到具体的实现类,轻松可插拔.
使用场景:不关心用户的输入参数类型,用户只关心相互输入后得到的结果 eg:JDBC、JNDI、Dubbo、SpringBoot都使用了SPI技术
架构师的一剂灵丹妙药
利用配置文件实现热插拔 eg:SpringBoot(META-INF/spring.factories)
eg:解析歌曲的项目(SPI应用)
- **song-parser 项目。**负责定义通用的歌曲解析接口,并不提供任何具体的歌曲解析器实现。
- **song-parser-XXX(mp3) 项目。**实现了 song-parser 项目的歌曲解析接口,实现了 mp3 格式歌曲的解析。
- **song-parser-XXX(mp4) 项目。**实现了 song-parser 项目的歌曲解析接口,实现了 mp4 格式歌曲的解析。
SPI与API的不同应对场景
API (Application Programming Interface)在
大多数情况下,都是实现方
制定接口并完成对接口的实现,调用方
仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)
是调用方
来制定接口规范,提供给外部来实现,调用方在调用时则
选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
SPI机制
-
创建指定的文件夹【/resources/META-INF/services】并创建于接口的全限定名一致的文件,SPI内部加载实现类的时候指定了该扫描目录
-
可以实现扩展的热插拔,需要添加扩展只需要扩展实现该接口,打包成.mvn依赖添加即可
-
JDBC,DUBBO等实现都是基于spi机制实现的。
SPI原理 关键实现ServiceLoader
源码了解:
public final class ServiceLoader<S> implements Iterable<S> {
//扫描目录前缀
private static final String PREFIX = "META-INF/services/";
// 被加载的类或接口
private final Class<S> service;
// 用于定位、加载和实例化实现方实现的类的类加载器
private final ClassLoader loader;
// 上下文对象
private final AccessControlContext acc;
// 按照实例化的顺序缓存已经实例化的类
private LinkedHashMap<String, S> providers = new LinkedHashMap<>();
// 懒查找迭代器
private java.util.ServiceLoader.LazyIterator lookupIterator;
// 私有内部类,提供对所有的service的类的加载与实例化
private class LazyIterator implements Iterator<S> {
Class<S> service;
ClassLoader loader;
Enumeration<URL> configs = null;
String nextName = null;
//...
private boolean hasNextService() {
if (configs == null) {
try {
//获取目录下所有的类 转换为-URL,工作流的方式获取每一行的数据并截取第一个‘#’之前的数据,并去除前后的空格
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
//...
}
//....
}
}
private S nextService() {
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//反射加载类 将上面获取的url进行转换为对象类
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
}
try {
//实例化
S p = service.cast(c.newInstance());
//放进缓存
providers.put(cn, p);
return p;
} catch (Throwable x) {
//..
}
//..
}
}
}
SPI使用
要使用SPI比较简单,只需要按照以下几个步骤操作即可:
- 在jar包的META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名
- 接口实现类所在的jar包在classpath下
- 主程序通过java.util.ServiceLoader动态状态实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
- SPI的实现类必须带一个无参构造方法
Demo
eud-spi-play:定义接口
pom.xml
package org.dh.spi;
public interface SpiPlayService {
String play();
}
edu-spi-mp3play:接口的实现
pom.xml
<dependency>
<groupId>org.dh</groupId>
<artifactId>eud-spi-play</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
org.dh.spi.SpiPlayService:
org.dh.spi.impl.Mp3PlayServiceImpl
java:
public class Mp3PlayServiceImpl implements SpiPlayService {
@Override
public String play() {
return "mp3-play";
}
}
edu-spi-mp4play:接口的实现
pom.xml
<dependency>
<groupId>org.dh</groupId>
<artifactId>eud-spi-play</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
org.dh.spi.SpiPlayService:
org.dh.spi.impl.Mp4PlayServiceImpl
java:
public class Mp4PlayServiceImpl implements SpiPlayService {
@Override
public String play() {
return "mp4-play";
}
}
**edu-spi-demo:**demo
pom.xml
<dependency>
<groupId>org.dh</groupId>
<artifactId>edu-spi-mp3play</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.dh</groupId>
<artifactId>edu-spi-mp4play</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
java:
public class SpiTest {
@Test
public void testSpi(){
ServiceLoader<SpiPlayService> serviceLoader = ServiceLoader.load(SpiPlayService.class);
for(SpiPlayService spiPlayService:serviceLoader){
System.out.println("paly:"+spiPlayService.play());
}
}
}
sout.out:
paly:mp3-play
paly:mp4-play
总结:
关于spi的详解到此就结束了,总结下spi能带来的好处:
- 不需要改动源码就可以实现扩展,解耦。
- 实现扩展对原来的代码几乎没有侵入性。
- 只需要添加配置就可以实现扩展,符合开闭原则。