WebService(基于 SOAP 协议)仍是跨系统数据交互的主流方式之一。本文以「医疗系统患者信息查询接口」为例,完整讲解 Java 如何实现 WebService 的联网调用,包括 SOAP 请求构建、HTTP 传输、响应解析全流程,并结合实战代码拆解核心要点与避坑指南。
一、应用背景
本次实战场景为:通过第三方提供的 WebService 接口,传入患者登记号,查询并解析患者姓名、性别、出生日期、电话等核心信息,最终封装为BookInfo业务对象返回。第三方接口约定:
- 协议:SOAP 1.1
- 请求方法:
HIPMessageServer - 请求参数:
input1=xxxxxx(接口标识)、input2=患者登记号 - 响应数据:XML 格式,核心内容封装在 CDATA 块中
二、WebService 联网调用整体流程
WebService 基于 HTTP 传输 SOAP 协议的 XML 数据,核心流程可概括为「请求构建→网络传输→响应解析→数据封装」,具体如下:
客户端应用
↓ 构建符合SOAP 1.1规范的XML请求
创建SOAP请求(含命名空间、方法名、参数)
↓ 通过HTTP POST发送到第三方服务地址
发送SOAP请求到服务器
↓ 服务器处理后返回SOAP格式响应
接收服务器SOAP响应
↓ 解析响应体,提取CDATA内的业务XML
解析SOAP响应(处理CDATA/XML节点)
↓ 映射到业务对象
封装BookInfo返回
三、核心实现步骤(附代码拆解)
1. 环境准备与基础常量定义
首先定义接口地址、命名空间等全局常量(统一维护,便于后续修改),并初始化日志、回调等基础组件:
java
// 核心常量(第三方接口约定)
private static final String SERVICE_URL = "http://xxx.xxx.xx.xxx/xx/xx/xxxx.xxx.xxx.xxx.xxxxx.CLS";
private static final String DHCC_NAMESPACE = "http://www.dhcc.com.cn";
// 日志与回调(可选,用于异常通知)
private LinkCallback callback;
protected final LoggerUtil logger = new LoggerUtil(ResultInfo.class.getName());
2. 创建 SOAP 连接通道
通过SOAPConnectionFactory创建与服务器的连接,这是网络通信的基础通道:
java
// 创建SOAP连接
SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance();
SOAPConnection soapConnection = soapConnectionFactory.createConnection();
关键说明:
SOAPConnection是 JDK 内置的 SOAP 通信连接,底层基于 HTTP 协议实现,默认无超时(生产环境需手动设置,见下文优化)。
3. 构建符合规范的 SOAP 请求
SOAP 请求需严格遵循 1.1 协议规范,包含命名空间、方法名、参数、请求头四大核心要素:
java
private SOAPMessage createSOAPRequest(String regNo) throws Exception {
// 1. 创建SOAP 1.1格式的消息实例
MessageFactory messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL);
SOAPMessage soapMessage = messageFactory.createMessage();
// 2. 获取SOAP信封,添加命名空间(第三方接口约定)
SOAPPart soapPart = soapMessage.getSOAPPart();
SOAPEnvelope envelope = soapPart.getEnvelope();
envelope.addNamespaceDeclaration("dhcc", DHCC_NAMESPACE); // 命名空间前缀dhcc
// 3. 构建SOAP Body,添加请求方法与参数
SOAPBody soapBody = envelope.getBody();
// 创建请求方法:HIPMessageServer(第三方约定)
SOAPElement soapBodyElem = soapBody.addChildElement("HIPMessageServer", "dhcc");
// 添加参数1:接口标识xxxxx
SOAPElement param1 = soapBodyElem.addChildElement("input1", "dhcc");
param1.addTextNode("xxxxx");
// 添加参数2:患者登记号
SOAPElement param2 = soapBodyElem.addChildElement("input2", "dhcc");
param2.addTextNode(regNo);
// 4. 设置HTTP请求头(SOAP核心)
MimeHeaders headers = soapMessage.getMimeHeaders();
// SOAPAction:告诉服务器要调用的具体方法
String soapAction = "\"" + DHCC_NAMESPACE + "/HIPMessageServer\"";
headers.addHeader("SOAPAction", soapAction);
// Content-Type:指定XML格式与编码
headers.addHeader("Content-Type", "text/xml; charset=UTF-8");
soapMessage.saveChanges();
return soapMessage;
}
关键要点:
- 命名空间必须与第三方接口一致,否则服务器无法识别请求方法;
SOAPAction头是 SOAP 1.1 的核心要求,值需包含命名空间 + 方法名,且需加双引号;- 参数名(input1/input2)需严格匹配第三方接口定义,大小写敏感。
4. 发送请求并接收响应
通过SOAPConnection的call方法发送请求,底层自动通过 HTTP POST 传输 XML 数据:
java
// 发送SOAP请求到指定服务地址,接收响应
SOAPMessage soapResponse = soapConnection.call(soapRequest, SERVICE_URL);
说明:
call方法是同步调用,会阻塞直到服务器返回响应或抛出异常。
5. 解析 SOAP 响应(核心难点)
第三方响应的核心业务数据封装在HIPMessageServerResult节点的 CDATA 块中,需分两步解析:先提取 CDATA 内容,再解析内部 XML:
java
private BookInfo parseSOAPResponse(SOAPMessage soapResponse) throws Exception {
BookInfo bookInfo = new BookInfo();
// 第一步:检查SOAP Fault(服务器端错误)
if (soapResponse.getSOAPBody().hasFault()) {
String faultString = soapResponse.getSOAPBody().getFault().getFaultString();
logger.info("服务器返回SOAP错误: " + faultString);
return bookInfo;
}
// 第二步:提取HIPMessageServerResult节点的CDATA内容
SOAPBody soapBody = soapResponse.getSOAPBody();
NodeList resultNodes = soapBody.getElementsByTagNameNS(DHCC_NAMESPACE, "HIPMessageServerResult");
if (resultNodes.getLength() > 0) {
Element resultElement = (Element) resultNodes.item(0);
String cdataContent = resultElement.getTextContent(); // 提取CDATA文本
// 第三步:解析CDATA内的业务XML
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false); // CDATA内XML无命名空间,关闭感知
DocumentBuilder builder = factory.newDocumentBuilder();
org.w3c.dom.Document doc = builder.parse(new ByteArrayInputStream(cdataContent.getBytes(StandardCharsets.UTF_8)));
// 第四步:提取业务节点(Result)中的字段
NodeList resultList = doc.getElementsByTagName("Result");
if (resultList.getLength() > 0) {
Element resultDataElement = (Element) resultList.item(0);
// 提取患者姓名
String name = getNodeValue(resultDataElement, "Name");
bookInfo.setPatient_name(name);
// 提取出生日期并计算年龄
String birthDay = getNodeValue(resultDataElement, "BirthDay");
if (birthDay != null && !birthDay.isEmpty()) {
bookInfo.setAge(calculateAge(birthDay));
bookInfo.setStrAge(String.valueOf(bookInfo.getAge()));
}
// 其他字段(电话、性别等)同理
}
}
return bookInfo;
}
// 通用方法:提取XML节点文本值
private String getNodeValue(Element element, String tagName) {
try {
NodeList nodeList = element.getElementsByTagName(tagName);
if (nodeList.getLength() > 0) {
Node node = nodeList.item(0);
if (node != null && node.getFirstChild() != null) {
return node.getFirstChild().getNodeValue();
}
}
} catch (Exception e) {
logger.error("提取节点[" + tagName + "]失败", e);
}
return "";
}
核心难点:
- CDATA 块用于包裹含特殊字符的 XML,解析时需先提取文本内容,再单独解析;
- 关闭
NamespaceAware避免因命名空间问题无法识别 CDATA 内的节点;- 节点提取需兼容 "节点不存在 / 节点为空" 的情况,避免空指针。
6. 辅助功能:出生日期计算年龄
基于yyyy-MM-dd格式的出生日期计算年龄,保证线程安全(每次创建新的SimpleDateFormat):
java
private int calculateAge(String birthDay) throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false); // 严格校验日期格式(拒绝1991-02-30等非法日期)
Calendar birthDate = Calendar.getInstance();
birthDate.setTime(sdf.parse(birthDay));
Calendar now = Calendar.getInstance();
int age = now.get(Calendar.YEAR) - birthDate.get(Calendar.YEAR);
// 校正年龄:当前月日小于出生月日,年龄减1
if (now.get(Calendar.MONTH) < birthDate.get(Calendar.MONTH) ||
(now.get(Calendar.MONTH) == birthDate.get(Calendar.MONTH) &&
now.get(Calendar.DAY_OF_MONTH) < birthDate.get(Calendar.DAY_OF_MONTH))) {
age--;
}
return age;
}
四、完整实战代码
以下是可直接运行的完整代码(已适配生产环境日志规范、异常处理):
java
package com.sent.connect;
import com.sent.db.BookInfo;
import com.sent.db.GasStatus;
import com.sent.link.ResultInfo;
import com.sent.logger.LoggerUtil;
import javax.xml.soap.*;
import javax.xml.namespace.QName;
import org.w3c.dom.Node;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import javax.xml.parsers.*;
import java.io.ByteArrayOutputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Calendar;
/**
* WebService实现类:调用第三方接口查询患者信息
* 协议:SOAP 1.1
* 接口地址:
* 方法名:HIPMessageServer
*/
public class LinkImp implements LinkInterface{
// 接口常量(统一维护)
private static final String SERVICE_URL =
private static final String DHCC_NAMESPACE = "http://www.dhcc.com.cn";
private LinkCallback callback;
protected final LoggerUtil logger = new LoggerUtil(ResultInfo.class.getName());
public LinkImp() {}
public LinkImp(LinkCallback callback) {
this.callback = callback;
}
@Override
public BookInfo getBookInfo(String strInput) {
BookInfo bookInfo = new BookInfo();
logger.info("开始查询患者信息,登记号:" + strInput);
SOAPConnection soapConnection = null;
try {
// 1. 创建SOAP连接(设置30秒超时,避免线程阻塞)
SOAPConnectionFactory soapConnectionFactory = SOAPConnectionFactory.newInstance();
soapConnection = soapConnectionFactory.createConnection();
if (soapConnection instanceof com.sun.xml.internal.messaging.saaj.client.p2p.HttpSOAPConnection) {
((com.sun.xml.internal.messaging.saaj.client.p2p.HttpSOAPConnection) soapConnection).setTimeout(30000);
}
logger.info("SOAP连接创建成功(超时时间:30秒)");
// 2. 构建SOAP请求
SOAPMessage soapRequest = createSOAPRequest(strInput);
// 3. 发送请求并接收响应
SOAPMessage soapResponse = soapConnection.call(soapRequest, SERVICE_URL);
logger.info("成功接收服务器响应");
// 4. 解析响应数据
bookInfo = parseSOAPResponse(soapResponse);
logger.info("患者信息解析完成");
} catch (Exception e) {
logger.error("查询患者信息异常", e);
if (callback != null) {
logger.info("触发回调处理异常:" + e.getMessage());
}
} finally {
// 关闭连接,释放资源
if (soapConnection != null) {
try {
soapConnection.close();
logger.info("SOAP连接已关闭");
} catch (SOAPException e) {
logger.error("关闭SOAP连接失败", e);
}
}
}
logger.info("患者信息查询流程结束");
return bookInfo;
}
/**
* 构建SOAP 1.1请求消息
* @param regNo 患者登记号
*/
private SOAPMessage createSOAPRequest(String regNo) throws Exception {
logger.info("开始构建SOAP请求");
MessageFactory messageFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL);
SOAPMessage soapMessage = messageFactory.createMessage();
// 设置SOAP信封与命名空间
SOAPPart soapPart = soapMessage.getSOAPPart();
SOAPEnvelope envelope = soapPart.getEnvelope();
envelope.addNamespaceDeclaration("dhcc", DHCC_NAMESPACE);
// 构建请求体
SOAPBody soapBody = envelope.getBody();
SOAPElement methodElem = soapBody.addChildElement("HIPMessageServer", "dhcc");
// 添加接口标识参数
SOAPElement input1 = methodElem.addChildElement("input1", "dhcc");
input1.addTextNode("xxxxx");
// 添加登记号参数
SOAPElement input2 = methodElem.addChildElement("input2", "dhcc");
input2.addTextNode(regNo);
// 设置请求头
MimeHeaders headers = soapMessage.getMimeHeaders();
headers.addHeader("SOAPAction", "\"" + DHCC_NAMESPACE + "/HIPMessageServer\"");
headers.addHeader("Content-Type", "text/xml; charset=UTF-8");
soapMessage.saveChanges();
// 打印请求日志(调试用)
ByteArrayOutputStream requestBaos = new ByteArrayOutputStream();
soapMessage.writeTo(requestBaos);
logger.info("SOAP请求内容:\n" + new String(requestBaos.toByteArray(), StandardCharsets.UTF_8));
return soapMessage;
}
/**
* 解析SOAP响应,提取患者信息
*/
private BookInfo parseSOAPResponse(SOAPMessage soapResponse) throws Exception {
logger.info("开始解析SOAP响应");
BookInfo bookInfo = new BookInfo();
// 打印响应日志(调试用)
ByteArrayOutputStream responseBaos = new ByteArrayOutputStream();
soapResponse.writeTo(responseBaos);
logger.info("SOAP响应内容:\n" + new String(responseBaos.toByteArray(), StandardCharsets.UTF_8));
// 检查服务器错误
if (soapResponse.getSOAPBody().hasFault()) {
String fault = soapResponse.getSOAPBody().getFault().getFaultString();
logger.error("服务器返回SOAP错误:" + fault);
return bookInfo;
}
// 提取CDATA内容
SOAPBody soapBody = soapResponse.getSOAPBody();
NodeList resultNodes = soapBody.getElementsByTagNameNS(DHCC_NAMESPACE, "HIPMessageServerResult");
if (resultNodes.getLength() == 0) {
logger.warn("未找到HIPMessageServerResult节点");
return bookInfo;
}
Element resultElement = (Element) resultNodes.item(0);
String cdataContent = resultElement.getTextContent();
logger.info("提取CDATA内容:" + cdataContent);
// 解析CDATA内的XML
try {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
factory.setNamespaceAware(false);
DocumentBuilder builder = factory.newDocumentBuilder();
org.w3c.dom.Document doc = builder.parse(new ByteArrayInputStream(cdataContent.getBytes(StandardCharsets.UTF_8)));
// 提取Result节点
NodeList resultList = doc.getElementsByTagName("Result");
if (resultList.getLength() == 0) {
logger.warn("未找到Result业务节点");
return bookInfo;
}
Element resultData = (Element) resultList.item(0);
// 解析核心字段
bookInfo.setHistory(getNodeValue(resultData, "RegNo")); // 登记号
bookInfo.setPatient_name(getNodeValue(resultData, "Name")); // 姓名
bookInfo.setSex(getNodeValue(resultData, "SexDesc")); // 性别
bookInfo.setBedNo(getNodeValue(resultData, "TelPhone")); // 电话
// 解析出生日期并计算年龄
String birthDay = getNodeValue(resultData, "BirthDay");
logger.info("患者出生日期:" + birthDay);
if (birthDay != null && !birthDay.isEmpty()) {
try {
int age = calculateAge(birthDay);
bookInfo.setAge(age);
bookInfo.setStrAge(String.valueOf(age));
} catch (Exception e) {
logger.error("年龄计算失败", e);
bookInfo.setAge(-1); // 标记计算失败
bookInfo.setStrAge("未知");
}
}
} catch (Exception e) {
logger.error("解析CDATA内容失败", e);
}
logger.info("SOAP响应解析完成");
return bookInfo;
}
/**
* 通用XML节点值提取方法
*/
private String getNodeValue(Element element, String tagName) {
try {
NodeList nodeList = element.getElementsByTagName(tagName);
if (nodeList.getLength() > 0) {
Node node = nodeList.item(0);
if (node != null && node.getFirstChild() != null) {
String value = node.getFirstChild().getNodeValue();
logger.info("提取节点[" + tagName + "]值:" + value);
return value;
}
}
logger.warn("节点[" + tagName + "]不存在或为空");
} catch (Exception e) {
logger.error("提取节点[" + tagName + "]异常", e);
}
return "";
}
/**
* 计算年龄(线程安全)
*/
private int calculateAge(String birthDay) throws Exception {
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
sdf.setLenient(false); // 严格校验日期格式
Calendar birthDate = Calendar.getInstance();
birthDate.setTime(sdf.parse(birthDay));
Calendar now = Calendar.getInstance();
int age = now.get(Calendar.YEAR) - birthDate.get(Calendar.YEAR);
// 校正年龄:未到生日则减1
if (now.get(Calendar.MONTH) < birthDate.get(Calendar.MONTH) ||
(now.get(Calendar.MONTH) == birthDate.get(Calendar.MONTH) &&
now.get(Calendar.DAY_OF_MONTH) < birthDate.get(Calendar.DAY_OF_MONTH))) {
age--;
}
return age;
}
// 未实现的接口方法(按需扩展)
@Override
public BookInfo getBookInfoType(String strInput, String strType) { return null; }
@Override
public String uploadResult(GasStatus obj) { return null; }
@Override
public boolean createPic(GasStatus obj) { return false; }
@Override
public boolean uploadPicByFtp(GasStatus obj) { return false; }
}
五、生产环境避坑与优化建议
1. 核心避坑点
- IP 白名单问题:若调用失败且日志提示 "连接拒绝 / 超时",优先排查第三方是否将本地 IP 加入白名单(本次实战中核心问题);
- SOAP 版本兼容 :需与第三方确认 SOAP 版本(1.1/1.2),1.2 无需设置
SOAPAction头; - CDATA 解析乱码:必须指定 UTF-8 编码,避免因编码不一致导致解析失败;
- 日期格式非法 :开启
setLenient(false),拒绝非法日期(如 2 月 30 日)。
2. 生产环境优化
- 超时设置 :必须为
SOAPConnection设置超时(如 30 秒),避免线程无限阻塞; - 异常分级处理:区分 "网络异常""解析异常""业务异常",分别记录日志并触发回调;
- 日志规范 :用
logger.error记录异常(含堆栈),logger.warn记录非致命问题,避免用System.out; - 资源释放 :
SOAPConnection必须在finally块中关闭,避免资源泄漏; - 线程安全 :
SimpleDateFormat每次调用新建实例,避免多线程并发问题。
六、总结
Java 实现 WebService 联网调用的核心是严格遵循 SOAP 协议规范:
- 构建请求时需匹配第三方的命名空间、方法名、参数名;
- 传输时需正确设置
SOAPAction和Content-Type头; - 解析响应时需处理 CDATA 块、SOAP Fault、节点空值等边界情况;
- 生产环境需重点关注超时、异常、资源释放三大问题。