登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  java教程

Java ServiceLoader在模块路径中注册服务实现的实现方法

来源:17golang原创

时间:2026-09-20 06:09:29 378浏览 收藏

Java ServiceLoader 放到模块路径上使用时,关键不是扫描所有 JAR,而是让消费模块和实现模块在 module-info.java 中分别声明服务关系:消费方写 uses com.example.codec.Codec,提供方写 provides com.example.codec.Codec with com.example.codec.internal.TextCodec。服务接口所在包需要被导出,真正的实现包可以保持封装。

官方资料:https://docs.oracle.com/en/java/javase/26/docs/api/java.base/java/util/ServiceLoader.html

最小闭环是“API 模块导出服务接口、应用模块 uses、插件模块 provides with”,然后用 ServiceLoader.load(Codec.class) 读取实现。

先把三个模块的边界分清

示例用一个 Codec 服务接口连接三个模块:com.example.codec.api 只放接口,com.example.codec.provider 放具体实现,com.example.codec.app 负责加载。这样应用依赖的是接口,而不是实现类。

// 服务接口模块:只公开稳定的 API 类型
module com.example.codec.api {
    exports com.example.codec;
}

// 消费模块:声明它可能发现 Codec 实现
module com.example.codec.app {
    requires com.example.codec.api;
    uses com.example.codec.Codec;
}

// 提供模块:注册实现,但不必 exports 内部包
module com.example.codec.provider {
    requires com.example.codec.api;
    provides com.example.codec.Codec
        with com.example.codec.internal.TextCodec;
}

这里的 uses 是消费方的服务依赖,不会引入某个具体实现;provides ... with ... 则把服务类型和实现类绑定起来。实现包不写 exports 是有意的:调用方只看到 Codec,实现细节仍由提供模块控制。

Java ServiceLoader 三模块边界说明图:API 服务接口、应用 uses 与提供模块 provides with 的关系
图1:模块边界说明图,展示 API、消费方与提供方之间的 uses/provides 关系,不是运行截图。

实现类要满足 ServiceLoader 的实例化规则

实现类可以实现服务接口,也可以提供一个公开的静态 provider() 工厂方法。为了让示例最容易排查,先采用 public 无参构造器,并把实现放在未导出的内部包中。

// 提供模块内部实现:构造器供 ServiceLoader 调用
package com.example.codec.internal;

import com.example.codec.Codec;

public final class TextCodec implements Codec {
    public TextCodec() {
        // 保留公开无参构造器,避免模块化加载时无法实例化
    }

    @Override
    public String name() {
        return "text";
    }
}

如果改用 provider 方法,方法必须是 public static、无参数,并返回可赋给服务类型的对象;这时工厂类本身不一定实现服务接口。无论采用哪种方式,提供方都必须列在 provideswith 后面,类名必须是全限定名。

应用侧用 ServiceLoader 读取已注册实现

应用模块只引用服务接口。迭代时才会逐个创建实现实例,应用不需要知道提供方的模块名或内部包名。

// 应用入口:只依赖 Codec 服务,不直接依赖 TextCodec
package com.example.codec.app;

import com.example.codec.Codec;
import java.util.ServiceLoader;

public final class Main {
    public static void main(String[] args) {
        ServiceLoader loader = ServiceLoader.load(Codec.class);
        boolean found = false;
        for (Codec codec : loader) {
            found = true;
            System.out.println("发现实现: " + codec.name());
        }
        if (!found) {
            // 空结果说明模块路径或服务声明仍有一处没有接通
            System.err.println("未发现 Codec 提供方");
        }
    }
}

应用模块的 module-info.java 仍然要写 uses,即使代码里已经导入了 ServiceLoader。导入 API 只解决编译期类型依赖,uses 才把服务发现关系写进模块描述符。

ServiceLoader 发现契约说明图:uses 服务类型、模块路径上的提供方和迭代器实例化结果
图2:发现契约说明图,展示 ServiceLoader 从服务类型到提供方实例的静态关系,不是运行证据。

用模块路径启动并按证据排查空结果

假设三个模块的源目录分别为 src/com.example.codec.apisrc/com.example.codec.providersrc/com.example.codec.app,可以把编译输出分开放入 mods

预期会看到 发现实现: text。若没有任何实现,先确认提供模块的输出目录里存在 module-info.class,再检查 provides 的服务类型是否与 uses 完全一致。若出现访问或实例化错误,重点看服务接口包是否 exports、实现类是否 public,以及构造器或 provider 方法是否符合规则。

还要区分“没有提供方”和“服务类型本身不可用”:前者通常是模块路径或 provides 漏配,后者是服务接口所在模块没有被正确读取。生产环境可以在启动检查中记录发现的实现名称,但不要把内部实现包名当成应用配置契约。

常见边界与速查

检查项正确做法典型后果
服务接口包API 模块 exports,消费方 requires编译或解析阶段无法访问服务类型
消费声明应用 module-info 写 uses模块化应用无法按服务关系发现
提供声明provider 写 provides 接口 with 实现迭代器为空或加载失败
实现类public 无参构造器,或 public static provider()ServiceConfigurationError

相关问题

为什么实现包可以不 exports?因为模块系统把服务提供关系单独记录在 provides 中,调用方依赖服务接口,不需要直接访问实现类型。

什么时候还要用 META-INF/services?自动模块或未命名模块仍可用传统 provider-configuration 文件;本文的重点是显式命名模块在模块路径中的 uses/provides 声明。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>