java SPI 机制
- SPI java机制
- 1.java实现原理
- 2.代码示例
- 服务
- 服务提供商A
- 服务提供商B
- 服务提供调用
- 服务加载配置
- 3.java源码解析
- 4.其他注意事项
SPI java机制
SPI英文为Service Provider Interface 是Java提供的一套用来被第三方实现或者扩展的API,它可以用来启用框架扩展和替换组件。
java中实现spi的主要类为 ServiceLoader<S>,一个简单的服务提供商加载工具。 一个服务是一组众所周知的接口(通常是抽象的)类。 服务提供商是服务的具体实现。 提供者中的类通常实现接口并对服务本身定义的类进行子类化。 服务提供商可以以Java扩展的形式安装在Java平台的实现中,也就是将jar文件放置到任何通常的扩展目录中。 提供商也可以通过将它们添加到应用程序的类路径或某些其他平台特定的方式来提供。
1.java实现原理
java 中通过实现接口,通过基于接口的编程模式,将服务规范与服务提供相分离,实现不同的厂商提供服务
要使用Java SPI,需要遵循如下约定:
- 当服务提供者提供了接口的具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名
- 引入接口实现类所在的jar包
- 主程序通过java.util.ServiceLoder<S>动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM
- SPI的实现类必须携带一个不带参数的构造方法
2.代码示例
服务由单一类型表示,即单个接口或抽象类。
服务
public interface IPrintServiceSPI {
/**
* 打印对象
* @param o
*/
void print(Object o);
}
服务提供商A
public class JavaPrintServiceSPI implements IPrintServiceSPI{
@Override
public void print(Object o) {
System.out.println(getClass().getSimpleName()+":"+o.toString());
}
}
服务提供商B
public class JSONPrintServiceSPI implements IPrintServiceSPI {
@Override
public void print(Object o) {
System.out.println(getClass().getSimpleName()+":"+JSON.toJSONString(o));
}
}
服务提供调用
public static void main(String[] args) {
ServiceLoader<IPrintServiceSPI> serviceLoader = ServiceLoader.load(IPrintServiceSPI.class); //加载服务
Iterator<IPrintServiceSPI> iterator = serviceLoader.iterator(); //迭代所用服务
while (iterator.hasNext()) {
iterator.next().print("test"); //使用服务
}
}
服务加载配置
在 META-INF/services.文件夹下放入服务全限定名文件,eg:IPrintServiceSPI 全路径名com.kwxyzk.spi.IPrintServiceSPI
文件内放入服务提供商想要提供的服务
com.kwxyzk.spi.JavaPrintServiceSPI
com.kwxyzk.spi.JSONPrintServiceSPI
3.java源码解析
服务提供商加载工具ServiceLoader<S>
ServiceLoader 使用迭代器设计模式和反射机制,使用迭代器遍历所有的供应商提供的申明服务名,使用反射通过实现的服务名反射为服务对象。
步骤
- 使用服务名加载服务,此时服务使用懒加载模式,只是知名服务和类加载器
- 迭代服务,在迭代时,加载 META-INF/services/服务名 文件中申明的服务实现类名,该文件包含具体提供程序类的完全限定二进制名称列表,每行一个。 忽略每个名称周围的空格和制表符,以及空白行。 注释字符为’#’ ( ‘\u0023’ , NUMBER SIGN ); 在每行上,忽略第一个注释字符之后的所有字符。 文件必须以UTF-8编码。 供应商懒惰地定位和实例化,也就是按需。 服务加载器维护到目前为止已经加载的提供程序的缓存。 每次调用iterator方法都会返回一个迭代器,首先按照实例化顺序生成缓存的所有元素,然后懒惰地定位和实例化任何剩余的提供程序,依次将每个元素添加到缓存中。 缓存可以通过reload方法清除。
- 当使用服务时,将迭代器中保存的服务实现类名反射为类对象使用服务
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
···
public static <S> ServiceLoader<S> load(Class<S> service,
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
···
//懒加载服务到迭代器中
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
//hasnext 开始迭代时
public boolean hasNext() {
if (knownProviders.hasNext())
return true;
return lookupIterator.hasNext();
}
//判断时候还有实现服务
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//PREFIX = "META-INF/services/";
String fullName = PREFIX + service.getName();
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
//Iterator<String> pending = null;
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
//从文件或者url中获取内容,放入pending 迭代器,内容为
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
//使用服务 先迭代出services下文件中服务实现列表名,再映射为服务对象
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//获取提供者中申明使用的实现服务
String fullName = PREFIX + service.getName();
if (loader == null)
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 = pending.next();
return true;
}
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
}
4.其他注意事项
- 服务提供商申明使用的服务名可以放入本地jar包META-INF/services.该文件的名称是服务类型的全资格binary name 中,也可以使用远程url中
- ServiceLoader<S>类的实例不能安全地被多个并发线程使用。
- 使用Java SPI机制的优势是实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离,而不是耦合在一起。应用程序可以根据实际业务情况启用框架扩展或替换框架组件。