调用 HTTP 接口和读取 XML,看起来都能封装成“输入字符串,返回字符串或 Map”。但如果超时、错误状态、编码和结构丢失都藏在工具方法里,调用者反而更难知道失败发生在哪里。

下面使用 Java 8 的 JDK API 说明几个边界,不是完整的微信接入库,也不替代签名校验、身份认证和业务错误处理。

GET 参数和 POST 正文不能混用

GET 参数需要真正拼入查询字符串,而且应分别编码名称和值,而不是对整条 URL 编码。例如,在一个尚无查询串或片段的固定接口地址上:

String query = "name=" + java.net.URLEncoder.encode("张三", "UTF-8");
java.net.URL url = new java.net.URL("https://example.com/api?" + query);

这只是查询参数形式的示意。POST 则应按接口契约设置 Content-Type 与正文;JSON、表单和 XML 不能只靠“都是字符串”就互换。

HTTP 成功状态和业务成功分开判断

以下方法处理受信任、由应用配置的 HTTP(S) 地址,返回最多 1 MiB 的原始响应字节。传入 body 为 null 时发 GET,否则按给定媒体类型发 POST;调用者负责准备编码正确的正文。

import java.io.*;
import java.net.*;

public static byte[] request(URL url, byte[] body, String contentType)
        throws IOException {
    if (!"https".equalsIgnoreCase(url.getProtocol())
            && !"http".equalsIgnoreCase(url.getProtocol())) {
        throw new IllegalArgumentException("Only HTTP(S) is supported");
    }
    HttpURLConnection connection = (HttpURLConnection) url.openConnection();
    connection.setConnectTimeout(5000);
    connection.setReadTimeout(5000);
    connection.setInstanceFollowRedirects(false);
    try {
        connection.setRequestMethod(body == null ? "GET" : "POST");
        if (body != null) {
            if (contentType == null || contentType.trim().isEmpty()) {
                throw new IllegalArgumentException("Content-Type is required");
            }
            connection.setDoOutput(true);
            connection.setRequestProperty("Content-Type", contentType);
            connection.setFixedLengthStreamingMode(body.length);
            try (OutputStream output = connection.getOutputStream()) {
                output.write(body);
            }
        }
        int status = connection.getResponseCode();
        InputStream stream = status >= 400
                ? connection.getErrorStream() : connection.getInputStream();
        byte[] bytes;
        try (InputStream input = stream;
             ByteArrayOutputStream output = new ByteArrayOutputStream()) {
            if (input != null) {
                byte[] buffer = new byte[8192];
                int count;
                while ((count = input.read(buffer)) != -1) {
                    if (output.size() + count > 1024 * 1024) {
                        throw new IOException("Response exceeds size limit");
                    }
                    output.write(buffer, 0, count);
                }
            }
            bytes = output.toByteArray();
        }
        if (status < 200 || status >= 300) {
            throw new IOException("Unexpected HTTP status: " + status);
        }
        return bytes;
    } finally {
        connection.disconnect();
    }
}

这是放在类中的静态方法,不是完整 HTTP 客户端。错误正文被限量读取,但没有拼进异常或日志,避免泄漏令牌和用户资料;需要上报错误详情时应设计脱敏后的结果类型。响应为空也未必失败,例如无正文的成功响应,应由具体接口契约判断。

返回 JSON/XML 时,再按接口约定解码字节,不能默认所有响应都是 UTF-8。连接和读取超时也不是整个调用的总时限,更不是自动重试策略。HttpURLConnection 与错误流

方法只限制协议,不是 SSRF 防护;不可直接接收用户提供的任意 URL。目标允许列表、网络出站限制和 DNS/重定向策略仍应在调用边界实施。

解析 XML 前限制外部访问

对不需要 DTD 的小型 XML 文本,可以拒绝 DOCTYPE,并关闭外部 DTD、Schema 与 XInclude。以下为 JAXP 的配置方式,不能原样套到 DOM4J/SAXReader:

import javax.xml.XMLConstants;
import javax.xml.parsers.DocumentBuilderFactory;
import org.w3c.dom.Document;
import org.xml.sax.InputSource;
import java.io.StringReader;

public static Document parseSmallXml(String xml) throws Exception {
    if (xml == null || xml.length() > 16384) {
        throw new IllegalArgumentException("XML text exceeds allowed size");
    }
    DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
    factory.setNamespaceAware(true);
    factory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
    factory.setFeature(
        "http://apache.org/xml/features/disallow-doctype-decl", true);
    factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_DTD, "");
    factory.setAttribute(XMLConstants.ACCESS_EXTERNAL_SCHEMA, "");
    factory.setXIncludeAware(false);
    factory.setExpandEntityReferences(false);
    return factory.newDocumentBuilder().parse(
        new InputSource(new StringReader(xml)));
}

解析器不支持安全选项时应让初始化失败,不要 catch 后继续用不受限配置。运行时应使用受维护的 JDK;字符数限制也不替代请求体、解压大小、解析深度和资源预算限制。OWASP Java XML 防护

XML 变 Map 是有损投影,不是通用转换

只读取根节点的直接子元素,适合结构明确的平面字段,但不能无损表达任意 XML。例如:

<xml>
  <Tag>first</Tag>
  <Tag>second</Tag>
  <User><Name>Ada</Name></User>
</xml>

若简单执行 map.put(elementName, text),第二个 Tag 会覆盖第一个,User 的层级也会消失。需要重复字段时用 List 保存同名值;需要嵌套、命名空间、属性或混合内容时,保留 DOM 或映射到明确 DTO,而不是用一个 Map<String, String> 假装保留全部信息。

如果确实只允许无命名空间的平面子元素,下面的方法可以与前面的解析方法配合:同名字段保留为列表,遇到嵌套结构则明确拒绝,而不是悄悄合并文本。所需集合及 DOM 类型已在示例中使用全限定名。

public static java.util.Map<String, java.util.List<String>> directFields(
        org.w3c.dom.Document document) {
    java.util.Map<String, java.util.List<String>> fields =
        new java.util.LinkedHashMap<>();
    org.w3c.dom.NodeList children =
        document.getDocumentElement().getChildNodes();
    for (int i = 0; i < children.getLength(); i++) {
        org.w3c.dom.Node node = children.item(i);
        if (node.getNodeType() != org.w3c.dom.Node.ELEMENT_NODE) continue;
        org.w3c.dom.Element element = (org.w3c.dom.Element) node;
        if (element.getNamespaceURI() != null
                || element.getElementsByTagName("*").getLength() > 0) {
            throw new IllegalArgumentException("Only flat fields are supported");
        }
        fields.computeIfAbsent(element.getTagName(),
            key -> new java.util.ArrayList<>()).add(element.getTextContent());
    }
    return fields;
}

这仍然只提取直接子元素文本,不保留属性、根级混合文本或完整文档结构;格式有这些需求时,应使用更合适的模型。前面的含 User 嵌套示例会被明确拒绝。

接收回调还应在解析前按平台要求验证请求真实性,限制请求大小;解析成功只说明语法可读,不说明消息可信或业务已经成功。