SPI的全名为Service Provider Interface.大多数开发人员可能不熟悉,因为这个是针对厂商或者插件的
。
我们系统里抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,xml解析模块、jdbc模块的方案等。
面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,
如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻×××实现的机制。
当服务的提供者,提供了服务接口的一种实现之后,在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。
该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入
。
基于这样一个约定就能很好的找到服务接口的实现类,而不需要再代码里制定。
jdk提供服务实现查找的一个工具类:java.util.ServiceLoader
前面说过SPI是针对厂商或者第三方插件的,但是那样不便于举例,不能很好的引导读者,我们先来个单机的。
服务(接口):一个IProgrammer接口
服务提供者(接口实现):两个实现类 GJProgrammer和JGSProgrammer
资源目录:META-INF/services
代码如下:
public interface IProgrammer {
String title();
}
public class GJProgrammer implements IProgrammer {
@Override
public String title() {
return "高级程序员";
}
}
public class JGSProgrammer implements IProgrammer {
static {
System.out.println("加载了");
}
@Override
public String title() {
return "架构师";
}
}
资源文件:
整体代码和资源如图所示:
就是META-INF/services里边有一个名为IProgrammer限定名的文件,内容就是两个实现了类的限定名。
com.alipay.campus.demos.spi.GJProgrammer
com.alipay.campus.demos.spi.JGSProgrammer
测试类:
public static void main(String[] args) {
ServiceLoader serviceLoader = ServiceLoader.load(IProgrammer.class);
Iterator it = serviceLoader.iterator();
while(it.hasNext()){
System.out.println("Iterator next()方法调用.." );
IProgrammer iProgrammer = it.next();
String title = iProgrammer.title();
System.out.println("programmer title="+title);
}
}
输出结果:
Iterator< IProgrammer> next()方法调用..
programmer title=高级程序员
Iterator< IProgrammer> next()方法调用..
加载了
programmer title=架构师
我们只是将实现类放到了META-INF/services下,文件名命名为IProgrammer接口限定名,内容放上两个实现类的限定名,然后使用ServiceLoader.loader 就能获取到所有实现类的实例,并能调用相应的接口方法。
此时我们自己是开发者也是使用者,我们完全可以自己new出这两个实现类,然后调用相应的方法。
我们可以换一个角度,以JDK获取数据库驱动为例:
JDK定义了一套接口规范(Driver接口)获取数据库驱动(jdk的jar),
mysql是数据库厂商(mysql-connector的jar),
oracle也是数据库厂商(oracle-connector的jar)
说道这里你大概知道是怎么回事了吧,jdk可不知道有哪些数据库厂商会接入,实现根本不在jdk哪里,在厂商哪里,那么程序员在写程序的时候引入jdk和mysql驱动怎么就能获取到mysql数据库的驱动了呢?
JDK和Mysql使用SPI原理
我们先来看下jdk是如何使用
DriverManager.getConnection(url,username,password)
DriverManager有一个静态代码块:
static {
loadInitialDrivers();
println("JDBC DriverManager initialized");
}
private static void loadInitialDrivers() {
String drivers;
try {
drivers = AccessController.doPrivileged(new PrivilegedAction() {
public String run() {
return System.getProperty("jdbc.drivers");
}
});
} catch (Exception ex) {
drivers = null;
}
AccessController.doPrivileged(new PrivilegedAction() {
public Void run() {
//ServiceLoader的使用
ServiceLoader loadedDrivers = ServiceLoader.load(Driver.class);
Iterator driversIterator = loadedDrivers.iterator();
try{
while(driversIterator.hasNext()) {
driversIterator.next();
}
} catch(Throwable t) {
// Do nothing
}
return null;
}
});
println("DriverManager.initialize: jdbc.drivers = " + drivers);
if (drivers == null || drivers.equals("")) {
return;
}
String[] driversList = drivers.split(":");
println("number of Drivers:" + driversList.length);
for (String aDriver : driversList) {
try {
println("DriverManager.Initialize: loading " + aDriver);
Class.forName(aDriver, true,
ClassLoader.getSystemClassLoader());
} catch (Exception ex) {
println("DriverManager.Initialize: load failed: " + ex);
}
}
}
这次就明了!!!!
但是为什么要讲这个呢,主要是看SpringBoot的时候看了启动过程,有段代码很类似,在Spring中也有一种类似与Java SPI的加载机制。它在META-INF/spring.factories文件中配置接口的实现类名称,然后在程序中读取这些配置文件并实例化。
这种自定义的SPI机制是Spring Boot Starter实现的基础,引入一个jar包,里边有某个接口的实现并将接口实现放入到/META-INF/spring.factories里,SpringBoot应用就自动回装载这些类,就能完成一些拓展功能,是不是很强大。