簡介
在多系統交互中,有時候需要以Java作為客戶端來調用SOAP方式的WebService服務,本文通過分析不同的調用方式,以Demo的形式,幫助讀者在生產實踐中選擇合適的調用方式。
本文JDK環境為JDK17。
結論
推薦使用Axis2或者Jaxws,以無客戶端的形式來調用WebService。
有客戶端,推薦Maven插件。
有客戶端調用
主要時利用wsdl文檔,自動生成對應的Java代碼來實現
建議在pom文件中,配置對應的Maven插件來實現WebService客戶端代碼的自動生成。
JDK wsimport命令生成(不推薦)
簡介
主要是利用jdk的自帶工具wsimport工具實現,執行命令如下:
wsimport?-s C:\tmp\com?-p?com.example.demo5.wsdl?-encoding?utf-8?http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx?wsdl
優點
通常裝有JDK的電腦或者服務器都可以直接運行,方便生成。
缺點
在實際的運用中wsimport命令會有很多問題,首先只有JDK1.8才支持這個命令,即使能使用,仍然存在一些問題。其次,在JDK17以上沒有自帶這個工具,可能要安裝插件才能使用,但是筆者安裝了一些插件仍然無法使用。
ApacheCXF自動生成(不推薦)
簡介
ApacheCXF通過安裝也可以自動生成對應的WebService客戶端代碼。具體操作可見鏈接。
缺點
需要額外安裝ApacheCXF插件。
Maven插件自動生成(推薦)
簡介
通過spring.io網址的Demo示例,可以配置pom的maven插件,自動生成代碼。
demo獲取鏈接如下:
Getting Started | Consuming a SOAP web service (spring.io)
pom配置示例如下:
<build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><!-- tag::wsdl[] --><plugin><groupId>com.sun.xml.ws</groupId><artifactId>jaxws-maven-plugin</artifactId><version>3.0.0</version><executions><execution><goals><goal>wsimport</goal></goals></execution></executions><configuration><packageName>com.example.consumingwebservice.wsdl</packageName><wsdlUrls>
<!-- <wsdlUrl>http://localhost:8080/ws/countries.wsdl</wsdlUrl>--><wsdlUrl>http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx?wsdl</wsdlUrl></wsdlUrls><sourceDestDir>${sourcesDir}</sourceDestDir><destDir>${classesDir}</destDir><extension>true</extension></configuration></plugin><!-- end::wsdl[] --></plugins></build>
生成代碼如下:
調用方式:
@RequestMapping(value = "/{ip}", method = RequestMethod.GET)public ArrayOfString searchIp(@PathVariable("ip") String ip) {IpAddressSearchWebServiceSoap ipAddressSearchWebServiceSoap = new IpAddressSearchWebService().getIpAddressSearchWebServiceSoap();ArrayOfString response = ipAddressSearchWebServiceSoap.getCountryCityByIp(ip);return response;}
優點
操作簡單,改動小。
缺點
唯一的缺點,也是有客戶端調用普遍存在的,自動生成代碼后,需要重新部署一次。
Springboot集成Git插件實現
通過Springboot集成git插件,可以通過接口的形式來修改maven的wsdlUrls配置,然后推送到git服務,最后觸發Jenkins自動部署。
以Git推送代碼的形式來實現代碼的自動生成,其缺點是,每次根據一份wsdl文件生成完代碼,需要重啟一次服務,但是筆者通過自動配置的形式可以做到一鍵部署。
其中觸發Jenkins自動部署,可以通過git的配置實現,通過訪問特定的url實現。配置好的git部署鏈接如下:
http://192.168.22.22:8080/job/demo_test/build?token=1987654567890hjkoijghfvgjjnmkjkmk
其中token的值可以自動定義,這樣在借助代碼的形式就可以做到一鍵部署。
其實現代碼如下:
package com.example.consumingwebservice;import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.util.Date;import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.transport.CredentialsProvider;
import org.eclipse.jgit.transport.HttpConfig;
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider;public class GitUtil {//private static Log log = LogFactory.getLog(GitUtil.class);private GitUtil() {}public static Git getGit(String uri, CredentialsProvider credentialsProvider, String localDir) throws Exception {Git git = null;if (new File(localDir).exists() ) {git = Git.open(new File(localDir));} else {git = Git.cloneRepository().setCredentialsProvider(credentialsProvider).setURI(uri).setDirectory(new File(localDir)).call();}//設置一下post內存,否則可能會報錯Error writing request body to servergit.getRepository().getConfig().setInt(HttpConfig.HTTP, null, HttpConfig.POST_BUFFER_KEY, 512*1024*1024);return git;}public static CredentialsProvider getCredentialsProvider(String username, String password) {return new UsernamePasswordCredentialsProvider(username, password);}public static Repository getRepository(Git git) {return git.getRepository();}public static void pull(Git git, CredentialsProvider credentialsProvider) throws Exception {git.pull().setRemote("origin").setCredentialsProvider(credentialsProvider).call();}public static void push(Git git, CredentialsProvider credentialsProvider, String filepattern, String message)throws Exception {git.add().addFilepattern(filepattern).call();git.add().setUpdate(true);git.commit().setMessage(message).call();git.push().setCredentialsProvider(credentialsProvider).call();}public static void main(String[] args) throws Exception {String uri = "http://192.168.9.11/test/webservice.git";String username = "343535@qq.com";String password = "xdfetrfrr";CredentialsProvider credentialsProvider = getCredentialsProvider(username, password);String localDir = "C:/tmp/git_test";Git git = getGit(uri, credentialsProvider, localDir);pull(git, credentialsProvider);changeFile(localDir + "/pom.xml");// push(git, credentialsProvider, ".", "提交文件");push(git, credentialsProvider, "pom.xml", "修改pom文件" + new Date());}private static final String newText = " <wsdlUrl>http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx?wsdl</wsdlUrl>\r\n </wsdlUrls>";protected static void changeFile(String filePath) {try {// 讀取文本文件的內容Path path = Paths.get(filePath);String content = Files.readString(path);System.out.println(content);// 替換內容String modifiedContent = content.replace("</wsdlUrls>", newText);// 將修改后的內容寫回文本文件Files.write(path, modifiedContent.getBytes(), StandardOpenOption.WRITE);System.out.println("文本文件內容已成功修改!");} catch (IOException e) {System.out.println("修改文本文件內容時出現錯誤:" + e.getMessage());}}}
?如要實現流程圖的規劃,可以后臺通過http的get請求上文的git部署鏈接,實現接口的自動部署。
無客戶端調用
也就是不需要按wsdl的格式來生成對應的Java代碼,原理時通過構建xml的形式來訪問WebService。
這里推薦使用Axis2或者Jaxws的方式來調用,二者各有優劣。
Axis調用(不推薦)
簡介
通過pom引入axis依賴,實現無客戶端訪問,所需依賴如下:
<dependency>
<groupId>axis</groupId>
<artifactId>axis</artifactId>
<version>1.4</version>
</dependency>
代碼實現如下:
public static void main(String[] args){try {String nameSpac = "http://WebXml.com.cn/";URL url = new URL("http://ws.webxml.com.cn/WebServices/MobileCodeWS.asmx?wsdl");QName sname = new QName(nameSpac, "MobileCodeWS");QName pname = new QName(nameSpac, "MobileCodeWSSoap");Service service = new Service( url, sname);Call call = (Call)service.createCall(pname);call.setSOAPActionURI(nameSpac + "getMobileCodeInfo");call.setOperationName(new QName(nameSpac, "getMobileCodeInfo")); // 需要請求的方法call.addParameter(new QName(nameSpac, "mobileCode"), XMLType.XSD_STRING, ParameterMode.IN); // 入參call.addParameter(new QName(nameSpac, "userID"), XMLType.XSD_STRING, ParameterMode.IN); // 入參
// call.addParameter("param3", XMLType.SOAP_STRING, ParameterMode.IN); // 入參String param1 = "15932582632"; // 參數String param2 = null; // 參數call.setReturnClass(String.class); // 設置返回值call.setUseSOAPAction(true);Object invoke = call.invoke(new Object[]{param1, param2});// 調用獲取返回值
// Object invoke = call.invoke(new Object[]{});// 調用獲取返回值System.out.println(invoke);}catch (Exception e){e.printStackTrace();}}
優點
較少的代碼量,依賴需要少,實現簡單
缺點
通過筆者的實驗,發現Axis的調用并不穩定,對于不同的接口,有的接口無參數調用可以調通,有參數調用會報錯,有的接口有參數調用可以調通(如例),無參數調用會報錯。
實際上,這個依賴在2006年便沒有維護了,它的功能轉移到了Axis2。
Axis2調用(推薦)
簡介
通過pom引入axis2依賴,實現無客戶端訪問,所需依賴如下:
<dependency>
<groupId>org.apache.axis2</groupId>
<artifactId>axis2-jaxws</artifactId>
<version>1.7.0</version>
</dependency><dependency>
<groupId>org.apache.axis2</groupId>
<artifactId>axis2-adb-codegen</artifactId>
<version>1.7.0</version>
</dependency><dependency>
<groupId>org.apache.axis2</groupId>
<artifactId>axis2-transport-local</artifactId>
<version>1.7.0</version>
</dependency><dependency>
<groupId>org.apache.axiom</groupId>
<artifactId>com.springsource.org.apache.axiom</artifactId>
<version>1.2.5</version>
</dependency>
代碼實現如下:
import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axis2.AxisFault;
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.transport.http.impl.httpclient3.HttpTransportPropertiesImpl;/**** @ClassName: MobileClientDoc* @Description: TODO* 方法二: 應用document方式調用 用ducument方式應用現對繁瑣而靈活。現在用的比較多。因為真正擺脫了我們不想要的耦合* 即使用org.apache.axis2.client.ServiceClient類進行遠程調用web服務,不生成客戶端** @date 2017年11月9日 下午1:27:17**/
public class SoapAxis2Client {private static String requestName = "getCountryCityByIp";public static void ipWS() {try {ServiceClient serviceClient = new ServiceClient();//創建服務地址WebService的URL,注意不是WSDL的URLString url = "http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.asmx";EndpointReference targetEPR = new EndpointReference(url);Options options = serviceClient.getOptions();options.setTo(targetEPR);//確定調用方法(wsdl 命名空間地址 (wsdl文檔中的targetNamespace) 和 方法名稱 的組合)options.setAction("http://WebXml.com.cn/" + requestName);//設置密碼HttpTransportPropertiesImpl.Authenticator auth = new HttpTransportPropertiesImpl.Authenticator();
// auth.setUsername(username); //服務器訪問用戶名
// auth.setPassword(password); //服務器訪問密碼
// options.setProperty(HTTPConstants.AUTHENTICATE, auth);OMFactory fac = OMAbstractFactory.getOMFactory();/** 指定命名空間,參數:* uri--即為wsdl文檔的targetNamespace,命名空間* perfix--可不填*/OMNamespace omNs = fac.createOMNamespace("http://WebXml.com.cn/", "");// 指定方法OMElement method = fac.createOMElement(requestName, omNs);// 指定方法的參數OMElement theIpAddress = fac.createOMElement("theIpAddress", omNs);theIpAddress.setText("111.249.198.56");
// OMElement userID = fac.createOMElement("userID", omNs);
// userID.setText("");method.addChild(theIpAddress);
// method.addChild(userID);method.build();//遠程調用web服務OMElement result = serviceClient.sendReceive(method);//值得注意的是,返回結果就是一段由OMElement對象封裝的xml字符串。String xml = result.cloneOMElement().toString();System.out.println(xml);} catch (AxisFault axisFault) {axisFault.printStackTrace();}}public static void main(String[] args) throws AxisFault {ipWS();}}
優點
代碼量較少,通過配置xml節點實現系統調用,可以設置靈活的調用方式。經過實驗,對各種WebService接口的有參無參調用,都能取得正確的返回結果。
測試結果如下:
<getCountryCityByIpResponse xmlns="http://WebXml.com.cn/"><getCountryCityByIpResult><string>111.249.198.56</string><string>臺灣省 ?</string></getCountryCityByIpResult></getCountryCityByIpResponse>
缺點
所需的pom配置文件較多,且引用不正確較難排查問題,且各個pom之間的版本沖突也需要解決。
Jaxws調用(推薦)
簡介
引入對于的pom配置文件
<dependency><groupId>org.apache.axis2</groupId><artifactId>axis2-jaxws</artifactId><version>1.7.0</version>
</dependency>
這里提前說下,下面代碼大部分來自于csdn作者——LengYouNuan的文章,但是實在找不到對于作者了,提前聲明。
還有它的原始代碼并不能正常運行,會有服務器未能識別 HTTP 頭 SOAPAction 的值的報錯,筆者通過實驗和研究,添加了如下配置,才能正常運行:
//這句話很重要,否則報錯服務器未能識別 HTTP 頭 SOAPAction 的值
dispatch.getRequestContext().put(SOAPACTION_URI_PROPERTY, nameSpace + elementName);
dispatch.getRequestContext().put(SOAPACTION_USE_PROPERTY, true);
由于使用的JDK17,對應的配置和以前不一樣了:
public interface BindingProvider {String USERNAME_PROPERTY = "jakarta.xml.ws.security.auth.username";String PASSWORD_PROPERTY = "jakarta.xml.ws.security.auth.password";String ENDPOINT_ADDRESS_PROPERTY = "jakarta.xml.ws.service.endpoint.address";String SESSION_MAINTAIN_PROPERTY = "jakarta.xml.ws.session.maintain";String SOAPACTION_USE_PROPERTY = "jakarta.xml.ws.soap.http.soapaction.use";String SOAPACTION_URI_PROPERTY = "jakarta.xml.ws.soap.http.soapaction.uri";
......
應該主要是javax和jakarta的區別。
完整可運行代碼如下:
package com.example.consumingwebservice;import com.sun.xml.ws.client.BindingProviderProperties;
import com.sun.xml.ws.developer.JAXWSProperties;
import jakarta.xml.soap.*;
import jakarta.xml.ws.Dispatch;
import jakarta.xml.ws.Service;
import org.w3c.dom.Document;import javax.xml.namespace.QName;
import java.net.URL;
import java.util.HashMap;
import java.util.Map;import static jakarta.xml.ws.BindingProvider.SOAPACTION_URI_PROPERTY;
import static jakarta.xml.ws.BindingProvider.SOAPACTION_USE_PROPERTY;/*** soap方式調用webservice方式客戶端** @author LengYouNuan* @create 2021-05-31 下午2:35*/
public class SoapJaxwsClient {String nameSpace = ""; //wsdl的命名空間String wsdlUrl = ""; //wsdl文檔地址String serviceName = ""; //服務的名字String portName = "";String responseName = ""; //@WebResult:注解上的name值String elementName = ""; //默認是要訪問的方法名 如果@WebMethod屬性name有值 則是該值,實際還是以wsdl文檔為主int timeout = 20000;/*** @param nameSpace* @param wsdlUrl* @param serviceName* @param portName* @param element* @param responseName*/public SoapJaxwsClient(String nameSpace, String wsdlUrl,String serviceName, String portName, String element,String responseName) {this.nameSpace = nameSpace;this.wsdlUrl = wsdlUrl;this.serviceName = serviceName;this.portName = portName;this.elementName = element;this.responseName = responseName;}/*** @param nameSpace* @param wsdlUrl* @param serviceName* @param portName* @param element* @param responseName* @param timeOut 毫秒*/public SoapJaxwsClient(String nameSpace, String wsdlUrl,String serviceName, String portName, String element,String responseName, int timeOut) {this.nameSpace = nameSpace;this.wsdlUrl = wsdlUrl;this.serviceName = serviceName;this.portName = portName;this.elementName = element;this.responseName = responseName;this.timeout = timeOut;}public String sendMessage(HashMap<String, String> inMsg) throws Exception {// 創建URL對象URL url = null;try {url = new URL(wsdlUrl);} catch (Exception e) {e.printStackTrace();return "創建URL對象異常";}// 創建服務(Service)QName sname = new QName(nameSpace, serviceName);Service service = Service.create(url, sname);// 創建Dispatch對象Dispatch<SOAPMessage> dispatch = null;try {dispatch = service.createDispatch(new QName(nameSpace, portName), SOAPMessage.class, Service.Mode.MESSAGE);} catch (Exception e) {e.printStackTrace();return "創建Dispatch對象異常";}// 創建SOAPMessagetry {//這句話很重要,否則報錯服務器未能識別 HTTP 頭 SOAPAction 的值dispatch.getRequestContext().put(SOAPACTION_URI_PROPERTY, nameSpace + elementName);dispatch.getRequestContext().put(SOAPACTION_USE_PROPERTY, true);SOAPMessage msg = MessageFactory.newInstance(SOAPConstants.SOAP_1_1_PROTOCOL).createMessage();msg.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, "UTF-8");SOAPEnvelope envelope = msg.getSOAPPart().getEnvelope();// 創建SOAPHeader(不是必需)// SOAPHeader header = envelope.getHeader();// if (header == null)// header = envelope.addHeader();// QName hname = new QName(nameSpace, "username", "nn");// header.addHeaderElement(hname).setValue("huoyangege");// 創建SOAPBodySOAPBody body = envelope.getBody();QName ename = new QName(nameSpace, elementName, "");SOAPBodyElement ele = body.addBodyElement(ename);// 增加Body元素和值for (Map.Entry<String, String> entry : inMsg.entrySet()) {ele.addChildElement(new QName(nameSpace, entry.getKey())).setValue(entry.getValue());}// 超時設置dispatch.getRequestContext().put(BindingProviderProperties.CONNECT_TIMEOUT, timeout);dispatch.getRequestContext().put(JAXWSProperties.REQUEST_TIMEOUT, timeout);// 通過Dispatch傳遞消息,會返回響應消息SOAPMessage response = dispatch.invoke(msg);// 響應消息處理,將響應的消息轉換為doc對象Document doc = response.getSOAPPart().getEnvelope().getBody().extractContentAsDocument();String ret = doc.getElementsByTagName(responseName).item(0).getTextContent();return ret;} catch (Exception e) {e.printStackTrace();throw e;}}public static void main(String[] args) throws Exception {
// SoapClient soapClient=new SoapClient("http://spring.io/guides/gs-producing-web-service","http://localhost:8080/ws/countries.wsdl",
// "CountriesPortService","CountriesPortSoap11","getCountry",
// "getCountryResponse",
// 2000);SoapJaxwsClient soapClient = new SoapJaxwsClient("http://WebXml.com.cn/", "http://ws.webxml.com.cn/WebServices/MobileCodeWS.asmx?wsdl","MobileCodeWS", "MobileCodeWSSoap", "getDatabaseInfo","getDatabaseInfoResponse",2000);
// SoapClient soapClient=new SoapClient("http://WebXml.com.cn/","http://www.webxml.com.cn/WebServices/IpAddressSearchWebService.wsdl",
// "IpAddressSearchWebService","IpAddressSearchWebServiceSoap","getCountryCityByIp",
// "getCountryCityByIpResult",
// 2000);//封裝請求參數HashMap<String, String> msg = new HashMap<>();
// msg.put("theIpAddress","111.249.198.56");
// msg.put("mobileCode","18702750020");
// msg.put("userID","");String s = soapClient.sendMessage(msg);System.out.println(s);}
}
測試結果:
優點
pom配置簡單,無須解決各種版本依賴的問題。
缺點
可以看到Jaxws的調用和Axis2一樣,都具有較高的靈活性,都可以自定義xml的節點數據。
所不同的是,它的調用代碼稍顯繁瑣,但如果在生產中,有良好的封裝,這應該不是問題。
小結
對于SOAP方式WebService的調用,有客戶端的調用,推薦maven插件自動生成代碼的形式,唯一的缺點是需要重新部署一次。
對于無客戶端的調用,推薦Axis2或者Jaxws的形式,考慮到二者實現其實各有優劣,有需要的讀者可以自行甄別選用。