1.引言

接口是对行为的形象,是接口的实现者和调用者之间建设的约定(协定-protocol)。咱们能够依据须要为接口提供不同的实现。通过将不同的接口实现注入到调用方,就能够实现在不批改调用方代码的状况下扭转调用方的行为。

SPI(Service Provider Interface)就是服务提供者接口模式。框架或者零碎能够 SPI 模式反对,让应用方能够依据须要为某个行为抉择不同的提供方实现版本,从而实现灵便的可定制扩大。比方Java SDK JDBC 定义了数据库操作的标准接口,然而 Java SDK 并没有提供具体的实现,而是由各个数据库厂商来实现。应用方依据本人的须要抉择对应数据库的JDBC 实现就好了。

2.SPI 模式

2.1 模式蕴含的组件

服务提供者接口模式中有 4 个重要的组件

  • 服务接口 Service Interface
  • 服务接口实现:不同的服务提供方能够提供一个或多个实现;框架或者零碎自身也能够提供默认的实现。
  • 提供者注册 API(Provider Registration API),这是提供者用来注册实现的;
  • 服务拜访 API (Service Access API) ,这是调用方用来获取服务的实例的接口。

2.2 简略示例

举个例子,如果咱们有个软件产品。该产品须要实现一个新性能,以反对从其余内容零碎检索信息。 为了可能反对任意可能的内容零碎,咱们决定应用 SPI 模式来解决这个问题。Java 提供了 SPI 反对,咱们的示例将会基于 Java SPI。

2.2.1 定义服务接口

首先,咱们须要定义服务接口

package com.examples.spipublic interface Searchable {    public List<String> searchDoc(String keyword);   }

2.2.2 提供者实现服务接口

而后,服务提供者实现接口。在咱们的内容搜寻服务中,咱们能够客户提供实现,客户本人如果有开发能力,能够能够为本人的内容零碎提供搜寻实现。

咱们有很多客户应用的内容零碎就是同一个电脑上的文件系统,为了这个这类客户,咱们提供了反对文件系统的 Searchable 实现:

package com.examples.spi.impl.file;public class FileSearchEngine implements Searchable{    @Override    public List<String> searchDoc(String keyword) {        log.info("在文件系统中搜寻蕴含 {} 的内容", keyword);        ... //省略具体实现        return contents;    }}

除了应用文件内容零碎的客户,其余的客户应用数据库内容零碎,为此,咱们提供了反对数据库系统的 Searchable 实现:

package com.examples.spi.impl.db;public class DatabaseSearchEngine implements Searchable{    @Override    public List<String> searchDoc(String keyword) {        log.info("在数据库系统中搜寻蕴含 {} 的内容", keyword);        ... //省略具体实现        return contents;    }}

2.2.3 注册服务提供者的实现

实现实现后,咱们须要将实现通过注册接口注册到零碎中。 Java SPI 提供了注册机制。该机制是通过配置文件:META-INF/services/interface全限定名 来实现服务实现注册。即在应用服务的代码工程中创立 META-INF/services/目录,而后新建文件名为接口全限定名的服务实现注册文件:com.examples.spi.Searchable。如果咱们在这个服务实现注册文件中配置com.examples.spi.impl.file.FileSearchEngine,零碎将会应用 FileSearchEngine;如果咱们在这个服务实现注册文件中配置com.examples.spi.impl.db.DatabaseSearchEngine,那么零碎应用的就是 DatabaseSearchEngine。

2.2.4 通过服务拜访接口调用服务

Java SPI 的服务拜访接口时 ServiceLoader。ServiceLoader 能够从服务实现注册文件中加载服务实现,并返回服务对象实例。上面咱们通过简略的例子来看看如果获取服务实现对象,并调用服务接口。在这个例子中咱们采纳了反对多个服务实现的形式。所以如果在服务实现注册文件配置了多个实现,那么将会能够从多个内容零碎搜寻内容。

public class SeachableTest {    public static void main(String[] args) {        ServiceLoader<Search> s = ServiceLoader.load(Searchable.class);        Iterator<Search> iterator = s.iterator();        while (iterator.hasNext()) {           Search search =  iterator.next();           search.searchDoc("hello world");        }    }}

如果你是公司基础架构组的,负责提供公司范畴内应用的根底框架和基础设施组件,你们组提供的框架和组件都是用 log4j 写日志。然而业务开发组有的用log4j、有的用logback、有的用JUL(Java Util Logging)。没有应用log4j 的开发组不得不为工程保护两个日志配置文件。

2.3 示例:JDBC

在JDBC4.0之前,咱们开发有连贯数据库的时候,通常会用Class.forName("com.mysql.jdbc.Driver")这句先加载数据库相干的驱动,而后再进行获取连贯等的操作。而JDBC4.0之后不须要用Class.forName("com.mysql.jdbc.Driver")来加载驱动,间接获取连贯就能够了,当初这种形式就是应用了Java的SPI扩大机制来实现

2.3.1 JDBC 服务接口定义

Java SDK 中定义了接口java.sql.Driver,并且没有具体的实现,具体的实现都是由不同数据库厂商来提供的。

2.3.2 实现

mysql 实现

在 mysql 的 jar 包 mysql-connector-java-6.0.6.jar 中,能够找到META-INF/services目录,该目录下会有一个名字为java.sql.Driver的文件,文件内容是 com.mysql.cj.jdbc.Driver,这外面的内容就是针对Java中定义的接口的实现。

postgresql 实现

同样在 postgresql 的 jar 包 postgresql-42.0.0.jar中,也能够找到同样的配置文件,文件内容是 org.postgresql.Driver,这是postgresql对Java的java.sql.Driver的实现。

服务拜访接口

下面说了,当初应用SPI扩大来加载具体的驱动,咱们在Java中写连贯数据库的代码的时候,不须要再应用Class.forName("com.mysql.jdbc.Driver")来加载驱动了,而是间接应用如下代码:

String url = "jdbc:xxxx://xxxx:xxxx/xxxx";Connection conn = DriverManager.getConnection(url,username,password);.....

因为JDBC 服务接口波及内容多,所以 Java SDK 提供了服务拜访接口的封装 DriverManagerDriverManager.loadInitialDrivers 办法中调用了ServiceLoader.load

private static void loadInitialDrivers() {    String drivers;    ...    AccessController.doPrivileged(new PrivilegedAction<Void>() {        public Void run() {            //应用SPI的ServiceLoader来加载接口的实现            ServiceLoader<Driver> loadedDrivers = ServiceLoader.load(Driver.class);            Iterator<Driver> driversIterator = loadedDrivers.iterator();             try{                 // 遍历所有的驱动实现,在遍历的时候,首先调用`driversIterator.hasNext()`办法,                // 搜寻classpath下和jar包中所有的`META-INF/services`目录下的`java.sql.Driver`文件,并找到文件中的实现类的名字                while (driversIterator.hasNext()) {                     driversIterator.next(); }             } catch(Throwable t) {                 // Do nothing             }            ...        }    });    println("DriverManager.initialize: jdbc.drivers = " + drivers);    ...}

咱们当初更多的是应用 Spring 及 ORM(MyBatis),所以咱们基本上很少会之间与 JDBC 打交道了,这里只是作为 SPI 的示例解说。JDBC 是经典的 SPI 模式的利用。

2.4 示例:slf4j 日志框架

3 总结

还有很多咱们日常应用的组件/框架应用了SPI,比方 slf4j,common-logging,Springboot 的主动拆卸插件机制等等。当一个服务行为须要由第三方或者应用方依据场景做不同的适配或扩大的时候,能够应用 SPI 模式来实现灵便的定制扩大。