概述
SPI,Service Provider Interface,一种服务发现机制,指一些提供给你继承、扩展,完成自定义功能的类、接口或方法。
在SPI机制中,服务提供者为某个接口实现具体的类,而在运行时通过SPI机制,查找到对应的实现类,并将其加载进行使用。
JDK 6(参考java.util.ServiceLoader
类的注释Since字段)引进的一个特性,可实现动态加载具体实现类的机制,通过它可以具体实现代码的解耦,也可实现类似于IoC的效果。
SPI的实现方式:提供实现的实现类打包成jar文件,jar文件里面必须有个META-INF/services
目录,其下有一个文本文件,文件名即为SPI接口的全限定名(完整路径名),文件的内容是该jar包中提供的SPI接口的实现类名。文件编码是UTF-8。
作用有两个:
- 把标准定义和接口实现分离,在模块化开发中很好地实现解耦
- 实现功能的扩展,更好地满足定制化的需求
SPI的不足之处:不能根据需求去加载扩展实现,每次都会加载扩展接口的所有实现类并进行实例化,实例化会造成性能开销,并且加载一些不需要用到的实现类,会导致内存资源的浪费。
API和SPI
API,提供给他人使用的具备某项功能的类、接口或方法。
SPI用于一些通用的标准中,为标准的实现产商提供扩展点。标准在上层提供API,API内部使用SPI,当API被客户使用时,会动态得从当前运行的classpath中寻找该SPI的实现,然后使用该SPI的实现来完成API的功能。
实例
JDBC
java.sql.Driver
数据库驱动接口,JDK中只提供接口的定义,具体的实现类由各个数据库厂商提供的驱动包来完成,程序在运行的时候会根据当前导入的驱动包来完成对应数据库的连接。
如上图,java.sql.Driver
文件内容:com.mysql.cj.jdbc.Driver
。
同理也能在postgresql-42.7.2.jar
下找到java.sql.Driver
文件,内容为org.postgresql.Driver
,截图省略。
com.mysql.cj.jdbc.Driver
源码很简单:
核心方法就一行:DriverManager.registerDriver(new Driver());
,在PG源码org.postgresql.Driver
里也能找到DriverManager.registerDriver()
。
基于JDK 22源码,在DriverManager
类里搜索文件名java.sql.Driver
,找到方法ensureDriversInitialized:
ensureDriversInitialized()
方法的调用方有getDriver()
和getConnection()
两个,即获取驱动和获取连接。
Spring
在Spring源码里也有SPI的影子,如spring.factories
文件和SpringFactoriesLoader,两者一起构成Spring Boot Starter实现的基础。
以3.2.4版本来讲解,位于spring-boot-actuator-autoconfigure-3.2.4.jar
下的spring.factories
文件片段:
可见,此文件实际上是.properties
文件格式,不同类型的配置项用空行隔开,方便阅读,支持#
作为注释字符。配置是键值对形式,Key和Value都是代表类的完整包名,Value可以有多个,用,
分隔,多行文本以\
分隔;并且Key是接口、注解、或抽象类,Value是Key的实现类。
而SpringFactoriesLoader自spring 3.2版本开始就已经存在,其属性如下:
核心方法有两个,forResourceLocation是一个静态方法,用于获取实例:
参考resourceLocation
可以传入指定的路径,如果为空,则从META-INF/spring.factories
查找。
load方法则用于获取指定类型的实现类集合:
Dubbo
打开dubbo-3.2.14
源码,发现META-INF/services
目录下有个org.apache.dubbo.common.extension.LoadingStrategy
文本文件,内容如下:
DubboInternalLoadingStrategy用于查找META-INF/dubbo/internal/
目录下的文件,DubboLoadingStrategy用于查找META-INF/dubbo/
目录下的文件,ServicesLoadingStrategy用于查找META-INF/services/
目录下的文件。
META-INF/dubbo/internal/
目录下有91个文件,所有文件名都是Dubbo下的全路径名指向的接口类,这些接口都添加有注解@SPI(value = "", scope = FRAMEWORK)
,文件内容则是多行键值对,采用Key=Value形式,Key是一个简称,Value则是Dubbo下接口(文件名对应的)的实现类。
SPI注解源码如下:
ExtensionScope是一个枚举类:
- FRAMEWORK:
- APPLICATION:默认值
- MODULE:
- SELF:
ExtensionLoader是个泛型类,入口方法是getExtension:
接下来看看createExtension方法:
核心方法是getExtensionClasses:
继续看loadExtensionClasses:
继续看loadDirectory,两个逻辑,一是调用方法loadDirectoryInternal,二是判断Dubbo 2的向下兼容性,核心方法是loadDirectoryInternal,继续调用方法loadResource,用于读取和解析配置文件,并通过反射加载类,这里面可看到对前面提到的文本文件进行逐行解析,判断=
分隔符的逻辑,最后调用 loadClass方法用于操作缓存。loadClass方法调用缓存相关方法,如cachedAdaptiveClass、cachedWrapperClasses和cachedNames等。