1. 引言
本文将深入探讨 Java 的 JEP 418,它为互联网主机和地址解析建立了一个全新的 服务提供者接口 (SPI) 机制。这个改进让开发者能够灵活替换默认的地址解析实现,满足更复杂的网络编程需求。
2. 互联网地址解析基础
任何连接到计算机网络的设备都会被分配一个数值标识,即 IP (互联网协议) 地址。 IP 地址用于在网络中唯一标识设备,并负责数据包的路由传输。
IP 地址主要分两类:
- IPv4:第四代 IP 标准,32 位地址
- IPv6:新一代 IP 标准,地址空间更大,包含十六进制字符
此外,网络设备(如以太网端口或 网络接口卡 (NIC))还有 MAC (媒体访问控制) 地址。这些地址全球唯一分配,所有网络接口设备都能通过 MAC 地址被唯一识别。
互联网地址解析本质上是将高层网络地址(如域名 baeldung.com
或 URL https://www.baeldung.com
)转换为底层网络地址(如 IP 地址或 MAC 地址)的过程。
3. Java 中的地址解析机制
Java 通过 java.net.InetAddress
API 提供多种地址解析方式。该 API 内部依赖操作系统的原生解析器进行 DNS 查询。
当前 InetAddress
使用的操作系统原生解析流程包含多个步骤:
- 系统级 DNS 缓存:存储常用查询记录
- 缓存未命中时,通过系统解析器配置获取 DNS 服务器信息
- 向配置的 DNS 服务器发起查询(可能递归多次)
- 查询成功时在各层级缓存结果并返回客户端
- 查询失败时触发迭代查询:
- 向根服务器请求权威域名服务器 (ANS) 信息
- ANS 存储顶级域名 (TLD) 信息(如
.com
、.org
)
最终,若域名有效则返回对应 IP 地址,否则返回失败。
4. 使用 Java 的 InetAddress API
InetAddress
API 提供了丰富的 DNS 查询和解析方法, 这些方法位于 java.net
包中。
4.1. getAllByName() API
该方法将主机名映射到一组 IP 地址:
InetAddress[] inetAddresses = InetAddress.getAllByName(host);
Assert.assertTrue(Arrays.stream(inetAddresses)
.map(InetAddress::getHostAddress)
.toArray(String[]::new).length > 1);
这被称为正向查找。
4.2. getByName() API
与 getAllByName()
类似,但只返回第一个匹配的 IP 地址:
InetAddress inetAddress = InetAddress.getByName("www.google.com");
Assert.assertNotNull(inetAddress.getHostAddress()); // 返回 IP 地址
4.3. getByAddress() API
执行反向查找的基础方法,通过 IP 地址获取关联主机:
InetAddress inetAddress = InetAddress.getByAddress(ip);
Assert.assertNotNull(inetAddress.getHostName()); // 返回主机名(如 google.com)
4.4. getCanonicalHostName() 和 getHostName() API
执行类似反向查找,返回完全限定域名 (FQDN):
InetAddress inetAddress = InetAddress.getByAddress(ip);
Assert.assertNotNull(inetAddress.getCanonicalHostName()); // 返回 FQDN
Assert.assertNotNull(inetAddress.getHostName());
5. 服务提供者接口 (SPI)
SPI 是软件开发中的重要设计模式,其核心目标是实现特定服务的可插拔组件和实现。 它允许开发者在不修改核心服务契约的情况下扩展系统功能,并自由切换实现。
5.1. InetAddress 的 SPI 组件
遵循 SPI 设计模式,JEP 418 提供了替换默认系统解析器的方案。该 SPI 从 Java 18 开始提供,需要通过服务定位器识别具体实现。若定位失败则回退到默认实现。
SPI 包含四个核心组件:
组件 | 说明 | 实例 |
---|---|---|
服务 (Service) | 提供特定功能的接口/类集合 | 互联网地址解析服务 |
服务提供者接口 (SPI) | 代理服务的接口/抽象类 | InetAddressResolver 接口 |
服务提供者 (Provider) | SPI 的具体实现 | InetAddressResolverProvider 抽象类 |
服务加载器 (Loader) | 绑定所有组件的机制 | ServiceLoader 机制 |
关键点:
- JVM 维护单一系统级解析器,由
InetAddress
使用 ServiceLoader
负责定位并设置默认解析器- 失败时自动回退到系统默认实现
5.2. 自定义 InetAddressResolverProvider 实现
SPI 相关类位于 java.net.spi
包,新增类包括:
InetAddressResolverProvider
InetAddressResolver
InetAddressResolver.LookupPolicy
InetAddressResolverProvider.Configuration
下面实现自定义解析器替换系统默认解析器。首先创建工具类从文件加载地址映射到内存缓存。
步骤 1:定义 CustomAddressResolverImpl
继承 InetAddressResolverProvider
并实现两个方法:
@Override
public String name() {
return "CustomInternetAddressResolverImpl";
}
@Override
public InetAddressResolver get(Configuration configuration) {
LOGGER.info("Using Custom Address Resolver :: " + this.name());
LOGGER.info("Registry initialised");
return new InetAddressResolver() {
@Override
public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy)
throws UnknownHostException {
return registry.getAddressesfromHost(host);
}
@Override
public String lookupByAddress(byte[] addr) throws UnknownHostException {
return registry.getHostFromAddress(addr);
}
};
}
5.3. Registry 类实现
使用 HashMap
存储主机名与 IP 地址的映射关系:
Map<String, List<byte[]>> registry = new HashMap<>();
正向查找实现(主机名 → IP 地址):
public Stream<InetAddress> getAddressesfromHost(String host) throws UnknownHostException {
LOGGER.info("Performing Forward Lookup for HOST : " + host);
if (!registry.containsKey(host)) {
throw new UnknownHostException("Missing Host information in Resolver");
}
return registry.get(host)
.stream()
.map(add -> constructInetAddress(host, add))
.filter(Objects::nonNull);
}
⚠️ 注意: 返回 Stream<InetAddress>
以支持多 IP 映射。
反向查找实现(IP 地址 → 主机名):
public String getHostFromAddress(byte[] arr) throws UnknownHostException {
LOGGER.info("Performing Reverse Lookup for Address : " + Arrays.toString(arr));
for (Map.Entry<String, List<byte[]>> entry : registry.entrySet()) {
if (entry.getValue()
.stream()
.anyMatch(ba -> Arrays.equals(ba, arr))) {
return entry.getKey();
}
}
throw new UnknownHostException("Address Not Found");
}
最后通过 ServiceLoader
加载自定义实现:
- 在
resources/META-INF/services
创建文件:java.net.spi.InetAddressResolverProvider
- 文件内容填写实现类的全限定名:
com.baeldung.inetspi.providers.CustomAddressResolverImpl
6. 替代方案
若不想实现自定义解析器,可考虑以下替代方案:
✅ 使用 JNDI + DNS 提供者
但无法利用 InetAddress
的丰富 API
✅ 通过 Project Panama 的 JNI 调用系统原生解析器
需要处理本地代码复杂性
❌ 修改 JDK 系统属性
如设置 jdk.net.hosts.file
指定主机映射文件
⚠️ 缺点:维护完整映射列表困难
7. 总结
本文深入分析了 Java 中互联网地址解析的工作原理,重点探讨了 JEP 418 引入的 SPI 机制。我们实现了自定义地址解析提供者,并讨论了替代方案。这种 SPI 设计为 Java 网络编程带来了前所未有的灵活性,特别适合需要特殊解析策略的场景(如测试环境、私有 DNS 等)。
完整示例代码请查阅 GitHub 仓库。