SPI全称Service Provider Interface,见名知意,是提供给服务供应商的一个接口,是解耦思想的一个体现,它通过将服务进行接口定义,提前对接口进行一个基础功能模板的编程的形式,充分运用了模板方法的设计思想,让每个开发商根据自己的实际实现对应的接口方法,更加灵活,使用起来也更加方便。
实际操作
开发一个SPI接口需要两步,分别为服务使用端接口定义和服务供应商具体实现,像JDBC的具体实现就是如此,如果要增加可插拔性,也可以将接口独立出来,形成一个项目,让主项目去依赖,下面做一个最基础的实现。
服务使用端接口定义
想象一下我们需要做一个万能遥控器,能够操作对应跑步机的跑步功能,但是具体哪个开发商要来使用我们这个遥控器不得而知
1.创建一个Maven项目
2.开发接口类
2.1 目录结构
2.2 service接口
package com.spi;
public interface ServiceInterface {
public void run();
}
2.3 使用服务
package com.service;
import com.spi.ServiceInterface;
import java.util.Iterator;
import java.util.ServiceLoader;
public class Controller {
public void start(){
//通过ServiceLoader去找对应META-INF/services目录下的文件,通过文件能够找到接口在jar中的具体实现位置,然后反射进行一个实例化
ServiceLoader<ServiceInterface> serviceInterfaces = ServiceLoader.load(ServiceInterface.class);
Iterator<ServiceInterface> iterator = serviceInterfaces.iterator();
while (iterator.hasNext()){
ServiceInterface next = iterator.next();
next.run();
}
}
}
2.4 测试类
package com;
import com.service.Controller;
import org.junit.Test;
public class TestController {
@Test
public void test(){
Controller controller = new Controller();
controller.start();
}
}
3.安装到本地maven仓库
当然此时还不能正确允许测试类因为没有具体实现
服务使用端接口定义好了之后,用maven install指令安装到本地仓库,以方便服务供应商引入
服务供应商具体实现
1.创建一个Maven项目,并引入刚刚安装的接口
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>service-provider</artifactId>
<version>1.0-SNAPSHOT</version>
<!-- 为了引入接口 -->
<dependencies>
<dependency>
<groupId>org.example</groupId>
<artifactId>spi-interface</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
<!-- 加入插件防止maven进行build的时候把META-INF目录下面的文件进行一个覆盖,因为我们的目的是要往META-INF下写入实现类的信息 -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<configuration>
<archive>
<addMavenDescriptor>false</addMavenDescriptor>
</archive>
</configuration>
</plugin>
</plugins>
</build>
</project>
2.具体实现类
2.1 目录结构
用idea用习惯的同学注意一下META-INF/services这个目录创建的时候分两步创建,不要用点去分割,build的时候会误认为这是一个名叫“META-INF.services”的目录,导致最后ServiceLoader找不到对应的路径
2.2 实现类
package com;
import com.spi.ServiceInterface;
public class Provider1 implements ServiceInterface {
public void run() {
System.out.println("Provider1 running machine running !!!! ");
}
}
2.3 service下面创建一个以接口名为名字的文件,内容是接口的具体实现
2.4 maven install当前类
测试
1.准备工作基本完成,但是还需要将两者联系起来,回到服务使用端,将刚刚安装的实现类使用maven引入
2.运行测试类,发现使用到了供应商提供的类实例
为什么会去读取META-INF/services下面的文件
带着这个疑问,翻阅了下ServiceLoader的源码(jdk1.8),发现默认ServiceLoader采用一种懒加载的形式,在load的时候实际并没有将类加载到内存,只是以当前类加载器和类的class对象创建了一个ServiceLoader实例而已,真正读取数据是在ServiceLoader实现的迭代器方法中,具体过程如下
1.这个方法是实际加载类的方法,最终会回调nextService方法
public S next() {
if (acc == null) {
return nextService();
} else {
PrivilegedAction<S> action = new PrivilegedAction<S>() {
public S run() { return nextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
2.第一行是关键,先去找是否有服务供应商提供具体服务,也就是去找到对应具体类全名,所以下一步查看hasNextService方法
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
3.如下,hasNextService的具体代码,分析写在代码中了
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//这里就是去拼写出要找的类路径,PREFIX定义如下
//PREFIX = "META-INF/services/"
//service.getName()即为类全名,所以会去找
//META-INF/services/com.spi.ServiceInterface的文件
String fullName = PREFIX + service.getName();
if (loader == null)
//这边使用ClassLoader.getSystemResources打破双亲委派机制
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
//解析文件中的内容
pending = parse(service, configs.nextElement());
}
//这边对nextName进行赋值,为之后实例化做准备
nextName = pending.next();
return true;
}