先点赞再看,养成好习惯
SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的实质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样能够在运行时,动静为接口替换实现类。正因而个性,咱们能够很容易的通过 SPI 机制为咱们的程序提供拓展性能。
本文次要是个性 & 用法介绍,不波及源码解析(源码都很简略,置信你肯定一看就懂)
SPI 有什么用?
举个栗子,当初咱们设计了一款全新的日志框架:super-logger。默认以 XML 文件作为咱们这款日志的配置文件,并设计了一个配置文件解析的接口:
package com.github.kongwu.spisamples;
public interface SuperLoggerConfiguration {void configure(String configFile);
}
而后来一个默认的 XML 实现:
package com.github.kongwu.spisamples;
public class XMLConfiguration implements SuperLoggerConfiguration{public void configure(String configFile){......}
}
那么咱们在初始化,解析配置时,只须要调用这个 XMLConfiguration 来解析 XML 配置文件即可。
package com.github.kongwu.spisamples;
public class LoggerFactory {
static {SuperLoggerConfiguration configuration = new XMLConfiguration();
configuration.configure(configFile);
}
public static getLogger(Class clazz){......}
}
这样就实现了一个根底的模型,看起来也没什么问题。不过扩展性不太好,因为如果想定制 / 扩大 / 重写解析性能的话,我还得从新定义入口的代码,LoggerFactory 也得重写,不够灵便,侵入性太强了。
比方当初用户 / 应用方想减少一个 yml 文件的形式,作为日志配置文件,那么只须要新建一个 YAMLConfiguration,实现 SuperLoggerConfiguration 就能够。然而……怎么注入呢,怎么让 LoggerFactory 中应用新建的这个 YAMLConfiguration?难不成连 LoggerFactory 也重写了?
如果借助 SPI 机制的话,这个事件就很简略了,能够很不便的实现这个入口的扩大性能。
上面就先来看看,利用 JDK 的 SPI 机制怎么解决下面的扩展性问题。
JDK SPI
JDK 中 提供了一个 SPI 的性能,外围类是 java.util.ServiceLoader。其作用就是,能够通过类名获取在 ”META-INF/services/” 下的多个配置实现文件。
为了解决下面的扩大问题,当初咱们在 META-INF/services/
下创立一个 com.github.kongwu.spisamples.SuperLoggerConfiguration
文件(没有后缀)。文件中只有一行代码,那就是咱们默认的com.github.kongwu.spisamples.XMLConfiguration
(留神,一个文件里也能够写多个实现,回车分隔)
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:
com.github.kongwu.spisamples.XMLConfiguration
而后通过 ServiceLoader 获取咱们的 SPI 机制配置的实现类:
ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
SuperLoggerConfiguration configuration;
while(iterator.hasNext()) {
// 加载并初始化实现类
configuration = iterator.next();}
// 对最初一个 configuration 类调用 configure 办法
configuration.configure(configFile);
最初在调整 LoggerFactory 中初始化配置的形式为当初的 SPI 形式:
package com.github.kongwu.spisamples;
public class LoggerFactory {
static {ServiceLoader<SuperLoggerConfiguration> serviceLoader = ServiceLoader.load(SuperLoggerConfiguration.class);
Iterator<SuperLoggerConfiguration> iterator = serviceLoader.iterator();
SuperLoggerConfiguration configuration;
while(iterator.hasNext()) {configuration = iterator.next();// 加载并初始化实现类
}
configuration.configure(configFile);
}
public static getLogger(Class clazz){......}
}
等等,这里为什么是用 iterator ? 而不是 get 之类的只获取一个实例的办法?
试想一下,如果是一个固定的 get 办法,那么 get 到的是一个固定的实例,SPI 还有什么意义呢?
SPI 的目标,就是加强扩展性。将固定的配置提取进去,通过 SPI 机制来配置。那既然如此,个别都会有一个默认的配置,而后通过 SPI 的文件配置不同的实现,这样就会存在一个接口多个实现的问题。要是找到多个实现的话,用哪个实现作为最初的实例呢?
所以这里应用 iterator 来获取所有的实现类配置。方才曾经在咱们这个 super-logger 包里减少了默认的 SuperLoggerConfiguration 实现。
为了反对 YAML 配置,当初在应用方 / 用户的代码里,减少一个 YAMLConfiguration 的 SPI 配置:
META-INF/services/com.github.kongwu.spisamples.SuperLoggerConfiguration:
com.github.kongwu.spisamples.ext.YAMLConfiguration
此时通过 iterator 办法,就会获取到默认的 XMLConfiguration 和咱们扩大的这个 YAMLConfiguration 两个配置实现类了。
在下面那段加载的代码里,咱们遍历 iterator,遍历到最初,咱们 ** 应用最初一个实现配置作为最终的实例。
再等等?最初一个?怎么算最初一个?
应用方 / 用户自定义的的这个 YAMLConfiguration 肯定是最初一个吗?
这个真的不肯定,取决于咱们运行时的 ClassPath 配置,在后面加载的 jar 天然在前,最初的 jar 里的天然当然也在前面。所以 如果用户的包在 ClassPath 中的程序比 super-logger 的包更靠后,才会处于最初一个地位;如果用户的包地位在前,那么所谓的最初一个依然是默认的 XMLConfiguration。
举个栗子,如果咱们程序的启动脚本为:
java -cp super-logger.jar:a.jar:b.jar:main.jar example.Main
默认的 XMLConfiguration SPI 配置在super-logger.jar
,扩大的 YAMLConfiguration SPI 配置文件在main.jar
,那么 iterator 获取的最初一个元素肯定为 YAMLConfiguration。
但这个 classpath 程序如果反了呢?main.jar 在前,super-logger.jar 在后
java -cp main.jar:super-logger.jar:a.jar:b.jar example.Main
这样一来,iterator 获取的最初一个元素又变成了默认的 XMLConfiguration,咱们应用 JDK SPI 没啥意义了,获取的又是第一个,还是默认的 XMLConfiguration。
因为这个加载程序(classpath)是由用户指定的,所以无论咱们加载第一个还是最初一个,都有可能会导致加载不到用户自定义的那个配置。
所以这也是 JDK SPI 机制的一个劣势,无奈确认具体加载哪一个实现,也无奈加载某个指定的实现,仅靠 ClassPath 的程序是一个十分不谨严的形式
Dubbo SPI
Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未应用 Java 原生的 SPI 机制,而是对其进行了加强,使其可能更好的满足需要。在 Dubbo 中,SPI 是一个十分重要的模块。基于 SPI,咱们能够很容易的对 Dubbo 进行拓展。如果大家想要学习 Dubbo 的源码,SPI 机制务必弄懂。接下来,咱们先来理解一下 Java SPI 与 Dubbo SPI 的用法,而后再来剖析 Dubbo SPI 的源码。
Dubbo 中实现了一套新的 SPI 机制,性能更弱小,也更简单一些。相干逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,咱们能够加载指定的实现类。Dubbo SPI 所需的配置文件需搁置在 META-INF/dubbo 门路下,配置内容如下(以下 demo 来自 dubbo 官网文档)。
optimusPrime = org.apache.spi.OptimusPrime
bumblebee = org.apache.spi.Bumblebee
与 Java SPI 实现类配置不同,Dubbo SPI 是通过键值对的形式进行配置,这样咱们能够按需加载指定的实现类。另外在应用时还须要在接口上标注 @SPI 注解。上面来演示 Dubbo SPI 的用法:
@SPI
public interface Robot {void sayHello();
}
public class OptimusPrime implements Robot {
@Override
public void sayHello() {System.out.println("Hello, I am Optimus Prime.");
}
}
public class Bumblebee implements Robot {
@Override
public void sayHello() {System.out.println("Hello, I am Bumblebee.");
}
}
public class DubboSPITest {
@Test
public void sayHello() throws Exception {
ExtensionLoader<Robot> extensionLoader =
ExtensionLoader.getExtensionLoader(Robot.class);
Robot optimusPrime = extensionLoader.getExtension("optimusPrime");
optimusPrime.sayHello();
Robot bumblebee = extensionLoader.getExtension("bumblebee");
bumblebee.sayHello();}
}
Dubbo SPI 和 JDK SPI 最大的区别就在于反对“别名”,能够通过某个扩大点的别名来获取固定的扩大点。就像下面的例子中,我能够获取 Robot 多个 SPI 实现中别名为“optimusPrime”的实现,也能够获取别名为“bumblebee”的实现,这个性能十分有用!
通过 @SPI 注解的 value 属性,还能够默认一个“别名”的实现。比方在 Dubbo 中,默认的是 Dubbo 公有协定:dubbo protocol – dubbo://
**
来看看 Dubbo 中协定的接口:
@SPI("dubbo")
public interface Protocol {......}
在 Protocol 接口上,减少了一个 @SPI 注解,而注解的 value 值为 Dubbo,通过 SPI 获取实现时就会获取 Protocol SPI 配置中别名为 dubbo 的那个实现,com.alibaba.dubbo.rpc.Protocol
文件如下:
filter=com.alibaba.dubbo.rpc.protocol.ProtocolFilterWrapper
listener=com.alibaba.dubbo.rpc.protocol.ProtocolListenerWrapper
mock=com.alibaba.dubbo.rpc.support.MockProtocol
dubbo=com.alibaba.dubbo.rpc.protocol.dubbo.DubboProtocol
injvm=com.alibaba.dubbo.rpc.protocol.injvm.InjvmProtocol
rmi=com.alibaba.dubbo.rpc.protocol.rmi.RmiProtocol
hessian=com.alibaba.dubbo.rpc.protocol.hessian.HessianProtocol
com.alibaba.dubbo.rpc.protocol.http.HttpProtocol
com.alibaba.dubbo.rpc.protocol.webservice.WebServiceProtocol
thrift=com.alibaba.dubbo.rpc.protocol.thrift.ThriftProtocol
memcached=com.alibaba.dubbo.rpc.protocol.memcached.MemcachedProtocol
redis=com.alibaba.dubbo.rpc.protocol.redis.RedisProtocol
rest=com.alibaba.dubbo.rpc.protocol.rest.RestProtocol
registry=com.alibaba.dubbo.registry.integration.RegistryProtocol
qos=com.alibaba.dubbo.qos.protocol.QosProtocolWrapper
而后只须要通过 getDefaultExtension,就能够获取到 @SPI 注解上 value 对应的那个扩大实现了
Protocol protocol = ExtensionLoader.getExtensionLoader(Protocol.class).getDefaultExtension();
//protocol: DubboProtocol
还有一个 Adaptive 的机制,尽管非常灵活,但……用法并不是很“优雅”,这里就不介绍了
Dubbo 的 SPI 中还有一个“加载优先级”,优先加载内置(internal)的,而后加载内部的(external),按优先级程序加载,如果遇到反复就跳过不会加载 了。
所以如果想靠 classpath 加载程序去笼罩内置的扩大,也是个不太理智的做法,起因同上 – 加载程序不谨严
Spring SPI
Spring 的 SPI 配置文件是一个固定的文件 – META-INF/spring.factories
,性能上和 JDK 的相似,每个接口能够有多个扩大实现,应用起来非常简单:
// 获取所有 factories 文件中配置的 LoggingSystemFactory
List<LoggingSystemFactory>> factories =
SpringFactoriesLoader.loadFactories(LoggingSystemFactory.class, classLoader);
上面是一段 Spring Boot 中 spring.factories 的配置
# Logging Systems
org.springframework.boot.logging.LoggingSystemFactory=\
org.springframework.boot.logging.logback.LogbackLoggingSystem.Factory,\
org.springframework.boot.logging.log4j2.Log4J2LoggingSystem.Factory,\
org.springframework.boot.logging.java.JavaLoggingSystem.Factory
# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader
# ConfigData Location Resolvers
org.springframework.boot.context.config.ConfigDataLocationResolver=\
org.springframework.boot.context.config.ConfigTreeConfigDataLocationResolver,\
org.springframework.boot.context.config.StandardConfigDataLocationResolver
......
Spring SPI 中,将所有的配置放到一个固定的文件中,省去了配置一大堆文件的麻烦。至于多个接口的扩大配置,是用一个文件好,还是每个独自一个文件好这个,这个问题就见仁见智了(集体喜爱 Spring 这种,干净利落)。
Spring 的 SPI 尽管属于 spring-framework(core),然而目前次要用在 spring boot 中……
和后面两种 SPI 机制一样,Spring 也是反对 ClassPath 中存在多个 spring.factories 文件的,加载时会依照 classpath 的程序顺次加载这些 spring.factories 文件,增加到一个 ArrayList 中。因为没有别名,所以也没有去重的概念,有多少就增加多少。
但因为 Spring 的 SPI 次要用在 Spring Boot 中,而 Spring Boot 中的 ClassLoader 会优先加载我的项目中的文件,而不是依赖包中的文件。所以如果在你的我的项目中定义个 spring.factories 文件,那么你我的项目中的文件会被第一个加载,失去的 Factories 中,我的项目中 spring.factories 里配置的那个实现类也会排在第一个
如果咱们要扩大某个接口的话,只须要在你的我的项目(spring boot)里新建一个 META-INF/spring.factories
文件,只增加你要的那个配置,不要残缺的复制一遍 Spring Boot 的 spring.factories 文件而后批改
**
比方我只想增加一个新的 LoggingSystemFactory 实现,那么我只须要新建一个 META-INF/spring.factories
文件,而不是残缺的复制 + 批改:
org.springframework.boot.logging.LoggingSystemFactory=\
com.example.log4j2demo.Log4J2LoggingSystem.Factory
比照
JDK SPI | DUBBO SPI | Spring SPI | |
---|---|---|---|
文件形式 | 每个扩大点独自一个文件 | 每个扩大点独自一个文件 | 所有的扩大点在一个文件 |
获取某个固定的实现 | 不反对,只能按程序获取所有实现 | 有“别名”的概念,能够通过名称获取扩大点的某个固定实现,配合 Dubbo SPI 的注解很不便 | 不反对,只能按程序获取所有实现。但因为 Spring Boot ClassLoader 会优先加载用户代码中的文件,所以能够保障用户自定义的 spring.factoires 文件在第一个,通过获取第一个 factory 的形式就能够固定获取自定义的扩大 |
其余 | 无 | 反对 Dubbo 外部的依赖注入,通过目录来辨别 Dubbo 内置 SPI 和内部 SPI,优先加载外部,保障外部的优先级最高 | 无 |
文档残缺度 | 文章 & 三方材料足够丰盛 | 文档 & 三方材料足够丰盛 | 文档不够丰盛,但因为性能少,应用非常简单 |
IDE 反对 | 无 | 无 | IDEA 完满反对,有语法提醒 |
三种 SPI 机制比照之下,JDK 内置的机制是最弱鸡的,然而因为是 JDK 内置,所以还是有肯定利用场景,毕竟不必额定的依赖;Dubbo 的性能最丰盛,但机制有点简单了,而且只能配合 Dubbo 应用,不能齐全算是一个独立的模块;Spring 的性能和 JDK 的相差无几,最大的区别是所有扩大点写在一个 spring.factories 文件中,也算是一个改良,并且 IDEA 完满反对语法提醒。
各位看官们大佬们,你们感觉 JDK/Dubbo/Spring 三种 SPI 的机制,哪个更好呢?欢送评论区留言
参考
- Introduction to the Service Provider Interfaces – Oracle
- Dubbo SPI – Apache Dubbo
- Creating Your Own Auto-configuration – Spring
原创不易,未经受权禁止转载。如果我的文章对您有帮忙,请点赞 / 珍藏 / 关注激励反对一下吧❤❤❤❤❤❤