Java之SPI机制详解
Java之SPI机制详解
一、SPI概述
1、概述
SPI 即 Service Provider Interface
,字面意思就是:“服务提供者的接口”,专门提供给服务提供者或者扩展框架功能的开发者去使用的一个接口。SPI 将服务接口和具体的服务实现分离开来,将服务调用方和服务实现者解耦,能够提升程序的扩展性、可维护性。修改或者替换服务实现并不需要修改调用方。例如SpringBoot 的自动装配就是基于spring 的 SPI 扩展机制和EnableAutoConfiguration实现的
2、SPI 和 API 的区别
API 中的接口是服务提供者给服务调用者的一个功能列表,而 SPI 中更多强调的是,服务调用者对服务实现的一种约束,服务提供者根据这种约束实现的服务,可以被服务调用者发现
一般模块之间都是通过通过接口进行通讯,那我们在服务调用方和服务实现方(也称服务提供者)之间引入一个“接口”。当实现方提供了接口和实现,我们可以通过调用实现方的接口从而拥有实现方给我们提供的能力,这就是 API ,这种接口和实现都是放在实现方的。
**当接口存在于调用方这边时,就是 SPI **,由接口调用方确定接口规则,然后由不同的厂商去根据这个规则对这个接口进行实现,从而提供服务。
举个例子:公司 H 是一家科技公司,新设计了一款芯片,然后现在需要量产了,而市面上有好几家芯片制造业公司,这个时候,只要 H 公司指定好了这芯片生产的标准(定义好了接口标准),那么这些合作的芯片公司(服务提供者)就按照标准交付自家特色的芯片(提供不同方案的实现,但是给出来的结果是一样的)
二、Demo演示
1、调用方创建(Service Provider Interface)
新建一个java项目,新建 Logger
接口,这个就是 SPI , 服务提供者接口,后面的服务提供者就要针对这个接口进行实现
1 | package com.shawn.up.spi; |
接下来就是 LoggerService
类,这个主要是为服务使用者(调用方)提供特定功能的。这个类也是实现 Java SPI 机制的关键所在,如果存在疑惑的话可以先往后面继续看。
1 | package com.shawn.up.spi; |
新建 Main
类(服务使用者,调用方),启动程序查看结果。
1 | package com.shawn.up.spi; |
此时我们只是空有接口,并没有为 Logger
接口提供任何的实现,所以输出结果中没有按照预期打印相应的结果。可以使用命令或者直接使用 IDEA 将整个程序直接打包成 jar 包
2、服务方(Service Provider)
接下来新建一个项目用来实现 Logger
接口,新建 Logback
类
1 | package com.shawn.up.spi.service; |
将 service-provider-interface
的 jar 导入项目中。新建 lib 目录,然后将 jar 包拷贝过来,再添加到项目中,然后右键Add as Library
,完成后就可以在项目中导入 jar 包里面的一些类和方法了,就像 JDK 工具类导包一样的(打完jar包后也可以直接maven引入,这个更方便)
实现 Logger
接口,在 src
目录下新建 META-INF/services
文件夹,然后新建文件com.shawn.up.spi.Logger
(SPI 的全类名),文件里面的内容是:com.shawn.up.spi.service.Logback
(Logback 的全类名,即 SPI 的实现类的包名 + 类名),这是 JDK SPI 机制 ServiceLoader 约定好的标准
Java 中的 SPI 机制就是在每次类加载的时候会先去找到 class 相对目录下的
META-INF
文件夹下的 services 文件夹下的文件,将这个文件夹下面的所有文件先加载到内存中,然后根据这些文件的文件名和里面的文件内容找到相应接口的具体实现类,找到实现类后就可以通过反射去生成对应的对象,保存在一个 list 列表里面,所以可以通过迭代或者遍历的方式拿到对应的实例对象,生成不同的实现。
所以会提出一些规范要求:文件名一定要是接口的全类名,然后里面的内容一定要是实现类的全类名,实现类可以有多个,直接换行就好了,多个实现类的时候,会一个一个的迭代加载。接下来同样将 service-provider
项目打包成 jar 包,这个 jar 包就是服务提供方的实现。
3、服务发现
新建 Main 方法测试
1 | public class TestJavaSPI { |
通过使用 SPI 机制,可以看出服务(LoggerService
)和 服务提供者两者之间的耦合度非常低,如果说我们想要换一种实现,那么其实只需要修改 service-provider
项目中针对 Logger
接口的具体实现就可以了,只需要换一个 jar 包即可,也可以有在一个项目里面有多个实现,这不就是 SLF4J 原理吗?
如果某一天需求变更了,此时需要将日志输出到消息队列,或者做一些别的操作,这个时候完全不需要更改 Logback 的实现,只需要新增一个服务实现(service-provider)可以通过在本项目里面新增实现也可以从外部引入新的服务实现 jar 包。我们可以在服务(LoggerService)中选择一个具体的 服务实现(service-provider) 来完成我们需要的操作。
4、SPI应用
要说 spi 的实际应用,大家最常见的应该就是日志框架slf4j
了,它利用 spi 实现了插槽式接入其他具体的日志框架。说白了,slf4j
本身就是个日志门面,并不提供具体的实现,需要绑定其他具体实现才能真正的引入日志功能。例如我们可使用log4j2
作为具体的绑定器,只需要在 pom 中引入slf4j-log4j12
,就可以使用具体功能。
1 | <dependency> |
引入项目后,点开它的 jar 包看一下具体结构:
回头看一下 jar 包的META-INF.services
里面,通过 spi 注入了Reload4jServiceProvider
这个实现类,它实现了SLF4JServiceProvider
这一接口,在它的初始化方法initialize()
中,会完成初始化等工作,后续可以继续获取到LoggerFactory
和Logger
等具体日志对象
三、SPI原理
1、ServiceLoader介绍
ServiceLoader
是 JDK 提供的一个工具类, 位于package java.util;
包下。JDK 官方注释:**一种加载服务实现的工具。**再往下看,我们发现这个类是一个 final
类型的,所以是不可被继承修改,同时它实现了 Iterable
接口。之所以实现了迭代器,是为了方便后续我们能够通过迭代的方式得到对应的服务实现
1 | public final class ServiceLoader<S> implements Iterable<S>{ xxx...} |
可以看到一个熟悉的常量定义:private static final String PREFIX = "META-INF/services/";
下面是 load
方法:可以发现 load
方法支持两种重载后的入参
1 | public static <S> ServiceLoader<S> load(Class<S> service) { |
根据代码的调用顺序,在 reload()
方法中是通过一个内部类 LazyIterator
实现的。ServiceLoader
实现了 Iterable
接口的方法后,具有了迭代的能力,在这个 iterator
方法被调用时,首先会在 ServiceLoader
的 Provider
缓存中进行查找,如果缓存中没有命中那么则在 LazyIterator
中进行查找
1 | public Iterator<S> iterator() { |
其中providers
就是一个缓存,在迭代器中如果先从这里面进行查找,如果里面有就继续往下找,没有了的话就用这个懒加载的lookupIterator
查找。在调用 LazyIterator
时,看看它里面的hasNext()
和next()
两个方法是怎么实现的
1 | public boolean hasNext() { |
这个acc
是一个安全管理器,在前面通过System.getSecurityManager()
判断并赋值,debug 看一下这里都是null
,所以直接看hasNextService()
和nextService()
方法就可以了。
在hasNextService()
方法中,会取出接口取出实现类的类名放到nextName
中:
接下来,在nextService()
方法中,则会先加载这个实现类,然后实例化对象,最终放入缓存中去。
在迭代器的迭代过程中,会完成所有实现类的实例化,其实归根结底,还是基于 java 反射去实现的。
2、自己实现一个 ServiceLoader
1 | public class MyServiceLoader<S> { |
主要的流程就是:
- 通过 URL 工具类从 jar 包的
/META-INF/services
目录下面找到对应的文件 - 读取这个文件的名称找到对应的 spi 接口
- 通过
InputStream
流将文件里面的具体实现类的全类名读取出来 - 根据获取到的全类名,先判断跟 spi 接口是否为同一类型,如果是的,那么就通过反射的机制构造对应的实例对象
- 将构造出来的实例对象添加到
Providers
的列表中
四、总结
其实不难发现,SPI 机制的具体实现本质上还是通过反射完成的。即:我们按照规定将要暴露对外使用的具体实现类在 META-INF/services/
文件下声明。
另外,SPI 机制在很多框架中都有应用:Spring 框架的基本原理也是类似的方式。还有 Dubbo 框架提供同样的 SPI 扩展机制,只不过 Dubbo 和 spring 框架中的 SPI 机制具体实现方式跟咱们今天学得这个有些细微的区别,不过整体的原理都是一致的
通过 SPI 机制能够大大地提高接口设计的灵活性,但是 SPI 机制也存在一些缺点,比如:
- 遍历加载所有的实现类,这样效率还是相对较低的;
- 当多个
ServiceLoader
同时load
时,会有并发问题。
参考文章