Java 实现 WebService(SOAP)联网调用:从原理到实战

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. 发送请求并接收响应

通过SOAPConnectioncall方法发送请求,底层自动通过 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 协议规范

  1. 构建请求时需匹配第三方的命名空间、方法名、参数名;
  2. 传输时需正确设置SOAPActionContent-Type头;
  3. 解析响应时需处理 CDATA 块、SOAP Fault、节点空值等边界情况;
  4. 生产环境需重点关注超时、异常、资源释放三大问题。
相关推荐
静水楼台x1 小时前
Java之String系列--intern方法的作用及原理
java·spring
专注于大数据技术栈1 小时前
java学习--枚举(Enum)
java·学习
愤怒的代码1 小时前
Java 面试 100 题深度解析 · 专栏总览与大纲
java·面试
银迢迢2 小时前
idea控制台中文乱码采用好几种方法一直解决不了
java·ide·intellij-idea
悦悦子a啊2 小时前
将学生管理系统改造为C/S模式 - 开发过程报告
java·开发语言·算法
步步为营DotNet2 小时前
深度解析C# 11的Required成员:编译期验证保障数据完整性
java·前端·c#
万邦科技Lafite2 小时前
一键获取淘宝关键词商品信息指南
开发语言·数据库·python·商品信息·开放api·电商开放平台
fqbqrr2 小时前
2512C++,clangd支持模块
开发语言·c++
han_hanker2 小时前
泛型的基本语法
java·开发语言