原创

URL 放进 HashMap:键比较为何可能触发主机地址查询

一次普通 Map.get 也可能卡在主机解析上。如果Map的键是 java.net.URL,不要先把注意力全部放到HashMap扩容:URL的相等和散列语义本来就可能涉及主机地址。

URL表示可定位资源,比较行为由协议处理器参与。对于常见网络协议,主机是否指向同一地址可能进入判断;相等与hashCode又必须协调。于是看起来只是保存一个地址对象的容器,可能带来与纯字符串比较不同的等待和身份含义。

下面固定Java 17,使用自定义处理器计数,不真的查询外部DNS。这样既能看见键操作调用了主机解析入口,又不把本机DNS缓存和网络波动误当稳定测试条件。

import java.io.IOException;
import java.net.*;
import java.util.*;
public class UrlMapKeys {
    static final class CountingHandler extends URLStreamHandler {
        int lookups;
        protected URLConnection openConnection(URL url) throws IOException {
            throw new IOException("connections disabled in this experiment");
        }
        protected InetAddress getHostAddress(URL url) {
            lookups++;
            try { return InetAddress.getByAddress(new byte[]{127, 0, 0, 1}); }
            catch (UnknownHostException impossible) { throw new AssertionError(impossible); }
        }
    }
    public static void main(String[] args) throws Exception {
        CountingHandler handler = new CountingHandler();
        URL first = new URL(null, "demo://one.invalid/a", handler);
        URL second = new URL(null, "demo://two.invalid/a", handler);
        Map<URL, String> map = new HashMap<>();
        map.put(first, "stored");
        System.out.println("value=" + map.get(second));
        System.out.println("resolver_used=" + (handler.lookups > 0));
        URI a = URI.create("demo://one.invalid/a");
        URI b = URI.create("demo://two.invalid/a");
        System.out.println("uri_equal=" + a.equals(b));
    }
}

保存为 UrlMapKeys.java,运行 java UrlMapKeys.java:

value=stored
resolver_used=true
uri_equal=false

这段实验说明什么,没有说明什么

自定义处理器把两个示例主机都解释成同一个回环地址。因此两个URL能够按这份处理器的资源身份规则找到同一个值,Map操作也确实调用了 getHostAddress。我们没有发起连接,openConnection 反而直接拒绝调用。

这不是一次真实DNS耗时测量,更不能据此声称每次URL操作都会发出网络包。具体协议处理器、对象和系统缓存都可能减少实际解析。官方URL契约提示的风险是比较与散列可能使用主机解析,不应在要求纯计算、低延迟的键操作中忽略这条路径。

测试还刻意没有断言解析调用次数。调用次数属于具体实现与缓存细节,换一个JDK更新版本或处理器可能变化。能够稳定断言的是:本例Map键操作触达了该入口,且自定义的主机身份影响查找结果。

换成 URI 后,先决定想表达的身份

URI的相等和散列不要求解析主机地址,适合保留结构化引用。样本里两个不同主机名的URI不相等,即使测试处理器给URL返回相同IP。网站可能共享IP、同一主机也可能有多个地址,地址相同本来就不代表HTTP资源可以互换。

但换成URI不是万能的规范化。显式默认端口与省略端口、路径点段、查询参数顺序、片段是否参与缓存,都要按应用协议决定。URI有自己的组件比较规则,不能未经设计就声称它等同于“最终HTTP请求完全相同”。

如果缓存键的语义就是一份配置中的原始端点,可以直接保留固定字符串;如果允许等价形式,则先做明确、可测试的规范化,再生成不可变键。不要为了多命中几次缓存随意删除查询参数或合并路径大小写,那些差异可能正是不同资源。

已有 URL 键缓存怎样迁移

先列出目前键的来源与用途:是否存储长寿命缓存,是否会从不可信输入构造,是否依赖两个主机解析成相同地址而合并。多数业务并不需要最后这种合并,但迁移前应验证,不能只把泛型URL替换成URI就结束。

持久化缓存还要考虑旧条目。新旧键语义改变后,旧数据可能不能沿用;采用新的命名空间或重建缓存,通常比把两种规则混在同一容器里更清楚。对线程热点或请求入口,可以通过线程栈确认等待是否真的在解析路径,再决定是否需要这项改动。

URL对象在Java 17仍可用于建立连接,本文讨论的是把它用作纯身份键时的意外代价。连接与比较是不同动作,能成功访问一个地址并不要求容器查找也必须采用网络身份。

样本中的处理器只为演示可观察调用,不应部署为“禁用DNS的通用URL处理器”。它改变了资源身份,真实协议还有默认端口、路径和其他规则。把所需身份建模成一个明确值,比试图悄悄改掉URL的含义更容易维护。

正文到此结束
评论插件初始化中...
Loading...
本文目录