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/servies
中的配置文件,配置文件中有这个接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了,JDK查找服务的实现工具类时:java.util.ServiceLoader
。
2. SPI机制的简单示例
先定一个内容搜索接口,搜索的实现可能时基于文件系统的搜索,也可能是基于数据库的搜索
定义接口
1
2
3public interface Search {
List<String> searchDoc(String keyword);
}文件搜索实现
1
2
3
4
5
6
7public class FileSearch implements Search{
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索"+keyword);
return null;
}
}数据库搜索实现
1
2
3
4
5
6
7public class DatabaseSearch implements Search{
public List<String> searchDoc(String keyword) {
System.out.println("数据库搜索"+keyword);
return null;
}
}配置
META-INF/services
在resources下新建 META-INF/services目录,然后新建接口全限定名的文件
com.ygb.Search
,里面添加需要用到的实现类1
2com.ygb.FileSearch
com.ygb.DatabaseSearch测试
1
2
3
4
5
6
7
8
9
10
11
12
13public class TestSPI {
public static void main(String[] args) {
ServiceLoader<Search> service = ServiceLoader.load(Search.class);
Iterator<Search> iterator = service.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("hello world");
}
}
}
//在services目录下配置了两个实现类,那么会输出:
// 文件搜索hello world
// 数据库搜索hello world总结
ServiceLoader.load(Search.class)
在加载某接口时,会去META-INF/services
下查找接口的全限定名文件,再根据里面的内容加载相应的实现类。这就是SPI思想,接口的实现由provider实现,provider只用在提交的jar包里面的
META-INF/services
下根据平台定义的接口新建文件,并添加相应的实现内容就可以。
3. SPI机制的应用
3.1. JDBC DriverManger
JDBC4.0之前,我们开发连接数据库的时候,通常会用Class.forName(“com.mysql.jdbc.Driver”)这句先加载数据库驱动,然后在获取数据库连接等操作。
JDBC4.0之后,直接获取连接即可,不需要采用Class.forName这种方式。
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扩展来加载具体的驱动,我们在使用数据库连接代码时,不需要再使用Class.forName()
来加载驱动来,而是使用以下代码
1 | String url = "jdbc:mysq://192.168.0.1:3306/db"; |
这里并没有涉及到SPI的实现,接着看下面的解析。
3.1.5. 源码实现
上面的代码没有来加载驱动的代码,怎么去确定使用那个数据库连接的驱动呢?这里就涉及到Java的SPI扩展机制来查找相关驱动的东西来,
关于驱动的查找其实都在DriverManager
中,在DriverManager
有一个静态代码块如下:
1 | /** |
可以看到是加载实例化驱动的,接着看loadInitialDrivers方法:
1 | private static void loadInitialDrivers() { |
上面的代码主要步骤时:
从系统变量中获取有关驱动的定义
使用SPI来获取驱动的实现
便利使用SPI获取到的具体实现,实例话各个实现类
根据第一步获取到的驱动列表来实例话具体实现类
3.2. Common-Logging
common-logging(Jakarta Commons Logging,缩写JCL)是常用的日志库门面,可以看下它是怎么解耦的
首先,日志示例是通过LogFactory
的getLog(String)
方法创建的
1 | //private Log log = LogFactory.getLog(TestSPI.class); |
LogFactory是一个抽象类,它负责加载具体的日志实现,分析其Factory getFactory()方法:
1 | public static LogFactory getFactory() throws LogConfigurationException { |
可以看出,抽象类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. Spring中的SPI机制
在springboot的自动装配过程中,最终会加载META-INF/spring.factories
文件,而加载的过程是由SpringFactoriesLoader
加载的。从CLASSPATH
下的每个Jar包中搜寻所有META-INF/spring.factories
配置文件,然后将解析properties文件,找到指定名称的配置后返回。需要注意的是,其实这里不仅仅是会去ClassPath路径下查找,会扫描所有路径下的Jar包,只不过这个文件只会在Classpath下的jar包中。
1 | public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories"; |
4. SPI机制深入理解
4.1. SPI机制通常怎么使用
看完上面的几个例子解析,应该都知道大致的流程了:
组织或公司定义标准
- 就是定义接口,比如
java.sql.Driver
- 就是定义接口,比如
具体厂商或框架开发者实现
- 在
META-INF/services
目录下定一个名字为接口全限定名的文件,文件内容是具体实现类的全限定名,比如com.cj.mysql.Driver
- 在
使用
- 引用jar来实现功能
1
2
3
4
5
6
7
8ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);
//获取迭代器
Iterator<Driver> driversIterator = loadedDrivers.iterator();
//遍历
while(driversIterator.hasNext()) {
driversIterator.next();
//可以做具体的业务逻辑
}
- 引用jar来实现功能
规范
- 总结以下jdk中SPI需要遵循的规范
- 总结以下jdk中SPI需要遵循的规范
4.2. SPI和API的区别
SPI: “接口”位于调用方所在的包中
概念上更依赖调用方
位于调用方所在的包中
实现位于独立的包中
常见的例子:插件模式的插件
API: “接口”位于实现方所在的包中
概念上更接近实现方
位于实现方所在的包中
实现和接口在一个包中
4.3. SPI机制实现原理
看下JDK中ServiceLoader<S>
方法的具体实现:
1 | //ServiceLoader实现了Iterable接口,可以遍历所有的服务实现者 |
首先,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>类型)然后返回实例对象
4.4. SPI缺陷
通过上面的解析,可以发现,我们使用SPI机制的缺陷:
不能按需加载,需要遍历所有的实现,并实例话,然后在循环中才能找到我们需要的实现,如果不像用某些实现类,或则某些类实例化很耗时,它也被载入实例化,造成浪费
获取某个实现类的方式不够灵活,只能通过
Iterator
形式获取,不能根据某个参数来获取对应的实现类多个并发多线程使用
ServiceLoader
类的示例是不安全的
参考文章:Java常用机制 - SPI机制详解