引入:java
一般意義上說,SOAP消息就是一個帶字符串的內容包,其實CXF還能夠發送/接收帶附件的SOAP消息,這樣SOAP的消息看起來就以下所示:web
咱們這篇文章就着重講解如何來從客戶端發送帶附件的SOAP消息到服務器Endpoint,而且給出詳細的例子,下篇文章會講解相反過程,如何客戶端接收而且處理來自Endpoint的帶附件的SOAP消息。spring
實踐:apache
咱們假想有這樣一個需求,假設咱們如今有個ProfileManagement系統,用戶須要在客戶端吧本身的Profile上傳到ProfileManagement系統,而且這個Profile中除了包含姓名,年齡外還包含本身的頭像圖片(portrait),因此若是用CXF來作的話,就是要上傳一個帶附件的SOAP消息。服務器
服務端:app
首先,咱們要定義一個VO來表示Profile信息:框架
/** * 這個一個VO,咱們定義了一個Profile類型,它會帶有圖片的附件信息。接下來,咱們會用JAXB框架將其映射爲wsdl文件中的類型定義 */ package com.charles.cxfstudy.server.vo; import javax.activation.DataHandler; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; import javax.xml.bind.annotation.XmlMimeType; import javax.xml.bind.annotation.XmlRootElement; import javax.xml.bind.annotation.XmlType; /** * 咱們定義一個有姓名(name),年齡(age),圖片(portrait)的Profile類 * @author charles.wang * */ @XmlType(name="profile") //注意,這裏必須用下面這行指定XmlAccessType.FIELD,來標註利用JAXB框架在JAVA和XML之間轉換隻關注字段, //而不會關注getter(),不然默認2個都會關注則會發生如下錯誤: //com.sun.xml.bind.v2.runtime.IllegalAnnotationException //Class has two properties of the same name "portrait",我調試了很久才解決這個問題的 @XmlAccessorType(XmlAccessType.FIELD) public class Profile { private String name; private int age; //這個字段是一個圖片資源 @XmlMimeType("application/octet-stream") private DataHandler portrait; private String p_w_picpathFileExtension; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public DataHandler getPortrait() { return portrait; } public void setPortrait(DataHandler portrait) { this.portrait = portrait; } public String getImageFileExtension() { return p_w_picpathFileExtension; } public void setImageFileExtension(String p_w_picpathFileExtension) { this.p_w_picpathFileExtension = p_w_picpathFileExtension; } }
和通常狀況同樣,咱們這裏用了JAXB的註釋,這樣JAXB框架會自動的把咱們的基於JAX-WS的請求/響應參數轉爲XML格式的消息。特別注意有2點:ide
a.對於附件資源,咱們這裏是用了@XmlMimeType("application/octet-stream")來標示,而且其字段類型爲DataHandler,這樣的目的是爲了能讓JAXB框架正確的處理附件資源,其實這裏MIME類型也能夠設爲具體類型,好比 p_w_picpath/jpeg, 可是這種用法更通用。測試
b.須要加@XmlAccessorType(XmlAccessType.FIELD),這樣可使得JAXB只處理字段類型到附件的映射,而不會處理getter()方法,不然會報錯this
com.sun.xml.bind.v2.runtime.IllegalAnnotationException
//Class has two properties of the same name "portrait"
而後,由於咱們用的是JAX-WS的調用方式,因此咱們可能從代碼中看不到真正SOAP消息長什麼樣子,因此咱們這裏定義了一個LogHandler(具體參見http://supercharles888.blog.51cto.com/609344/1361866 ),它能夠把入Endpoint和出Endpoint的SOAP消息進行區分,而且把消息內容打印到服務器的控制檯上:
/** * SOAP Handler能夠用來對SOAP消息進行訪問。 * 這裏演示的是第一種,它必須實現SOAPHandler<SOAPMessageContext>接口 */ package com.charles.cxfstudy.server.handlers; import java.util.Set; import javax.xml.namespace.QName; import javax.xml.soap.SOAPMessage; import javax.xml.ws.handler.MessageContext; import javax.xml.ws.handler.soap.SOAPHandler; import javax.xml.ws.handler.soap.SOAPMessageContext; /** * 記錄進/出Endpoint的消息到控制檯的Handler * * @author charles.wang * */ public class LogHandler implements SOAPHandler<SOAPMessageContext> { /** * 如何去處理SOAP消息的邏輯。 這裏會先判斷消息的類型是入站仍是出站消息,而後把消息寫到標準輸出流 */ public boolean handleMessage(SOAPMessageContext context) { // 先判斷消息來源是入站仍是出站的 // (入站表示是發送到web service站點的消息,出站表示是從web service站點返回的消息) boolean outbound = (Boolean) context .get(MessageContext.MESSAGE_OUTBOUND_PROPERTY); // 若是是出站消息 if (outbound) { System.out.println("這是從Endpoint返回到客戶端的SOAP消息"); } else { System.out.println("這是客戶端的請求SOAP消息"); } SOAPMessage message = context.getMessage(); try { message.writeTo(System.out); System.out.println('\n'); } catch (Exception ex) { System.err.print("Exception occured when handling message"); } return true; } /** * 如何去處理錯誤的SOAP消息 這裏會先打印當前調用的方法,而後從消息上下文中取出消息,而後寫到標準輸出流 */ public boolean handleFault(SOAPMessageContext context) { SOAPMessage message = context.getMessage(); try { message.writeTo(System.out); System.out.println(); } catch (Exception ex) { System.err.print("Exception occured when handling fault message"); } return true; } /** * 這裏沒有資源清理的需求,因此咱們只打印動做到控制檯 */ public void close(MessageContext context) { System.out.println("LogHandler->close(context) method invoked"); } public Set<QName> getHeaders() { return null; } }
咱們定義一個HandlerChain文件,把LogHandler加進去:
<?xml version="1.0" encoding="UTF-8"?> <handler-chains xmlns="http://java.sun.com/xml/ns/javaee"> <handler-chain> <!-- 配置能夠記錄出/入Endpoint消息內容到控制檯的Handler --> <handler> <handler-name>LogHandler</handler-name> <handler-class>com.charles.cxfstudy.server.handlers.LogHandler</handler-class> </handler> </handler-chain> </handler-chains>
如今咱們就開始寫服務器端的處理邏輯了,咱們可讓其從客戶端發過來的帶附件的SOAP消息中提取通常字段和附件,對於附件的圖片(頭像文件),咱們複製到指定位置。
首先,咱們定義SEI,它是個服務接口:
/** * 這是一個web服務接口定義,定義瞭如何對於上傳的Profile進行處理 */ package com.charles.cxfstudy.server.services; import javax.jws.WebParam; import javax.jws.WebService; import com.charles.cxfstudy.server.vo.Profile; /** * @author Administrator * */ @WebService public interface IUploadProfileService { /** * 上傳Profile,而且對於Profile進行處理 */ void uploadProfile(@WebParam(name="profile") Profile profile); }
而後咱們定義SIB,它實現了SEI:
/** * 服務實現類,提供上傳Profile的服務 */ package com.charles.cxfstudy.server.services; import java.io.FileOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import javax.activation.DataHandler; import javax.jws.HandlerChain; import javax.jws.WebParam; import javax.jws.WebService; import com.charles.cxfstudy.server.vo.Profile; /** * @author charles.wang * */ @WebService(endpointInterface = "com.charles.cxfstudy.server.services.IUploadProfileService") @HandlerChain(file="/handler_chains.xml") public class UploadProfileServiceImpl implements IUploadProfileService { /** * 上傳Profile,而且對於Profile進行處理 */ public void uploadProfile(@WebParam(name="profile") Profile profile){ //從參數中得到相關信息 String name = profile.getName(); //姓名 int age = profile.getAge(); //年齡 DataHandler portrait = profile.getPortrait(); //肖像圖片 String p_w_picpathFileExtension = profile.getImageFileExtension(); //肖像圖片的擴展名 try{ //獲取輸入流,來獲取圖片資源 InputStream is = portrait.getInputStream(); //打開一個輸出流,來保存得到的圖片 OutputStream os = new FileOutputStream("d:/tmp/uploadTo/"+name+"."+p_w_picpathFileExtension); //進行復制,把得到的肖像圖片複製到 byte[] b = new byte[10000]; int byteRead=0; while ( (byteRead=is.read(b))!= -1){ os.write(b,0,byteRead); } os.flush(); os.close(); is.close(); }catch(IOException ex){ ex.printStackTrace(); } } }
大致上代碼很容易讀懂(呵呵,我自信我代碼的可讀性),和慣例同樣,咱們用@HandlerChain註解來使得LogHandler能夠做用到當前的服務類上而且打印進出當前服務器類的SOAP消息。
爲了讓MTOM生效(也就是讓服務器端支持對帶附件的SOAP消息處理),咱們必須在當前SIB的bean定義文件中打開MTOM開關,以下的20-22行(在beans.xml中):
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:jaxws="http://cxf.apache.org/jaxws" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://cxf.apache.org/jaxws http://cxf.apache.org/schemas/jaxws.xsd"> <!-- 導入cxf中的spring的一些配置文件,他們都在cxf-<version>.jar文件中 --> <import resource="classpath:META-INF/cxf/cxf.xml" /> <import resource="classpath:META-INF/cxf/cxf-servlet.xml" /> <!-- 這裏是第一個web service,它提供上傳用戶Profile的功能(主要演示發送帶附件的SOAP消息到服務器) --> <jaxws:endpoint id="uploadProfileService" implementor="com.charles.cxfstudy.server.services.UploadProfileServiceImpl" address="/uploadProfile" > <!-- 下面這段註釋在服務器端開啓了MTOM,因此它能夠正確的處理來自客戶端的帶附件的SOAP消息 --> <jaxws:properties> <entry key="mtom-enabled" value="true"/> </jaxws:properties> </jaxws:endpoint> ... </beans>
如今打包完部署應用到服務器上,運行就能夠了。
客戶端:
如今咱們來編寫客戶端,其實很簡單,主要就是封裝一個Profile對象(帶附件),而後調用業務方法進行上傳操做。
/** * 客戶端測試代碼 */ package com.charles.mtom.sendattachedsoap; import java.util.HashMap; import java.util.Map; import javax.activation.DataHandler; import javax.activation.DataSource; import javax.activation.FileDataSource; import org.apache.cxf.jaxws.JaxWsProxyFactoryBean; /** * @author charles.wang * */ public class MainTest { public static void main(String [] args){ JaxWsProxyFactoryBean factory = new JaxWsProxyFactoryBean(); factory.setServiceClass(IUploadProfileService.class); //下面三行代碼:激活客戶端能處理帶附件的SOAP消息的功能: Map<String,Object> props = new HashMap<String,Object>(); props.put("mtom-enabled", Boolean.TRUE); factory.setProperties(props); factory.setAddress("http://localhost:8080/cxf_mtom_service/services/uploadProfile"); //調用業務方法 IUploadProfileService service = (IUploadProfileService) factory.create(); //建立 一個要上傳的Profile對象 Profile profile = new Profile(); profile.setName("Charles"); profile.setAge(28); //下面兩行特別注意如何去吧一個附件附加到請求中的 DataSource source = new FileDataSource("F:/p_w_picpaths/myprofileImage.jpg"); profile.setPortrait(new DataHandler(source)); profile.setImageFileExtension("jpg"); //調用業務方法,發送soap請求 service.uploadProfile(profile); } }
條理也很清楚,這裏特別注意是第23-25行激活了客戶端對MTOM的支持。第36到39行演示瞭如何把一個附件(好比圖片文件)附加到SOAP消息上。
咱們運行例子。
能夠清楚的看到從客戶端發送的SOAP消息和從服務器端返回的SOAP消息,顯然,發送的消息是以p_w_upload的形式附加在SOAP消息上的,符合咱們文章開始的結構示意圖。
咱們去文件系統檢查,果真,客戶端經過調用,把圖片文件從F:/Images/myProfileImage.jpg傳遞給了webservice,而後webservice從SOAP消息中拿到附件,而且更名爲Charles.jpg,而後存儲到了D:/tmp/uploadTo目錄
額外話題:
注意,對於客戶端代碼的開啓MTOM的支持是必不可少的:
//下面三行代碼:激活客戶端能處理帶附件的SOAP消息的功能: Map<String,Object> props = new HashMap<String,Object>(); props.put("mtom-enabled", Boolean.TRUE); factory.setProperties(props);
若是沒有這3行的話,咱們發送的SOAP消息就是一個不帶附件的消息,而是把咱們的資源文件(好比圖片)轉爲Base64,而後把編碼後的內容和普通字段同樣放在SOAP消息中。
好比,當咱們註釋掉上面幾行,再發送請求的時候,其請求的SOAP消息就以下:
這顯然就不是一個帶附件的SOAP消息了,由於Base64編碼很低效,要大量運算,因此若是附件文件很大,那麼轉爲Base64就會花費不少時間,這不是一個很好的選擇。