调用 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 嵌套示例会被明确拒绝。
接收回调还应在解析前按平台要求验证请求真实性,限制请求大小;解析成功只说明语法可读,不说明消息可信或业务已经成功。