Webservice服务发布、调用示例,WSS认证调用实现
SpringBoot发布Webservice服务
SpringBoot发布Webservice其实就是通过Webservice注解将JAVA的Class类暴露为Webservice服务。
发布服务所需依赖
// webservice发布所需依赖
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-frontend-jaxws</artifactId>
<version>3.1.5</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-transports-http-jetty</artifactId>
<version>3.1.5</version>
</dependency>
发布Webservice服务只需要这两个依赖就好了,JDK使用的是1.8。依赖引入后就开始看代码吧!
编写待暴露的服务类
在编写代码之前需要了解Webservice相关注解的使用【自行百度了解】。这里就不做说明了。直接上代码!
import org.apache.cxf.annotations.WSDLDocumentation;
import org.apache.cxf.annotations.WSDLDocumentationCollection;
import javax.jws.WebMethod;
import javax.jws.WebParam;
import javax.jws.WebResult;
import javax.jws.WebService;
import javax.jws.soap.SOAPBinding;
import javax.xml.ws.Endpoint;
/**
* @Author: zp
* @Description: TODO
* @Date: 2021/6/24 14:17
* @Version: 1.0
*/
@WebService(serviceName="EWebService",targetNamespace="http://com.zpg.fun")
@SOAPBinding(style = SOAPBinding.Style.RPC)
//wsdl xml文档说明
@WSDLDocumentation(value = "此服务为SpringBoot将JAVA Class注解为WebService服务,服务消费时可按照JSON和XML的数据格式进行不同的服务选择。此服务仅作为服务示例,无服务的具体实现。",
placement = WSDLDocumentation.Placement.TOP)
public class EWebService {
@WebMethod(operationName="RequestDataByJson")
@WebResult(name="jsonReturn")
@WSDLDocumentationCollection(
{
@WSDLDocumentation("RequestDataByJson接收json格式的入参进行服务的消费"),
@WSDLDocumentation(value = "入参须遵循json数据格式进行服务的消费",
placement = WSDLDocumentation.Placement.INPUT_MESSAGE),
@WSDLDocumentation(value = "RequestDataByJson服务响应内容",
placement = WSDLDocumentation.Placement.OUTPUT_MESSAGE)
}
)
public Object RequestDataByJson(@WebParam(name="jsonReqParam")String jsonReqParam,@WebParam(name="param2")String param2){
System.out.println("===========RequestDataByJson============="+param2);
return RequestOther(jsonReqParam);
}
@WebMethod(operationName="RequestDataByXml")
@WebResult(name="xmlReturn")
@WSDLDocumentationCollection(
{
@WSDLDocumentation("RequestDataByXml接收xml格式的入参进行服务的消费"),
@WSDLDocumentation(value = "入参须遵循xml数据格式进行服务的消费",
placement = WSDLDocumentation.Placement.INPUT_MESSAGE),
@WSDLDocumentation(value = "RequestDataByXml服务响应内容",
placement = WSDLDocumentation.Placement.OUTPUT_MESSAGE)
}
)
public Object RequestDataByXml(@WebParam(name="xmlReqParam")String xmlReqParam){
System.out.println("===========RequestDataByXml=============");
return RequestOther(xmlReqParam);
}
@WebMethod(exclude=true)//当前方法不被发布出去
public Object RequestOther(String object){
// 进行其他服务逻辑处理
// SayHello sayHello = new SayHello();
// sayHello.say("服务调用成功");
return "RequestOther服务响应成功!入参为:"+object;
}
public static void main(String[] args) {
String url = "http://youIP:youPort/ESBWebservice";
Endpoint publish = Endpoint.publish(url,new EWebService());
if(publish.isPublished()) {
System.out.println("=============server is running==============");
}else {
System.out.println("=============server is error================");
}
}
}
Webservice服务类就已经准备就绪了,直接运行该类中的main方法即可发布Webservice服务。
发布服务
运行该类中的main方法,注意修改自己的ip和端口哈。运行后效果如图:
在浏览器中访问http://youIP:youPort/ESBWebservice?wsdl进行查看服务是否发布成功!如图:
看到这样的界面就表示服务发布成功了!下面说如何进行服务的调用。
Webservice服务的常规调用
常规调用方式如下:
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
public static void main(String[] args) {
try {
String url ="http://youIP:youPort/ESBWebservice?wsdl";
//生成客户端实例
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
Client client = dcf.createClient(url);
//服务参数
String paramString = "参数内容";
//进行自动代理发起请求
Object[] obj = client.invoke("RequestDataByJson",paramString);
System.out.println("调用返回值为:"+obj[0]);
} catch (Exception e) {
e.printStackTrace();
}
}
}
调用结果如图:
服务成功响应。但是此类调用方法可能存在部分Webservice服务在SOAPUI上面正常调用,但是使用该方法访问时就出现各类异常,导致服务调用不通的问题。
Webservice服务调用常见问题
1、No operation was found with the name
2、Unmarshalling Error: 意外的元素 (uri:“urn:hl7-org:v3”, local:“return”)。所需元素为<{urn:hl7-org:v3}HIPMessageServerResult>
3、返回值是一个对象,须进行解析取值
推荐的Webservice调用方式
本方式是结合了hutool中的调用方式和jdk自带的wsdl解析进行封装的调用方式。
调用所需依赖
// 自定义调用所需依赖
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.1.0</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.10</version>
</dependency>
调用工具类
package com.zpg.fun.utils;
import cn.hutool.http.webservice.SoapClient;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.ibm.wsdl.PortImpl;
import com.ibm.wsdl.extensions.soap.SOAPAddressImpl;
import com.ibm.wsdl.extensions.soap12.SOAP12AddressImpl;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.wsdl.Definition;
import javax.wsdl.Service;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLReader;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPConstants;
import javax.xml.soap.SOAPMessage;
import java.io.ByteArrayInputStream;
import java.nio.charset.Charset;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* @Author: zp
* @Description: webservice服务请求工具
* @Date: 2021/8/24 10:57
* @Version: 1.0
*/
public class WebserviceUtils {
public static void main(String[] args) {
Map<String,Object> params = new HashMap<>();
params.put("jsonReqParam","you service params....");
params.put("param2","you service param2....");
String url = "http://youIP:youPort/ESBWebservice?wsdl";
String methodName = "RequestDataByJson";
System.out.println("服务响应值为:"+requestWsdl(url,methodName,params));
}
/**
* wsdl解析
* @param url wsdl地址
* @param methodName 请求的方法名
* @param parameter 参数值
* @return
*/
public static String requestWsdl(String url, String methodName, Map<String, Object> parameter) {
try {
WSDLFactory factory = WSDLFactory.newInstance();
WSDLReader reader = factory.newWSDLReader();
reader.setFeature("javax.wsdl.verbose", true);
reader.setFeature("javax.wsdl.importDocuments", true);
//解析wsdl地址
Definition def = reader.readWSDL(url);
// 元素所属命名空间
String namespaceURI = def.getTargetNamespace();
String wsdlService = null;
String soapVersion = SOAPConstants.SOAP_1_1_PROTOCOL;
//根据wsdl地址和命名空间获取服务和服务所在的绑定名
Map<QName, Object> map1 = def.getServices();
for (Map.Entry<QName, Object> vo : map1.entrySet()) {
wsdlService = vo.getKey().getLocalPart();
}
QName sname = new QName(namespaceURI, wsdlService);
//获取改指定空间下的服务信息
Service service = def.getService(sname);
Map<String, Object> ports = service.getPorts();
Iterator<?> itor = ports.entrySet().iterator();
while (itor.hasNext()) {
Map.Entry<String, Object> map = (Map.Entry<String, Object>) itor.next();
//如果服务的wsdl地址和服务方法名的绑定地址不统一,那么需要获取服务名所在的地址
PortImpl valueMap = (PortImpl) map.getValue();
@SuppressWarnings("rawtypes")
List extensList = valueMap.getExtensibilityElements();
if (extensList != null) {
for (int i = 0; i < extensList.size(); i++) {
//获取服务的绑定地址
ExtensibilityElement extElement = (ExtensibilityElement) extensList.get(i);
//获取locationURI 获取后直接访问改地址进行服务消费 不再使用wsdl的地址
if (extElement instanceof SOAPAddressImpl) {
SOAPAddressImpl soapAddress = (SOAPAddressImpl) extElement;
url = soapAddress.getLocationURI();
soapVersion = SOAPConstants.SOAP_1_1_PROTOCOL;
}
if (extElement instanceof SOAP12AddressImpl) {
SOAP12AddressImpl soapAddress = (SOAP12AddressImpl) extElement;
url = soapAddress.getLocationURI();
soapVersion = SOAPConstants.SOAP_1_2_PROTOCOL;
}
}
}
//避免有些服务提供商将一个服务以多中方式提供而报错 此处获取到后不再获取【如天气查询 soap1.1 soap1.3 httpPost httpGet均提供】
break;
}
return sendMessage(url, namespaceURI, methodName, parameter, soapVersion).trim();
} catch (Exception e) {
return "网络错误,请联系管理员"+e.getMessage();
}
}
/**
* <p>Title: sendMessage</p>
* <p>Description:发送soap请求获取原服务返回值</p>
* @param wsdlUrl 服务地址或者服务所在的binding地址
* @param namespaceURI 命名空间
* @param methodName 服务名
* @param params 参数map
* @param soapVersion soap版本
* @return
* @throws Exception
*/
private static String sendMessage(String wsdlUrl, String namespaceURI,String methodName,Map<String, Object> params, String soapVersion) {
try {
SoapClient client = SoapClient.create(wsdlUrl)
// 设置要请求的方法,此接口方法前缀为web,传入对应的命名空间
.setMethod(methodName,namespaceURI)
// 设置参数,此处自动添加方法的前缀:
.setParams(params);
String returnStr = client.send(true);
// System.out.println("服务端响应信息为:"+returnStr);
if(StringUtils.isNotBlank(returnStr)) {
// 不同的服务定义的返回信息节点不同,且所在命名空间不同,导致无法定位取值地点 --》转换为soap消息体进行取值
return formatSoapString(returnStr,soapVersion);
}else {
return "取值失败;服务端响应信息为:"+returnStr;
}
} catch (Exception e1) {
e1.printStackTrace();
return "服务调用失败!"+e1.getMessage();
}
}
/**
* <p>Title: formatSoapString</p>
* <p>Description: 根据soap返回的消息体字符串和soap版本进行字符串转soapMessage消息体</p>
* @param soapString
* @param soapVersion
* @return
*/
public static String formatSoapString(String soapString,String soapVersion){
try{
MessageFactory msgFactory = null;
if("SOAP 1.1 Protocol".equals(soapVersion)) {
msgFactory = MessageFactory.newInstance();
}
if("SOAP 1.2 Protocol".equals(soapVersion)) {
msgFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
}
SOAPMessage reqMsg = msgFactory.createMessage(new MimeHeaders(), new ByteArrayInputStream(soapString.getBytes(Charset.forName("UTF-8"))));
// reqMsg.saveChanges();保存修改后的soapmessage
if(null!=reqMsg) {
Document doc = reqMsg.getSOAPPart().getEnvelope().getBody().extractContentAsDocument();
//soapBody中的返回信息节点
Node node = doc.getFirstChild();
if(node != null) {
return node.getTextContent().trim();
}else {
return "返回值取值失败";
}
}else{
return "返回值取值失败";
}
}catch (Exception e){
return null;
}
}
}
经验证,用这样的方式调用就可以完美避免上面说遇到的这些问题了!
调用带WSS认证的Webservice服务
在接口对接过程中经常遇到第三方开发的Webservice服务添加了WSS认证,在服务调用的时候需要进行特殊的配置才能访问服务信息。
添加WSS认证所需依赖
// WSS所需依赖
<!--wss4j begin-->
<dependency>
<groupId>org.springframework.ws</groupId>
<artifactId>spring-ws-security</artifactId>
<version>3.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.ws.security</groupId>
<artifactId>wss4j</artifactId>
<version>1.6.19</version>
</dependency>
<dependency>
<groupId>org.apache.cxf</groupId>
<artifactId>cxf-rt-ws-security</artifactId>
<version>3.2.8</version>
<exclusions>
<exclusion>
<artifactId>cxf-rt-bindings-soap</artifactId>
<groupId>org.apache.cxf</groupId>
</exclusion>
<exclusion>
<artifactId>wss4j-ws-security-dom</artifactId>
<groupId>org.apache.wss4j</groupId>
</exclusion>
</exclusions>
</dependency>
<!--wss4j end-->
添加获取认证回调类
主要作用用于获取WSS认证后的回调信息,让client可以有权限进行服务调用。
import org.apache.wss4j.common.ext.WSPasswordCallback;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
/**
* @author zp
* ws CallbackHandler
*/
public class PasswordHandler implements CallbackHandler {
private Map<String, String> passwords = new HashMap<String, String>();
public PasswordHandler(String user, String psw) {
passwords.put(user, psw);
}
public void handle(Callback[] callbacks) throws IOException,UnsupportedCallbackException {
for (int i = 0; i < callbacks.length; i++) {
WSPasswordCallback pc = (WSPasswordCallback) callbacks[i];
String identifier = pc.getIdentifier();//用户名
int usage = pc.getUsage();//验证方式
if (usage == WSPasswordCallback.USERNAME_TOKEN) {
pc.setPassword(passwords.get(identifier));
} else if (usage == WSPasswordCallback.SIGNATURE) {
pc.setPassword(passwords.get(identifier));
}
}
}
}
完善服务调用工具类
将前面的调用工具进行完善,提供了2种服务调用方案,CxfInvoke为较常见的调用方式,在此基础上添加了WSS认证的服务调用功能,requestWsdl调用场景为发布的Webservice服务的wsdl地址和服务所在的SOAPAddress不对应的时候,采用该方法可以成功调用。
import cn.hutool.http.webservice.SoapClient;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.ibm.wsdl.PortImpl;
import com.ibm.wsdl.extensions.soap.SOAPAddressImpl;
import com.ibm.wsdl.extensions.soap12.SOAP12AddressImpl;
import org.apache.cxf.binding.soap.saaj.SAAJOutInterceptor;
import org.apache.cxf.endpoint.Client;
import org.apache.cxf.jaxws.endpoint.dynamic.JaxWsDynamicClientFactory;
import org.apache.cxf.ws.security.wss4j.WSS4JOutInterceptor;
import org.apache.wss4j.dom.handler.WSHandlerConstants;
import org.w3c.dom.Document;
import org.w3c.dom.Node;
import javax.wsdl.Definition;
import javax.wsdl.Service;
import javax.wsdl.extensions.ExtensibilityElement;
import javax.wsdl.factory.WSDLFactory;
import javax.wsdl.xml.WSDLReader;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPConstants;
import javax.xml.soap.SOAPMessage;
import java.io.ByteArrayInputStream;
import java.nio.charset.Charset;
import java.util.*;
/**
* @Author: zp
* @Description: webservice服务请求工具
* @Date: 2021/8/24 10:57
* @Version: 1.0
*/
public class WebserviceUtils {
public static void main(String[] args) {
WebserviceUtils webserviceUtils = new WebserviceUtils();
Map<String,Object> params = new HashMap<>();
params.put("jsonReqParam","参数1");
params.put("param2","参数2");
String url = "http://192.168.4.16:8081/ESBWebservice?wsdl";
String methodName = "RequestDataByJson";
System.out.println("服务响应值1为:"+webserviceUtils.requestWsdl(url,methodName,params));
List<Object> paramList = new ArrayList<>();
paramList.add("jsonReqParam参数1");
paramList.add("参数2");
System.out.println("服务响应值2为:"+webserviceUtils.CxfInvoke(url,methodName,paramList,false,"","","",""));
}
/**
* webservice服务调用方法1 wsdl解析+SoapClient
* @param url wsdl地址
* @param methodName 请求的方法名
* @param parameter 参数值
* @return
*/
public String requestWsdl(String url, String methodName, Map<String, Object> parameter) {
try {
WSDLFactory factory = WSDLFactory.newInstance();
WSDLReader reader = factory.newWSDLReader();
reader.setFeature("javax.wsdl.verbose", true);
reader.setFeature("javax.wsdl.importDocuments", true);
//解析wsdl地址
Definition def = reader.readWSDL(url);
// 元素所属命名空间
String namespaceURI = def.getTargetNamespace();
String wsdlService = null;
String soapVersion = SOAPConstants.SOAP_1_1_PROTOCOL;
//根据wsdl地址和命名空间获取服务和服务所在的绑定名
Map<QName, Object> map1 = def.getServices();
for (Map.Entry<QName, Object> vo : map1.entrySet()) {
wsdlService = vo.getKey().getLocalPart();
}
QName sname = new QName(namespaceURI, wsdlService);
//获取改指定空间下的服务信息
Service service = def.getService(sname);
Map<String, Object> ports = service.getPorts();
Iterator<?> itor = ports.entrySet().iterator();
while (itor.hasNext()) {
Map.Entry<String, Object> map = (Map.Entry<String, Object>) itor.next();
//如果服务的wsdl地址和服务方法名的绑定地址不统一,那么需要获取服务名所在的地址
PortImpl valueMap = (PortImpl) map.getValue();
@SuppressWarnings("rawtypes")
List extensList = valueMap.getExtensibilityElements();
if (extensList != null) {
for (int i = 0; i < extensList.size(); i++) {
//获取服务的绑定地址
ExtensibilityElement extElement = (ExtensibilityElement) extensList.get(i);
//获取locationURI 获取后直接访问改地址进行服务消费 不再使用wsdl的地址
if (extElement instanceof SOAPAddressImpl) {
SOAPAddressImpl soapAddress = (SOAPAddressImpl) extElement;
url = soapAddress.getLocationURI();
soapVersion = SOAPConstants.SOAP_1_1_PROTOCOL;
}
if (extElement instanceof SOAP12AddressImpl) {
SOAP12AddressImpl soapAddress = (SOAP12AddressImpl) extElement;
url = soapAddress.getLocationURI();
soapVersion = SOAPConstants.SOAP_1_2_PROTOCOL;
}
}
}
//避免有些服务提供商将一个服务以多中方式提供而报错 此处获取到后不再获取【如天气查询 soap1.1 soap1.3 httpPost httpGet均提供】
break;
}
return sendMessage(url, namespaceURI, methodName, parameter, soapVersion).trim();
} catch (Exception e) {
return "网络错误,请联系管理员"+e.getMessage();
}
}
/**
* <p>Title: sendMessage</p>
* <p>Description:发送soap请求获取原服务返回值</p>
* @param wsdlUrl 服务地址或者服务所在的binding地址
* @param namespaceURI 命名空间
* @param methodName 服务名
* @param params 参数map
* @param soapVersion soap版本
* @return
* @throws Exception
*/
private String sendMessage(String wsdlUrl, String namespaceURI,String methodName,Map<String, Object> params, String soapVersion) {
try {
SoapClient client = SoapClient.create(wsdlUrl)
// 设置要请求的方法,此接口方法前缀为web,传入对应的命名空间
.setMethod(methodName,namespaceURI)
// 设置参数,此处自动添加方法的前缀:
.setParams(params);
String returnStr = client.send(true);
if(StringUtils.isNotBlank(returnStr)) {
// 不同的服务定义的返回信息节点不同,且所在命名空间不同,导致无法定位取值地点 --》转换为soap消息体进行取值
return formatSoapString(returnStr,soapVersion);
}else {
return "取值失败;服务端响应信息为:"+returnStr;
}
} catch (Exception e1) {
e1.printStackTrace();
return "服务调用失败!"+e1.getMessage();
}
}
/**
* <p>Title: formatSoapString</p>
* <p>Description: 根据soap返回的消息体字符串和soap版本进行字符串转soapMessage消息体</p>
* @param soapString
* @param soapVersion
* @return
*/
public String formatSoapString(String soapString,String soapVersion){
try{
MessageFactory msgFactory = null;
if("SOAP 1.1 Protocol".equals(soapVersion)) {
msgFactory = MessageFactory.newInstance();
}
if("SOAP 1.2 Protocol".equals(soapVersion)) {
msgFactory = MessageFactory.newInstance(SOAPConstants.SOAP_1_2_PROTOCOL);
}
SOAPMessage reqMsg = msgFactory.createMessage(new MimeHeaders(), new ByteArrayInputStream(soapString.getBytes(Charset.forName("UTF-8"))));
// reqMsg.saveChanges();保存修改后的soapmessage
if(null!=reqMsg) {
Document doc = reqMsg.getSOAPPart().getEnvelope().getBody().extractContentAsDocument();
//soapBody中的返回信息节点
Node node = doc.getFirstChild();
if(node != null) {
return node.getTextContent().trim();
}else {
return "返回值取值失败";
}
}else{
return "返回值取值失败";
}
}catch (Exception e){
return null;
}
}
/**
* webservice服务调用方法2
* @param wsUrl wsdl地址
* @param methodName 方法名
* @param params 参数
* @param wssAuEnable 是否有WSS认证
* @param action 认证服务名
* @param pwType 密码传输类型
* @param userName 认证用户名
* @param pwd 认证密码
* @return
*/
public String CxfInvoke(String wsUrl,String methodName,List<Object> params,Boolean wssAuEnable,String action,String pwType,String userName,String pwd) {
try {
//生成客户端实例
JaxWsDynamicClientFactory dcf = JaxWsDynamicClientFactory.newInstance();
Client client = dcf.createClient(wsUrl);
if(wssAuEnable){
setWssAuth(client, action, pwType, userName, pwd);
}
Object[] obj = client.invoke(methodName,params.toArray());
return obj[0].toString();
} catch (Exception e) {
e.printStackTrace();
return e.getMessage();
}
}
/**
* 设置wssAuth权限认证
* @param client webservice 客户端
* @param action 服务名 取值见WSHandlerConstants.ACTION
* @param pswType 密码传输类型 取值见WSHandlerConstants.PASSWORD_TYPE
* @param user 用户名
* @param psw 密码
*/
private void setWssAuth(Client client, String action, String pswType, String user, String psw) {
Map<String, Object> outProps = new HashMap<>();
outProps.put(WSHandlerConstants.ACTION, action);
outProps.put(WSHandlerConstants.USER, user);
outProps.put(WSHandlerConstants.PASSWORD_TYPE, pswType);
// 指定在调用远程ws之前触发的回调函数WsClinetAuthHandler,其实类似于一个拦截器
// outProps.put(WSHandlerConstants.PW_CALLBACK_CLASS,
// PasswordHandler.class.getName());
PasswordHandler ph = new PasswordHandler(user, psw);
outProps.put(WSHandlerConstants.PW_CALLBACK_REF, ph);
ArrayList list = new ArrayList();
// 添加cxf安全验证拦截器,必须
list.add(new SAAJOutInterceptor());
list.add(new WSS4JOutInterceptor(outProps));
client.getOutInterceptors().addAll(list);
}
}