URL / URI / InetAddress 地址解析源码
概述
java.net 的三个地址类构成了 Java 网络编程的寻址基础:URL(统一资源定位符)、URI(统一资源标识符)、InetAddress(IP 地址抽象)。它们分别处理"怎么访问"、"怎么描述"、"怎么解析"。
URL依赖URLStreamHandler按协议分派;URI有严格的分段解析器(URI.Parser);InetAddress通过NameService与平台 native 实现完成 DNS 解析,并带两级缓存。
本文基于 OpenJDK 21 源码拆解三条解析链路的实现。
核心源码解析
① URL(String spec) 的解析
java
public final class URL implements java.io.Serializable {
private String protocol; // 协议,如 http
private String host; // 主机名
private int port = -1; // 端口(-1 表示默认)
private String file; // 路径 + 查询
private String query; // 查询参数
private String authority; // host[:port] 或 userinfo@host[:port]
private String ref; // 锚点 #
private String userInfo; // user:password
private transient URLStreamHandler handler; // 协议处理器
public URL(String spec) throws MalformedURLException {
this(null, spec);
}
public URL(URL context, String spec, URLStreamHandler handler) {
...
parser.parseURL(this, spec, start, limit); // 拆解各段
}
}parseURL按协议规则扫描 spec:定位://前的协议名、/前的 host:port、?前的路径、#前的 query,填充各字段。- 未显式指定 handler 时,
getURLStreamHandler(protocol)通过URLStreamHandlerFactory或内置协议表(sun.net.www.protocol.*)查找处理器。 - 相对 URL:
new URL(context, spec)会基于 context 的路径补全(如基于http://a/b/c解析d→http://a/b/d)。
② URL.openConnection() 的协议分发
java
public URLConnection openConnection() throws java.io.IOException {
return handler.openConnection(this); // 委托协议处理器
}URLStreamHandler是协议实现的核心抽象(sun.net.www.protocol.http.Handler、https.Handler、file.Handler、jar.Handler等),openConnection由它创建对应协议的连接对象。URLStreamHandlerFactory:应用可通过URL.setURLStreamHandlerFactory注册自定义工厂,createURLStreamHandler(protocol)返回协议处理器——这是扩展自定义协议(如ftp、自定义 scheme)的 SPI 入口。- 查找顺序:工厂注册的自定义 handler > 内置协议处理器(按协议包名反射加载)> 失败抛
MalformedURLException。 URLConnection的connect()/getInputStream()最终落到 handler 对应的连接实现(HttpURLConnection等)。
③ URI 的 RFC 严格解析
java
public final class URI implements Comparable<URI>, Serializable {
// URI.Parser 是内部解析器
private static class Parser {
private final String input;
...
private void parse(boolean rsa) throws URISyntaxException {
// ① 定位 scheme 与分隔符
...
scan(uri, input, 0); // ② 分段扫描
checkChars(0, p, L_SCHEME, input); // ③ 字符合法性校验
...
}
private int scan(int start, int end, char[] target, ...) { ... }
}
}URI的解析严格遵循 RFC 2396/3986 语法:scheme://authority/path?query#fragment逐段扫描,非法字符(空格、控制字符、未转义的非 ASCII)直接抛URISyntaxException。HierarchicalvsOpaque:有//权威部分的为 hierarchical(层级 URI,可相对解析);无 scheme 或 scheme 后直接是 path 的为 opaque(不透明 URI,不可 resolve)。- 编码规范:
toASCIIString()对非 ASCII 做 percent-encoding;URI.create是new URI(str)的便捷入口(非法输入抛IllegalArgumentException)。
④ URI.resolve(URI uri) 的路径合并
java
public URI resolve(URI uri) {
if (uri.isOpaque() || isOpaque()) return uri; // 不透明:直接返回
if (uri.isAbsolute()) return uri; // 已是绝对 URI
URI base = this;
...
String path = base.getPath();
... // 拼接基路径与相对路径
return resolveRelativeURI(uri.getPath()); // 核心合并
}
private URI resolveRelativeURI(String encodedPath) {
...
path = removeDotSegments(path); // 去除 ./ 与 ../
...
normalize(path); // 规范化
}removeDotSegments:按 RFC 3986 算法处理.(当前段)与..(上级段),如/a/b/../c→/a/c。normalize:合并连续斜杠、还原空段,保证合并后的路径最小化。- 与
URL的差异:URL.resolve不保证语法正确性(URL 不做严格校验),URI.resolve则保证结果 URI 合法;Web 开发中优先用URI做路径运算。
⑤ InetAddress.getByName(String host) 的 DNS 解析
java
public class InetAddress implements java.io.Serializable {
static final InetAddressImpl impl; // Inet4AddressImpl / Inet6AddressImpl
private static final NameService nameService = new NameService();
transient String hostName; // 主机名
byte[] addr; // 4/16 字节地址
public static InetAddress getByName(String host) throws UnknownHostException {
return InetAddress.getAllByName(host)[0];
}
public static InetAddress[] getAllByName(String host) throws UnknownHostException {
// ① 先查缓存
InetAddress[] addresses = getCachedAddresses(host);
if (addresses == null) {
addresses = nameService.lookupAllHostAddr(host); // ② 委托 NameService
cacheAddresses(host, addresses, success); // ③ 缓存结果
}
return addresses;
}
}NameService.lookupAllHostAddr:先尝试平台 native(impl.lookupAllHostAddr(host),如 Linuxgetaddrinfo),失败回退到HostsFileNameService(读/etc/hosts/hosts文件)。- 字面地址优化:
isIPv4LiteralAddress/isIPv6LiteralAddress直接解析数字地址,不发起 DNS 查询。 - 返回数组:
getByName取第一个,getAllByName返回全部解析结果(负载均衡场景常用)。
⑥ InetAddress 的反向缓存与正向缓存
java
private static Cache addressCache = new Cache(Cache.Type.Positive); // 正向缓存
private static Cache negativeCache = new Cache(Cache.Type.Negative); // 负向缓存
// Cache 内部 CacheTable:ConcurrentHashMap + 过期时间戳
private static InetAddress[] getCachedAddresses(String hostname) {
hostname = hostname.toLowerCase(Locale.ROOT); // 键不区分大小写
return addressCache.get(hostname); // 命中返回
}| 缓存 | 键 | 值 | 作用 |
|---|---|---|---|
| 正向缓存 | hostname(小写) | InetAddress[] | 缓存成功解析,避免重复 DNS 查询 |
| 负向缓存 | hostname(小写) | UNKNOWN 标记 | 缓存失败结果,防 DNS 风暴 |
- TTL 策略:由系统属性
networkaddress.cache.ttl(正向)与networkaddress.cache.negative.ttl(负向)控制;-1永不过期(java.security.Security默认安全策略networkaddress.cache.ttl=30)。 - 命中判断:
CacheEntry携带过期时间,超过 TTL 即失效并从表中移除;impl.isReachable等不经过缓存(直连探测)。 - 反向解析
getHostName():对已知 IP 查getHostByAddr0native,失败时返回 IP 字符串本身(不抛异常)。
⑦ InetAddress.getLocalHost() 的本地地址
java
public static InetAddress getLocalHost() throws UnknownHostException {
SecurityManager security = System.getSecurityManager();
try {
String local = impl.getLocalHostName(); // native:gethostname()
if (local != null && !local.isEmpty()) {
return getByName(local); // 解析本机名 → 地址
}
} catch (UnknownHostException uhe) { ... }
return impl.lookupAllHostAddr("localhost"); // 兜底:解析 localhost
}impl.getLocalHostName():Linuxgethostname(),WindowsGetComputerName——拿到本机主机名后再按 ⑤ 的链路解析。- 多网卡场景:返回的可能是环回地址(
127.0.0.1)或首块网卡地址,取决于/etc/hosts与 DNS 顺序;要精确获取网卡地址应使用NetworkInterface.getNetworkInterfaces()。 Inet4Address/Inet6Address是InetAddress的两个子类:按地址长度(4 字节 vs 16 字节)与平台支持选择实例化。
⑧ InetSocketAddress 的构造
java
public InetSocketAddress(String hostname, int port) {
checkPort(port);
this.hostname = hostname;
this.port = port;
// 惰性:构造时不做 DNS 解析
}
public InetAddress getAddress() {
return getAddressImpl(); // 首次调用时才解析
}
private InetAddress getAddressImpl() {
if (addr == null) {
try {
addr = InetAddress.getByName(hostname); // 延迟解析
} catch (UnknownHostException e) { ... }
}
return addr;
}- 关键语义:构造不解析——
new InetSocketAddress("example.com", 8080)只是存字符串,DNS 解析延迟到getAddress()首次调用。这让"创建地址对象"零开销、不抛UnknownHostException。 isUnresolved():解析失败或未解析时返回 true,此时对象只携带 hostname + port,可用于Socket.connect前的延迟解析。- 字面地址优化:构造时
isIPv6LiteralAddress快速识别::1等 IP 字面量直接构造InetAddress,避免后续字符串重复判断。
总结
| 类 | 解析职责 | 核心机制 |
|---|---|---|
URL | 定位资源 | parseURL 拆段 + URLStreamHandler 协议分派 |
URI | 描述资源 | Parser.scan 严格语法 + removeDotSegments/normalize |
InetAddress | 解析地址 | NameService → native getaddrinfo/hosts 文件 + 正负缓存 |
InetSocketAddress | 端口绑定 | 惰性解析 + unresolved 状态 |
四类分工明确:URI 管"合法描述"、URL 管"协议访问"、InetAddress 管"名字到 IP"、InetSocketAddress 管"端点"。理解它们的解析时机(URL/URI 构造即解析、InetAddress 缓存解析、InetSocketAddress 惰性解析),能避开"误以为构造不抛异常却抛了 UnknownHostException"之类的常见坑。