Java SPI机制简介
SPI是Service Provider Interfaces的简称。根据Java的SPI规范,我们可以定义一个服务接口,具体的实现由对应的实现者去提供,即Service Provider(服务提供者)。然后在使用的时候只要根据SPI的规范去获取对应的服务提供者的服务实现即可。为了便于理解,我们先来看一个使用SPI的示例。
假设我们有一个日志服务LogService,其只定义了一个info方法用于输出日志信息,我们希望把它作为SPI,然后具体的实现由对应的服务提供者去实现。LogService的定义如下所示。
package com.elim.learn.basic.spi.service;
public interface LogService {
public void info(String msg);
}
然后基于这个服务我们会有自己的实现,示例中笔者用了三个实现,分别是ConsoleLogService、FileLogService和DBLogService,其实现都只是简单的打印一下日志类别信息,ConsoleLogService的实现如下所示,其它两个是类似的。
package com.elim.learn.basic.spi.service.impl;
import com.elim.learn.basic.spi.service.LogService;
public class ConsoleLogService implements LogService {
@Override
public void info(String msg) {
System.out.println("----console log ----");
}
}
根据SPI的规范我们的服务实现类必须有一个无参构造方法。我们的SPI服务提供者需要将其在classpath下的META-INF/services目录下以服务接口全路径名命名的文件中写对应的实现类的全路径名称,每一行代表一个实现,如果需要注释信息可以使用“#”进行注释,根据官方的要求,这个文件的编码格式必须是UTF-8。我们示例中的LogService的全路径名是com.elim.learn.basic.spi.service.LogService,所以我们需要在类路径下的META-INF/services目录下创建一个名称为com.elim.learn.basic.spi.service.LogService文件。在本示例中我们一个提供了三个实现,所以该文件的内容如下。
#console
com.elim.learn.basic.spi.service.impl.ConsoleLogService
#file
com.elim.learn.basic.spi.service.impl.FileLogService
#db
com.elim.learn.basic.spi.service.impl.DBLogService
至此,我们的服务定义和实现配置就完成了,接下来就是使用了。使用的时候核心是ServiceLoader类,我们需要通过这个工具类来加载服务提供者,即对应的服务实现,也需要通过它来获得对应的服务实现。ServiceLoader类的核心入口是其提供的三个可以创建ServiceLoader实例的静态方法,分别是load(Class> , ClassLoader)、load(Class>)和loadInstalled(Class>),三者的区别就在于使用的ClassLoader不一样。load(Class>)方法将使用当前线程持有的ClassLoader,loadInstalled(Class>)方法将使用最顶级的ClassLoader。三者的实现分别如下。
public static ServiceLoaderservice, load(Class
ClassLoader loader)
{
return new ServiceLoader<>(service, loader);
}
public static ServiceLoaderservice) { load(Class
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
public static ServiceLoaderservice) { loadInstalled(Class
ClassLoader cl = ClassLoader.getSystemClassLoader();
ClassLoader prev = null;
while (cl != null) {
prev = cl;
cl = cl.getParent();
}
return ServiceLoader.load(service, prev);
}
ServiceLoader是实现了java.util.Iterator接口的,而且是基于我们所使用的服务的实现,所以可以通过ServiceLoader的实例来遍历其中的服务实现者,从而调用对应的服务提供者。示例如下。
@Test
public void test() {
ServiceLoader
LogService logService = null;
for (Iterator
logService = iter.next();
logService.info("Hello SPI");
}
//由于ServiceLoader是实现了java.util.Iterator接口的,也可以使用增强的for循环
for (LogService service : serviceLoader ) {
service.info("Hello SPI");
}
}
调用结果如下:
在上述示例中我们的基于SPI规范的服务定义和服务实现都是在一个工程里面的,且都是可以看到源码的,但在实际应用中我们的服务提供者往往是以jar包的形式来提供对应的服务实现的。ServiceLoader不是一实例化以后立马就去读配置文件中的服务实现者,并且进行对应的实例化工作的,而是会等到需要通过其Iterator实现获取对应的服务提供者时才会加载对应的配置文件进行解析,具体来说是在调用Iterator的hasNext方法时会去加载配置文件进行解析,在调用next方法时会将对应的服务提供者进行实例化并进行缓存。所有的配置文件只加载一次,服务提供者也只实例化一次,如需要重新加载配置文件可调用ServiceLoader的reload方法。有兴趣的朋友可以参考一下ServiceLoader的完整源码实现。