Java WSDL 服務(wù)調(diào)用入門分享
第一次自己使用代碼直接調(diào)用webservice叽奥。對(duì)于這種網(wǎng)上很難找資料椅文,而且找到了不會(huì)用,會(huì)用但是會(huì)報(bào)錯(cuò)的代碼同衣。內(nèi)心是非常痛苦的。要靜下心來仔細(xì)學(xué)習(xí)webservice也是不可能的翩剪,因?yàn)榉浅Zs乳怎。所以就把我終于學(xué)會(huì)調(diào)用webservice的關(guān)鍵過程寫下
webservice說明
此為不專業(yè)的說明,僅做了解用前弯。且代碼都是從片段里面截取出來的,僅供思路參考秫逝,不能直接拿來用恕出。
webservice應(yīng)該就是想用于跨平臺(tái)的交互方式,因?yàn)樵谒鼜?fù)雜的xml里面违帆,規(guī)定了調(diào)用的方式浙巫,參數(shù)的對(duì)象(強(qiáng)類型)等等信息。而WSDL(Web Services Description Language)應(yīng)該就是一種針對(duì)webservice更加詳細(xì)的約定,因?yàn)閃SDL描述了webservice的函數(shù)的畴、參數(shù)渊抄、返回值等說明。
如何調(diào)用
自己寫調(diào)用代碼丧裁,我不會(huì)护桦,只能祈禱大家以后對(duì)接能少遇到webservice- 使用CFX生成。下面我講一下如何使用cfx生成可以直接調(diào)用的代碼煎娇,以及如何調(diào)用二庵。
CFX生成可直接調(diào)用的代碼1
思路如下:下載插件 -> 根據(jù)給出的xml生成對(duì)應(yīng)的代碼 -> 在代碼里面找到main方法 -> 模仿main方法自己去調(diào)用 -> 封裝讓接口更加易用
下載cfx的軟件。http://mirrors.tuna.tsinghua.edu.cn/apache/cxf/3.3.3/apache-cxf-3.3.3.zip
解壓后缓呛,打開bin文件夾催享,找到我們需要運(yùn)行的 wsdl2java
-
在bin文件夾,打開CMD哟绊。輸入命令:
wsdl2java -p 這里填寫你的包名 -d 這里填寫你生成代碼的位置 -client -encoding utf-8 -noAddressBinding 這里填寫WSDL的地址
注意:使用git bash可能會(huì)有以下問題因妙,可嘗試使用自帶的CMD:
你的包名:指的是接下來生成的文件會(huì)在XX.XX包下面。比如:我填寫的是com.google
票髓,那么生成的文件結(jié)構(gòu)如此:package com.google.test;
代碼的位置:指的是生成的文件會(huì)在XXX路徑下面兰迫。比如:我填寫的是C:\Users\shihu\Documents\wsdl
,那么生成的文件就會(huì)在C:\Users\shihu\Documents\wsdl
文件下炬称。
WSDL的地址:指的是你將要調(diào)用的wsdl的地址汁果,一般就是返回一個(gè)xml文件。比如:我填寫http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl
玲躯,那么就是根據(jù)該地址返回的xml文件去生成代碼据德。這里我挑選了亞馬遜的測(cè)試?yán)印?a target="_blank">這里有更多的例子,只需要將地址替換即可跷车。
所以棘利,完整的命令可以如下:
wsdl2java -p com.google -d C:\Users\shihu\Documents\wsdl -client -encoding utf-8 -noAddressBinding http://webservices.amazon.com/AWSECommerceService/AWSECommerceService.wsdl
生成以后的目錄結(jié)構(gòu)如下非常多文件,但是別怕下面會(huì)大致說明注1:
C:.
└─com
└─google
Accessories.java --dom實(shí)體
Arguments.java --dom實(shí)體
AWSECommerceService.java --webservice 的service
AWSECommerceServicePortType.java --可以獲取webservice入口的
AWSECommerceServicePortType_AWSECommerceServicePortCA_Client.java --入口
AWSECommerceServicePortType_AWSECommerceServicePortCN_Client.java --入口
AWSECommerceServicePortType_AWSECommerceServicePortDE_Client.java --入口
AWSECommerceServicePortType_AWSECommerceServicePortES_Client.java --入口
...
-
導(dǎo)入并運(yùn)行代碼
面對(duì)這些陌生的代碼朽缴,我們?cè)撊绾沃稚泼担亢芎?jiǎn)單,找到入口(main方法)就可以密强。
- main方法在**Endpoint_Client **結(jié)尾的文件里面茅郎;里面自動(dòng)生成了所有服務(wù)端提供的方法。我們只需要調(diào)用一下main方法就可以幫我們調(diào)試所有的接口了或渤。
- 然后我們模仿main方法系冗,自己將service實(shí)例化出來,就可以直接調(diào)用服務(wù)端接口了薪鹦。
額外產(chǎn)物
將返回的數(shù)據(jù)轉(zhuǎn)化為json
返回之后的是一個(gè)dom數(shù)據(jù)掌敬,那么我們?nèi)绾伟阉D(zhuǎn)化為JSON對(duì)象呢惯豆?
返回的實(shí)體大致如下:
@XmlAccessorType(XmlAccessType.FIELD)
@XmlType(name = "", propOrder = {
"username",
"password"
})
@XmlRootElement(name = "login")
public class Login {
@XmlElementRef(name = "username", namespace = "http://sys.ws.xxx.com", type = JAXBElement.class, required = false)
protected JAXBElement<String> username;
@XmlElementRef(name = "password", namespace = "http://sys.ws.xxx.com", type = JAXBElement.class, required = false)
protected JAXBElement<String> password;
...
如果你和我情況類似,那么你可以使用以下方法轉(zhuǎn)化為JSON奔害。我封裝了2部分楷兽,一個(gè)是只轉(zhuǎn)化一個(gè),另外一個(gè)是轉(zhuǎn)化集合的华临。其實(shí)本質(zhì)都是一樣的芯杀。最主要的代碼是final String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(e);
/**
* 轉(zhuǎn)化xml對(duì)象為json
* @param tClass
* @param value
* @param <E>
* @return
*/
public static <E extends Object> JSONObject convertToObject(Class<E> tClass, E value){
List<E> list = new ArrayList<E>();
list.add(value);
final List<JSONObject> jsonObjects = convertToObject(tClass, list);
return jsonObjects.isEmpty()?new JSONObject():jsonObjects.get(0);
}
/**
* 批量轉(zhuǎn)化xml對(duì)象為json
* @param tClass
* @param list
* @param <E>
* @return
*/
public static <E extends Object> List<JSONObject> convertToObject(Class<E> tClass, List<E> list){
List<JSONObject> result = new ArrayList<>();
for(E e : list){
try {
final String json = mapper.writerWithDefaultPrettyPrinter().writeValueAsString(e);
result.add( JSONObject.parseObject(json));
} catch (JsonProcessingException ex) {
logger.error(marker,"解析xml對(duì)象錯(cuò)誤!",e);
}
}
return result;
}
處理SOAP異常
只要服務(wù)端有異常:包含程序未捕獲的異常银舱、參數(shù)校驗(yàn)不通過等瘪匿,都會(huì)返回ServerSOAPFaultException異常,我們主要是處理ServerSOAPFaultException異常寻馏。
思路:添加一個(gè)切面棋弥,然后統(tǒng)一捕獲ServerSOAPFaultException異常。目的:封裝一層诚欠,將ServerSOAPFaultException異常轉(zhuǎn)化為我們自己的業(yè)務(wù)異常顽染,將SOAP的異常前后綴除去,讓他人更容易明白轰绵。
-
實(shí)現(xiàn):下面是切面中的around:
@Around(value = "log()") public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable { Object result = null; String argsJson = ""; try{ Object[] arguments = proceedingJoinPoint.getArgs();//傳入的參數(shù) argsJson = JSON.toJSONString(arguments); }catch (Exception e){ } final String methodName = proceedingJoinPoint.getSignature().getName(); try { result = proceedingJoinPoint.proceed(); } catch (ServerSOAPFaultException e) { String EXCEPTION_PREFIX = "Client received SOAP Fault from server: "; String EXCEPTION_SUFFIX = " Please see the server log to find more detail regarding exact cause of the failure."; final String message = e.getMessage().replaceFirst(EXCEPTION_PREFIX,"").replaceFirst(EXCEPTION_SUFFIX,""); logger.error("調(diào)用接口異常",e); throw new CommonException(message); } catch (Exception e) { logger.error("調(diào)用接口異常",e); throw e; } return result; }
如何添加頭部
有時(shí)候粉寞,我們需要在SOAP頭部(服務(wù)器端的XML說明)或者h(yuǎn)ttp頭部添加驗(yàn)證信息(僅僅是請(qǐng)求的http頭部)。那么如何做到呢左腔?
- SOAP頭部的添加唧垦,參考網(wǎng)上的2。思路如下:
實(shí)現(xiàn)HandlerResolver 接口
實(shí)現(xiàn)SOAPHandler<SOAPMessageContext> 接口
重寫handleMessage方法。
new 一個(gè)HandlerResolver,并將實(shí)現(xiàn)類放入service
從service獲取endpoint調(diào)用
-
代碼:
/** * 實(shí)現(xiàn)SOAPHandler * @return */ private class RequesterCredentials implements SOAPHandler<SOAPMessageContext> { private Map<String,String> headers; public RequesterCredentials(Map<String,String> headers) { super(); this.headers = headers; } public Set<QName> getHeaders() { return null; } @Override public void close(MessageContext context) { } @Override public boolean handleFault(SOAPMessageContext context) { // TODO return true logger.error("ws錯(cuò)誤處理映挂。"); return true; } // 處理請(qǐng)求上下文 @Override public boolean handleMessage(SOAPMessageContext context) { try { Boolean outboundProperty = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); if (outboundProperty.booleanValue()) { SOAPMessage message = context.getMessage(); SOAPHeader header = message.getSOAPHeader(); if (header == null) { message.getSOAPPart().getEnvelope().addHeader(); header = message.getSOAPHeader(); } SOAPElement authenticationToken = header.addChildElement("auth", "", ""); authenticationToken.addChildElement("user").addTextNode("user"); authenticationToken.addChildElement("password").addTextNode("password"); authenticationToken.addChildElement("token").addTextNode("token"); } } catch (Exception e) { e.printStackTrace(); } return true; } } /** * 實(shí)現(xiàn)SOAPHandler * @return */ public class HeaderHandlerResolver implements HandlerResolver { private RequesterCredentials requesterCredentials; public HeaderHandlerResolver(RequesterCredentials requesterCredentials){ this.requesterCredentials=requesterCredentials; } @Override public List getHandlerChain(PortInfo portInfo) { return Arrays.asList(requesterCredentials); } }
/** * 放入SOAPHandler,并調(diào)用 * @return */ RequesterCredentials r=new RequesterCredentials (headers); HeaderHandlerResolver headerHandlerResolver=new HeaderHandlerResolver (r); mathService.setHandlerResolver(headerHandlerResolver); Deposit1ServicePortType port = mathService.getDeposit1ServiceHttpSoap11Endpoint();
-
添加http的頭部
和上方的soap差不多坊秸。主要就是實(shí)現(xiàn)HandlerResolver,重寫handleMessage澎怒,修改請(qǐng)求的上下文褒搔。
-
唯一的區(qū)別就是重寫的handleMessage不同
/** * 實(shí)現(xiàn)SOAPHandler,一個(gè)headers成員變量,到時(shí)候放入header喷面。 * @return */ private class RequesterCredentials implements SOAPHandler<SOAPMessageContext> { private Map<String,String> ; ... // 改變一 @Override public boolean handleMessage(SOAPMessageContext context) { Boolean outboundProperty = (Boolean) context.get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); if (outboundProperty.booleanValue()) { Map<String, List<String>> requestHeaders = new HashMap<>(); for(String key:headers.keySet()){ requestHeaders.put(key, Arrays.asList(headers.get(key))); } context.put(MessageContext.HTTP_REQUEST_HEADERS, requestHeaders); } return true; }
/** * 放入SOAPHandler星瘾,并調(diào)用 * @return */ RequesterCredentials r=new RequesterCredentials (headers); HeaderHandlerResolver headerHandlerResolver=new HeaderHandlerResolver (r); mathService.setHandlerResolver(headerHandlerResolver); Deposit1ServicePortType port = mathService.getDeposit1ServiceHttpSoap11Endpoint();
-
- 需要注意的是,添加的header是 Map<String, List<String>>乖酬,千萬不要手快死相,寫成 了Map<String, Object>,網(wǎng)上很多都是這里被坑了咬像。注意value是一個(gè)String的list。
將java對(duì)象轉(zhuǎn)化為dom對(duì)象工具
當(dāng)我們調(diào)用webservice方法的時(shí)候,傳入的參數(shù)并不是普通的java對(duì)象县昂,而是dom對(duì)象肮柜,那么我們?nèi)绾螌ava對(duì)象轉(zhuǎn)化為dom對(duì)象呢?
非常需要的注意的大前提:DOM對(duì)象的Object屬性需要通過ObjectFactory統(tǒng)一create出來倒彰。
-
我們這里借助了hutool的反射工具
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.6.8</version> </dependency>
思路:我們可以通過觀察dom對(duì)象結(jié)構(gòu) =》將java對(duì)象對(duì)應(yīng)的屬性轉(zhuǎn)化為dom對(duì)象需要的屬性=》把轉(zhuǎn)化好的屬性set進(jìn)dom對(duì)象
下面我們觀察一下CXF 生成的dom對(duì)象的Object屬性和普通屬性get审洞、set方法:
/**
* 設(shè)置username屬性的值。
*
* @param value
* allowed object is
* {@link JAXBElement }{@code <}{@link String }{@code >}
*
*/
public void setUsername(JAXBElement<String> value) {
this.username = value;
}
...
/**
* 設(shè)置money屬性的值待讳。
*
* @param value
* allowed object is
* {@link Double }
*
*/
public void setMoney(Double value) {
this.money = value;
}
為什么要把Object類和java基類分開芒澜,因?yàn)橹挥蠴bject的子類(像String)才不是普通的set方法,而基類是普通的set方法创淡。
可以看到是一個(gè)JAXBElement包裝了一個(gè)普通的java類痴晦,所有在java類和dom類的類型保持一致的情況下(比如java是String,那么dom也是String)琳彩,我們可以通過反射誊酌,將對(duì)應(yīng)的屬性通過get方法,獲取到j(luò)ava對(duì)象原本的值露乏,然后再通過dom的ObjectFactoryt的create方法碧浊,組合成dom需要的類型的值,將轉(zhuǎn)化之后的屬性放入dom對(duì)象中瘟仿。
代碼如下:
/**
* 可以轉(zhuǎn)化普通java的屬性為dom的屬性
* @param objecrFactory 對(duì)象工廠實(shí)例
* @param klass soap的類
* @param property 對(duì)應(yīng)java類的屬性名(soap和java的屬性名字和類型需要保持一致)
* @param value 需要設(shè)置的值
* @param <T> 對(duì)象工廠類
* @param <E> soap的類
* @param <V> 屬性的類
* @return
*/
public static <T,E,V> JAXBElement<V> invokeSOAP(T objecrFactory, Class<E> klass, String property , V value){
try{
final String methodName = String.format("create%s%s", klass.getSimpleName(),
property.substring(0, 1).toUpperCase() + property.substring(1));
return ReflectUtil.invoke(objecrFactory,methodName,value);
}catch (Exception e){
logger.error("設(shè)置屬性失斚淙瘛!",e);
return null;
}
}
/**
* 將一個(gè)普通java對(duì)象轉(zhuǎn)化為dom對(duì)象
* @param entity java對(duì)象
* @param objecrFactory 創(chuàng)建的dom工廠
* @param soapClass soap對(duì)應(yīng)的對(duì)象class
* @param <T> 實(shí)體的類
* @param <O> dom對(duì)象工廠的類
* @param <E> soap的類
*/
public static <T,O,E> E convertToSOAP(T entity,O objecrFactory, Class<E> soapClass){
final E soapInstance = ReflectUtil.newInstance(soapClass);
try{
final Class<?> entityClass = entity.getClass();
final Set<String> methodNames = ReflectUtil.getPublicMethodNames(entityClass);
for(String methodName: methodNames){
if(methodName.startsWith("get") && !objectMethods.contains(methodName) && ReflectUtil.getMethodByNameIgnoreCase(soapClass, methodName)!=null){//如果是get方法
final String property = methodName.substring(3, methodName.length());
Object value = ReflectUtil.invoke(entity, methodName);
if(value instanceof BigDecimal){//BigDecimal需要轉(zhuǎn)化為double劳较,并且直接設(shè)置屬性
value = ((BigDecimal) value).doubleValue();
ReflectUtil.invoke(soapInstance,"set"+property,value);
continue;
}
if(value!=null){
ReflectUtil.invoke(soapInstance,"set"+property,invokeSOAP(objecrFactory, soapClass, property, value));
}
}
}
return soapInstance;
}catch (Exception e){
logger.error("批量設(shè)置屬性失斁灾埂!",e);
return soapInstance;
}
}
如何在postman中調(diào)試
拿到對(duì)方接口的第一時(shí)間兴想,就是想使用postman進(jìn)行測(cè)試幢哨。但是,webservice如何使用postman進(jìn)行測(cè)試呢嫂便?
物料:谷歌瀏覽器捞镰、postman、wizdler插件毙替、以及一個(gè)wsdl網(wǎng)址
我們這里可以借助一個(gè)谷歌插件對(duì)請(qǐng)求參數(shù)進(jìn)行生成:Wizdler
-
安裝插件之后岸售,在谷歌瀏覽器打開你可以需要調(diào)試的wsdl的網(wǎng)址。那么插件就會(huì)顯示出該wsdl對(duì)應(yīng)的方法厂画,如下圖:
然后點(diǎn)擊需要調(diào)試的方法就可以生成請(qǐng)求體凸丸,其實(shí)簡(jiǎn)單的請(qǐng)求,可以直接通過該插件調(diào)試了
- 比如袱院,我們就嘗試請(qǐng)求這個(gè)方法(http://www.webxml.com.cn/WebServices/WeatherWebService.asmx):直接使用插件屎慢,在頁面調(diào)用效果瞭稼,如下圖:
修改請(qǐng)求體之后,我們可以直接通過頁面對(duì)方法進(jìn)行調(diào)用腻惠。點(diǎn)擊GO按鈕就可以直接發(fā)起請(qǐng)求了环肘。對(duì)應(yīng)的響應(yīng)如下:
同時(shí),我們可以將請(qǐng)求體復(fù)制到postman集灌,然后設(shè)置對(duì)應(yīng)的Content-Type悔雹,以及請(qǐng)求方法,使用post請(qǐng)求就可以在postman發(fā)起webservice請(qǐng)求了欣喧。以這里例子為例腌零,需要POST請(qǐng)求;body是xml格式唆阿;Content-Typeapplication/soap+xml; charset="UTF-8"益涧;就可以發(fā)送請(qǐng)求了。
webservice 網(wǎng)絡(luò)斷開后處理
webservice有一個(gè)奇葩的問題酷鸦。如果對(duì)方的webservice服務(wù)端是離線的狀態(tài)(服務(wù)器重啟/關(guān)閉導(dǎo)致我們無法獲取到WSDL的xml文件)饰躲,或?qū)е挛覀兛蛻舳?“啟動(dòng)的時(shí)候” 無法初始化webservice的一些東西,或者當(dāng)我們客戶端運(yùn)行的時(shí)候臼隔,服務(wù)端重啟了嘹裂,服務(wù)端重啟完畢之后,webservice的一些實(shí)例會(huì)變成null摔握,這就導(dǎo)致了我們無法正常使用webservice了寄狼。我是如何解決這個(gè)問題的呢?
-
為什么要這樣處理氨淌?因?yàn)槲以?strong>類初始化的時(shí)候泊愧,將webservice的service和port全部初始化了,并放入靜態(tài)變量中盛正,這樣效率就高删咱,因?yàn)椴恍枰看味糿ew一次。所以豪筝,痰滋,事先初始化了webservice相關(guān)的,如果網(wǎng)絡(luò)斷開了续崖,那么就會(huì)拋出NPE異常了敲街。
初始化代碼大致如下:
@Service public class WsService{ public static LoginService loginService; public static LoginServicePortType portType; public WsService(){ loginService = new (new URL("url"), LOGIN_SERVICE_NAME); portType = loginService.getLoginServiceHttpSoap11Endpoint(); } ... }
-
思路:加入切面 =》代理webservice的每一個(gè)方法 =》每次進(jìn)入方法前,檢測(cè)ws相關(guān)的類是否為空 =》空則重新初始化
把原來初始化的代碼封裝成一個(gè)statis方法:
@Service public class WsService{ public static LoginService loginService; public static LoginServicePortType portType; public WsService(){} public static initService(){ try{//如果啟動(dòng)應(yīng)用的時(shí)候严望,恰好服務(wù)端無法使用多艇,那么會(huì)拋出異常,所以需要處理像吻。 //不能把異常拋出峻黍,因?yàn)閽伋霎惓?huì)導(dǎo)致應(yīng)用無法啟動(dòng)复隆。為了保證應(yīng)用能正常啟動(dòng),必須處理異常 //每次進(jìn)入方法的時(shí)候再檢查ws對(duì)象奸披,這樣就不影響應(yīng)用正常啟動(dòng)了昏名,等服務(wù)端正常了涮雷,就會(huì)自動(dòng)初始化了 if(loginService == null){ loginService = new (new URL("url"), LOGIN_SERVICE_NAME); } if(portType == null){ portType = loginService.getLoginServiceHttpSoap11Endpoint(); } }catch(Exception e){ e.printStackTrace(); } } }
切面類:
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/**
* 接口日志
* @author shihu
*/
@Aspect
@Component
public class WsAspect {
private Logger logger = LoggerFactory.getLogger(WsAspect.class);
@Pointcut("execution(public * com.google.ws.WsService.*(..))")
public void cut(){}
@Before("cut()")
public void doBefore(JoinPoint joinPoint){
WsAspect.initService();// 確保每次進(jìn)入都驗(yàn)證是否為空
}
}
總結(jié)
因?yàn)椴皇煜ebservice阵面,所以一開始完全就是一頭霧水。但是洪鸭,當(dāng)CFX自動(dòng)生成了代碼能直接調(diào)用之后样刷,整個(gè)流程就通了,豁然開朗览爵。但是置鼻,之后又為設(shè)置header頭痛。因?yàn)椴皇煜を阎瘢酝耆恢纇eader還區(qū)分soap的header和普通的http的header箕母。而前者,我花了大量的時(shí)間去研究俱济,最終都沒研究出來(因?yàn)榉?wù)端壓根就沒有嘶是,所以以后要問清楚,但是完全不懂ws所以也就不存在清不清楚的問題了)蛛碌,最終才得知是設(shè)置http的header聂喇,這個(gè)就簡(jiǎn)單了,就是處理請(qǐng)求的上下文蔚携,在上下文添加header就可以了希太。
另
-
參考資料:
- 《cxf生成java客戶端 webservice》https://blog.csdn.net/yinkgh/article/details/52472770
- 《webservice之自定義請(qǐng)求頭實(shí)現(xiàn)》https://blog.csdn.net/do_bset_yourself/article/details/79561852
- 《java web service client, adding http headers》https://stackoverflow.com/questions/6666060/java-web-service-client-adding-http-headers
- 《常用網(wǎng)絡(luò)上的webservice地址》
- 《Eclipse根據(jù)wsdl文件自動(dòng)生成webservice client》
- 《webservice到底是什么》
- 《解析webservice》
- 《自己調(diào)用webservice方法總結(jié)(帶請(qǐng)求頭SoapHeader)》