Java SPI机制简介

1 SPI机制简介 SPI的全名为Service Provider Interface.大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的。在java.util.ServiceLoader的文档里有比较详细的介绍。简单的总结下java spi机制的思想。我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。 java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。

2 SPI具体约定 java spi的具体约定为:当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。jdk提供服务实现查找的一个工具类:java.util.ServiceLoader

3 应用场景 1.common-loggingapache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现, 发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通过读取该文件的内容找到日志提工商实现类。只要我们的日志实现里包含了这个文件,并在文件里制定 LogFactory工厂接口的实现类即可。
2.jdbcjdbc4.0以前, 开发人员还需要基于Class.forName("xxx")的方式来装载驱动,jdbc4也基于spi的机制来发现驱动提供商了,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者.
4 案例说明 假设我们有一个日志服务LogService,其只定义了一个info方法用于输出日志信息,我们希望把它作为SPI,然后具体的实现由对应的服务提供者去实现。LogService的定义如下所示。

package service; public interface LogService { public void info(String msg); }

然后基于这个服务我们会有自己的实现,示例中笔者用了三个实现,分别是ConsoleLogService、FileLogService和DBLogService,其实现都只是简单的打印一下日志类别信息,ConsoleLogService的实现如下所示,其它两个是类似的。
package service; public class ConsoleLogService implements LogService { @Override public void info(String msg) { System.out.println("----console log ----"); } }

package service; public class DBLogService implements LogService { @Override public void info(String msg) { System.out.println("----DB log ----"); } }

package service; public class FileLogService implements LogService { @Override public void info(String msg) { System.out.println("----file log ----"); } }

根据SPI的规范我们的服务实现类必须有一个无参构造方法。我们的SPI服务提供者需要将其在classpath下的META-INF/services目录下以服务接口全路径名命名的文件中写对应的实现类的全路径名称,每一行代表一个实现,如果需要注释信息可以使用“#”进行注释,根据官方的要求,这个文件的编码格式必须是UTF-8。我们示例中的LogService的全路径名是service.LogService,所以我们需要在类路径下的META-INF/services目录下创建一个名称为service.LogService文件。在本示例中我们一个提供了三个实现,所以该文件的内容如下。
service.ConsoleLogService service.DBLogService service.FileLogService

至此,我们的服务定义和实现配置就完成了,接下来就是使用了。使用的时候核心是ServiceLoader类,我们需要通过这个工具类来加载服务提供者,即对应的服务实现,也需要通过它来获得对应的服务实现。ServiceLoader类的核心入口是其提供的三个可以创建ServiceLoader实例的静态方法,分别是load(Class , ClassLoader)、load(Class)和loadInstalled(Class),三者的区别就在于使用的ClassLoader不一样。load(Class)方法将使用当前线程持有的ClassLoader,loadInstalled(Class)方法将使用最顶级的ClassLoader。三者的实现分别如下。
public staticServiceLoader load(Class service, ClassLoader loader){return new ServiceLoader<>(service, loader); } public staticServiceLoader load(Class service) {ClassLoader cl = Thread.currentThread().getContextClassLoader(); return ServiceLoader.load(service, cl); } public staticServiceLoader loadInstalled(Class service) {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的实例来遍历其中的服务实现者,从而调用对应的服务提供者。示例如下。
package service; import java.util.Iterator; import java.util.ServiceLoader; public class AppTest { public static void main(String[] args) { ServiceLoader serviceLoader = ServiceLoader.load(LogService.class); LogService logService = null; for (Iterator iter = serviceLoader.iterator(); iter.hasNext(); ) {logService = iter.next(); logService.info("Hello SPI"); }//由于ServiceLoader是实现了java.util.Iterator接口的,也可以使用增强的for循环for (LogService service : serviceLoader ) {service.info("Hello SPI"); }} }

调用结果如下:
Java SPI机制简介
文章图片

在上述示例中我们的基于SPI规范的服务定义和服务实现都是在一个工程里面的,且都是可以看到源码的,但在实际应用中我们的服务提供者往往是以jar包的形式来提供对应的服务实现的。ServiceLoader不是一实例化以后立马就去读配置文件中的服务实现者,并且进行对应的实例化工作的,而是会等到需要通过其Iterator实现获取对应的服务提供者时才会加载对应的配置文件进行解析,具体来说是在调用Iterator的hasNext方法时会去加载配置文件进行解析,在调用next方法时会将对应的服务提供者进行实例化并进行缓存。所有的配置文件只加载一次,服务提供者也只实例化一次,如需要重新加载配置文件可调用ServiceLoader的reload方法。

【Java SPI机制简介】











    推荐阅读