目录
SPI简介
JdkSPI机制
SpringSPI机制
DubboSPI机制
SPI全称是Service Provider Interface是一种自动服务发现注册机制。本质是将接口实现类的全限定名配置在约定的配置文件,然后由服务加载器读取指定的加载文件名称,再动态加载实现类。这样我们可以在运行的时候动态替换接口实现,通过spi机制可以轻松实现我们应用程序的拓展功能。
那么spi到底有什么用?在哪里用到了呢?下面将会以mysql根据jdk中定义数据库驱动接入顶级接口 java.sql.Driver,实现了自己定义具体实现类加载,作为例子讲述spi的好处。
jdk在rt包中定义了数据库驱动顶级接口java.sql.Driver;
mysql在services中定义了数据库驱动具体实现的全限定名称com.mysql.jdbc.Driver;
mysql实现jdk的Driver接口规范的mysql驱动实现类,通过这样的方式就可以通过loader获取加载的mysql驱动;
public class Driver extends NonRegisteringDriver implements java.sql.Driver {
public Driver() throws SQLException {
}
static {
try {
DriverManager.registerDriver(new Driver());
} catch (SQLException var1) {
throw new RuntimeException("Can't register driver!");
}
}
}
这就是jdk spi 在数据库驱动中的应用,jdk定义数据库驱动接口规范和spi的加载机制。不同的数据库厂商不需要修改原有的jdk封装的代码,只需要根据接口和spi机制,就可以轻松实现自己的数据库驱动加载共功能,这就是spi的好处。
原理
jdk的spi是通过ServiceLoader类实现,里面定义了实现文件加载路径“META-INF/services/”,获取配置文件名称解析获取配置接口中的全限定名称,然后通过全限定名称和反射将对象初始化。
private static final String PREFIX = "META-INF/services/";
private boolean hasNextService() {
if (nextName != null) {
return true;
}
if (configs == null) {
try {
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;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class> c = null;
try {
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 {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}
示例
首先定义任务执行的顶级接口TaskJob;
/**
* 定时任务接口
* @author zy
*/
public interface TaskJob {
/**
* 执行任务
*/
void handle();
然后在创建了订单清除任务ClearOrderTaskJob;
/**
* 订单清除任务
* @author zy
* @Description:
*/
public class ClearOrderTaskJob implements TaskJob {
@Override
public void handle() {
System.out.println("############订单清除逻辑##############");
}
}
制定接口实现加载的配置,在“META-INF/services/”中创建com.zy.order文件,添加ClearOrderTaskJob全限定名称路径地址(多个实现用回车分隔);
ServiceLoader实现了迭代器并且重写了迭代器中的方法,所以可以通过迭代器获取到任务的具体实现类,进行后面的任务执行。
public class TaskJobFactory {
public TaskJob getTaskJob() {
ServiceLoader serviceLoader = ServiceLoader.load(TaskJob.class);
Iterator iterator = serviceLoader.iterator();
while (iterator.hasNext()) {
return iterator.next();
}
return null;
}
}
现在新增加了一个ClearLogTaskJob的实现类,配置如下;
com.zy.order.ClearOrderTaskJob com.zy.order.ClearLogTaskJob
这时获取任务执行发现执行的还是第一个任务执行器;这是因为代码中获取第一个配置的任务就返回执行。所以不管加了多少个实现都是第一个,那么是否可以改为获取最后一个配置返回呢?
在同一个jar包是这种情况,但是在不同jar包中定义了不同实现,比如在a.jar包中定了ClearLogTaskJob,在b.jar包中定义了ClearOrderTaskJob,那么加载顺序就取决于在运行ClassPath配置,在前面加载的jar自然在前,在后面加载的自然在后。
比如按照下面启动脚本启动应用程序,任务ClearLogTaskJob就在第一个。
java -cp a.jar:b.jar:main.jar app.Main
按照下面启动脚本启动应用程序,任务ClearOrderTaskJob就在第一个。
java -cp b.jar:a.jar:main.jar app.Main
按照下面启动脚本启动应用程序,main.jar中有实现就会获取main中的实现类。
java -cp main.jar:b.jar:a.jar app.Main
由于加载顺序由用户指定,所以不管怎么配置都有可能导致加载不了用户想要的那个实现类。
所以jdk的spi劣势就是无法确定具体加载的哪一个实现类,都是一哈梭全部都给加载进来,无法指定需要加载的具体实现,仅靠配置顺序和classPath加载jar顺序是非常不严谨的。
spring的spi机制主要在springboot中使用,springboot通过固定的文件META-INF/spring.factories加载我们需要扩展的接口,并且支持一个接口有多个扩展实现,使用起来十分简单。
下面截取了springboot默认配置,其中是读取配置文件实现,事件监听实现,不同上下文实现。
# PropertySource Loaders org.springframework.boot.env.PropertySourceLoader=\ org.springframework.boot.env.PropertiesPropertySourceLoader,\ org.springframework.boot.env.YamlPropertySourceLoader # Run Listeners org.springframework.boot.SpringApplicationRunListener=\ org.springframework.boot.context.event.EventPublishingRunListener # Application Context Initializers org.springframework.context.ApplicationContextInitializer=\ org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\ org.springframework.boot.context.ContextIdApplicationContextInitializer,\ org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\ org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer
然后统一由SpringFactoriesLoader读取spring.factories,获取对应的实现类进行实例化。
spring也支持多个spring.factories文件,加载顺序会按照classpath顺序依次加载,最后添加到一个ArrayList中。如果项目中定了自己的spring.factories文件,那么项目中的spring.factories将会首先加载。如果我们需要扩展某个接口,只需要新建一个spring.factories文件,然后配置自己定义的实现类全路径。
org.springframework.boot.env.PropertySourceLoader=\
com.my.dd.XmlPropertySourceLoader
最后揭秘dubbo的spi机制,dubbo的spi完全是自己实现的一套机制,功能更加强大,也更加复杂,更加重。其主要逻辑封装在ExtensionLoader中,通过ExtensionLoader类我们可以加载指定的类。下面是ExtensionLoader的部分源码:
private Map> loadExtensionClasses() {
SPI defaultAnnotation = (SPI)this.type.getAnnotation(SPI.class);
if (defaultAnnotation != null) {
String value = defaultAnnotation.value();
if (value != null && (value = value.trim()).length() > 0) {
String[] names = NAME_SEPARATOR.split(value);
if (names.length > 1) {
throw new IllegalStateException("more than 1 default extension name on extension " + this.type.getName() + ": " + Arrays.toString(names));
}
if (names.length == 1) {
this.cachedDefaultName = names[0];
}
}
}
Map> extensionClasses = new HashMap();
this.loadFile(extensionClasses, "META-INF/dubbo/internal/");
this.loadFile(extensionClasses, "META-INF/dubbo/");
this.loadFile(extensionClasses, "META-INF/services/");
return extensionClasses;
}
dubbospi加载有一个加载优先级,优先加载内置的,然后加载外部的(internal),按照优先级顺序加载(dubbo)。 “META-INF/services/”这个地址可以看出dubbo支持jdk原生的spi,“META-INF/dubbo/internal/”dubbo内部的spi文件,“META-INF/dubbo/”自定义扩张dubbo文件配置,如果遇到重复的就跳过不会配置了。配置的样式如下:
threadlocal=com.alibaba.dubbo.cache.support.threadlocal.ThreadLocalCacheFactory lru=com.alibaba.dubbo.cache.support.lru.LruCacheFactory jcache=com.alibaba.dubbo.cache.support.jcache.JCacheFactory
与jdk的spi机制不一样,dubbo的spi是通过键值对的方式进行配置,这样就解决了在jdkspi中无法指定具体实现类的问题,使用时按照需要在接口上标注@SPI注解。
@SPI("lru")
public interface CacheFactory {
@Adaptive({"cache"})
Cache getCache(URL var1);
}
public static void main(String[] args) {
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(CacheFactory.class);
CacheFactory lru = extensionLoader.getExtension("lru");
}
@SPI注解的value属性表示默认别名实现,比如上面的“lru”表示默认实现是LruCacheFactory类。然后通过getDefaultExtension()方法就可以获取到value属性上对应那个扩展实现了。
ExtensionLoader extensionLoader = ExtensionLoader.getExtensionLoader(CacheFactory.class);
CacheFactory defaultExtension = extensionLoader.getDefaultExtension();
String defaultExtensionName = extensionLoader.getDefaultExtensionName();