首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

複雜 XML 轉 Java 實體實戰:以支付寶回調報文為例

     編輯:Java 綜合教學

複雜 XML 轉 Java 實體實戰:以支付寶回調報文為例

對接第三方支付(如支付寶、微信)時,常需解析結構複雜、層級深、且成功/失敗報文結構不同的 XML。本文以支付寶「查詢訂單」介面返回報文為例,示範如何優雅地將 XML 映射為 Java 物件。

一、報文結構分析

1. 失敗報文 (結構簡單)

<?xml version="1.0" encoding="utf-8"?>
<alipay>
  <is_success>F</is_success>
  <error>TRADE_NOT_EXIST</error>
</alipay>

2. 成功報文 (結構複雜,含巢狀對象、列表、屬性)

<?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 類。正確流程:

  1. 將 XML 字符串解析為通用結構(如 `Map`、`Document`、或 `JsonNode`)。
  2. 提取 `is_success` 值。
  3. 若 `T`,將 `response/trade` 片段反序列化為 `Trade` 實體;若 `F`,解析 `error` 碼拋出業務異常。

三、方案一:使用 XStream (註解驅動,支援屬性、別名、轉換器)

Maven 依賴:

<dependency>
    <groupId>com.thoughtworks.xstream</groupId>
    <artifactId>xstream</artifactId>
    <version>1.4.20</version>
</dependency>

1. 實體類定義

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 略
}

2. 通用解析工具類

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);
    }
}

3. 業務調用代碼

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())) {
    // 更新訂單狀態
}

四、方案二:使用 Jackson XML (現代化、高效能、Spring Boot 整合原生)

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>

實體類 (Jackson 註解)

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();
    // ...
}

五、方案對比與選型建議

維度XStreamJackson XML
上手難度低 (註解直觀)中 (JAXB 註解較繁瑣)
效能中等高 (流式解析、零拷貝)
Spring Boot 整合需手動配置原生支援 (HttpMessageConverter)
複雜結構支援強 (轉換器、別名靈活)強 (需熟悉 JAXB 註解)
維護狀態較慢更新活躍維護

建議:新專案優先 Jackson XML,與 JSON 處理統一技術棧;舊專案或極度複雜映射 (如屬性與文本混合、多態集合) 可用 XStream。

六、避坑指南

  1. 安全防範:務必配置允許類型白名單 (XStream `allowTypesByWildcard`、Jackson `enableDefaultTyping` 慎用),防止反序列化漏洞 (CVE-2013-7285 等)。
  2. 編碼一致性:確保 HTTP 響應 `Content-Type` charset 與 XML 宣告 `encoding` 一致 (均為 UTF-8),避免中文亂碼。
  3. 命名空間處理:若 XML 含 `xmlns`,Jackson 需 `@JacksonXmlRootElement(namespace = "...")` 或配置 `XmlMapper` 的 `NamespaceMapper`。
  4. 大數值/金額:XML 中金額建議映射為 `String` 或 `BigDecimal`,避免 `double` 精度損失。
  5. 日期時間:支付寶時間格式 `yyyy-MM-dd HH:mm:ss`,可用 `@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")` (Jackson) 或自定義轉換器 (XStream) 直接映射為 `LocalDateTime`。

掌握「先判斷狀態碼,再針對性反序列化」的核心模式,配合成熟的 XML 綁定框架,即可輕鬆應對各類複雜第三方報文解析。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved