第3章 编写第一个SPI服务:定义接口、编写实现、配置文件、加载调用

本章将引导你从零搭建一个完整的SPI服务,涵盖接口定义、实现类编写、服务描述文件创建以及客户端加载调用的全流程。这也是后续章节中所有模块边界优化的基础。

3.1 整体实现流程

一次标准的SPI调用包含四个角色:服务接口、服务提供者、服务描述文件、ServiceLoader客户端。下图展示了它们之间的协作关系。

SPI 服务调用流程 服务接口 IShape (抽象) 实现Provider-1 RoundShape 实现Provider-2 SquareShape 服务描述文件 META-INF/services/com.example.IShape com.example.RoundShape | com.example.SquareShape ServiceLoader.load(IShape.class) 迭代获取所有实现实例 实现类必须拥有无参构造器,ServiceLoader通过反射实例化 描述文件使用UTF-8编码,每行一个完全限定类名,可包含注释行(#开头)

3.2 定义服务接口

服务接口是SPI的契约,所有提供者都必须实现该接口。它通常是一个抽象类型(接口或抽象类),定义一组业务方法。

// 文件:src/main/java/com/example/spi/IShape.java
package com.example.spi;

/**
 * 图形绘制服务接口 —— SPI 契约
 */
public interface IShape {
    /**
     * 绘制图形
     * @return 绘制结果的字符串描述
     */
    String draw();
}
接口设计建议:SPI接口尽量使用接口(interface)而非抽象类,避免多重继承限制。方法签名应保持稳定,因为接口变更会影响所有提供者。

3.3 编写两个实现类

每个实现类都必须拥有一个公共无参构造器(可以是默认构造器)。ServiceLoader在加载时会通过Class.newInstance()实例化,如果没有无参构造器会抛出InstantiationException。

3.3.1 圆形实现

// 文件:src/main/java/com/example/spi/impl/RoundShape.java
package com.example.spi.impl;

import com.example.spi.IShape;

public class RoundShape implements IShape {
    @Override
    public String draw() {
        return "绘制圆形:半径 10px,圆心 (0,0)";
    }
}

3.3.2 方形实现

// 文件:src/main/java/com/example/spi/impl/SquareShape.java
package com.example.spi.impl;

import com.example.spi.IShape;

public class SquareShape implements IShape {
    @Override
    public String draw() {
        return "绘制正方形:边长 20px,左下角 (5,5)";
    }
}
常见错误:实现类不能带参数构造器且未显式声明无参构造器。如果你的类中定义了有参构造器,请务必同时添加一个无参构造器。

3.4 创建服务描述文件

服务描述文件是SPI的“注册表”,必须放置在META-INF/services/目录下,文件名与接口的全限定名一致。文件内容为每行一个实现类的全限定名。

# 文件:META-INF/services/com.example.spi.IShape
# 注意:该文件路径相对于类路径根目录
com.example.spi.impl.RoundShape
com.example.spi.impl.SquareShape
配置项 要求 说明
文件名 接口的全限定名 例如 com.example.spi.IShape,大小写敏感
文件路径 META-INF/services/ 相对于编译后 classpath 根目录
编码 UTF-8 避免出现中文乱码
每行内容 实现类的完全限定名 不含空格,可以有空行或 # 注释行
实例化方式 无参构造器反射调用 如果构造器抛出异常,该提供者会被跳过

3.5 编写客户端加载并调用

客户端使用java.util.ServiceLoader统一加载所有提供者,然后通过迭代器获取实例。

// 文件:src/main/java/com/example/spi/client/ShapeClient.java
package com.example.spi.client;

import com.example.spi.IShape;
import java.util.ServiceLoader;

public class ShapeClient {
    public static void main(String[] args) {
        // 加载所有 IShape 服务提供者
        ServiceLoader<IShape> loader = ServiceLoader.load(IShape.class);

        System.out.println("===== 使用 SPI 加载所有图形服务 =====");
        int index = 0;
        for (IShape shape : loader) {
            index++;
            System.out.println("服务 #" + index + ":" + shape.draw());
        }

        if (index == 0) {
            System.err.println("警告:未发现任何 IShape 服务提供者!");
        }
    }
}

3.6 编译与运行

假设你使用 Maven 或 Gradle 管理项目,类路径会自动包含src/main/resources/META-INF/services/下的文件。如果使用纯命令行编译,请确保资源文件被正确复制到输出目录。

# 编译所有 Java 文件
javac -d out src/main/java/com/example/spi/IShape.java src/main/java/com/example/spi/impl/RoundShape.java src/main/java/com/example/spi/impl/SquareShape.java src/main/java/com/example/spi/client/ShapeClient.java

# 复制服务描述文件到输出目录
mkdir -p out/META-INF/services
cp src/main/resources/META-INF/services/com.example.spi.IShape out/META-INF/services/

# 运行客户端
java -cp out com.example.spi.client.ShapeClient

预期输出结果:

===== 使用 SPI 加载所有图形服务 =====
服务 #1:绘制圆形:半径 10px,圆心 (0,0)
服务 #2:绘制正方形:边长 20px,左下角 (5,5)

3.7 调试与常见错误

  • 找不到服务文件:检查编译后META-INF/services目录是否存在于 classpath 中。使用 System.out.println(ShapeClient.class.getResource("/META-INF/services/com.example.spi.IShape")); 进行定位。
  • 没有加载任何实现:确认文件名与接口全限定名完全一致(包括大小写)。检查文件内容是否每行末尾有回车符或空格。
  • InstantiationError / NoSuchMethodException:实现类缺少 public 无参构造器。显式添加 public RoundShape() {} 即可解决。
  • ClassNotFoundException:实现类的全限定名拼写错误,或者该类未被编译/打包到 classpath 中。

3.8 性能与稳定性要点

  • 延迟加载:ServiceLoader 使用迭代器,调用iterator()时才会查找服务描述文件并反射实例化,而不是在load()时。如果提供者很多,可以考虑按需获取。
  • 缓存机制:同一个 ServiceLoader 实例会缓存已经加载过的服务,多次迭代不会重复实例化。但不同 ServiceLoader 实例之间独立。
  • 线程安全:ServiceLoader 不是线程安全的。在多线程环境中应使用 synchronized 或 ConcurrentHashMap 包装自己的加载逻辑。
  • 资源释放:在 Java 9+ 中,ServiceLoader 实现了 AutoCloseable,可以使用 try-with-resources 管理流式模块,但在传统类路径模式下仍可通过 reload() 清除缓存。
第一个SPI服务已完成构建。
你现在掌握了从接口定义、实现编写、配置文件创建到客户端调用的完整链路。下一阶段将深入ServiceLoader的加载机制与类加载细节,帮助你理解SPI在大型项目中的边界隔离能力。