一、什么是SPI ?
SPI 全称:Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。
面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。
为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。java spi就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。
SPI的作用就是为被扩展的API寻找服务实现。
SPI(Service Provider Interface),是JDK内置的一种 服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是 解耦。
本质:Java SPI 实际上是“基于接口的编程+策略模式+约定配置文件” 组合实现的动态加载机制,在JDK中提供了工具类:“java.util.ServiceLoader”来实现服务查找。
SPI整体机制图如下:
当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。
二、SPI 的不足
1.不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
2.获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。(Spring 的BeanFactory,ApplicationContext 就要高级一些了。)
3.多个并发多线程使用 ServiceLoader 类的实例是不安全的。
三、API 与 SPI
API代表应用程序编程接口,其中API是一种访问某种软件或平台提供的服务/功能的方法。
SPI代表服务提供商接口,其中SPI是注入,扩展或更改软件或平台行为的方式。
API通常是用作客户端访问服务,并且具有以下属性:
1、API是一种访问服务以实现特定行为或输出的编程方式。
2、但是API一旦被客户端使用,除非有适当的通信,否则它不能(也不应)被更改/删除。
SPI面向服务提供者,并且具有以下属性:
1、SPI是一种扩展/更改软件或平台行为的方式。
2、添加SPI接口将导致问题并可能破坏现有的实现。
换句话说,API会告诉您特定的类/方法为您执行什么操作,而SPI则告诉您必须执行哪些操作才能符合要求。
API (Application Programming Interface)在大多数情况下,都是实现方制定接口并完成对接口的实现,调用方仅仅依赖接口调用,且无权选择不同实现。 从使用人员上来说,API 直接被应用开发人员使用。
SPI (Service Provider Interface)是调用方来制定接口规范,提供给外部来实现,调用方在调用时则选择自己需要的外部实现。 从使用人员上来说,SPI 被框架扩展人员使用。
四、SPI 应用场景
SPI扩展机制应用场景有很多,比如Common-Logging,JDBC,Dubbo等等。
SPI流程:
- 有关组织和公式定义接口标准
- 第三方提供具体实现: 实现具体方法, 配置 META-INF/services/${interface_name} 文件
- 开发者使用
比如JDBC场景下:
首先在Java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商提供。
在MySQL的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
同样在PostgreSQL的jar包PostgreSQL-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是PostgreSQL对Java的java.sql.Driver的实现。
个人简单描述下使用的过程(源码就不贴了):
- 建立链接程序提前执行ServiceLoader对java.sql.Driver的具体实现进行实例化。
- 具体的实例化中会获取当前服务的一些配置属性,封装完善该Driver具体实现类。
- 建立链接的方法使用到具体的Driver实现类建立链接。
五、ServiceLoader原理
服务端很好理解,就是一个定义,客户端来看看ServiceLoader的源码。
首先,ServiceLoader实现了Iterable接口,所以它有迭代器的属性,这里主要都是实现了迭代器的hasNext和next方法。这里主要都是调用的lookupIterator的相应hasNext和next方法,lookupIterator是懒加载迭代器。
其次,LazyIterator中的hasNext方法,静态变量PREFIX就是”META-INF/services/”目录,这也就是为什么需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件。
最后,通过反射方法Class.forName()加载类对象,并用newInstance方法将类实例化,并把实例化后的类缓存到providers对象中,(LinkedHashMap<String,S>类型) 然后返回实例对象。
六、代码实践案例
1、利用三方jar包来利用注解生成META-INFO/services, 注解:@MetaInfServices
<dependency>
<groupId>org.kohsuke.metainf-services</groupId>
<artifactId>metainf-services</artifactId>
</dependency>
定义客户端为interface工程,定义学习接口Study,定义方法为学习方式
定义服务端是provider工程,定义三个实现类为配置组件,加入注解 @MetaInfServices
编译之后target存在配置文件
2、直接使用框架的SPI机制(Spring SPI)
Spring SPI介绍:
- 工具类:Spring中使用的类是SpringFactoriesLoader,在org.springframework.core.io.support包中
- SpringFactoriesLoader 会扫描 classpath 中的 META-INF/spring.factories文件。
- SpringFactoriesLoader 会加载并实例化 META-INF/spring.factories 中的指定类型。
- META-INF/spring.factories 内容必须是 properties 的Key-Value形式,多值以逗号隔开。
使用细节:
- spring.factories内容的key不只能是接口,也可以是抽象类、具体的类。但是有个原则:=后面必须是key的实现类(子类)
- key还可以是注解,比如SpringBoot中的的key:org.springframework.boot.autoconfigure.EnableAutoConfiguration,它就是一个注解。
- 文件的格式需要保证正确,否则会返回[](不会报错)
- =右边必须不是抽象类,必须能够实例化。且有空的构造函数~
- loadFactories依赖方法loadFactoryNames。loadFactoryNames方法只拿全类名,loadFactories拿到全类名后会立马实例化。
- 此处特别注意:loadFactories实例化完成所有实例后,会调用AnnotationAwareOrderComparator.sort(result)排序,所以它是支持Ordered接口排序的,这个特点特别的重要。
更多精彩文章请访问我的个人博客(zhuoerhuobi.cn)