Spring系列笔记-SPI机制
写在前面
大家好,我是代码小郭,又来和大家分享知识啦!
小郭写文章的目的就是为了促进自己技术的成长,同时分享给大家一起学习交流,如果您对 Java领域感兴趣,可以关注我,我们一起学习。
SPI机制
一、简介
SPI 全称 Service Provider Interface,是一套用来被第三方实现或者扩展的接口,经常用来替换框架本身组件或者进行框架功能扩展。
SPI的作用就是寻找扩展的服务实现,可以实现 解耦 (接口和实现分离),提高框架的 可拓展性(第三方可以自己实现,达到插拔式的效果)。
SPI的本质其实就是基于接口+策略模式+配置文件来实现动态加载。
SPI分为多个角色:Service、Service Provider、ServiceLoader和资源文件。
二、JAVA的SPI设计
JAVA内置了一套最基础的SPI,我们先来看看是如何使用的。
首先定义一个SPI顶层接口:
public interface MySpi {
String getName();
void hello();
}
然后定义两个SPI的不同实现:
public class ASpiImpl implements MySpi {
@Override
public String getName() {
return "A";
}
@Override
public void hello() {
System.out.println(getName() + "执行");
}
}
public class BSpiImpl implements MySpi {
@Override
public String getName() {
return "B";
}
@Override
public void hello() {
System.out.println(getName() + "执行");
}
}
在指定目录(META-INF.services 必须是该目录,jdk源码中写死的)下创建文件:
文件名是接口的全类名,文件内容是实现类的全类名。
文件中填写SPI的实现类的全路径
com.gyd.springdemo.spi.ASpiImpl
com.gyd.springdemo.spi.BSpiImpl
最后写一个启动测试类:
public class TestMain {
public static void main(String[] args) {
ServiceLoader<MySpi> load = ServiceLoader.load(MySpi.class);
Iterator<MySpi> iterator = load.iterator();
while (iterator.hasNext()) {
MySpi next = iterator.next();
System.out.println(next.getName() + " 准备执行");
next.hello();
}
System.out.println("执行结束");
}
}
程序执行输出日志如下:
A 准备执行
A执行
B 准备执行
B执行
执行结束
通过上面的执行结果可以看到,我们针对MySpi的所有实现类的指定方法都得到了执行,这都是java.util.ServiceLoader的功劳。
ServiceLoader是一个简单的服务提供者加载工具。下面是JDK1.8中对应的部分源码:
public static <S> ServiceLoader<S> load(Class<S> service) {
ClassLoader cl = Thread.currentThread().getContextClassLoader();
return ServiceLoader.load(service, cl);
}
可以看出load方法是通过获取currentThread当前线程的 ClassLoader线程上下文类加载器 实例来加载的。
Java应用运行的初始线程的上下文类加载器默认是系统类加载器。这里其实 破坏了双亲委派模型,因为Java应用收到类加载的请求时,按照双亲委派模型会向上请求父类加载器完成,这里并没有这么做(如果面试官让你举例破坏双亲委派模型相关问题,可以用本案例)。
iterator.hasNext()主要是通过 hasNextService()来实现的,我们来看一下主要代码。
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;
}
hasNextService方法会去加载 PREFIX 变量路径下的配置,PREFIX 是一个固定路径,这也就是我们为什么要在META-INF/services/下创建文件的原因。并根据 PREFIX 加上全类名获取到实现类所在的全路径。
上面使用的变量PREFIX,我们再查看ServiceLoader类,发现有一个PREFIX变量,就是前面提到的配置文件的目录路径(这也是为什么只能是这个目录的原因):
public final class ServiceLoader<S>
implements Iterable<S>
{
private static final String PREFIX = "META-INF/services/";
....
}
总结一下,java的spi的流程:
JAVA SPI机制的一个劣势,是无法确认具体加载哪一个实现,也无法加载某个指定的实现,只能加载配置文件中的全部实现,而且仅靠ClassPath的顺序加载是一个非常不严谨的方式。
三、Spring的SPI设计
Spring SPI其实就是基于Java SPI的设计进行了再次封装。我们只需要在 META-INF/spring.factories 中配置接口实现类名,即可通过服务发现机制,在运行时加载接口的实现类。
编辑好META-INF/spring.factories,基于之前JAVA的spi示例,修改一下启动程序:
public class TestSpringMain {
public static void main(String[] args) {
List<MySpi> helloSpiList = SpringFactoriesLoader.loadFactories(MySpi.class,TestSpringMain.class.getClassLoader());
Iterator<MySpi> iterator = helloSpiList.iterator();
while (iterator.hasNext()) {
MySpi next = iterator.next();
System.out.println(next.getName() + " 准备执行");
next.hello();
}
System.out.println("执行结束");
}
}
程序执行输出日志如下:
A 准备执行
A执行
B 准备执行
B执行
执行结束
执行效果和JAVA的SPI一样。
Spring的SPI支持多个扩展点配置到一个文件中,如SpringBoot的spring-boot-autoconfigure-2.7.14.jar中的META-INF/spring.factories:
# Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.autoconfigure.SharedMetadataReaderFactoryContextInitializer,\
org.springframework.boot.autoconfigure.logging.ConditionEvaluationReportLoggingListener
# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.autoconfigure.BackgroundPreinitializer
# Environment Post Processors
org.springframework.boot.env.EnvironmentPostProcessor=\
org.springframework.boot.autoconfigure.integration.IntegrationPropertiesEnvironmentPostProcessor
....
Spring的SPI 虽然属于spring-framework(core),但是目前主要用在spring boot中……
四、SPI思想的应用举例
在各种流行框架如Dubbo、JDBC、Druid、SpringBoot 中都使用到了SPI机制。虽然他们之间的实现方式不同,但原理都差不多。
下面以SpringBoot和Dubbo为例做简单说明。
1、SpringBoot自动配置
SpringBoot中大量运用了Spring的SPI设计,保证了良好的扩展性。
@EnableAutoConfiguration 可以借助 SpringFactoriesLoader 这个特性将标注了 @Configuration 的 JavaConfig 类全部汇总并加载到最终的 ApplicationContext。
例如阿里的数据库连接池组件druid-spring-boot-starter:
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.21</version>
</dependency>
SpringBoot启动时,会自动扫描依赖包中的META-INF,通过配置文件(如spring.factories)来实例化所指定的配置类,及一些启动时的初始化操作。
如上图所示,SpringBoot启动时会将DruidDataSourceAutoConfigure作为一个配置类进行加载。
2、Dubbo的SPI设计
Dubbo 中的扩展能力是从 JDK 标准的 SPI 扩展点发现机制加强而来。
通过SPI思想,用户能够基于 Dubbo 提供的扩展能力,很方便基于自身需求扩展其他协议、过滤器、路由等。
Dubbo 扩展能力使得 Dubbo 项目很方便的切分成一个一个的子模块,实现热插拔特性。用户完全可以基于自身需求,替换 Dubbo 原生实现,来满足自身业务需求。
Dubbo SPI 的相关逻辑被封装在了 ExtensionLoader 类中,通过 ExtensionLoader,我们可以加载指定的实现类。Dubbo SPI 所需的配置文件需放置在 META-INF/dubbo 路径下。
下面还是拿最开始的JAVA SPI的示例稍作改动来演示。
配置文件修改为:
代码调整:
//增加该注解
@SPI
public interface MySpi {
String getName();
void hello();
}
执行入口类:
public class TestDubboMain {
public static void main(String[] args) {
ExtensionLoader<MySpi> extensionLoader =
ExtensionLoader.getExtensionLoader(MySpi.class);
MySpi a = extensionLoader.getExtension("a");
a.hello();
MySpi b = extensionLoader.getExtension("b");
b.hello();
}
}
程序执行输出日志如下:
11:17:29.009 [main] INFO org.apache.dubbo.common.logger.LoggerFactory - using logger: org.apache.dubbo.common.logger.slf4j.Slf4jLoggerAdapter
A执行
B执行
Process finished with exit code 0
Dubbo相对于JAVA的spi的最大的亮点是:JDK 标准的 SPI 会一次性实例化扩展点所有实现,如果有扩展实现初始化很耗时,但如果没用上也加载,会很浪费资源。而Dubbo支持只加载指定扩展实现。
五、参考资料
啰嗦一句
小郭今天的学习成果跟大家分享完毕,大家有觉得不好的地方欢迎在下方评论区开喷👇👇👇 也可以给我点点关注鼓励一下呀👍👍👍