java语言的JDK针对第三方厂家库的开发或者插件开发提供了一种服务加载方式SPI(Service Provider Interface):
1)根据SPI规范,可以定义一个标准的服务接口,具体的实现交给服务的提供者去实现,该机制可以理解为本地的服务注册、发现机制。
2)SPI机制的优势是提供了一种动态替换发现机制,避免代码中将服务提供者写死,实现模块间解耦。
SPI基本组成部分:
1.ServiceLoader
该类是SPI机制的主体类,主要封装了服务提供者的加载和使用机制。
基本机制如下:
ServiceLoader类主要封装了动态加载接口统一实现的服务对象能力,主要实现思路:
1)该类为final类型,禁止继承实现,为JDK提供基础能力;
2)ServiceLoader核心能力就是规约了资源扫描目录作为和接口定义分离的实现类信息存放的标准目录;
3)另外ServiceLoader采用懒加载的方式来,让外部使用者来决定怎样加载使用这些具体实现类对象,对象都是通过封装的内部迭代器类来一个个动态加载的。
从调用关系的角度来分析源码分析如下:
1.通过ServiceLoader类提供的load接口,完成扫描加载资源一系列准备;
2.通过封装的实现的Iterable的iterator()对外提供具体实现类惰性加载使用。
1.ServiceLoader类外部的应用无法直接去构建该类类型对象实例,而是通过开放的静态方法load作为入口来构建使用的。
标准的load方法定义如下:
该方法返回一个ServiceLoader类构造的对象实例,供外部使用。
该方法两个输入参数:
1)service 定义一个类的抽象类,暂且可以这么称呼,Class在java中同样继承至Object,另外Class抽象了一般类类型,实例化的通常是通过如下方法获取的(稍后在使用上就可以知道这个参数其实是用于传递使用的接口类的):
java.lang.Class classObj = ClassName.class;
2)ClassLoader loader定义当前类加载器,主要用于定位资源扫描路径的。
封装load方法定义如下:
该方法只传入一个接口抽象类的参数,内部默认直接指定了当前线程上下文的类加载器,相当于直接默认帮助获取了该对象c1。内部最终还是在调用load标准的方法。
2.以load方法为入口点,继续追踪下去,load方法中返回了构造ServiceLoader对象实例,在该构造方法中基本逻辑如下:
可以看到该类构造方法为私有,只能从开放的load方法中去定义获取,外部无法直接定义对象使用。
内部出了初始化下类的数据成员之外,一个重要的触发方法reload,该方法:
1)先清空了服务实例提供者的列表;
2)构建了内部加载服务实例的迭代器对象;
下面我们看下这个内部定义的迭代器是怎么玩的。
内部除了构造传递进来的本地化数据成员定义之外,几个关键成员如下:
使用该迭代器上,实现通过构造方法构造对象:
构造对象传递了抽象类类型和当前类加载器参数。提供了两个重要的方法:
这两个方法主要是一个用于遍历判断资源目录下的URL列表,一个是用于加载列表中的具体实现类对象。源码分析如下:
1)hasNext方法具体是逐条解析URL资源列表,将一个URL里面的内容解析出来。
public boolean hasNext() {
if (nextName != null) { //遍历下一个临时元素不为空,直接返回true
return true;
}
if (configs == null) {//否则开始判断URL列表是否为空,为空根据指定路径加载
try {
获取相对全路径,这里路径后面的名字以接口类名为文件结尾
String fullName = PREFIX + service.getName();
//判断当前类加载器是否为空,为空则从类加载器重新扫描获取资源
//否则,直接根据fullName从当前加载器对象中获取
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//加载到资源目录下的URL列表之后,开始循环去解析
//这个循环中是将configs里面所有的URL列表遍历一遍
//然后解析其中的内容,全部拼接到pending上去
//pending 实际上指向ArrayList names
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
//解析configs中的一条url里面的内容,支持一个文件里面多条记录返回
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
2)next方法就具体讲解析出来的文件里面的内容拿出来进行实例化了,稍后实例中可以看出怎样实现思路。
public S next() {
if (!hasNext()) { //增加了一次校验,通常外部会结合hasNext使用
throw new NoSuchElementException();
}
String cn = nextName; //这里拿到返回的文件内容,是某个具体实现类的全名
nextName = null;
Class<?> c = null;
//运用反射的原理,创建一个个对象,这里是单个对象创建,外部循环调用next来完成
//全部URL列表文件中的实现类加载和创建
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列表
providers.put(cn, p);
//返回单个具体实现类实例对象
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated: " + x,
x);
}
throw new Error(); // This cannot happen
SPI使用实例:
实例准备一个最简单的接口扩展机制,类的结构如上:
1)接口类people,里面就一个简单的people接口方法;
2)实现类Boy和Girl,各自实现了该people方法;
工程结构如下:
1)模块化构建工程
2)分为接口、两个实现类模块,以及一个core里面封装了如何使用ServiceLoader,test用于运行测试。
1)实现类模块结构boy和girl相同结构:
定义好实现类包,实现类Boy,代码很简单,打印输出信息:
package com.wangfeng.boy;
import com.wangfeng.people.People;
public class Boy implements People {
public void people(){
System.out.println("boy!!!");
}
}
资源目录标准为resources下的结构,加上接口类全名命名的文件,文件内部配置实现类:
com.wangfeng.boy.Boy
定义pom文件,定义依赖关系,依赖接口模块:
2)接口模块
接口类定义代码:
package com.wangfeng.people;
public interface People {
public void people();
}
3)使用ServiceLoader封装
该类封装如下:
package com.wangfeng.core;
import com.wangfeng.people.People;
import java.util.Iterator;
import java.util.ServiceLoader;
public class PeopleFactory {
public void invoker(){
ServiceLoader services = ServiceLoader.load(People.class);
Iterator peoples = services.iterator();
boolean notFound = true;
while(peoples.hasNext()){
notFound = false;
People people = peoples.next();
people.people();
}
if(notFound){
throw new RuntimeException("cant find class!!!");
}
}
}
pom文件定义,依赖接口模块:
4)测试工程模块
代码如下:
import com.wangfeng.core.PeopleFactory;
public class TestMain {
public static void main(String[] args){
//ServiceLoader printerPeople = ServiceLoader.load(People.class);
PeopleFactory peopleFactory = new PeopleFactory();
peopleFactory.invoker();
}
}
pom文件如下,依赖想要使用的实现类模块:
5.运行测试工程
可以根据动态的添加resources下的资源文件,来动态的加载和使用实现类。
可以根据resources下动态调整实现类配置,来开放扩展实现服务。
总结:
1.SPI机制对扩展性比较好的场景非常有用,比如数据库驱动类Driver接口体系抽象和实现,比如服务和接口分离的模式下的本地动态注册和发现能力等。
2.当然研究和学习一个机制,除了要知道该机制场景应用之外,也要通过对其实现过程,去体会和提炼一些精妙的设计思路!