天天看點

Apache CXF 學習-使用MTOM來從用戶端發送帶附件的SOAP消息到服務端

引入:

通常意義上說,SOAP消息就是一個帶字元串的内容包,其實CXF還可以發送/接收帶附件的SOAP消息,這樣SOAP的消息看起來就如下所示:

Apache CXF 學習-使用MTOM來從用戶端發送帶附件的SOAP消息到服務端

我們這篇文章就着重講解如何來從用戶端發送帶附件的SOAP消息到伺服器Endpoint,并且給出詳細的例子,下篇文章會講解相反過程,如何用戶端接收并且處理來自Endpoint的帶附件的SOAP消息。

實踐:

我們假想有這樣一個需求,假設我們現在有個ProfileManagement系統,使用者需要在用戶端吧自己的Profile上傳到ProfileManagement系統,并且這個Profile中除了包含姓名,年齡外還包含自己的頭像圖檔(portrait),是以如果用CXF來做的話,就是要上傳一個帶附件的SOAP消息。

服務端:

首先,我們要定義一個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點:

a.對于附件資源,我們這裡是用了@XmlMimeType("application/octet-stream")來标示,并且其字段類型為DataHandler,這樣的目的是為了能讓JAXB架構正确的處理附件資源,其實這裡MIME類型也可以設為具體類型,比如 p_w_picpath/jpeg, 但是這種用法更通用。

b.需要加@XmlAccessorType(XmlAccessType.FIELD),這樣可以使得JAXB隻處理字段類型到附件的映射,而不會處理getter()方法,否則會報錯

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消息上的,符合我們文章開始的結構示意圖。

Apache CXF 學習-使用MTOM來從用戶端發送帶附件的SOAP消息到服務端

我們去檔案系統檢查,果然,用戶端通過調用,把圖檔檔案從F:/Images/myProfileImage.jpg傳遞給了webservice,然後webservice從SOAP消息中拿到附件,并且改名為Charles.jpg,然後存儲到了D:/tmp/uploadTo目錄

Apache CXF 學習-使用MTOM來從用戶端發送帶附件的SOAP消息到服務端

額外話題:

注意,對于用戶端代碼的開啟MTOM的支援是必不可少的:

//下面三行代碼:激活用戶端能處理帶附件的SOAP消息的功能:
Map<String,Object> props = new HashMap<String,Object>();
props.put("mtom-enabled", Boolean.TRUE);
factory.setProperties(props);      

如果沒有這3行的話,我們發送的SOAP消息就是一個不帶附件的消息,而是把我們的資源檔案(比如圖檔)轉為Base64,然後把編碼後的内容和普通字段一樣放在SOAP消息中。