SPI简介
SPI全称是Service Provider Interface,可称之为“服务供给接口”,是JDK内置的一种服务提供发现机制,其实这个确切地说是一种思想,并不是Java独有的专利。谈到SPI不得不提的一个概念就是“面向接口编程”,SPI通过事先定义的接口来找到其所有的接口实现类(服务),而后通过反射逐个实例化对象,这便是SPI的本质。
为什么需要SPI
SPI也被叫做服务发现机制,之所以需要服务发现,是因为接口实现的不确定性。就拿java.sql.Driver来说,如果你没有引入类似于MySQL这样的实现,你不会知道它的实现类:
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)。
想象一下,如果你想要使用其它的实现,直接在调用模块的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