Java的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
第一步:准备一个简单的服务接口,
1 | public interface SomeSer { |
第二步:实现这个接口,一般是第三方的jar包实现的这个接口:
1 | public class OneImpl implements SomeSer { |
第三步:在resouces下创建META-INF/services
文件夹,并创建一个与接口同名的文件,如:com.abumaster.example.workcommon.spidemo.service.SomeSer
,然后,在其中填写刚刚的实现类的全路径,这一步骤通常也是第三方jar包去创建,多个实现,可以填写多个实现类;
第四步:使用,通过ServiceLoader
进行加载:
1 | public class MainUsage { |
SPI机制的广泛应用
JDBC DriverManager
jdbc的接口定义位于jdk的包java.sql.Driver
中,不提供具体的实现,具体的实现由各个数据库来提供,比如常用的postgresql和mysql,需要使用这些数据库的时候则需要引入对应的jar包。
这些jar包的META-INF/services
文件夹下,也有对应的实现类,如postgresql的文件内容为:org.postgresql.Driver
;
如何使用的?
实际使用的时候,使用如下代码来初始化数据库链接:
1 | String url = "jdbc:xxxx://xxxx:xxxx/xxxx"; |
向下跟踪,找到这个初始化函数:
1 | private static void loadInitialDrivers() { |
- 从系统变量中获取有关驱动的定义。
- 使用SPI来获取驱动的实现。
- 遍历使用SPI获取到的具体实现,实例化各个实现类。
- 根据第一步获取到的驱动列表来实例化具体实现类。
SPI和API的区别
SPI - “接口”位于“调用方”所在的“包”中
- 概念上更依赖调用方。
- 组织上位于调用方所在的包中。
- 实现位于独立的包中。
- 常见的例子是:插件模式的插件。
API - “接口”位于“实现方”所在的“包”中
- 概念上更接近实现方。
- 组织上位于实现方所在的包中。
- 实现和接口在一个包中。
SPI机制的缺陷
- 不能按需加载,需要遍历所有的实现,并实例化,然后在循环中才能找到我们需要的实现。如果不想用某些实现类,或者某些类实例化很耗时,它也被载入并实例化了,这就造成了浪费。
- 获取某个实现类的方式不够灵活,只能通过 Iterator 形式获取,不能根据某个参数来获取对应的实现类。
- 多个并发多线程使用 ServiceLoader 类的实例是不安全的。