「这是我参与2022首次更文挑战的第8天,活动详情查看:2022首次更文挑战」。
之前参加华为面试时,面试官问了一个问题,你了解过SPI吗,当时是一脸懵逼。过了很久也没有深入去了解这究竟是一个什么东西。最近有时间就深入研究一下。
概述
服务提供者接口 (SPI),是Java 6引入的一项功能,用于发现和加载与给定接口匹配的实现。Java SPI 实际上是“基于接口的编程+策略模式+配置文件”组合实现的动态加载机制。SPI的核心思想就是解耦。
SPI术语和定义
服务
一组众所周知的编程接口和类,它们提供对某些特定应用程序功能或特性的访问。
服务提供者接口
充当代理或服务端点的接口或抽象类。如果服务是一个接口,则它与服务提供者接口相同。
服务提供者
SPI 的具体实现是服务提供程序包含一个或多个实现或扩展服务类型的具体类。服务提供者是通过我们放在资源目录META-INF /services中的提供商配置文件进行配置和识别的。文件名是 SPI 的完全限定名,其内容是 SPI 实现的完全限定名。
服务提供程序以扩展的形式安装,我们将其放置在应用程序类路径,Java扩展类路径或用户定义的类路径中的jar文件。
服务加载器
SPI的核心是ServiceLoader类,这个类具有延迟发现和加载实现的作用。它使用上下文累路径来定位提供者实现并将它们放入内部缓存中。
使用场景
调用者根据实际使用需要,启用、扩展、或者替换框架的实现策略
Java中的SPI示例
Java 提供了许多 SPI。以下是服务提供者接口及其提供的服务的一些示例:
- CurrencyNameProvider: 为Currency类提供本地化的货币符号
- LocaleNameProvider: 为Locale类提供本地化名称。
- TimeZoneNameProvider: 为TimeZone类提供本地化的时区名称。
- DateFormatProvider: 提供指定区域设置的日期和时间格式。
- NumberFormatProvider:为NumberFormat类提供货币、整数和百分比值。
- Driver: 从 4.0 版开始,JDBC API 支持 SPI 模式。旧版本使用Class.forName() 方法加载驱动程序。
- PersistenceProvider: 提供 JPA API 的实现。
- JsonProvider: 提供JSON处理对象。
- JsonbProvider: 提供 JSON 绑定对象。
- Extension: 为 CDI 容器提供扩展。
- ConfigSourceProvider:提供用于检索配置属性的来源。
使用介绍
要使用Java SPI,需要遵循如下约定:
- 1、当服务提供者提供了接口的一种具体实现后,在jar包的META-INF/services目录下创建一个以“接口全限定名”为命名的文件,内容为实现类的全限定名;
- 2、接口实现类所在的jar包放在主程序的classpath中;
- 3、主程序通过java.util.ServiceLoder动态装载实现模块,它通过扫描META-INF/services目录下的配置文件找到实现类的全限定名,把类加载到JVM;
- 4、SPI的实现类必须携带一个不带参数的构造方法;
实例应用:货币汇率应用程序
现在我们了解了基础知识,让我们描述设置汇率应用程序所需的步骤。
为了突出这些步骤,我们至少需要使用三个项目:exchange-rate-api、exchange-rate-impl和exchange-rate-app。
构建 API
首先创建一个名为exchange-rate-api的 Maven 项目,然后我们创建一个模型类来表示汇率货币:
package com.tudo.rate.api; public class Quote { private String currency; private LocalDate date; ... }
然后通过创建接口QuoteManager来定义我们的服务来检索报价:
package com.tudo.rate.api public interface QuoteManager { List<Quote> getQuotes(String baseCurrency, LocalDate date); }
接下来,我们为我们的服务创建一个SPI:
package com.tudo.rate.spi; public interface ExchangeRateProvider { QuoteManager create(); }
最后,我们需要创建一个可供客户端代码使用的实用程序类ExchangeRate.java 。 此类委托给ServiceLoader
。
首先,我们调用静态工厂方法load() 来获取*ServiceLoader 的实例:
ServiceLoader<ExchangeRateProvider> loader = ServiceLoader .load(ExchangeRateProvider.class);
然后我们调用iterate() 方法来搜索和检索所有可用的实现。
Iterator<ExchangeRateProvider> = loader.iterator();
搜索结果被缓存,因此我们可以调用ServiceLoader.reload() 方法来发现新安装的实现:
Iterator<ExchangeRateProvider> = loader.reload();
这是我们的实用程序类:
``` public class ExchangeRate { ServiceLoader loader = ServiceLoader .load(ExchangeRateProvider.class);
public Iterator providers(boolean refresh) { if (refresh) { loader.reload(); } return loader.iterator(); } } ```
现在我们有了获取所有已安装实现的服务,我们可以在客户端代码中使用所有这些来扩展我们的应用程序,或者通过选择首选实现来扩展应用程序。
构建服务提供者
创建一个名为exchange-rate-impl的 Maven 项目,并将 API 依赖项添加到pom.xml
<dependency> <groupId>com.tudo</groupId> <artifactId>exchange-rate-api</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>
然后我们创建一个实现我们的 SPI 的类:
``` public class YahooFinanceExchangeRateProvider implements ExchangeRateProvider {
@Override public QuoteManager create() { return new YahooQuoteManagerImpl(); } } ```
创建QuoteManager接口的实现:
public class YahooQuoteManagerImpl implements QuoteManager { @Override public List<Quote> getQuotes(String baseCurrency, LocalDate date) { // fetch from Yahoo API } }
为了被发现,我们创建了一个提供者配置文件:
META-INF/services/com.tudo.rate.spi.ExchangeRateProvider
该文件的内容是 SPI 实现的全限定类名:
com.tudo.rate.impl.YahooFinanceExchangeRateProvider
集成
创建一个名为exchange-rate-app的客户端项目,并将依赖项 exchange-rate-api 添加到类路径中:
<dependency> <groupId>com.baeldung</groupId> <artifactId>exchange-rate-api</artifactId> <version>1.0.0-SNAPSHOT</version> </dependency>
此时,我们可以从我们的应用程序中调用 SPI :
ExchangeRate.providers().forEach(provider -> ... );
运行应用程序
构建我们所有的模块:
mvn clean package
然后我们使用Java命令运行我们的应用程序,而不考虑提供程序:
java -cp ./exchange-rate-api/target/exchange-rate-api-1.0.0-SNAPSHOT.jar:./exchange-rate-app/target/exchange-rate-app-1.0.0-SNAPSHOT.jar com.tudo.rate.app.MainApp
现在我们将在java.ext.dirs扩展中包含我们的提供程序并再次运行应用程序:
java -Djava.ext.dirs=$JAVA_HOME/jre/lib/ext:./exchange-rate-impl/target:./exchange-rate-impl/target/depends -cp ./exchange-rate-api/target/exchange-rate-api-1.0.0-SNAPSHOT.jar:./exchange-rate-app/target/exchange-rate-app-1.0.0-SNAPSHOT.jar com.tudo.rate.app.MainApp
可以看到我们的提供程序已加载