使用JAXB与SOAPMessage序列化SOAP消息时避免命名空间重复
我之前在做WS-Discovery设备适配ONVIF客户端的时候,也碰到过一模一样的命名空间重复问题。结合JAXB和SOAPMessage的话,有几个实用的方案可以解决这个问题,咱们一步步拆解:
方案一:通过JAXB配置+手动Envelope命名空间声明解决核心问题
这个方案的核心是让JAXB不重复输出命名空间声明,而是统一在SOAP Envelope层声明所有需要的命名空间,具体步骤如下:
1. 配置JAXB包级命名空间
在你的ProbeMatchesType和ProbeMatchType所在的包下,创建package-info.java,统一配置命名空间和前缀映射:
@XmlSchema( namespace = "http://schemas.xmlsoap.org/ws/2005/04/discovery", elementFormDefault = XmlNsForm.QUALIFIED, xmlns = { @XmlNs(prefix = "wsa", namespaceURI = "http://schemas.xmlsoap.org/ws/2004/08/addressing"), @XmlNs(prefix = "wsd", namespaceURI = "http://schemas.xmlsoap.org/ws/2005/04/discovery") } ) package com.your.discovery.package; import javax.xml.bind.annotation.XmlNs; import javax.xml.bind.annotation.XmlNsForm; import javax.xml.bind.annotation.XmlSchema;
2. 正确配置实体类的JAXB注解
确保你的ProbeMatchType中涉及WS-Addressing的字段(比如EndpointReference)明确指定命名空间,但不重复声明前缀:
@XmlRootElement(name = "ProbeMatch", namespace = "http://schemas.xmlsoap.org/ws/2005/04/discovery") @XmlType(namespace = "http://schemas.xmlsoap.org/ws/2005/04/discovery") public class ProbeMatchType { // 明确指定wsa命名空间,复用Envelope层的前缀 @XmlElement(name = "EndpointReference", namespace = "http://schemas.xmlsoap.org/ws/2004/08/addressing") private EndpointReferenceType endpointReference; // 其他字段、getter/setter }
ProbeMatchesType同理:
@XmlRootElement(name = "ProbeMatches", namespace = "http://schemas.xmlsoap.org/ws/2005/04/discovery") @XmlType(namespace = "http://schemas.xmlsoap.org/ws/2005/04/discovery") public class ProbeMatchesType { @XmlElement(name = "ProbeMatch", namespace = "http://schemas.xmlsoap.org/ws/2005/04/discovery") private List<ProbeMatchType> probeMatches; // 其他字段、getter/setter }
3. 序列化时禁用JAXB的独立命名空间输出
在生成SOAPMessage时,先手动给Envelope添加所有需要的命名空间,再用JAXB序列化实体到SOAPBody,关键是设置JAXB_FRAGMENT=true,让JAXB只输出内容片段,不生成自己的根元素和命名空间声明:
// 创建基础SOAP消息 SOAPMessage soapMessage = MessageFactory.newInstance().createMessage(); SOAPEnvelope envelope = soapMessage.getSOAPPart().getEnvelope(); // 手动在Envelope层声明所有需要的命名空间 envelope.addNamespaceDeclaration("wsa", "http://schemas.xmlsoap.org/ws/2004/08/addressing"); envelope.addNamespaceDeclaration("wsd", "http://schemas.xmlsoap.org/ws/2005/04/discovery"); // 获取SOAPBody用于插入内容 SOAPBody soapBody = soapMessage.getSOAPBody(); // 配置JAXB Marshaller JAXBContext jaxbContext = JAXBContext.newInstance(ProbeMatchesType.class); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); // 核心:不生成独立的XML根元素和命名空间 marshaller.setProperty(Marshaller.JAXB_FORMATTED_OUTPUT, true); // 自定义前缀映射,确保和Envelope层一致 marshaller.setProperty("com.sun.xml.bind.namespacePrefixMapper", new NamespacePrefixMapper() { @Override public String getPreferredPrefix(String namespaceUri, String suggestion, boolean requirePrefix) { if ("http://schemas.xmlsoap.org/ws/2004/08/addressing".equals(namespaceUri)) { return "wsa"; } else if ("http://schemas.xmlsoap.org/ws/2005/04/discovery".equals(namespaceUri)) { return "wsd"; } return suggestion; } }); // 将ProbeMatchesType序列化到SOAPBody中 marshaller.marshal(yourProbeMatchesInstance, soapBody); // 输出到ByteArrayOutputStream ByteArrayOutputStream baos = new ByteArrayOutputStream(); soapMessage.writeTo(baos);
方案二:通过DOM节点中转更精细控制命名空间
如果方案一的JAXB配置还是有问题,可以尝试先序列化到DOM节点,再插入到SOAPBody,这样能完全避免命名空间冲突:
SOAPMessage soapMessage = MessageFactory.newInstance().createMessage(); SOAPEnvelope envelope = soapMessage.getSOAPPart().getEnvelope(); envelope.addNamespaceDeclaration("wsa", "http://schemas.xmlsoap.org/ws/2004/08/addressing"); envelope.addNamespaceDeclaration("wsd", "http://schemas.xmlsoap.org/ws/2005/04/discovery"); SOAPBody soapBody = soapMessage.getSOAPBody(); Document doc = envelope.getOwnerDocument(); // 创建对应命名空间的空元素 Element probeMatchesElement = doc.createElementNS( "http://schemas.xmlsoap.org/ws/2005/04/discovery", "wsd:ProbeMatches" ); // 序列化JAXB实体到这个DOM元素 JAXBContext jaxbContext = JAXBContext.newInstance(ProbeMatchesType.class); Marshaller marshaller = jaxbContext.createMarshaller(); marshaller.setProperty(Marshaller.JAXB_FRAGMENT, true); marshaller.marshal(yourProbeMatchesInstance, probeMatchesElement); // 将DOM元素插入到SOAPBody soapBody.appendChild(probeMatchesElement); // 输出消息 soapMessage.writeTo(new ByteArrayOutputStream());
方案三:使用成熟框架简化开发
如果不想手动处理这些底层细节,推荐用Apache CXF或者Spring WS的WS-Discovery组件,这些框架已经内置了符合ONVIF规范的WS-Discovery实现,自动处理命名空间、SOAP结构等问题,比如CXF的WSDiscoveryServer可以直接发布设备的发现服务,完全不需要手动序列化SOAP消息。
内容的提问来源于stack exchange,提问作者W64BugBear

