需求:最近公司要求对接客户端的一个webservice soap协议的接口,根据手机号查询到用户信息。之前只做过http、ws、sse协议的接口,故记下soap协议分享一下。
协议请求:
根据批量手机号查询实体(自有或合作伙伴)人员接口
请求报文:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<msghead>
<appId>88888900</appId>
<appSecret>d6491592ad2a4832aa744caefae123</appSecret>
<token>CE0000001532919192384c23af9d87f164df4a49bc48489ce4be</token>
<reqTimestamp>1422343231</reqTimestamp>
<functionId>queryEntUserByBatchPhone</functionId>
<signature>fsere3</signature>
<version>1.3.5</version>
</msghead>
</soap:Header>
<soap:Body>
<ns2:queryEntUserByBatchPhone
xmlns:ns2="http://webservice.cfms.richinfo.cn/">
<batchPhone>13802883264,15820712220,18825176420</batchPhone>
</ns2:queryEntUserByBatchPhone>
</soap:Body>
</soap:Envelope>
返回报文:
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:queryEntUserByBatchPhoneResponse
xmlns:ns2="http://webservice.cfms.richinfo.cn/">
<return>
<code>0</code>
<data>
<entUser>
<companyEmail>test@cmic.com</companyEmail>
<departMentId>3037</departMentId>
<departMentName>工程部(信息中心)</departMentName>
<email>123@139.com</email>
<empNo>11133</empNo>
<isValid>1</isValid>
<mobilePhone>13802</mobilePhone>
<orgFullName>有限公司\系统工程部(信息安全中心)</orgFullName>
<orgId>4909</orgId>
<orgName>信息技室</orgName>
<portalUserId>test</portalUserId>
<portalUserName>谢x欣</portalUserName>
<position>1</position>
<sex>1</sex>
<userCategory>1</userCategory>
<userId>125</userId>
</entUser>
</data>
<msg>根据批量手机号查询实体(自有或合作伙伴)人员信息成功</msg>
</return>
</ns2:queryEntUserByBatchPhoneResponse>
</soap:Body>
</soap:Envelope>
一、为什么要用soap?使用http不行吗?
以下是soap和http的对比,简单来说就是soap协议更加严格定义了数据的传输格式,更适合银行支付、政府服务系统、健康医疗系统等。
对比维度 | SOAP(简单对象访问协议) | HTTP 请求(基于 HTTP 协议) |
---|---|---|
本质定位 | 数据交换协议(定义数据格式和交互规则) | 传输协议(定义数据在网络中的传输方式) |
消息格式 | 严格固定的 XML 格式(必须包含<Envelope> 等标签) | 无强制格式,可传输 JSON、XML、表单、文本等 |
依赖关系 | 依赖底层传输协议(常用 HTTP,也支持 SMTP、TCP 等) | 独立的传输协议,不依赖其他协议 |
数据格式约束 | 有严格约束(基于 XML Schema 定义数据类型) | 无约束,开发者可自由定义数据格式 |
传输方式 | 通常通过 HTTP POST 传输(消息作为 HTTP Body) | 支持多种方法(GET/POST/PUT/DELETE 等),按需选择 |
适用场景 | 企业级复杂系统集成(如银行、ERP 对接,需事务 / 安全保障) | 轻量交互场景(如 App 接口、前后端分离、开放平台 API) |
功能复杂度 | 内置丰富功能(安全、事务、错误处理等规范) | 功能简单(仅基础传输能力,需自行实现额外功能) |
错误处理 | 通过<Fault> 标签在 XML 中定义错误(统一格式) | 通过 HTTP 状态码(如 200/404/500)和自定义 Body 表示 |
性能 | 较差(XML 冗余大,解析复杂) | 较好(尤其搭配 JSON 时,轻量且解析快) |
二、springboot集成webservice soap
1、引入依赖配置
springboot版本
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
相关的pom依赖
<!-- Apache CXF Spring Boot Starter -->
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-spring-boot-starter-jaxws</artifactId>
<version>3.5.5</version>
</dependency>
<!-- JAXB Runtime implementation -->
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>4.0.2</version>
</dependency>
<!-- HTTP Client for SOAP calls -->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>4.5.13</version>
</dependency>
2、服务端实现
模拟一个soap协议的服务接口:
import com.oak.springbootredis.service.UserService;
import org.apache.cxf.Bus;
import org.apache.cxf.jaxws.EndpointImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.xml.ws.Endpoint;
/**
* webservice配置类
*/
@Configuration
public class CxfConfig {
@Autowired
private Bus bus;
@Autowired
private UserService userService;
/**
* 创建webservice服务
* @return
*/
@Bean
public Endpoint endpoint() {
EndpointImpl endpoint = new EndpointImpl(bus, userService);
endpoint.publish("/orgAndUserService");
return endpoint;
}
}
import com.oak.springbootredis.domain.resp.QueryUserResponse;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
/**
* @author xyguo
* @date 2025/7/15
*/
@WebService(targetNamespace = "http://webservice.cfms.richinfo.cn/")
public interface UserService {
@WebMethod
@WebResult(name = "return")
QueryUserResponse queryEntUserByBatchPhone(@WebParam(name = "batchPhone") String batchPhone);
}
import cn.hutool.core.collection.ListUtil;
import com.oak.springbootredis.domain.resp.EntUser;
import com.oak.springbootredis.domain.resp.QueryUserResponse;
import com.oak.springbootredis.domain.resp.ResponseData;
import com.oak.springbootredis.service.UserService;
import org.springframework.stereotype.Service;
import javax.jws.WebService;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@Service
@WebService(
serviceName = "UserService",
targetNamespace = "http://webservice.cfms.richinfo.cn/",
endpointInterface = "com.oak.springbootredis.service.UserService"
)
public class UserServiceImpl implements UserService {
/**
* 模拟接口返回数据
*
* @param batchPhone
* @return
*/
@Override
public QueryUserResponse queryEntUserByBatchPhone(String batchPhone) {
QueryUserResponse response = new QueryUserResponse();
response.setCode("0");
response.setMsg("根据批量手机号查询实体(自有或合作伙伴)人员信息成功");
if (response.getData()==null){
response.setData(new ResponseData());
}
List<EntUser> usr = response.getData().getEntUser();
List<EntUser> zusr = new ArrayList<>();
// 解析手机号并查询
String[] phones = {"17855954111"};
ArrayList<String> reqPhones = ListUtil.toList(batchPhone.split(","));
for (String phone : phones) {
EntUser user = createSampleUser(phone,"1");
zusr.add(user);
}
EntUser user = createSampleUser("15211111133","3");
zusr.add(user);
EntUser user2 = createSampleUser("15212425222","3");
zusr.add(user2);
List<EntUser> removes = zusr.stream().filter(e -> reqPhones.contains(e.getMobilePhone())).collect(Collectors.toList());
usr.addAll(removes);
return response;
}
private EntUser createSampleUser(String phone, String isValid) {
EntUser user = new EntUser();
String randomId = String.valueOf((int)(Math.random() * 90000) + 10000);
String[] names = {"张三", "李四", "王五", "赵六", "钱七", "孙八", "周九", "吴十"};
String[] domains = {"cmic.chinamobile.com", "aspirecn.com", "139.com", "163.com"};
String[] depts = {"系统工程部", "产品研发部", "市场营销部", "人力资源部", "财务部"};
String randomName = names[(int)(Math.random() * names.length)];
user.setCompanyEmail(randomName.toLowerCase() + "@" + domains[(int)(Math.random() * domains.length)]);
user.setDepartMentId(randomId);
user.setDepartMentName(depts[(int)(Math.random() * depts.length)]);
user.setEmail(phone + "@139.com");
user.setEmpNo("EMP" + randomId);
user.setIsValid(isValid);
user.setMobilePhone(phone);
user.setOrgFullName("中移互联网有限公司\\" + user.getDepartMentName());
user.setOrgId(randomId);
user.setOrgName(user.getDepartMentName());
user.setPortalUserId(randomName.toLowerCase());
user.setPortalUserName(randomName);
user.setPosition(String.valueOf((int)(Math.random() * 3) + 1));
user.setSex(String.valueOf((int)(Math.random() * 2) + 1));
user.setUserCategory(String.valueOf((int)(Math.random() * 2) + 1));
user.setUserId(randomId);
return user;
}
}
3、客户端实现
客户端对服务端接口调用即可:
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.jaxws.JaxWsProxyFactoryBean;
import org.apache.cxf.transport.http.HTTPConduit;
import org.apache.cxf.transports.http.configuration.HTTPClientPolicy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* 用于创建和配置一个Web Service客户端代理,以便调用远程的Web Service服务。
*/
@Configuration
public class WebServiceConfig {
@Bean(name = "orgAndUserServiceProxy")
public OrgAndUserService orgAndUserServiceClient() {
JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean();
factory.setServiceClass(OrgAndUserService .class);
factory.setAddress("http://localhost:8080/services/orgAndUserService");
// 创建客户端
OrgAndUserService client = (OrgAndUserService ) factory.create();
// 配置客户端超时时间
Client proxy = org.apache.cxf.frontend.ClientProxy.getClient(client);
HTTPConduit conduit = (HTTPConduit) proxy.getConduit();
HTTPClientPolicy policy = new HTTPClientPolicy();
policy.setConnectionTimeout(30000); // 连接超时 30 秒
policy.setReceiveTimeout(30000); // 接收超时 30 秒
conduit.setClient(policy);
return client;
}
}
import com.oak.springbootredis.domain.ObjectFactory;
import com.oak.springbootredis.domain.resp.QueryUserResponse;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.xml.bind.annotation.XmlSeeAlso;
import javax.xml.ws.RequestWrapper;
import javax.xml.ws.ResponseWrapper;
/**
* 定义Web Service服务契约 即要访问的接口协议
*/
@WebService(targetNamespace = "http://webservice.cfms.richinfo.cn/", name = "orgAndUserService")
@XmlSeeAlso({ObjectFactory.class})
public interface OrgAndUserService {
@WebMethod(operationName = "queryEntUserByBatchPhone")
@RequestWrapper(localName = "queryEntUserByBatchPhone", targetNamespace = "http://webservice.cfms.richinfo.cn/", className = "com.oak.springbootredis.domain.req.QueryEntUserByBatchPhone")
@ResponseWrapper(localName = "queryEntUserByBatchPhoneResponse", targetNamespace = "http://webservice.cfms.richinfo.cn/", className = "com.oak.springbootredis.domain.resp.QueryEntUserByBatchPhoneResponse")
@WebResult(name = "return", targetNamespace = "")
// 修正返回类型的大小写,与实际类名一致
QueryUserResponse queryEntUserByBatchPhone(
@WebParam(name = "batchPhone", targetNamespace = "")
String batchPhone
);
}
import com.oak.springbootredis.domain.resp.QueryUserResponse;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.frontend.ClientProxy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* WebService 客户端
*/
@Service
public class OrgAndUserServiceClient {
@Value("${service.orgAndUser.appId:25816083}")
private String appId;
@Value("${service.orgAndUser.appSecret:c0610ba01b3a45d793aa42afef6b71a}")
private String appSecret;
@Value("${service.orgAndUser.token:CE000000175210792112122e6190bf89e4cd18861c31accf4cac}")
private String token;
@Autowired
@Qualifier("orgAndUserServiceProxy") // 指定 Bean 名称
private OrgAndUserService userService;
public QueryUserResponse queryEntUserByBatchPhone(String batchPhone) {
return queryEntUserByBatchPhone(batchPhone, appId, appSecret, token,"queryEntUserByBatchPhone","fsere3","1.3.5");
}
public QueryUserResponse queryEntUserByBatchPhone(String batchPhone,
String appId,
String appSecret,
String token,
String functionId,
String signature,
String version) {
// 获取 CXF 客户端
Client client = ClientProxy.getClient(userService);
// 清除之前的拦截器
client.getOutInterceptors().clear();
// 添加自定义消息头拦截器
client.getOutInterceptors().add(new SoapHeaderInterceptor(
appId, appSecret, token, functionId, signature, version
));
// 调用 WebService 方法
return userService.queryEntUserByBatchPhone(batchPhone);
}
}
import org.apache.cxf.binding.soap.SoapMessage;
import org.apache.cxf.headers.Header;
import org.apache.cxf.helpers.DOMUtils;
import org.apache.cxf.interceptor.Fault;
import org.apache.cxf.phase.AbstractPhaseInterceptor;
import org.apache.cxf.phase.Phase;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import javax.xml.namespace.QName;
import java.util.Date;
/**
* 请求参数封装
*/
public class SoapHeaderInterceptor extends AbstractPhaseInterceptor<SoapMessage> {
private final String appId;
private final String appSecret;
private final String token;
private final String functionId;
private final String signature;
private final String version;
public SoapHeaderInterceptor(String appId, String appSecret, String token,
String functionId, String signature, String version) {
super(Phase.PREPARE_SEND);
this.appId = appId;
this.appSecret = appSecret;
this.token = token;
this.functionId = functionId;
this.signature = signature;
this.version = version;
}
@Override
public void handleMessage(SoapMessage message) throws Fault {
Document doc = DOMUtils.createDocument();
// 创建 msghead 元素
Element msghead = doc.createElement("msghead");
// 添加消息头字段
addElement(doc, msghead, "appId", appId);
addElement(doc, msghead, "appSecret", appSecret);
addElement(doc, msghead, "token", token);
addElement(doc, msghead, "reqTimestamp", String.valueOf(new Date().getTime() / 1000));
addElement(doc, msghead, "functionId", functionId);
addElement(doc, msghead, "signature", signature);
addElement(doc, msghead, "version", version);
// 将消息头添加到 SOAP 消息中
message.getHeaders().add(new Header(new QName("msghead"), msghead));
}
private void addElement(Document doc, Element parent, String name, String value) {
Element element = doc.createElement(name);
element.setTextContent(value);
parent.appendChild(element);
}
}
4、报文实体类映射
根据报文结构,编写返回实体类字段,若字段少了或者错了 都会导致接口报错。
结果报文结果:
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:queryEntUserByBatchPhoneResponse xmlns:ns2="http://webservice.cfms.richinfo.cn/">
<return>
<code>0</code>
<data>
<entUser>
<companyEmail>钱七@139.com</companyEmail>
<departMentId>16512</departMentId>
<departMentName>系统工程部</departMentName>
<empNo>EMP16512</empNo>
<email>17855954682@139.com</email>
<isValid>1</isValid>
<mobilePhone>17855954682</mobilePhone>
<orgFullName>中移互联网有限公司\系统工程部</orgFullName>
<orgId>16512</orgId>
<orgName>系统工程部</orgName>
<portalUserId>钱七</portalUserId>
<portalUserName>钱七</portalUserName>
<position>3</position>
<sex>2</sex>
<userCategory>2</userCategory>
<userId>16512</userId>
</entUser>
</data>
<msg>根据批量手机号查询实体(自有或合作伙伴)人员信息成功</msg>
</return>
</ns2:queryEntUserByBatchPhoneResponse>
</soap:Body>
</soap:Envelope>
实体类映射:
import lombok.Data;
import javax.xml.bind.annotation.*;
@Data
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "return", propOrder = {
"code",
"data",
"msg"
})
@XmlRootElement(name = "return")
public class QueryUserResponse {
@XmlElement(name = "code")
private String code;
@XmlElement(name = "data")
private ResponseData data;
@XmlElement(name = "msg")
private String msg;
}
import lombok.Data;
import javax.xml.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
@XmlAccessorType(XmlAccessType.FIELD)
@Data
public class ResponseData {
@XmlElement(name = "entUser")
public List<EntUser> entUser = new ArrayList<>();
}
import lombok.Data;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
@Data
@XmlAccessorType(XmlAccessType.FIELD)
public class EntUser {
@XmlElement(name = "companyEmail")
private String companyEmail;
@XmlElement(name = "departMentId")
private String departMentId;
@XmlElement(name = "departMentName")
private String departMentName;
@XmlElement(name = "empNo")
private String empNo;
@XmlElement(name = "email")
private String email;
@XmlElement(name = "isValid")
private String isValid;
@XmlElement(name = "mobilePhone")
private String mobilePhone;
@XmlElement(name = "orgFullName")
private String orgFullName;
@XmlElement(name = "orgId")
private String orgId;
@XmlElement(name = "orgName")
private String orgName;
@XmlElement(name = "portalUserId")
private String portalUserId;
@XmlElement(name = "portalUserName")
private String portalUserName;
@XmlElement(name = "position")
private String position;
@XmlElement(name = "sex")
private String sex;
@XmlElement(name = "userCategory")
private String userCategory;
@XmlElement(name = "userId")
private String userId;
}
5、测试
请求
curl --location 'http://10.10.118.124:8080/services/orgAndUserService' \
--header 'Content-Type: text/xml' \
--data '<?xml version="1.0" encoding="utf-8"?>
<soap:Envelope
xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Header>
<msghead>
<appId>25816083</appId>
<appSecret>c0610ba01b3a45d793aa42afef6b71a</appSecret>
<token>CE000000175210792112122e6190bf89e4cd18861c31accf4cac</token>
<reqTimestamp>1422343231</reqTimestamp>
<functionId>queryEntUserByBatchPhone</functionId>
<signature>fsere3</signature>
<version>1.3.5</version>
</msghead>
</soap:Header>
<soap:Body>
<ns2:queryEntUserByBatchPhone
xmlns:ns2="http://webservice.cfms.richinfo.cn/">
<batchPhone>17855954111</batchPhone>
</ns2:queryEntUserByBatchPhone>
</soap:Body>
</soap:Envelope>'
返回
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/">
<soap:Body>
<ns2:queryEntUserByBatchPhoneResponse xmlns:ns2="http://webservice.cfms.richinfo.cn/">
<return>
<code>0</code>
<data>
<entUser>
<companyEmail>钱七@139.com</companyEmail>
<departMentId>16512</departMentId>
<departMentName>系统工程部</departMentName>
<empNo>EMP16512</empNo>
<email>123@139.com</email>
<isValid>1</isValid>
<mobilePhone>17855954111</mobilePhone>
<orgFullName>中移互公司\系统工程部</orgFullName>
<orgId>16512</orgId>
<orgName>系统工程部</orgName>
<portalUserId>钱七</portalUserId>
<portalUserName>钱七</portalUserName>
<position>3</position>
<sex>2</sex>
<userCategory>2</userCategory>
<userId>16512</userId>
</entUser>
</data>
<msg>根据批量手机号查询实体(自有或合作伙伴)人员信息成功</msg>
</return>
</ns2:queryEntUserByBatchPhoneResponse>
</soap:Body>
</soap:Envelope>
6、源码
注释:源码中查看截图的部分即可