如何在 Java 中從 SOAP 訊息中取得原始 XML
1. 引言
在 Java 中使用基於 SOAP 的 Web 服務時,有時需要存取 SOAP 訊息的原始 XML。例如,我們可能需要用它來偵錯整合問題、記錄請求和回應以進行稽核、驗證訊息結構或排查系統間的互通性問題。
儘管 SOAP 框架對我們隱藏了大部分 XML 處理流程,但在實際專案中往往需要了解正在交換的 SOAP 封包的具體細節。我們採用的方法取決於應用程式中的 SOAP 技術堆疊,例如 SAAJ、JAX-WS、Apache CXF 或 Spring Web Services。
本文將探討在 Java 中從 SOAP 訊息中檢索原始 XML 的最實用方法。我們將分析不同的框架,解釋每種方法的適用場景,並重點介紹避免常見陷阱的重要注意事項。
2. 什麼是原始 XML
在實作任何解決方案之前,我們應該先明確原始 XML 在此上下文中的意義。根據具體需求,它可能指:
- 完整的 SOAP 信封,包括信紙和信正。
- 僅 SOAP 主體有效載荷
- 透過 HTTP 發送的確切位元組流
在大多數情況下,以字串形式檢索 SOAP 信封足以滿足日誌記錄和調試需求。但是,如果出於合規性或取證日誌記錄的要求,我們需要精確到位元組級的精確度,則可能需要攔截傳輸層,而不能僅依賴框架級 API。
明確了這一點之後,讓我們來看看最常見的實作方法。
3. 使用 SAAJ 和SOAPMessage
如果直接使用SOAPMessage ,提取原始 XML 非常簡單。可以將訊息寫入輸出流並轉換為字串。
讓我們來看下面的例子:
public static String soapMessageToString(SOAPMessage message) {
try {
if (message == null) {
throw new IllegalArgumentException("SOAPMessage cannot be null");
}
message.saveChanges();
ByteArrayOutputStream out = new ByteArrayOutputStream();
message.writeTo(out);
return out.toString(StandardCharsets.UTF_8.name());
} catch (Exception e) {
throw new RuntimeException("Failed to convert SOAPMessage to String", e);
}
}
在這個例子中,我們呼叫saveChanges()方法來確保對 SOAP 頭部或主體所做的任何修改在序列化之前都已生效。 `writeTo writeTo(OutputStream)方法將完整的 SOAP 信封(包括頭部和主體)寫入ByteArrayOutputStream中。
最後,我們將輸出流轉換為UTF-8字串表示形式。這種方法簡單可靠,適用於日誌記錄和偵錯。
4. 將 JAX-WS 與SOAPHandler結合使用
使用 JAX-WS 時,擷取原始 SOAP XML 最簡潔的方法是使用SOAPHandler將其存取訊息管道。為了保持程式碼的通用性並便於單元測試,我們不應該直接列印。相反,我們可以將捕獲的 XML 轉發到一個小型接口,該接口之後可以使用任何日誌框架、持久層或測試記錄器來實現。
讓我們來看下面的例子:
public class RawSoapCaptureHandler implements SOAPHandler<SOAPMessageContext> {
public interface SoapXmlSink {
void accept(Direction direction, String soapXml);
}
public enum Direction {
OUTBOUND,
INBOUND,
FAULT
}
private final SoapXmlSink sink;
public RawSoapCaptureHandler(SoapXmlSink sink) {
this.sink = sink;
}
@Override
public boolean handleMessage(SOAPMessageContext context) {
Boolean outbound = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY);
Direction direction = Boolean.TRUE.equals(outbound) ? Direction.OUTBOUND : Direction.INBOUND;
String xml = toString(context.getMessage());
sink.accept(direction, xml);
return true;
}
@Override
public boolean handleFault(SOAPMessageContext context) {
String xml = toString(context.getMessage());
sink.accept(Direction.FAULT, xml);
return true;
}
@Override
public void close(MessageContext context) {
}
@Override
public Set<QName> getHeaders() {
return Collections.emptySet();
}
private String toString(SOAPMessage message) {
try {
message.saveChanges();
ByteArrayOutputStream out = new ByteArrayOutputStream();
message.writeTo(out);
return out.toString(StandardCharsets.UTF_8.name());
} catch (Exception e) {
throw new RuntimeException("Failed to serialize SOAP message", e);
}
}
}
在此實作中,我們使用handleMessage()攔截入站和出站 SOAP 訊息,並使用handleFault()攔截 SOAP 錯誤。我們使用MESSAGE_OUTBOUND_PROPERTY來確定訊息方向,然後使用SOAPMessage.writeTo(OutputStream)序列化 SOAP 信封。
我們不直接列印,而是將原始 XML 傳遞給SoapXmlSink 。這樣可以保持處理程序框架無關性,並且方便測試。在單元測試中,我們可以提供一個將訊息儲存在記憶體中的接收器。在生產環境中,我們可以提供一個將 XML 轉送到日誌記錄器、資料庫或監控系統的接收器。
5. 使用 Apache CXF 攔截器
如果我們的應用程式使用 Apache CXF,該框架提供了內建攔截器,可以簡化 SOAP 訊息日誌記錄。
讓我們來看下面的例子:
Client client = ClientProxy.getClient(port);
client.getInInterceptors().add(new LoggingInInterceptor());
client.getOutInterceptors().add(new LoggingOutInterceptor());
在這種情況下,我們從產生的服務代理程式中取得 CXF 用戶端。然後,我們註冊LoggingInInterceptor和LoggingOutInterceptor 。
這些攔截器會自動記錄傳入和傳出訊息的完整 SOAP 信封。 CXF 內部管理流程緩衝,以避免意外消耗訊息流的風險。
6. 使用 Spring Web 服務
使用 Spring Web Services 時,我們可以使用ClientInterceptor來擷取 SOAP XML。與 JAX-WS 處理程序一樣,我們應該避免列印原始 XML,而是將其轉發到一個通用接收器,以便稍後新增日誌記錄或測試功能。
讓我們來看下面的例子:
public class SpringSoapCaptureInterceptor implements ClientInterceptor {
public interface SoapXmlSink {
void accept(Direction direction, String soapXml);
}
public enum Direction {
REQUEST,
RESPONSE,
FAULT
}
private final SoapXmlSink sink;
public SpringSoapCaptureInterceptor(SoapXmlSink sink) {
this.sink = sink;
}
@Override
public boolean handleRequest(MessageContext messageContext) {
String xml = toString(messageContext.getRequest());
sink.accept(Direction.REQUEST, xml);
return true;
}
@Override
public boolean handleResponse(MessageContext messageContext) {
String xml = toString(messageContext.getResponse());
sink.accept(Direction.RESPONSE, xml);
return true;
}
@Override
public boolean handleFault(MessageContext messageContext) {
String xml = toString(messageContext.getResponse());
sink.accept(Direction.FAULT, xml);
return true;
}
private String toString(WebServiceMessage message) {
try {
ByteArrayOutputStream out = new ByteArrayOutputStream();
message.writeTo(out);
return out.toString(StandardCharsets.UTF_8);
} catch (Exception e) {
throw new RuntimeException("Failed to serialize Spring WS message", e);
}
}
}
在這個範例中,我們擷取請求、回應和故障的原始 SOAP XML。 Spring WS 將訊息作為WebServiceMessage提供,而writeTo(OutputStream)方法會序列化完整的 SOAP 信封。
透過將 XML 轉發到SoapXmlSink ,我們可以保持攔截器的通用性和可重複使用性。之後,我們可以將其連接到我們首選的日誌框架,或插入測試接收器,該接收器可以斷言捕獲的 XML,而無需依賴控制台輸出。
7. 重要考慮因素
在取得原始 SOAP XML 時,必須考慮幾個重要因素。
首先,編碼通常為 UTF-8,但如果需要位元組級精度,則應進行驗證。此外,某些框架依賴內部輸入流。如果我們從流中讀取資料而沒有進行適當的緩衝,則可能會消耗掉資料並中斷後續處理。
除了技術層面的處理之外,在生產環境中記錄完整的 SOAP 訊息可能會影響效能並增加記憶體使用量。因此,最好有條件地啟用詳細日誌記錄。
最後,我們必須考慮安全性。 SOAP 信封可能包含敏感資訊,例如身分驗證令牌或個人資料。因此,我們必須確保妥善清理日誌並遵守安全策略。
處理非常大的 SOAP 訊息時,我們也應格外謹慎。使用ByteArrayOutputStream將大型有效負載序列化到記憶體中可能會增加高吞吐量系統的記憶體消耗。在這種情況下,我們應該評估串流日誌記錄或大小受限的日誌記錄策略。
8. 結論
在企業系統中,從 Java 的 SOAP 訊息中檢索原始 XML 是常見需求。無論我們是直接操作SOAPMessage 、使用 JAX-WS 處理程序攔截訊息,還是依賴框架特定的解決方案(例如 Apache CXF 攔截器),其基本原理都相同:安全地序列化 SOAP 信封並將其轉換為可讀格式。
透過了解這些技術,我們可以更深入地了解 SOAP 通信,並提高我們有效調試、監控和維護服務整合的能力。
與往常一樣,本教學的完整原始碼可在 GitHub 上找到。