Java 基础 - SPI机制详解
原文链接:Java 基础 - SPI机制详解
SPI(Service Provider Interface),是JDK内置的(1.6版本)一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用。
1. 什么是SPI机制
SPI(Service Provider Interface),是JDK内置的一种服务提供发现机制,可以用来启用框架扩展和替换组件,主要是被框架的开发人员使用,比如java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,MySQL和PostgreSQL都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。Java中SPI机制主要思想是将装配的控制权移到程序之外,在模块化设计中这个机制尤其重要,其核心思想就是解耦。
SPI整体机制图如下:

当服务的提供者提供了一种接口的实现之后,需要在classpath下的META-INF/services/目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。当其他的程序需要这个服务的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的META-INF/services/中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。JDK中查找服务的实现的工具类是:java.util.ServiceLoader。
SPI的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。
1.1 为什么需要使用SPI
面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。**一旦代码里涉及具体的实现类,就违反了可拔插的原则,如果需要替换一种实现,就需要修改代码。**为了实现在模块装配的时候不用在程序里动态指明,这就需要一种服务发现机制。Java SPI就是提供这样的一个机制:为某个接口寻找服务实现的机制。这有点类似IOC的思想,将装配的控制权移到了程序之外。
2. 简单示例
假如我们现在需要使用一个内容搜索接口,搜索的实现可能是基于文件系统的搜索,也可能是基于数据库的搜索。
先定义好接口
文件搜索实现
数据库搜索实现
resources 接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:
spi.Search,里面加上我们需要用到的实现类:测试方法
可以看到输出结果:
这就是因为ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services下找接口的全限定名文件,再根据里面的内容加载相应的实现类。
这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。
3. SPI机制的广泛应用
3.1 SPI机制 - JDBC DriverManager
在JDBC4.0之前,我们开发有连接数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相关的驱动,然后再进行获取连接等的操作。而JDBC4.0之后不需要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,直接获取连接就可以了,现在这种方式就是使用了Java的SPI扩展机制来实现。
3.1.1 JDBC接口定义
首先在java中定义了接口java.sql.Driver,并没有具体的实现,具体的实现都是由不同厂商来提供的。
3.1.2 mysql实现
在mysql的jar包mysql-connector-java-6.0.6.jar中,可以找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是com.mysql.cj.jdbc.Driver,这里面的内容就是针对Java中定义的接口的实现。
3.1.3 postgresql实现
同样在postgresql的jar包postgresql-42.0.0.jar中,也可以找到同样的配置文件,文件内容是org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。
3.1.4 使用方法
上面说了,现在使用SPI扩展来加载具体的驱动,我们在Java中写连接数据库的代码的时候,不需要再使用Class.forName("com.mysql.jdbc.Driver")来加载驱动了,而是直接使用如下代码:
这里并没有涉及到spi的使用,接着看下面的解析。
3.1.5 源码实现
上面的使用方法,就是我们普通的连接数据库的代码,并没有涉及到SPI的东西,但是有一点我们可以确定的是,我们没有写有关具体驱动的硬编码Class.forName("com.mysql.jdbc.Driver")!
上面的代码可以直接获取数据库连接进行操作,但是跟SPI有啥关系呢?上面代码没有了加载驱动的代码,我们怎么去确定使用哪个数据库连接的驱动呢?这里就涉及到使用Java的SPI扩展机制来查找相关驱动的东西了,关于驱动的查找其实都在DriverManager中,DriverManager是Java中的实现,用来获取数据库连接,在DriverManager中有一个静态代码块如下:
可以看到是加载实例化驱动的,接着看loadInitialDrivers方法:
上面的代码主要步骤是:
从系统变量中获取有关驱动的定义。
使用SPI来获取驱动的实现。
遍历使用SPI获取到的具体实现,实例化各个实现类。
根据第一步获取到的驱动列表来实例化具体实现类。
我们主要关注2,3步,这两步是SPI的用法,首先看第二步,使用SPI来获取驱动的实现,对应的代码是:
这里没有去META-INF/services目录下查找配置文件,也没有加载具体实现类,做的事情就是封装了我们的接口类型和类加载器,并初始化了一个迭代器。
接着看第三步,遍历使用SPI获取到的具体实现,实例化各个实现类,对应的代码如下:
在遍历的时候,首先调用driversIterator.hasNext()方法,这里会搜索classpath下以及jar包中所有的META-INF/services目录下的java.sql.Driver文件,并找到文件中的实现类的名字,此时并没有实例化具体的实现类(ServiceLoader具体的源码实现在下面)。
然后是调用driversIterator.next();方法,此时就会根据驱动名字具体实例化各个实现类了。现在驱动就被找到并实例化了。
可以看下截图,我在测试项目中添加了两个jar包,mysql-connector-java-6.0.6.jar和postgresql-42.0.0.0.jar,跟踪到DriverManager中之后:

可以看到此时迭代器中有两个驱动,mysql和postgresql的都被加载了。
3.2 SPI机制 - Common-Logging
common-logging(也称Jakarta Commons Logging,缩写 JCL)是常用的日志库门面,具体日志库相关可以看这篇。我们看下它是怎么解耦的。
首先,日志实例是通过LogFactory的getLog(String)方法创建的:
LogFatory是一个抽象类,它负责加载具体的日志实现,分析其Factory getFactory()方法:
可以看出,抽象类LogFactory加载具体实现的步骤如下:
从vm系统属性
org.apache.commons.logging.LogFactory使用SPI服务发现机制,发现
org.apache.commons.logging.LogFactory的实现查找classpath根目录
commons-logging.properties的org.apache.commons.logging.LogFactory属性是否指定factory实现使用默认factory实现,
org.apache.commons.logging.impl.LogFactoryImpl
LogFactory的getLog()方法返回类型是org.apache.commons.logging.Log接口,提供了从trace到fatal方法。可以确定,如果日志实现提供者只要实现该接口,并且使用继承自org.apache.commons.logging.LogFactory的子类创建Log,必然可以构建一个松耦合的日志系统。
3.3 SPI机制 - 插件体系
其实最具spi思想的应该属于插件开发,我们项目中也用到的这种思想,后面再说,这里具体说一下eclipse的插件思想。
Eclipse使用OSGi作为插件系统的基础,动态添加新插件和停止现有插件,以动态的方式管理组件生命周期。
一般来说,插件的文件结构必须在指定目录下包含以下三个文件:
META-INF/MANIFEST.MF: 项目基本配置信息,版本、名称、启动器等build.properties: 项目的编译配置信息,包括,源代码路径、输出路径plugin.xml:插件的操作配置信息,包含弹出菜单及点击菜单后对应的操作执行类等
当eclipse启动时,会遍历plugins文件夹中的目录,扫描每个插件的清单文件MANIFEST.MF,并建立一个内部模型来记录它所找到的每个插件的信息,就实现了动态添加新的插件。
这也意味着是eclipse制定了一系列的规则,像是文件结构、类型、参数等。插件开发者遵循这些规则去开发自己的插件,eclipse并不需要知道插件具体是怎样开发的,只需要在启动的时候根据配置文件解析、加载到系统里就好了,是spi思想的一种体现。
3.4 SPI机制 - Spring中SPI机制
在springboot的自动装配过程中,最终会加载META-INF/spring.factories文件,而加载的过程是由SpringFactoriesLoader加载的。从CLASSPATH下的每个Jar包中搜寻所有META-INF/spring.factories配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
4. SPI机制深入理解
4.1 SPI机制通常怎么使用
看完上面的几个例子解析,应该都能知道大概的流程了:
有关组织或者公司定义标准。
具体厂商或者框架开发者实现。
程序猿使用。
4.1.1 定义标准
定义标准,就是定义接口。比如接口java.sql.Driver
4.1.2 具体厂商或者框架开发者实现
厂商或者框架开发者开发具体的实现:
在META-INF/services目录下定义一个名字为接口全限定名的文件,比如java.sql.Driver文件,文件内容是具体的实现名字,比如me.cxis.sql.MyDriver。
写具体的实现me.cxis.sql.MyDriver,都是对接口Driver的实现。
4.1.3 程序猿使用
我们会引用具体厂商的jar包来实现我们的功能:
4.1.4 使用规范
最后总结一下jdk spi需要遵循的规范:

4.2 SPI和API的区别是什么
这里实际包含两个问题,第一个SPI和API的区别?第二个什么时候用API,什么时候用SPI?
SPI - “接口”位于“调用方”所在的“包”中
概念上更依赖调用方。
组织上位于调用方所在的包中。
实现位于独立的包中。
常见的例子是:插件模式的插件。
API - “接口”位于“实现方”所在的“包”中
概念上更接近实现方。
组织上位于实现方所在的包中。
实现和接口在一个包中。
参考:


4.3 SPI机制实现原理
不妨看下JDK中ServiceLoader<S>方法的具体实现:
首先,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>类型)然后返回实例对象。
所以我们可以看到ServiceLoader不是实例化以后,就去读取配置文件中的具体实现,并进行实例化。而是等到使用迭代器去遍历的时候,才会加载对应的配置文件去解析,调用hasNext方法的时候会去加载配置文件进行解析,调用next方法的时候进行实例化并缓存。
所有的配置文件只会加载一次,服务提供者也只会被实例化一次,重新加载配置文件可使用reload方法。
4.4 SPI机制的缺陷
通过上面的解析,可以发现,我们使用SPI机制的缺陷:
不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
多个并发多线程使用 ServiceLoader 类的实例是不安全的。
5. 参考
https://cxis.me/2017/04/17/Java%E4%B8%ADSPI%E6%9C%BA%E5%88%B6%E6%B7%B1%E5%85%A5%E5%8F%8A%E6%BA%90%E7%A0%81%E8%A7%A3%E6%9E%90/
https://stackoverflow.com/questions/2954372/difference-between-spi-and-api?answertab=votes#tab-top
https://zhuanlan.zhihu.com/p/28909673
http://blog.itpub.net/69912579/viewspace-2656555/
https://www.cnblogs.com/happyframework/archive/2013/09/17/3325560.html
https://blog.csdn.net/sakurainluojia/article/details/53534949
https://www.jianshu.com/p/0d196ad23915
最后更新于
这有帮助吗?