SOAP 消息结构
理解 SOAP 消息的结构很重要,一个典型的 SOAP 消息如下:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<!-- 可选的头部信息 -->
</soap:Header>
<soap:Body>
<m:GetStockPrice xmlns:m="http://www.example.org/stock">
<m:StockName>IBM</m:StockName>
</m:GetStockPrice>
</soap:Body>
</soap:Envelope>
<Envelope>: 整个 SOAP 消息的根元素。<Header>: 可选部分,用于包含认证、事务等元数据。<Body>: 必需部分,包含实际的业务数据和请求/响应信息。
使用标准 Java 库 (JAXP - DOM/SAX) - 纯 Java 方案
这是最基础的方法,不依赖任何第三方库,适用于简单的 XML 解析任务。
DOM (Document Object Model) - 适合小文档
DOM 会将整个 XML 文档加载到内存中,解析成一个树形结构,这种方式非常直观,易于操作,但如果 XML 文件很大,会消耗大量内存。
示例代码:
import org.w3c.dom.*;
import javax.xml.parsers.*;
import java.io.ByteArrayInputStream;
public class SoapDomParser {
public static void parseSoapMessage(String soapXml) {
try {
// 1. 创建 DocumentBuilderFactory 和 DocumentBuilder
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
// 启用命名空间支持,这对于 SOAP XML 非常重要
factory.setNamespaceAware(true);
DocumentBuilder builder = factory.newDocumentBuilder();
// 2. 解析 XML 字符串
Document document = builder.parse(new ByteArrayInputStream(soapXml.getBytes()));
// 3. 获取根元素 (Envelope)
Element envelope = document.getDocumentElement();
System.out.println("Root Element: " + envelope.getNodeName());
// 4. 获取 Body 元素
NodeList bodyList = envelope.getElementsByTagNameNS("http://schemas.xmlsoap.org/soap/envelope/", "Body");
if (bodyList.getLength() > 0) {
Element body = (Element) bodyList.item(0);
System.out.println("Found Body Element.");
// 5. 获取 Body 下的第一个子元素 ( GetStockPrice)
NodeList children = body.getChildNodes();
for (int i = 0; i < children.getLength(); i++) {
Node child = children.item(i);
if (child.getNodeType() == Node.ELEMENT_NODE) {
Element operationElement = (Element) child;
System.out.println("Operation Element: " + operationElement.getLocalName());
// 6. 获取操作元素下的子元素 ( StockName)
NodeList stockNameList = operationElement.getElementsByTagNameNS("http://www.example.org/stock", "StockName");
if (stockNameList.getLength() > 0) {
Element stockNameElement = (Element) stockNameList.item(0);
String stockName = stockNameElement.getTextContent();
System.out.println("Stock Name: " + stockName);
}
break; // 假设我们只关心第一个操作
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
String soapXml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
" <soap:Body>\n" +
" <m:GetStockPrice xmlns:m=\"http://www.example.org/stock\">\n" +
" <m:StockName>IBM</m:StockName>\n" +
" </m:GetStockPrice>\n" +
" </soap:Body>\n" +
"</soap:Envelope>";
parseSoapMessage(soapXml);
}
}
SAX (Simple API for XML) - 适合大文档
SAX 是一种事件驱动的解析模型,它不会将整个文档加载到内存,而是当解析器遇到 XML 的开始标签、结束标签、文本内容等事件时,会调用相应的方法处理,它非常节省内存,但编程模型比 DOM 复杂。
示例代码:
import org.xml.sax.Attributes;
import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import java.io.ByteArrayInputStream;
public class SoapSaxParser {
public static void parseWithSAX(String soapXml) {
try {
SAXParserFactory factory = SAXParserFactory.newInstance();
factory.setNamespaceAware(true); // 关键:启用命名空间
SAXParser saxParser = factory.newSAXParser();
DefaultHandler handler = new DefaultHandler() {
boolean inBody = false;
boolean inOperation = false;
boolean inStockName = false;
StringBuilder currentValue = new StringBuilder();
@Override
public void startElement(String uri, String localName, String qName, Attributes attributes) {
if ("Body".equals(localName) && "http://schemas.xmlsoap.org/soap/envelope/".equals(uri)) {
inBody = true;
System.out.println("Start Element: Body");
} else if (inBody && "GetStockPrice".equals(localName)) {
inOperation = true;
System.out.println("Start Element: Operation (" + localName + ")");
} else if (inOperation && "StockName".equals(localName) && "http://www.example.org/stock".equals(uri)) {
inStockName = true;
System.out.println("Start Element: StockName");
}
}
@Override
public void characters(char[] ch, int start, int length) {
if (inStockName) {
currentValue.append(ch, start, length);
}
}
@Override
public void endElement(String uri, String localName, String qName) {
if ("Body".equals(localName)) {
inBody = false;
System.out.println("End Element: Body");
} else if (inOperation && "GetStockPrice".equals(localName)) {
inOperation = false;
System.out.println("End Element: Operation");
} else if (inStockName && "StockName".equals(localName)) {
inStockName = false;
System.out.println("Stock Name: " + currentValue.toString().trim());
currentValue.setLength(0); // 清空 StringBuilder
}
}
};
saxParser.parse(new ByteArrayInputStream(soapXml.getBytes()), handler);
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
String soapXml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
" <soap:Body>\n" +
" <m:GetStockPrice xmlns:m=\"http://www.example.org/stock\">\n" +
" <m:StockName>IBM</m:StockName>\n" +
" </m:GetStockPrice>\n" +
" </soap:Body>\n" +
"</soap:Envelope>";
parseWithSAX(soapXml);
}
}
使用第三方库 (推荐)
对于复杂的 SOAP 交互,手动解析 XML 非常繁琐且容易出错,使用专门的 SOAP 库是更好的选择。
Apache Axis2 / Apache CXF (功能强大的 SOAP 框架)
这些是成熟的 Java Web Service 框架,它们不仅能生成客户端代码,也能让你以更面向对象的方式处理 SOAP 消息,你会使用它们来生成客户端 stub,然后直接调用方法,框架会自动处理序列化和反序列化。
场景:你已经有一个 WSDL (Web Services Description Language) 文件。
-
生成客户端代码: 使用 Axis2 或 CXF 的工具(如
wsdl2java)根据 WSDL 文件生成 Java 客户端代码。# Axis2 示例 wsdl2java -uri http://www.example.org/stock?wsdl -p org.example.stock
-
使用生成的客户端: 生成的代码会包含一个
StockServiceStub或类似的类,你可以直接调用其中的方法。// 假设生成的类名为 StockServiceStub StockServiceStub stub = new StockServiceStub(); GetStockPrice request = new GetStockPrice(); request.setStockName("IBM"); GetStockPriceResponse response = stub.GetStockPrice(request); String price = response.getGetStockPriceResult(); System.out.println("Stock Price: " + price);这种方式是最佳实践,因为它将 SOAP 的复杂性完全封装起来。
WSDL4J (轻量级 WSDL/SAX 解析器)
如果你只需要解析 WSDL 文件来获取服务信息(如端口、操作、参数等),而不想生成完整的客户端代码,WSDL4J 是一个很好的选择。
Woodstox (高性能的 StAX 解析器)
StAX (Streaming API for XML) 是介于 DOM 和 SAX 之间的一种解析方式,它允许你像 SAX 一样以流的方式读取 XML,但你可以主动控制解析器(pull 模式),而不是被动接收事件,Woodstox 是一个流行的高性能 StAX 实现。
示例代码 (使用 Woodstox 解析 SOAP Body):
import com.ctc.wstx.api.WstxInputProperties;
import com.ctc.wstx.stax.WstxInputFactory;
import org.codehaus.stax2.XMLStreamReader2;
import javax.xml.stream.XMLInputFactory;
import javax.xml.stream.XMLStreamConstants;
import javax.xml.stream.XMLStreamException;
import javax.xml.stream.XMLStreamReader;
import java.io.ByteArrayInputStream;
public class SoapStaxParser {
public static void parseWithStax(String soapXml) throws XMLStreamException {
// 使用 Woodstox 作为 StAX 实现
XMLInputFactory factory = new WstxInputFactory();
// 优化性能
factory.setProperty(WstxInputProperties.P_MAX_ATTRIBUTE_SIZE, 20 * 1024);
XMLStreamReader reader = factory.createXMLStreamReader(new ByteArrayInputStream(soapXml.getBytes()));
String stockName = null;
boolean inBody = false;
boolean inGetStockPrice = false;
boolean inStockName = false;
while (reader.hasNext()) {
int event = reader.next();
switch (event) {
case XMLStreamConstants.START_ELEMENT:
String localName = reader.getLocalName();
String nsUri = reader.getNamespaceURI();
if ("Body".equals(localName) && "http://schemas.xmlsoap.org/soap/envelope/".equals(nsUri)) {
inBody = true;
} else if (inBody && "GetStockPrice".equals(localName)) {
inGetStockPrice = true;
} else if (inGetStockPrice && "StockName".equals(localName) && "http://www.example.org/stock".equals(nsUri)) {
inStockName = true;
}
break;
case XMLStreamConstants.CHARACTERS:
if (inStockName) {
stockName = reader.getText().trim();
}
break;
case XMLStreamConstants.END_ELEMENT:
localName = reader.getLocalName();
if ("Body".equals(localName)) {
inBody = false;
} else if (inGetStockPrice && "GetStockPrice".equals(localName)) {
inGetStockPrice = false;
} else if (inStockName && "StockName".equals(localName)) {
inStockName = false;
}
break;
}
}
reader.close();
System.out.println("Stock Name (from StAX): " + stockName);
}
public static void main(String[] args) throws XMLStreamException {
String soapXml = "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\">\n" +
" <soap:Body>\n" +
" <m:GetStockPrice xmlns:m=\"http://www.example.org/stock\">\n" +
" <m:StockName>IBM</m:StockName>\n" +
" </m:GetStockPrice>\n" +
" </soap:Body>\n" +
"</soap:Envelope>";
parseWithStax(soapXml);
}
}
总结与选择建议
| 方法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| DOM (JAXP) | 编程模型简单直观,易于操作。 整个文档在内存中,方便随机访问。 |
内存消耗大,不适合处理大文件。 解析速度相对较慢。 |
解析小规模、结构固定的 SOAP 响应,快速获取少量数据。 |
| SAX (JAXP) | 内存占用极小,性能高,适合处理超大文件。 读取速度快。 |
编程模型复杂,是事件驱动,不易维护。 只能顺序读取,无法随机访问。 |
解析超大的 SOAP 消息,或者对内存有严格限制的环境。 |
| StAX (e.g., Woodstox) | 性能高,介于 SAX 和 DOM 之间。 Pull 模式,代码比 SAX 更清晰可控。 内存占用低。 |
比 DOM 复杂,需要维护状态。 | 需要高性能解析,且不希望像 SAX 那样编程繁琐的场景,是现代 Java XML 解析的推荐选择之一。 |
| SOAP 框架 (Axis2/CXF) | 最简单、最安全,完全屏蔽了 XML 细节。 提供类型安全的客户端调用。 支持 WS-* 标准(安全、事务等)。 |
引入框架依赖,项目体积增大。 需要 WSDL 文件,灵活性稍差。 |
强烈推荐,当有 WSDL 文件时,这是与 Web Service 交互的标准做法。 |
| WSDL4J | 专门用于解析 WSDL,API 清晰。 | 功能单一,不能直接解析 SOAP 消息体。 | 需要动态分析 WSDL 文档内容,获取服务元数据时使用。 |
最终建议
- 首选方案:如果你有 WSDL 文件,请务必使用 Apache Axis2 或 Apache CXF 生成客户端代码,这是最健壮、最省力、最不容易出错的方式。
- 次选方案:如果你没有 WSDL,或者需要手动处理一些 SOAP 消息,并且追求性能,请使用 StAX(如 Woodstox)。
- 备用方案:XML 文件非常小,且解析逻辑简单,为了不引入额外依赖,可以使用 DOM。
- 尽量避免:除非有特殊原因(如内存限制且文件超大),否则尽量避免直接使用 SAX,因为它的编程模型在复杂场景下难以维护。
