Application Programming Interface (API)?
-
The API is the description of classes/interfaces/methods/... that you call and use to achieve a goal, and
-
the SPI is the description of classes/interfaces/methods/... that you extend and implement to achieve a goal.
SPI 全称为 (Service Provider Interface) ,是JDK内置的一种服务提供发现机制。SPI是一种动态替换发现的机制, 比如有个接口,想运行时动态的给它添加实现,你只需要添加一个实现。我们经常遇到的就是java.sql.Driver接口,其他不同厂商可以针对同一接口做出不同的实现,mysql和postgresql都有不同的实现提供给用户,而Java的SPI机制可以为某个接口寻找服务实现。
首先放个图:我们在“调用方”和“实现方”之间需要引入“接口”,可以思考一下什么情况应该把接口放入调用方,什么时候可以把接口归为实现方。
先来看看接口属于实现方的情况,这个很容易理解,实现方提供了接口和实现,我们可以引用接口来达到调用某实现类的功能,这就是我们经常说的api,它具有以下特征:
-
是对实现的说明(我可以给你提供什么)
-
组织上位于实现方所在的包中
-
实现和接口在一个包中
当接口属于调用方时,我们就将其称为spi,全称为:service provider interface,spi的规则如下:
-
是对实现的约束(要提供这个功能,实现者需要做那些事情)
-
组织上位于调用方所在的包中
-
实现位于独立的包中(也可认为在提供方中)
简而言之
API会告诉您特定的类/方法为您执行什么操作,而SPI则告诉您必须执行哪些操作才能符合要求。通常,API和SPI是分开的。例如,在JDBC中,Driver类是SPI的一部分:如果只想使用JDBC,则不需要直接使用它,但是实现JDBC驱动程序的每个人都必须实现该类。但是,有时它们会重叠。Connection接口既是SPI,又是API:您在使用JDBC驱动程序时通常会使用它,并且需要由JDBC驱动程序的开发人员来实现。
JDK
在jdk6里面引进的一个新的特性ServiceLoader,从官方的文档来说,它主要是用来装载一系列的service provider。而且ServiceLoader可以通过service provider的配置文件来装载指定的service provider。当服务的提供者,提供了服务接口的一种实现之后,我们只需要在jar包的META-INF/services/目录里同时创建一个以服务接口命名的文件。该文件里就是实现该服务接口的具体实现类。而当外部程序装配这个模块的时候,就能通过该jar包META-INF/services/里的配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 可能上面讲的有些抽象,下面就结合一个示例来具体讲讲。
public interface Search {
public List<String> searchDoc(String keyword);
}
文件搜索实现
public class FileSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("文件搜索 "+keyword);
return null;
}
}
数据库搜索实现
public class DatabaseSearch implements Search{
@Override
public List<String> searchDoc(String keyword) {
System.out.println("数据搜索 "+keyword);
return null;
}
}
接下来可以在resources下新建META-INF/services/目录,然后新建接口全限定名的文件:com.cainiao.ys.spi.learn.Search,里面加上我们需要用到的实现类
com.github.spi.learn.FileSearch
测试
public class TestCase {
public static void main(String[] args) {
ServiceLoader<Search> s = ServiceLoader.load(Search.class);
Iterator<Search> iterator = s.iterator();
while (iterator.hasNext()) {
Search search = iterator.next();
search.searchDoc("hello world");
}
}
}
可以看到输出结果:文件搜索 hello world
如果在com.github.spi.learn.Search文件里写上两个实现类,那最后的输出结果就是两行了。 这就是因为ServiceLoader.load(Search.class)在加载某接口时,会去META-INF/services下找接口的全限定名文件,再根据里面的内容加载相应的实现类。
这就是spi的思想,接口的实现由provider实现,provider只用在提交的jar包里的META-INF/services下根据平台定义的接口新建文件,并添加进相应的实现类内容就好。
那为什么配置文件为什么要放在META-INF/services下面? 可以打开ServiceLoader的代码,里面定义了文件的PREFIX如下:
private static final String PREFIX = "META-INF/services/"
Java SPI的使用很简单。也做到了基本的加载扩展点的功能。但Java SPI有以下的不足:
-
需要遍历所有的实现,并实例化,然后我们在循环中才能找到我们需要的实现。
-
配置文件中只是简单的列出了所有的扩展实现,而没有给他们命名。导致在程序中很难去准确的引用它们。
-
扩展如果依赖其他的扩展,做不到自动注入和装配
-
不提供类似于Spring的IOC和AOP功能
-