1.什么是SPI
动态服务发现机制。它的主要实现是"基于接口的编程+策略模式+配置文件"组合实现的动态加载机制。
Java SPI机制,图解如下:
2.SPI机制设计思想
系统设计的各个抽象,往往有很多不同的实现方案,在面向对象的设计里,一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。为了实现在模块装配的时候能不在程序里动态指明,这就需要一种服务发现机制。
Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。有点类似IOC的思想,就是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要。所以SPI的核心思想就是解耦。
3.Java SPI使用场景
1.JDBC数据库驱动实现
JDBC4.0以前, 开发人员还需要基于Class.forName("xxx")的方式来装载驱动;JDBC4.0也是基于Java SPI机制来发现驱动服务,可以通过META-INF/services/java.sql.Driver文件里指定实现类的方式来暴露驱动提供者.
2.common-logging 日志实现统一接口
Apache最早提供的日志的门面接口。只有接口,没有实现。具体方案由各提供商实现,发现日志提供商是通过扫描 META-INF/services/org.apache.commons.logging.LogFactory配置文件,通过读取该文件的内容找到日志提供商的实现类。只要我们的日志实现里包含了这个文件,并在文件里制定 LogFactory工厂接口的实现类即可。
3.Dubbo 框架很多组件使用到SPI机制
Dubbo框架中大量使用了SPI技术,不过它对Java提供的原生SPI做了封装,并不是又有JDK的实现,但是它们的思想仍然是一样的。里面有很多个组件,每个组件在框架中都是以接口的形成抽象出来!具体的实现又分很多种,在程序执行时根据用户的配置来按需取接口的实现。方便了接口的各种实现灵活应用。例如:Dubbo中发布不同协议的Protocol接口
4.Java SPI使用规范
遵循"约定优于配置"原则,要使用Java SPI,需要遵循如下约定:
1.当服务提供者提供了接口的一种具体实现后,在resources/META-INF/services目录下创建一个以"接口全限定名"为命名的文件,内容为实现类的全限定名;
2.主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
3.如果SPI的实现类为Jar包,则需要放在主程序的ClassPath路径中
4.接口的具体实现类,必须有一个无参构造方法
5.Java SPI示例
本例以JDBC为Demo,来介绍Java SPI的具体实现。
①定义一组接口,作为官方提供的门面接口,只有接口,没有实现。具体方案由各提供商实现
package com.demo.spi;
public interface DataBaseDriver {
//数据库驱动接口规范
public void connect(String host);
}
②MySql数据库厂商,实现该接口(如果使用Maven,则需要依赖官方提供的接口。这就是为什么我们引入log4j包时,为什么必须引入common-logging包,因为common-log包是提供的接口包,log4j是具体的厂商实现包)
<dependency>
<groupId>com.demo.spi</groupId>
<artifactId>DataBaseDriver</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
package com.demo.spi;
public class MySqlDriver implements DataBaseDriver {
/**
* MySql实现数据库规范
*/
public void connect(String host) {
System.out.println("This is MySql DataBase impl,connect to " + host);
}
}
③约定实现。在resources目录下,新建META-INF/services目录。然后新建一个以"接口全限定名"为文件名的文件,本例为com.demo.spi.DataBaseDriver;文件内容为实现类的全限定名,本例为com.dmeo.spi.MySqlDriver
④在另一个新的工程中,如果需要连接数据库MySQL,则引入MySQL的Maven依赖,使用 ServiceLoader 类来加载配置文件中指定的实现
<dependency>
<groupId>com.demo.spi</groupId>
<artifactId>MySQLDriver</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
public class DataBaseConnect {
public static void main(String[] args) {
//ServiceLoader,通过反射获取对应类实例
ServiceLoader<DataBaseDriver> serviceLoader = ServiceLoader.load(DataBaseDriver.class);
for(DataBaseDriver driver : serviceLoader){
driver.connect("127.0.0.1");
}
}
}
输出结果:
This is MySql DataBase impl,connect to 127.0.0.1
⑤如需使用OracleDriver,只需要将Maven依赖修改,即可在不改变代码的情况下,由MySql实现替换为Oracle实现
6.ServiceLoader类源码解析
SPI机制,主要用到ServiceLoader这个类,ServiceLoader通过读取resources/META-INF/services/com.xxx.xxx.xxxService文件下的xxxService的spi实现类,通过反射获取对应类实例,并调用对应方法。
1.我们先瞅瞅一下ServiceLoader类
我们可以看到,该类中几个变量PREFIX、service、loader、acc、providers、lookupIterator,还有一些方法,我们也可以看到我们使用的ServiceLoader.load()方法,该方法是一个重载方法,不同区别在于使用自定义的或者是默认的ClassLoader。
public final class ServiceLoader<S> implements Iterable<S>{
//约定文件路径
private static final String PREFIX = "META-INF/services/";
// The class or interface representing the service being loaded
// 代表被加载的类或者接口
private final Class<S> service;
// The class loader used to locate, load, and instantiate providers
// 用于定位,加载和实例化providers的类加载器
private final ClassLoader loader;
// The access control context taken when the ServiceLoader is created
// 创建ServiceLoader时采用的访问控制上下文
private final AccessControlContext acc;
// Cached providers, in instantiation order
// 缓存providers,按实例化的顺序排列
private LinkedHashMap<String,S> providers = new LinkedHashMap<>();
// The current lazy-lookup iterator
// 懒查找迭代器
private LazyIterator lookupIterator;
......
}
2.我们来分析ServiceLoader的load()方法
//步骤1.
public static <S> ServiceLoader<S> load(Class<S> service) {
//根据上下文信息,来获取ClassLoader加载类
ClassLoader cl = Thread.currentThread().getContextClassLoader();
//执行load方法
return ServiceLoader.load(service, cl);
}
//步骤2.
public static <S> ServiceLoader<S> load(Class<S> service,ClassLoader loader){
//重新new一个带ClassLoader的ServiceLoader类
return new ServiceLoader<>(service, loader);
}
//步骤3.完成ServiceLoader类一些变量的初始化操作
private ServiceLoader(Class<S> svc, ClassLoader cl) {
service = Objects.requireNonNull(svc, "Service interface cannot be null");
loader = (cl == null) ? ClassLoader.getSystemClassLoader() : cl;
acc = (System.getSecurityManager() != null) ? AccessController.getContext() : null;
reload();
}
public void reload() {
//ServiceLoader先判断成员变量providers对象中(LinkedHashMap<String,S>类型)是否有缓存实例对象,如果有缓存,直接返回。如果没有缓存,执行类的装载,
providers.clear();
lookupIterator = new LazyIterator(service, loader);
}
private LazyIterator(Class<S> service, ClassLoader loader) {
this.service = service;
this.loader = loader;
}
3.通过迭代器接口,来获取对象实例
读取META-INF/services/下的配置文件,获得所有能被实例化的类的名称,会执行hasNextService()方法。然后再执行hasNext()方法,最后执行nextService()方法,通过反射方法Class.forName()加载类对象,并用instance()方法将类实例化。
//步骤1.
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
//获取实现类全路径(eg:META-INF/services/com.demo.spi.DataBaseDriver)
String fullName = PREFIX + service.getName();
//根据全路径,获取资源
if (loader == null)
configs = ClassLoader.getSystemResources(fullName);
else
configs = loader.getResources(fullName);
} catch (IOException x) {
fail(service, "Error locating configuration files", x);
}
}
while ((pending == null) || !pending.hasNext()) {
if (!configs.hasMoreElements()) {
return false;
}
pending = parse(service, configs.nextElement());
}
nextName = pending.next();
return true;
}
//步骤2.
public boolean hasNext() {
if (acc == null) {
return hasNextService();
} else {
PrivilegedAction<Boolean> action = new PrivilegedAction<
//执行hasNextService()方法
public Boolean run() { return hasNextService(); }
};
return AccessController.doPrivileged(action, acc);
}
}
//步骤3.
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
//通过反射方法Class.forName()加载类对象
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,"Provider " + cn + " not a subtype");
}
try {
//使用instance()方法将类实例化
S p = service.cast(c.newInstance());
//把实例化后的类缓存到providers对象中(LinkedHashMap<String,S>类型),然后返回实例对象
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,"Provider " + cn + " could not be instantiated",x);
}
throw new Error(); // This cannot happen
}
7.总结
优点:
实现解耦,使得第三方服务模块的装配控制的逻辑与调用者的业务代码分离。例如数据库驱动,更改数据库,只需修改Maven依赖或者替换引入的jar包即可。
缺点:
1.ServiceLoader加载,只能通过遍历获取,导致获取实现类的方式不灵活,只能用过Iterator形式来获取,不能根据指定的需求来获取需要的实现类,有点局限;
2.多线程并发编程中,使用ServiceLoader类来加载实例,线程不安全。
END