我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。
为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。
1、什么是SPI
这里先说下SPI的一个概念,SPI英文为Service Provider Interface单从字面可以理解为Service提供者接口,正如从SPI的名字去理解SPI就是Service提供者接口;我对SPI的定义:提供给服务提供厂商与扩展框架功能的开发者使用的接口。
很多框架都使用了java的SPI机制,如JDBC4中的java.sql.Driver的SPI实现(mysql驱动、oracle驱动等)、common-logging的日志接口实现、dubbo的扩展实现等等框架;
2、如何编写SPI
当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。
基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。
JDK提供服务实现查找的一个工具类:java.util.ServiceLoader。
3、Example
1)Interface
public interface Speak {
void sayHello();
}
2)Implement
有2个不同的实现,修改配置文件的时候就会实例化不同的对象.
public class SpeakImpl implements Speak {
@Override
public void sayHello() {
System.out.println("haha");
}
}
另外一个实现:
public class AnnSpeak implements Speak {
@Override
public void speak() {
System.out.println("hi, ann!");
}
}
3)Config
在resources/META-INF.services文件下添加以接口全路径命令的文件:
- resources
- META-INF.services
- com.weimob.jdk.spi.api.Speak
然后在文件中添加这个接口的实现。
- com.weimob.jdk.spi.api.Speak
- META-INF.services
com.weimob.jdk.spi.impl.CarlSpeak
4)Test
public class SPITest {
public static void main(String[] args) {
ServiceLoader<Speak> s = ServiceLoader.load(Speak.class);
Iterator<Speak> searchs = s.iterator();
if(searchs.hasNext()){
Speak speak = searchs.next();
speak.speak();
}
}
}
5)运行结果
修改配置文件为:
com.weimob.jdk.spi.impl.AnnSpeak
运行结果就是:
可以看出SPITest里没有任何和具体实现有关的代码,而是基于spi的机制去查找服务的实现。因为这只是举例这个例子是在同一个项目中运行的。在真实的环境中你可以接口与实现分离这也是能够运行成功的。
4、Q&A
1、配置文件为什么要放在META-INF/services下面?
我们可以在java.util.ServiceLoader中找到以下代码。
private static final String PREFIX = "META-INF/services/";
2、ServiceLoader读取实现类是什么时候实例化的?
ServiceLoader.LazyIterator.nextService中实例化,即load的结果迭代时才会被实例化。