← 返回 Java 后端知识路线
阶段 02集合与并发

网络与 XML:把外部输入当成不可信的边界

用 HttpClient 请求一个 JSON 文本并安全读取 XML 属性,串起超时、状态码、编码和实体展开风险。

第 15 / 35 篇
HttpClientHTTPXML安全

先看这一课值不值得学

学完后,你手里多了哪些代码积木

让两个进程交换字节,并把结构化文本还原成对象。

本课正式新增

语法 / API / 命令你必须会到什么程度
Socket / ServerSocket建立 TCP 客户端和服务端连接
getInputStream / getOutputStream通过连接收发字节
DocumentBuilder解析 XML 文档树

本课只借用,先别硬背

  • HTTP 协议在第 23 课,Web 容器不在本课展开

学完必须能独立写

  • 完成一次本地 TCP 请求响应
  • 读取 XML 配置并提取节点数据
本课目录
  1. 1. 现实问题:网络调用不是一个返回值
  2. 2. 最小可运行示例:请求有边界,解析有防护
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:外部适配器不要渗透业务
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:网络调用不是一个返回值

服务调用外部 API 时,DNS、连接、TLS、超时、状态码、响应体编码和对方限流都可能失败。把 send 包在一个 try 里只说明异常被捕获,不说明业务如何处理 404、429 和 500。XML 也一样:格式正确不代表内容可信,外部实体可能让解析器访问本地文件或内网地址。

先用 JDK 21 的 HttpClient 写一个超时请求,再演示 XMLInputFactory 的安全配置。示例不绑定具体业务服务,便于复制后替换 URL 和断言。

2. 最小可运行示例:请求有边界,解析有防护

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamReader;
import java.io.StringReader;

public class NetworkXmlDemo {
    public static void main(String[] args) throws Exception {
        HttpClient client = HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(3)).build();
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://example.com/health"))
                .timeout(Duration.ofSeconds(5)).header("Accept", "application/json").GET().build();
        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
        if (response.statusCode() / 100 != 2) throw new IllegalStateException("上游状态=" + response.statusCode());
        System.out.println(response.body());

        XMLInputFactory factory = XMLInputFactory.newFactory();
        factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
        factory.setProperty("javax.xml.stream.isSupportingExternalEntities", false);
        XMLStreamReader reader = factory.createXMLStreamReader(new StringReader("<item id=\"7\"/>"));
        while (reader.hasNext()) {
            if (reader.next() == XMLStreamConstants.START_ELEMENT) System.out.println(reader.getLocalName());
        }
        reader.close();
    }
}

example.com 只是可替换占位地址,运行前应换成自己允许访问的健康检查端点;课程不把外部站点的偶然状态当作测试证据。生产代码还需要解析响应 schema、限制响应大小和记录请求 id。

3. 调用链与对象变化

HttpRequest 保存 URI、头和超时配置,HttpClient 根据配置建立连接并把响应字节解码为 String;状态码、响应头和 body 分别属于不同结果,不要只返回 body。同步 send 会占用当前执行线程,异步 sendAsync 则返回 CompletableFuture,异常会进入 future 链。

XMLInputFactory 创建解析器,reader 按事件流读取元素;关闭 DTD 和 external entities 后,解析器不会按文档指令打开外部资源。属性仍是字符串,必须做长度、格式和枚举校验。解析器的关闭是资源生命周期的一部分,不能只在成功路径调用。

4. 为什么这样设计

超时是资源上限,不是错误处理的替代品;连接超时和整体请求超时解决不同阶段。状态码要翻译成业务语义,重试只对幂等操作、可恢复错误和明确退避策略开放。日志应带 URI 的安全摘要和 trace id,不记录 Authorization 与完整响应敏感内容。

XML 的实体和 DTD 是历史兼容能力,现代服务通常不需要却可能扩大攻击面。安全解析应显式关闭,并对输入来源、大小、嵌套深度做限制。JSON 也不是天然安全,未知字段、数值范围和文本长度仍要验证。

5. 项目落点:外部适配器不要渗透业务

把上游 HTTP 和 XML 解析封装在 adapter 中,返回领域需要的 HealthStatusCatalogItem,Service 不直接依赖 HttpResponse。adapter 可以在测试中替换为 fake,模拟超时、429、空 body 和非法 XML。真正上线前,用 WireMock 或 Testcontainers 做可控的集成测试。

练习:把同步请求改成 sendAsync,为 2xx、404、429、5xx 建立不同结果;XML 输入增加外部实体样例,验证关闭 DTD 后不会读取文件。再测试响应体超过限制时是否会提前终止,而不是等内存耗尽。

6. 易错排查

  • 只判断是否抛异常:HTTP 404/500 通常是正常返回,不会自动成为 Java 异常。
  • 没有请求超时:线程、连接和任务会长期占用,重试还会放大压力。
  • XML 解析器默认开启外部实体:显式设置安全属性并对异常配置做失败处理。
  • 使用共享 HttpClient 却每次创建:连接复用失效,线程和资源配置难以观察。

7. 一页复习

网络调用至少包含请求配置、连接、状态码、头、body、超时和重试;XML 输入至少包含格式校验、实体安全、大小限制和资源关闭。把外部协议收口在适配器,业务层只接收稳定领域对象。JDK HttpClient 能完成传输,但不能替你决定幂等和安全策略。

把一次上游调用记录成状态:请求构造完成但尚未连接;连接建立后等待响应头;收到状态码和响应头但 body 还可能很大;body 解码后才进入 JSON/领域解析;所有资源关闭后才算请求完成。连接超时、整体超时、非 2xx 和响应格式错误分别应有不同错误码,不能统一打印“网络失败”。

XML 解析的安全输入可以写成三类:普通 <item id="7"/> 应得到元素名和 id;包含 DTD 的文档应被拒绝或按策略忽略;超深嵌套、超长属性和未知元素应触发限制。关闭 external entities 只解决一部分外部访问风险,解析后的字符串仍要做长度和业务值校验,不能把 id 当成可信 SQL 片段。

项目文件可以把 CatalogClient 放在 infrastructure/http,把 XML reader 放在 adapter/parser,把 CatalogItem 放在 domain。Service 只接收成功或分类失败的结果,重试策略由调用边界决定;同步 HttpClient 和异步 future 不要在同一个方法里混成无法等待的半同步流程。日志带 host、路径模板、状态、耗时和 trace id,token 与 body 按敏感级别脱敏。

排错时先用 curl 或固定 fake 响应区分 DNS/连接问题,再看状态码,最后看字符集和解析器;429 不是 500,重试要遵守 Retry-After 和幂等;XML 解析慢可能是大文档或实体展开,不要只把超时调大;异步 future 卡住要检查执行器和异常链。练习是用本地测试服务器返回 200、404、429、5xx、非法 XML 和超大 body,验证每条路径。

网络和 XML 还要明确输入输出边界:请求输入是 URI、方法、头和 body,成功输出是状态码、headers 和受限大小的文本;解析成功输出领域对象,解析失败输出分类错误。不要把上游 JSON/XML 原文直接存进文章实体,也不要把上游状态码原样当成对外状态码,应由 adapter 翻译。

项目里可以为 CatalogClient 编写一个 fake transport,让它按测试返回 timeout、429、非法内容和正常响应;真实 HttpClient 只在少量集成测中验证。XML parser 的安全属性在工厂构造时固定,解析器对象在一次文档结束后关闭。调用外部服务不应持有数据库事务,重试也要在边界外控制次数和退避。

排错时网络连接失败先用最小 curl 复现,避免把 DNS 问题误判成序列化错误;响应 200 但字段为空要查字符集、schema 和未知字段;XML 解析器消耗 CPU 要查 DTD、嵌套和文档大小;CORS/代理问题应看浏览器 preflight 和服务端访问日志。练习是为每种失败保存可重放的 fixture,不依赖外部网站当天状态。

外部协议练习的最终产物是可重放 fixture 和分类错误,而不是依赖某个网站当天返回什么。每条请求都带超时、大小和关闭边界,服务层只接收已经安全解析的领域对象。

验证清单:用固定测试服务器依次返回 200 正常 XML、404、429、500、连接超时、非法 XML 和超大 body,记录请求阶段、状态码、重试次数、最终错误码与资源关闭结果。对带 DTD 的 XML 和深层嵌套文档确认解析器在安全配置或大小限制处拒绝;对正常响应确认只把白名单字段转成 CatalogItem,未知字段不会进入 SQL。复盘调用链时区分 transport、decoder、parser、domain 四个对象,任何一层都不应把上游原文直接当作可信业务值。

练习复盘:为 429 增加 Retry-After fixture,只对幂等 GET 重试,POST 不自动重放;为每个 fixture 保存请求、响应和预期分类错误,使本地构建不依赖外部站点。若浏览器报 CORS 而 curl 成功,单独检查 preflight 和响应头,不要把网络层、XML 层和浏览器策略混成一个 catch。

进阶附录:HTTP/2、连接池与背压

HttpClient 可以协商 HTTP/2,但对方、代理和 TLS 配置会影响结果。高并发请求要观察连接数、队列、响应时间和上游限流,异步 future 链也需要有界并发;“全异步”不等于没有背压。XML 大文档优先事件流,避免 DOM 一次性构建整棵树。

本课按「Java 21 HttpClient、XML 解析与输入安全」的学习范围组织,正文与示例均为本站原创整理。