使用服务解耦模块
在 Java 中,通常将 API 建模为接口(或有时是抽象类),然后根据情况选择最佳实现。理想情况下,API 的使用者与实现完全解耦,这意味着它们之间没有直接依赖关系。Java 的服务加载器 API 允许将此方法应用于 JAR(模块化或非模块化),并且模块系统将其作为一等概念与模块声明中的 uses
和 provides
指令集成在一起。
注意: 您需要了解 模块系统基础知识 才能充分利用本文。
Java 模块系统中的服务
举例说明问题
让我们从一个在三个模块中使用这三种类型的示例开始
- 类
Main
在 com.example.app 中 - 接口
Service
在 com.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
指令来表达其提供,其中$SERVICE
与uses
指令中的类型相同,而$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 以及可能另外一两个,那么为什么会有这么多其他模块呢?服务就是答案。
请记住,从 模块系统基础知识 中,只有在模块解析期间进入图的模块才能在运行时使用。为了确保所有服务提供者都能做到这一点,解析过程会考虑 uses
和 provides
指令。因此,除了跟踪依赖项之外,一旦它解析了使用服务的模块,它还会将所有提供该服务的模块添加到图中。此过程称为服务绑定。
上次更新: 2021 年 9 月 14 日