通俗易懂的解释 Java 中的 SPI 机制

前言

讲 SPI 机制之前,先说说 API ,从面向接口编程的思想来看,「服务调用方」应该通过调用「接口」而不是「具体实现」来处理逻辑。那么,对于「接口」的定义,应该在「服务调用方」还是「服务提供方」呢?

一般来说,会有两种选择:

  1. 「接口」定义在「服务提供方」
  2. 「接口」定义在「服务调用方」

情况1: 先来看看「接口」属于「提供方」的情况。这个很容易理解,提供方同时提供了「接口」和「实现类」,「调用方」可以调用接口来达到调用某实现类的功能,这就是我们日常使用的 API 。

API 的显著特征:接口和实现都在服务提供方中。自定义接口,自己去实现这个接口,也就是提供实现类,最后提供给外部去使用

情况2: 那么再来看看「接口」属于「调用方」的情况。这个其实就是 SPI 机制。以 JDBC 驱动为例,「调用方」定义了java.sql.Driver接口(没有实现这个接口),这个接口位于「调用方」JDK 的包中,各个数据库厂商(也就是服务提供方)实现了这个接口,比如 MySQL 驱动 com.mysql.jdbc.Driver 。

SPI的显著特征:「接口」在「调用方」的包,「调用方」定义规则,而实现类在「服务提供方」中

总结一下:

  1. API 其实是服务提供方,它把接口和实现都做了,然后提供给服务调用方来用,服务提供方是处于主导地位的,此时服务调用方是被动的
  2. SPI 则是服务调用方去定义了某种标准(接口),你要给我提供服务,就必须按照我的这个标准来做实现,此时服务调用方的处于主导的,而服务提供方是被动的

概念

SPI 全称:Service Provider Interface,是Java提供的一套用来被第三方实现或者扩展的接口,它可以用来启用框架扩展和替换组件。

这是一种JDK内置的一种服务发现的机制,用于制定一些规范,实际实现方式交给不同的服务厂商。如下图:
image.png

面向的对象的设计里,我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。一旦代码里涉及具体的实现类,就违反了可插拔的原则,如果需要替换一种实现,就需要修改代码。

Q:上图的基于接口编程体现在哪里?可插拔又是指什么?
A:调用方只会去依赖上图的标准服务接口,而不会去管实现类,这样做的优点有哪些呢?

  1. 如果你想再增加一个实现类,你只需要去实现这个接口就可以,其他地方的代码都不用动,这是扩展性的体现
  2. 又或者是某天你不需要实现类A了,那直接把实现类A去掉就可以了,对整个系统不会有大的改动,这就是可插拔和组件化思想的好处,此时整个系统还实现了充分的解偶

SPI 应用案例之 JDBC DriverManager

众所周知,关系型数据库有很多种,如:MySQL、Oracle、PostgreSQL 等等。Java 的 JDBC 提供了一套 API 供 Java 应用与数据库进行交互,但是,不同的数据库在底层实现上是有区别的呀,我现在想用这一套 API 对所有数据库都适用,那怎么办勒?此时就出现了一个东西,这个东西就是驱动。

**举个例子:**这就相比于你说中文,但是你的客户可能有说英语、法语、德语等等,此时你是不是希望有个翻译,而且是有多个翻译,有翻译成英语的,翻译法语的等等(假设一个翻译只能把中文翻译成一种语言),有了不同的翻译之后,这样就可以把你说的中文翻译给不同语言的人听,而驱动就是翻译

实现 SPI 的四步:

  1. 服务的调用者要先定义好接口
  2. 服务的提供者提供了接口的实现
  3. 需要在类目录下的 META-INF/services/ 目录里创建一个以服务接口命名的文件,这个文件里的内容就是这个接口的具体的实现类。
  4. 当其他的程序需要这个服务(服务提供者提供的)的时候,就可以通过查找这个jar包(一般都是以jar包做依赖)的 META-INF/services/ 中的配置文件,配置文件中有接口的具体实现类名,可以根据这个类名进行加载实例化,就可以使用该服务了。

接下来看看 JDBC 的实践是怎么做的:

  1. 这是 java.sql.Driver(JDK)中定义驱动的接口(对应在服务的调用者要先定义好接口)

图1

  1. 这是MySQL驱动中的Driver类,它实现了上面的Driver接口

图2

  1. 并且我们发现在META-INF/services/ 目录里创建一个以服务接口(java.sql.Driver)命名的文件,这个文件里的内容就是这个接口的具体的实现类

图3

  1. 怎么去把驱动的服务提供给调用者呢?现在常用的就是直接引入依赖就可以

图4

SPI 原理

上文中,我们了解了使用 Java SPI 的方法。那么 Java SPI 是如何工作的呢?实际上,Java SPI 机制依赖于 ServiceLoader 类去解析、加载服务。因此,掌握了 ServiceLoader 的工作流程,就掌握了 SPI 的原理。ServiceLoader 的代码本身很精练,接下来,让我们通过读源码的方式,逐一理解 ServiceLoader 的工作流程。

ServiceLoader 的成员变量

先看一下 ServiceLoader 类的成员变量,大致有个印象,后面的源码中都会使用到。

public final class ServiceLoader<S> implements Iterable<S> {
   

    // SPI 配置文件目录
    private static final String PREFIX = "META-INF/services/";

    // 将要被加载的 SPI 服务
    private final Class<S> service;

    // 用于加载 SPI 服务的类加载器
    private final ClassLoader loader;

    // ServiceLoader 创建时的访问控制上下文
    private final AccessControlContext acc;

    // SPI 服务缓存,按实例化的顺序排列
    private LinkedHashMap<String,S> providers = new LinkedHashMap<>();

    // 懒查询迭代器
    private LazyIterator lookupIterator;

    // ...
}
ServiceLoader 的工作流程

(1)ServiceLoader.load 静态方法
image.png
应用程序加载 Java SPI

<think>嗯,用户想让我解释策略模式,并且用Java代码示例。首先,我需要确保自己完全理解策略模式的概念,这样才能用通俗易懂的方式解释清楚。策略模式属于行为型设计模式,对吧?它的主要作用是将算法或行为封装成独立的类,使得它们可以互相替换,这样客户端可以根据需要选择不同的策略。 那用户可能是什么背景呢?可能是刚开始学习设计模式的开发者,或者是需要在实际项目中应用策略模式的人。他们可能已经对面向对象编程有一定了解,但对设计模式还不够熟悉。所以,我需要用简单明了的语言,避免使用太多专业术语,或者至少在使用时解释清楚。 策略模式的一个常见例子是支付方式的选择,比如支付宝、微信支付、信用卡支付等。不同的支付方式可以看作是不同的策略,用户可以根据需要切换。这样的例子比较贴近实际,容易理解。 接下来,我需要考虑如何用Java代码来展示这个例子。首先定义一个策略接口,比如PaymentStrategy,里面有一个支付方法。然后,各个具体策略类实现这个接口,比如AlipayStrategy、WechatPayStrategy。再有一个上下文类,负责持有策略对象,并调用其支付方法。 用户可能需要看到代码的结构和调用方式,所以需要展示接口、具体策略类、上下文类以及客户端的调用示例。此外,需要强调策略模式的优点,比如避免冗长的条件判断,提高代码的可维护性和扩展性。 还要注意代码的格式,确保正确使用Java语法,比如类定义、接口实现、主方法等。可能还需要解释上下文类的作用,以及如何通过构造函数或setter方法动态切换策略。 在解释过程中,要分步骤说明,先讲概念,再举例子,接着展示代码结构,最后总结优缺点。这样结构清晰,用户容易跟随。 可能用户也会想知道策略模式的应用场景,比如当有多个相似的类只在行为上不同时,或者需要在运行时选择不同的算法时。这时候可以简要提到这些应用场景,帮助用户更好地理解何时使用该模式。 最后,检查是否有遗漏的部分,比如是否提到了开闭原则,策略模式如何支持扩展而不修改现有代码。这些都是策略模式的重要优点,应该包含在解释中。 总结一下,我需要用支付方式的例子,分步骤解释策略模式的结构,展示Java代码,并讨论其优点和应用场景。这样用户就能全面理解策略模式,并知道如何在项目中应用。</think>我通过一个现实案例帮你理解策略模式,并用Java代码分步实现: 1. **核心思想**:把算法/行为封装成独立类,使它们可以像插件一样自由替换 2. **现实比喻**: - 支付方式选择(支付宝/微信/信用卡) - 导航策略(驾车/步行/公交) - 排序算法(快速排序/归并排序) 3. **Java代码实现**(支付场景): ```java // 步骤1:定义策略接口(算法标准) interface PaymentStrategy { void pay(int amount); } // 步骤2:实现具体策略类 class AlipayStrategy implements PaymentStrategy { @Override public void pay(int amount) { System.out.println("支付宝支付:" + amount + "元"); } } class WechatPayStrategy implements PaymentStrategy { @Override public void pay(int amount) { System.out.println("微信支付:" + amount + "元"); } } class CreditCardStrategy implements PaymentStrategy { @Override public void pay(int amount) { System.out.println("信用卡支付:" + amount + "元"); } } // 步骤3:创建上下文类(策略容器) class PaymentContext { private PaymentStrategy strategy; public PaymentContext(PaymentStrategy strategy) { this.strategy = strategy; } public void executePayment(int amount) { strategy.pay(amount); } // 允许动态切换策略 public void setStrategy(PaymentStrategy strategy) { this.strategy = strategy; } } // 步骤4:客户端使用 public class StrategyDemo { public static void main(String[] args) { PaymentContext context = new PaymentContext(new AlipayStrategy()); context.executePayment(100); // 输出:支付宝支付:100元 context.setStrategy(new WechatPayStrategy()); context.executePayment(200); // 输出:微信支付:200元 context.setStrategy(new CreditCardStrategy()); context.executePayment(300); // 输出:信用卡支付:300元 } } ``` 4. **模式优势**: - 消除复杂的条件判断(避免大量if-else) - 符合开闭原则(新增策略无需修改已有代码) - 算法可复用(策略类可被多个上下文使用) - 动态切换策略(运行时改变行为) 5. **应用场景**: - 需要多种算法变体 - 有多个仅在行为方式不同的相似类 - 需要隐藏算法实现细节 - 避免暴露复杂数据结构的类 **关键记忆点**:把会变化的部分(算法)封装起来,与不变的部分分离。就像给设备更换不同的电池,设备(上下文)不变,电池(策略)可换。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值