Java SPI机制浅析
- 2019 年 10 月 4 日
- 筆記
在平时开发项目的过程中,相信很多读者都看到过这样的目录,/META-INF/services目录,该目录下的文件名是接口的全称,其内容是具体的接口实现。这就是使用了SPI机制。如:
- mysql-connector

- JDK中的nio SPI

再如,logback-classic

接下来,我们就来聊聊java SPI机制,文章内容主要包含如下几个部分:
- SPI概念和规范
- 根据规范编写一个简单的SPI
- 小结
一、SPI概念和规范
1.1 SPI概念
SPI全称为Service Provicder Interface,是JDK内置的一种服务提供发现功能,一种动态替换发现的机制。
举个例子,要想在运行时动态给一个接口添加实现,只需要添加一个实现即可。比如JDBC的数据库驱动模块,不同数据库连接驱动接口相同但实现类不同,通常各大厂商(如Mysql、Oracle)会根据一个统一的规范(java.sql.Driver)开发各自的驱动实现逻辑。客户端使用jdbc时不需要去改变代码,直接引入不同的spi接口服务即可。
1.2 SPI规范
使用SPI也需要遵循一定的规范,主要包含如下几点:
- 需要设置/META-INF/目录
- /META-INF/services
- 放到classpath下
- /META-INF/services/目录下放置配置文件
- 文件名是接口全路径名
- 文件内部是要实现的接口实现类
- 文件编码为UTF-8
- 使用ServiceLoad的load方法
二、SPI示例
- 编写一个GreetingService接口
package com.wangmengjun.tutorial.spi; public interface GreetingService { void sayHello(); }
- 编写2个实现类,分别输出英文和中文
package com.wangmengjun.tutorial.spi.impl; import com.wangmengjun.tutorial.spi.GreetingService; public class EnglishGreetingServiceImpl implements GreetingService{ public void sayHello() { System.out.println("Hello , This is SPI"); } }
package com.wangmengjun.tutorial.spi.impl; import com.wangmengjun.tutorial.spi.GreetingService; public class ChineseGreetingServiceImpl implements GreetingService { public void sayHello() { System.out.println("你好,这是SPI"); } }
- 创建META-INF文件目录并设置实现类

- 使用ServiceLoader
package com.wangmengjun.tutorial.spi; import java.util.Iterator; import java.util.ServiceLoader; public class SpiMain { public static void main(String[] args) { ServiceLoader<GreetingService> loader= ServiceLoader.load(GreetingService.class); Iterator<GreetingService> greetingIter = loader.iterator(); while(greetingIter.hasNext()) { GreetingService service= greetingIter.next(); System.out.println(service.getClass().getName()); service.sayHello(); } } }
输出:
com.wangmengjun.tutorial.spi.impl.EnglishGreetingServiceImpl Hello , This is SPI com.wangmengjun.tutorial.spi.impl.ChineseGreetingServiceImpl 你好,这是SPI
经过上述几个步骤,一个spi的简单示例就完成了。
当执行ServiceLoader.load(GreetingService.class)构造完ServiceLoader实例我们可以看到这个时lookupIterator1的值还是null的。这个时候还没有去读取配置文件中的实现类信息。
// The lazy-lookup iterator for iterator operations private Iterator<Provider<S>> lookupIterator1; private final List<S> instantiatedProviders = new ArrayList<>();

当使用迭代器去遍历的时候,才会读取对应的配置文件去解析,调用hasNext方法的时候会去加载配置文件进行解析。文件读取采用BufferedReader的readLine来读取并解析。



三、小结
从上述的示例可以看出:虽然ServiceLoader也算是使用的延迟加载,但是需要通过迭代器迭代获取,所有配置的实现类都要实例化一遍。如果你并不想用某些实现类,它也被加载并实例化了,这就造成了浪费。