對接第三方支付(如支付寶、微信)時,常需解析結構複雜、層級深、且成功/失敗報文結構不同的 XML。本文以支付寶「查詢訂單」介面返回報文為例,示範如何優雅地將 XML 映射為 Java 物件。
<?xml version="1.0" encoding="utf-8"?> <alipay> <is_success>F</is_success> <error>TRADE_NOT_EXIST</error> </alipay>
<?xml version="1.0" encoding="utf-8"?>
<alipay>
<is_success>T</is_success>
<request>
<param name="_input_charset">utf-8</param>
<param name="service">single_trade_query</param>
<param name="partner">2088121424678319</param>
<param name="out_trade_no">H161021114440198N</param>
</request>
<response>
<trade>
<body>訂單:T16102111443568</body>
<buyer_email>1176760525@qq.com</buyer_email>
<buyer_id>2088402231747066</buyer_id>
<discount>0.00</discount>
<flag_trade_locked>0</flag_trade_locked>
<gmt_create>2016-10-21 11:45:06</gmt_create>
<gmt_last_modified_time>2016-10-21 11:45:16</gmt_last_modified_time>
<gmt_payment>2016-10-21 11:45:17</gmt_payment>
<is_total_fee_adjust>F</is_total_fee_adjust>
<operator_role>B</operator_role>
<out_trade_no>H161021114440198N</out_trade_no>
<payment_type>1</payment_type>
<price>0.01</price>
<quantity>1</quantity>
<seller_email>pay@gstarcad.com</seller_email>
<seller_id>2088121424678319</seller_id>
<subject>的生活費...</subject>
<time_out>2017-01-20 11:45:17</time_out>
<time_out_type>finishFPAction</time_out_type>
<to_buyer_fee>0.00</to_buyer_fee>
<to_seller_fee>0.01</to_seller_fee>
<total_fee>0.01</total_fee>
<trade_no>2016102121001004060245497067</trade_no>
<trade_status>TRADE_SUCCESS</trade_status>
<use_coupon>F</use_coupon>
</trade>
</response>
<sign>25217c932f49868371a301e3151201a2</sign>
<sign_type>MD5</sign_type>
</alipay>
is_success,再針對性解析由於成功/失敗報文根結構不同,不可直接映射到同一個 Java 類。正確流程:
Maven 依賴:
<dependency>
<groupId>com.thoughtworks.xstream</groupId>
<artifactId>xstream</artifactId>
<version>1.4.20</version>
</dependency>
package com.example.alipay;
import com.thoughtworks.xstream.annotations.*;
import java.util.List;
@XStreamAlias("alipay")
public class AlipayResponse {
@XStreamAlias("is_success")
private String isSuccess; // T/F
@XStreamAlias("error")
private String errorCode; // 失敗時存在
@XStreamAlias("request")
private Request request;
@XStreamAlias("response")
private Response response;
@XStreamAlias("sign")
private String sign;
@XStreamAlias("sign_type")
private String signType;
// Getters/Setters 略
public boolean isSuccess() { return "T".equals(isSuccess); }
}
class Request {
@XStreamImplicit(itemFieldName = "param")
private List params;
}
@XStreamAlias("param")
class Param {
@XStreamAsAttribute
private String name;
private String value; // 文本內容
}
class Response {
@XStreamAlias("trade")
private Trade trade;
}
class Trade {
// 直接映射所有子標籤為同名字段
private String body;
private String buyer_email;
private String buyer_id;
private String discount;
private String flag_trade_locked;
private String gmt_create;
private String gmt_last_modified_time;
private String gmt_payment;
private String is_total_fee_adjust;
private String operator_role;
private String out_trade_no;
private String payment_type;
private String price;
private String quantity;
private String seller_email;
private String seller_id;
private String subject;
private String time_out;
private String time_out_type;
private String to_buyer_fee;
private String to_seller_fee;
private String total_fee;
private String trade_no;
private String trade_status;
private String use_coupon;
// Getters/Setters 略
}
package com.example.util;
import com.thoughtworks.xstream.XStream;
import com.thoughtworks.xstream.io.xml.DomDriver;
public class XmlUtil {
private static final ThreadLocal XSTREAM_THREAD_LOCAL = ThreadLocal.withInitial(() -> {
XStream xs = new XStream(new DomDriver());
// 安全框架:僅允許自包下的類
xs.allowTypesByWildcard(new String[]{"com.example.alipay.**"});
xs.ignoreUnknownElements(); // 忽略實體中未定義的 XML 元素
xs.autodetectAnnotations(true); // 啟用註解
return xs;
});
public static T fromXml(String xml, Class clazz) {
XStream xs = XSTREAM_THREAD_LOCAL.get();
// 每次使用前重置安全策略 (線程安全)
xs.allowTypesByWildcard(new String[]{"com.example.alipay.**"});
return clazz.cast(xs.fromXML(xml));
}
public static String toXml(Object obj) {
return XSTREAM_THREAD_LOCAL.get().toXML(obj);
}
}
String xmlResponse = httpClient.post(alipayUrl, params); // 獲取原始 XML
// 1. 先解析通用響應判斷成敗
AlipayResponse baseResp = XmlUtil.fromXml(xmlResponse, AlipayResponse.class);
if (!baseResp.isSuccess()) {
throw new AlipayBusinessException(baseResp.getErrorCode(), "支付寶查單失敗");
}
// 2. 成功則解析交易詳情 (也可直接從 baseResp.getResponse().getTrade() 獲取)
Trade trade = baseResp.getResponse().getTrade();
// 業務邏輯:校驗金額、狀態、簽名...
if ("TRADE_SUCCESS".equals(trade.getTradeStatus())) {
// 更新訂單狀態
}
Maven 依賴:
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
<version>2.15.2</version>
</dependency>
<dependency> <!-- 支援 XML 屬性 -->
<groupId>com.fasterxml.jackson.module</groupId>
<artifactId>jackson-module-jaxb-annotations</artifactId>
<version>2.15.2</version>
</dependency>
package com.example.alipay.v2;
import com.fasterxml.jackson.dataformat.xml.annotation.*;
import jakarta.xml.bind.annotation.*;
import java.util.List;
@JacksonXmlRootElement(localName = "alipay")
@XmlAccessorType(XmlAccessType.FIELD)
public class AlipayResponseV2 {
@JacksonXmlProperty(localName = "is_success")
private String isSuccess;
@JacksonXmlProperty(localName = "error")
private String errorCode;
@JacksonXmlProperty(localName = "request")
private Request request;
@JacksonXmlProperty(localName = "response")
private Response response;
// 簽名字段
private String sign;
private String signType;
public boolean isSuccess() { return "T".equals(isSuccess); }
// Getters/Setters
}
class Request {
@JacksonXmlProperty(localName = "param")
@JacksonXmlElementWrapper(useWrapping = false)
private List params;
}
@XmlAccessorType(XmlAccessType.FIELD)
class Param {
@XmlAttribute(name = "name")
private String name;
@XmlValue
private String value;
}
class Response {
@JacksonXmlProperty(localName = "trade")
private Trade trade;
}
class Trade {
// 字段名與 XML 標籤一致 (下劃線) 可不加註解
private String body;
private String buyer_email;
private String buyer_id;
private String discount;
// ... 其他字段同 XStream 版本
private String trade_status;
// Getters/Setters
}
XmlMapper xmlMapper = new XmlMapper();
// 可選:配置忽略未知屬性
xmlMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
AlipayResponseV2 resp = xmlMapper.readValue(xmlResponse, AlipayResponseV2.class);
if (resp.isSuccess()) {
Trade trade = resp.getResponse().getTrade();
// ...
}
| 維度 | XStream | Jackson XML |
|---|---|---|
| 上手難度 | 低 (註解直觀) | 中 (JAXB 註解較繁瑣) |
| 效能 | 中等 | 高 (流式解析、零拷貝) |
| Spring Boot 整合 | 需手動配置 | 原生支援 (HttpMessageConverter) |
| 複雜結構支援 | 強 (轉換器、別名靈活) | 強 (需熟悉 JAXB 註解) |
| 維護狀態 | 較慢更新 | 活躍維護 |
建議:新專案優先 Jackson XML,與 JSON 處理統一技術棧;舊專案或極度複雜映射 (如屬性與文本混合、多態集合) 可用 XStream。
掌握「先判斷狀態碼,再針對性反序列化」的核心模式,配合成熟的 XML 綁定框架,即可輕鬆應對各類複雜第三方報文解析。