什么是SPI?

SPI简介

SPI全称是Service Provider Interface,可称之为“服务供给接口”,是JDK内置的一种服务提供发现机制,其实这个确切地说是一种思想,并不是Java独有的专利。谈到SPI不得不提的一个概念就是“面向接口编程”,SPI通过事先定义的接口来找到其所有的接口实现类(服务),而后通过反射逐个实例化对象,这便是SPI的本质。

为什么需要SPI

SPI也被叫做服务发现机制,之所以需要服务发现,是因为接口实现的不确定性。就拿java.sql.Driver来说,如果你没有引入类似于MySQL这样的实现,你不会知道它的实现类:

什么是SPI?

JDK就只是提供了这样一个接口,却并不打算给它实现,为什么?因为现实中的数据库厂商实在是太多了:MySQL、SQL Server、Oracle等等,如果都要由Java官方实现和维护,那工程量实在太大!除此之外还有众多因素制约。

我们知道Java中要通过反射实例化一个类是需要拿到具体类的全限定类名称的,像这样:

class Sample { public static void main(String args) { Class<?> serviceImpl = Class.forName(“com.tinysand.example.ServiceImpl”); serviceImpl.getDeclaredConstructor().newInstance(); } }

然而对于类似java.sql.Driver接口这样的情况你完全不可能知道它实现类的全限定类名。

延迟加载与可扩展性

SPI也是一种将实现类的加载延迟的一种机制,我们可以面向接口编程,但免不了要提供一种“在运行时也能正确找到并实例化实现类的途径”,上面的Class.forName(“com.tinysand.example.ServiceImpl”)不失为一种途径,但显然不够好,硬编码的缺点大家都懂,假如找不到指定的实现类抛出异常是必然的,更为致命的是:与实现类产生了严重耦合,要换用其它的实现类只能修改代码。

而在SPI中是这样做的:

调用方只需要制定接口(如Service.class)规范,同时针对这个接口加载服务:ServiceLoader<Service> services = ServiceLoader.load(Service.class);,其间没有涉及到任何的具体实现类。服务提供方(即,接口的实现者),实现接口,并在META-INF/services目录下提供一个名为接口全限定名的文件,内容则是接口实现类的全限定名。

从某种层面上说,也正是加载的延迟给予了程序可扩展的特性。

一个模块化的SPI样例

现在我们通过一个简单的多模块样例感受一下SPI带来的便利,体会它是如何做到灵活且无缝的扩展的。

首先在Idea中创建一个简单Maven工程,它将作为三个模块的父工程,这里不再给出具体操作指引。要注意把packaging改为pom(默认是jar)让它成为父工程,才能添加子模块:

<?xml version=”1.0″ encoding=”UTF-8″?> <project xmlns=“http://maven.apache.org/POM/4.0.0” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd”> <modelVersion>4.0.0</modelVersion> <groupId>com.tinysand</groupId> <artifactId>spi-example</artifactId> <version>1.0-SNAPSHOT</version> <modules> <module>spi-invoker</module> <!– 服务调用模块 –> <module>spi-provider</module> <!– 服务提供模块 –> <module>spi-spec</module> <!– 接口规范模块 –> </modules> <!– 必须为pom,父工程不能放代码,可将其src目录删除 –> <packaging>pom</packaging> <!– maven编译插件,主要是为了自动统一各个子模块的java版本和编码,不用手动挨个指定 –> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.1</version> <configuration> <source>11</source> <target>11</target> <encoding>UTF-8</encoding> </configuration> </plugin> </plugins> </build> </project>

接下来依次在父工程目录上右键 → new → Module…添加子模块:spi-invoker、spi-provider、spi-spec。

其实SPI的主体只有“服务调用者”跟“服务提供者”,但我这里出现了第三个模块:spi-spec,主要是因为接口是双方都要用的,调用方需要根据接口查找并调用服务,而提供者需要实现接口,如果把接口定义到调用者模块,那么提供者要实现接口就必须引入整个调用者模块!这不够轻量,所以各个模块都要用到的接口单独一个模块才是最明智的做法。

服务调用者模块spi-invoker的pom文件需要引入接口模块和提供者模块依赖:

<?xml version=”1.0″ encoding=”UTF-8″?> <project xmlns=“http://maven.apache.org/POM/4.0.0” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd”> // … <dependencies> <dependency> <groupId>com.tinysand</groupId> <artifactId>spi-spec</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <!– 想用不用的实现时,随时把这个依赖替换掉 –> <dependency> <groupId>com.tinysand</groupId> <artifactId>spi-provider</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>

而服务提供者模块只需要引入接口模块:

<?xml version=”1.0″ encoding=”UTF-8″?> <project xmlns=“http://maven.apache.org/POM/4.0.0” xmlns:xsi=“http://www.w3.org/2001/XMLSchema-instance” xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd”> // … <dependencies> <dependency> <groupId>com.tinysand</groupId> <artifactId>spi-spec</artifactId> <version>1.0-SNAPSHOT</version> </dependency> </dependencies> </project>

先在spi-spec模块新建接口com.tinysand.sender.spec.MessageSender:

public interface MessageSender { void send(String message); }

然后在spi-invoker模块新建调用类com.tinysand.spi.Invocation:

public class Invocation { private static ServiceLoader<MessageSender> senderServiceLoader = ServiceLoader.load(MessageSender.class); public static void main(String args) { senderServiceLoader.findFirst().ifPresent(sender -> sender.send(“这是要发送的消息”)); } }

再在spi-provider提供一个实现类com.tinysand.sender.provider.SmsSender:

public class SmsSender implements MessageSender { @Override public void send(String message) { System.out.println(String.format(“时间:[%s] – [SmsSender]开始发送消息:[%s]”, LocalDateTime.now().format(DateTimeFormatter.ofPattern( “yyyy-MM-dd HH:mm:ss”, Locale.CHINA)), message)); } }

最后不要忘了在spi-provider的resources资源目录下新建两个目录:META-INF/services,把接口的全限定类名(com.tinysand.sender.spec.MessageSender)作为文件名,内容则是接口实现类的全限定名(com.tinysand.sender.provider.SmsSender)。

什么是SPI?

想象一下,如果你想要使用其它的实现,直接在调用模块的pom文件中将原来的实现依赖替换即可,这是多么便捷!

Java SPI缺点

注意这里说的是Java SPI的缺点,SPI是一种机制,而Java SPI只是它的其中一种实现。

不能按需加载。虽然 ServiceLoader 做了延迟载入,但是基本只能通过遍历全部获取,也就是接口的实现类得全部载入并实例化一遍。容易造成资源浪费。获取某个实现类的方式不够灵活,只能通过 Iterator 形式迭代获取,不能根据某个参数来获取对应的实现类。多线程并发使用 ServiceLoader 类的实例存在安全隐患。实现类不能通过有参构造器实例化。

注意事项

接口实现类必须提供一个无参的构造器!

软件版本

软件版本Java11

结语

Java SPI的使用核心其实只有ServiceLoader类,本文只讲基本的使用,限于篇幅不打算深入其源码,有兴趣可以自行了解,需要注意JDK版本更迭可能会导致源码变动。

免责声明:文章内容来自互联网,版权归原作者所有,本站仅提供信息存储空间服务,真实性请自行鉴别,本站不承担任何责任,如有侵权等情况,请与本站联系删除。
转载请注明出处:什么是SPI? https://www.7ca.cn/zsbk/zt/23148.html

上一篇 2024年5月20日 19:14:54
下一篇 2024年5月20日 19:18:09

联系我们

在线咨询: QQ交谈

邮件:362039258#qq.com(把#换成@)

工作时间:周一至周五,10:30-16:30,节假日休息。