《Java SPI:像“插拔U盘”一样扩展你的程序》

博客大纲

1. 开场故事:从“USB接口”到SPI

2. 什么是SPI?一句话秒懂

3. 手撕JDBC:SPI的经典应用

4. 自己动手:实现一个“可插拔”的支付系统

5. 陷阱与进阶:SPI在SpringBoot中的魔改

6. 总结:什么时候该用SPI?


1. 开场故事:从“USB接口”到SPI


想象你买了一个多功能扩展坞,插上U盘能读文件,插上键盘能打字,插上显卡还能外接显示器——
而Java的SPI机制,就是代码世界的“万能扩展坞”!

  • 类比

    • USB接口 = Java的ServiceLoader

    • U盘/键盘 = 第三方实现类(如MySQL驱动、支付渠道)

    • 即插即用 = 无需修改主程序代码,直接加载新功能

2. 什么是SPI?一句话秒懂

 SPI(Service Provider Interface)
“你定接口规则,别人填实现,程序运行时自动发现!”

  • 官方话:一种服务发现机制,通过META-INF/services配置文件动态加载实现类。

  • 人话“甩锅架构”——把实现类的锅甩给别人,自己只定义接口。

举个栗子

// 你定义的接口(USB标准)
public interface PaymentService {
    void pay();
}

// 别人实现的“U盘”(支付宝/微信支付)
public class AlipayService implements PaymentService {
    @Override
    public void pay() { System.out.println("支付宝付款"); }
}

3. 手撕JDBC:SPI的经典应用

灵魂拷问:为什么JDBC能兼容各种数据库?

  • 秘密java.sql.Driver就是SPI接口,数据库厂商(MySQL/Oracle)自己实现驱动!

  • 实战演示

    1. 打开MySQL的jar包,找到META-INF/services/java.sql.Driver文件

    2. 文件内容:com.mysql.cj.jdbc.Driver(这就是MySQL的“U盘”实现)

// JDBC核心代码(背后是ServiceLoader在干活)
Class.forName("com.mysql.cj.jdbc.Driver"); // 实际可省略!SPI自动加载
Connection conn = DriverManager.getConnection(url, user, password);

4. 自己动手:实现一个“可插拔”的支付系统

目标:主程序0修改,随时新增支付方式

步骤1:定义SPI接口

public interface PaymentService {
    String getType();
    void pay(double amount);
}

步骤2:实现两个“支付U盘”

// 支付宝“U盘”
public class AlipayService implements PaymentService {
    public String getType() { return "alipay"; }
    public void pay(double amount) { 
        System.out.println("支付宝支付:" + amount + "元"); 
    }
}

// 微信支付“U盘”
public class WechatPayService implements PaymentService {
    public String getType() { return "wechat"; }
    public void pay(double amount) { 
        System.out.println("微信支付:" + amount + "元"); 
    }
}

步骤3:创建META-INF/services配置文件

文件路径:src/main/resources/META-INF/services/com.example.PaymentService

文件内容:

com.example.AlipayService
com.example.WechatPayService

步骤4:主程序调用(自动发现所有实现!)

ServiceLoader services = ServiceLoader.load(PaymentService.class);
for (PaymentService service : services) {
    if (service.getType().equals("wechat")) { // 按需选择
        service.pay(100.0);
        break;
    }
}

效果:新增一个BankPayService只需加jar包和配置文件,主程序无需重新编译!

5. 陷阱与进阶

⚠️ 坑1:配置文件写错位置或格式

  • 错误示例:META-INF/service(少了个s)→ 加载失败!

  • 必须严格匹配路径和文件名(区分大小写)。

⚡ 进阶:SpringBoot的SPI魔改

  • Spring的spring.factories(SPI加强版):

    • 支持批量加载(如自动配置类org.springframework.boot.autoconfigure.EnableAutoConfiguration

    • 示例:

# spring-boot-autoconfigure.jar中的配置
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.MyAutoConfiguration

6. 总结:什么时候该用SPI?

场景 举例 替代方案
需要动态扩展功能 支付渠道、数据库驱动 工厂模式+反射
希望解耦接口和实现 JDBC、日志门面(SLF4J) 依赖注入(DI)
第三方库需要自由实现 SpringBoot Starter机制 配置文件+ClassLoader

彩蛋:SPI面试三连
SPI和API的区别?

API是“调用方制定规则”,SPI是“实现方制定规则”。

SPI破坏了双亲委派模型吗?

是的!ServiceLoader用线程上下文类加载器(TCCL)加载实现类。

为什么Dubbo/SpringBoot不用JDK的SPI?

JDK的SPI只能全量加载,它们需要按需加载和注解支持。
 

你可能感兴趣的:(JAVA技术小册,java,开发语言)