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 使用的操作系统原生解析流程包含多个步骤:

  1. 系统级 DNS 缓存:存储常用查询记录
  2. 缓存未命中时,通过系统解析器配置获取 DNS 服务器信息
  3. 向配置的 DNS 服务器发起查询(可能递归多次)
  4. 查询成功时在各层级缓存结果并返回客户端
  5. 查询失败时触发迭代查询:
    • 向根服务器请求权威域名服务器 (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 加载自定义实现:

  1. resources/META-INF/services 创建文件:
    java.net.spi.InetAddressResolverProvider
    
  2. 文件内容填写实现类的全限定名:
    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 仓库


原始标题:Internet Address Resolution SPI in Java | Baeldung