系列中的上一篇
当前教程
使用服务解耦模块

系列中的上一篇: 限定的 exportsopens

系列中的下一篇: 类路径上的代码 - 未命名模块

使用服务解耦模块

在 Java 中,通常将 API 建模为接口(或有时是抽象类),然后根据情况选择最佳实现。理想情况下,API 的使用者与实现完全解耦,这意味着它们之间没有直接依赖关系。Java 的服务加载器 API 允许将此方法应用于 JAR(模块化或非模块化),并且模块系统将其作为一等概念与模块声明中的 usesprovides 指令集成在一起。

注意: 您需要了解 模块系统基础知识 才能充分利用本文。

Java 模块系统中的服务

举例说明问题

让我们从一个在三个模块中使用这三种类型的示例开始

  • Maincom.example.app
  • 接口 Servicecom.example.api
  • Implementation(实现 Service)在 com.example.impl

Main 想要使用 Service,但需要创建 Implementation 来获取实例

public class Main {

    public static void main(String[] args) {
        Service service = new Implementation();
        use(service);
    }

    private static void use(Service service) {
        // ...
    }

}

这会导致以下模块声明

module com.example.api {
    exports com.example.api;
}

module com.example.impl {
    requires com.example.api;
    exports com.example.impl;
}

module com.example.app {
    // dependency on the API: ✅
    requires com.example.api;
    // dependency on the implementation: ⚠️
    requires com.example.impl;
}

如您所见,使用接口来解耦用户和 API 提供者的挑战在于,在某个时候必须实例化特定的实现。如果这作为常规构造函数调用发生(如在 Main 中),它会创建对实现的依赖,从而在两个模块之间创建依赖关系。这就是服务解决的问题。

服务定位器模式作为解决方案

Java 通过使用类 ServiceLoader 作为中央注册表来实现 服务定位器模式 来解决此问题。以下是它的工作原理。

服务是一种可访问的类型(不必是接口;抽象类甚至具体类也可以正常工作),一个模块想要使用它,而另一个模块提供它的实例

  • 使用服务的模块必须在其模块描述符中使用 uses $SERVICE 指令来表达其需求,其中 $SERVICE 是服务类型的完全限定名称。
  • 提供服务的模块必须使用 provides $SERVICE with $PROVIDER 指令来表达其提供,其中 $SERVICEuses 指令中的类型相同,而 $PROVIDER 是另一个类的完全限定名称,它...
    • 要么 是一个扩展或实现 $SERVICE 的具体类,并且具有公共的无参数构造函数(称为提供者构造函数
    • 要么 是一个任意类型,具有公共的静态无参数方法 provide,该方法返回一个扩展或实现 $SERVICE 的类型(称为提供者方法

在运行时,依赖模块可以使用 ServiceLoader 类通过调用 ServiceLoader.load($SERVICE.class) 来获取服务的提供的所有实现。然后,模块系统将返回一个 ServiceLoader<$SERVICE>,您可以以多种方式使用它来访问服务提供者。的 Javadoc ServiceLoader 详细介绍了这一点(实际上,还介绍了所有与服务相关的内容)。

举例说明解决方案

以下是我们之前检查的三个类和模块如何使用服务。我们从模块声明开始

module com.example.api {
    exports com.example.api;
}

module com.example.impl {
    requires com.example.api;

    provides com.example.api.Service
        with com.example.impl.Implementation;
}

module com.example.app {
    requires com.example.api;

    uses com.example.api.Service;
}

请注意,com.example.app 不再需要 com.example.impl。相反,它声明它使用 Service,而 com.example.impl 声明它使用 Implementation 提供它。此外,com.example.impl 不再导出 com.example.impl 包。服务加载器不要求服务实现能够在模块外部访问,如果该包中的其他类不需要访问,我们可以停止导出它。这是服务的额外好处,因为它可以减少模块的 API 表面。

以下是 Main 如何获取 Service 的实现

public class Main {

    public static void main(String[] args) {
        Service service = ServiceLoader
            .load(Service.class)
            .findFirst()
            .orElseThrow();
        use(service);
    }

    private static void use(Service service) {
        // ...
    }

}

一些 JDK 服务

JDK 本身也使用服务。例如,包含 JDBC API 的 java.sql 模块使用 java.sql.Driver 作为服务

module java.sql {
    // requires...
    // exports...
    uses java.sql.Driver;
}

这也表明模块可以使用它自己的类型之一作为服务。

JDK 中服务的另一个示例是 java.lang.System.LoggerFinder。这是允许用户将 JDK 的日志消息(不是运行时的!)管道到他们选择的日志框架(例如 Log4J 或 Logback)的 API 的一部分。简而言之,JDK 不是写入标准输出,而是使用 LoggerFinder 创建 Logger 实例,然后使用它们记录所有消息。由于它使用 LoggerFinder 作为服务,因此日志框架可以提供它的实现。

module com.example.logger {
    // `LoggerFinder` is the service interface
    provides java.lang.System.LoggerFinder
        with com.example.logger.ExLoggerFinder;
}

public class ExLoggerFinder implements System.LoggerFinder {

    // `ExLoggerFinder` must have a parameterless constructor

    @Override
    public Logger getLogger(String name, Module module) {
        // `ExLogger` must implement `Logger`
        return new ExLogger(name, module);
    }

}

模块解析期间的服务

如果您曾经使用命令行选项 --show-module-resolution 启动过一个简单的模块化应用程序,并观察了模块系统到底在做什么,您可能会对解析的平台模块数量感到惊讶。对于一个足够简单的应用程序,唯一的平台模块应该是 java.base 以及可能另外一两个,那么为什么会有这么多其他模块呢?服务就是答案。

请记住,从 模块系统基础知识 中,只有在模块解析期间进入图的模块才能在运行时使用。为了确保所有服务提供者都能做到这一点,解析过程会考虑 usesprovides 指令。因此,除了跟踪依赖项之外,一旦它解析了使用服务的模块,它还会将所有提供该服务的模块添加到图中。此过程称为服务绑定


上次更新: 2021 年 9 月 14 日


系列中的上一篇
当前教程
使用服务解耦模块

系列中的上一篇: 限定的 exportsopens

系列中的下一篇: 类路径上的代码 - 未命名模块