處理 WebService 中的 Map 對象


最近,我們討論了關於 WebService 的相關問題。目前在 Smart 中,可發布兩種類型的 WebService,它們是:SOAP 服務 與 REST 服務,您可以根據需要自由選擇。

今天,我要與大家分享的是,在 WebService 中,處理復雜 Java 數據類型的解決方案。

對於普通的 Java 數據類型、JavaBean、List 而言,SOAP 服務可以完全將其處理(序列化與反序列化),這些都沒有任何問題,但對於 Map 對象而言,似乎就有些麻煩了。

請看下面這個例子:

@WebService(value = "/soap/ProductService", type = WebService.Type.SOAP)
public interface ProductService {

    boolean createProduct(Map<String, Object> productFieldMap);
}

為了創建一個 Product,我們需要傳遞一個 Map<String, Object> 類型的參數。實現該接口應該不難,關鍵是客戶端能否將 Map 對象傳遞過來?

Whatever,我們都要用一個客戶端來驗證一下:

public class ProductServiceSOAPTest {

    private String wsdl = "http://localhost:8080/smart-sample/ws/soap/ProductService";
    private ProductService productService = SOAPHelper.createClient(wsdl, ProductService.class);

    @Test
    public void createProductTest() {
        Map<String, Object> productFieldMap = new HashMap<String, Object>();
        productFieldMap.put("productTypeId", 1);
        productFieldMap.put("name", "1");
        productFieldMap.put("code", "1");
        productFieldMap.put("price", 1);
        productFieldMap.put("description", "1");

        boolean result = productService.createProduct(productFieldMap);
        Assert.assertTrue(result);
    }
}

看來開發一個客戶端也不難,關鍵是我們使用了 SOAPHelper,它為我們創建了一個 ProductService 的代理對象,所以接下來的一切都是那么簡單!

運行一下,看看結果究竟如何吧!

org.apache.cxf.interceptor.Fault: Marshalling Error: java.util.Map is not known to this context
    at org.apache.cxf.jaxb.JAXBEncoderDecoder.marshall(JAXBEncoderDecoder.java:265)
    at org.apache.cxf.jaxb.io.DataWriterImpl.write(DataWriterImpl.java:169)
    at org.apache.cxf.interceptor.AbstractOutDatabindingInterceptor.writeParts(AbstractOutDatabindingInterceptor.java:114)
    at org.apache.cxf.interceptor.BareOutInterceptor.handleMessage(BareOutInterceptor.java:68)
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:272)
    at org.apache.cxf.endpoint.ClientImpl.doInvoke(ClientImpl.java:565)
    at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:474)
    at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:377)
    at org.apache.cxf.endpoint.ClientImpl.invoke(ClientImpl.java:330)
    at org.apache.cxf.frontend.ClientProxy.invokeSync(ClientProxy.java:96)
    at org.apache.cxf.frontend.ClientProxy.invoke(ClientProxy.java:81)
    at com.sun.proxy.$Proxy31.createProduct(Unknown Source)
    at com.smart.sample.test.ProductServiceSOAPTest.createProductTest(ProductServiceSOAPTest.java:41)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:47)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:44)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:271)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:70)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:238)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:63)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:236)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:53)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:309)
    at org.junit.runner.JUnitCore.run(JUnitCore.java:160)
    at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:74)
    at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:202)
    at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:65)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
    at java.lang.reflect.Method.invoke(Method.java:597)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:120)
Caused by: javax.xml.bind.MarshalException
 - with linked exception:
[javax.xml.bind.JAXBException: java.util.Map is not known to this context]
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:326)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.marshal(MarshallerImpl.java:251)
    at javax.xml.bind.helpers.AbstractMarshallerImpl.marshal(AbstractMarshallerImpl.java:75)
    at org.apache.cxf.jaxb.JAXBEncoderDecoder.writeObject(JAXBEncoderDecoder.java:612)
    at org.apache.cxf.jaxb.JAXBEncoderDecoder.marshall(JAXBEncoderDecoder.java:240)
    ... 38 more
Caused by: javax.xml.bind.JAXBException: java.util.Map is not known to this context
    at com.sun.xml.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:247)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.reportError(XMLSerializer.java:262)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl$1.serializeBody(ElementBeanInfoImpl.java:148)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl$1.serializeBody(ElementBeanInfoImpl.java:131)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeBody(ElementBeanInfoImpl.java:333)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeRoot(ElementBeanInfoImpl.java:340)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl.serializeRoot(ElementBeanInfoImpl.java:76)
    at com.sun.xml.bind.v2.runtime.XMLSerializer.childAsRoot(XMLSerializer.java:494)
    at com.sun.xml.bind.v2.runtime.MarshallerImpl.write(MarshallerImpl.java:323)
    ... 42 more
Caused by: javax.xml.bind.JAXBException: java.util.Map is not known to this context
    at com.sun.xml.bind.v2.runtime.JAXBContextImpl.getBeanInfo(JAXBContextImpl.java:624)
    at com.sun.xml.bind.v2.runtime.ElementBeanInfoImpl$1.serializeBody(ElementBeanInfoImpl.java:145)
    ... 48 more
...

異常告訴我們:Marshalling Error: java.util.Map is not known to this context,意思是說,java.util.Map 序列化(Marshalling)錯誤。

看來 SOAP 果無法處理 Map 對象啊!怎么解決呢?

對於 SOAP 而言,確實有些復雜,JDK 的 JAXB 規范為我們提供了一個解決方案。

我們得自定義一個 XmlAdapter(XML 適配器),將 Map 對象轉換為 SOAP 可以處理的對象。

我們做的有兩件事情:

  1. 定義一個 StringObjectMapAdapter 類擴展 javax.xml.bind.annotation.adapters.XmlAdapter,目的是為了轉換 Map<String, Object> 對象。

  2. 使用 javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter 注解,標注需要轉換的 Map 對象。

Come on!

public class StringObjectMapAdapter extends XmlAdapter<StringObjectMapAdapter.Data, Map<String, Object>> {

    @Override
    public Map<String, Object> unmarshal(Data data) throws Exception {
        Map<String, Object> map = new HashMap<String, Object>();
        for (Data.Entry entry : data.getList()) {
            map.put(entry.getKey(), entry.getValue());
        }
        return map;
    }

    @Override
    public Data marshal(Map<String, Object> map) throws Exception {
        Data data = new Data();
        for (Map.Entry<String, Object> entry : map.entrySet()) {
            data.addEntry(entry.getKey(), entry.getValue());
        }
        return data;
    }

    public static class Data {

        private List<Entry> list = new ArrayList<Entry>();

        public void addEntry(String fieldName, Object fieldValue) {
            Entry entry = new Entry();
            entry.setKey(fieldName);
            entry.setValue(fieldValue);
            list.add(entry);
        }

        public List<Entry> getList() {
            return list;
        }

        public void setList(List<Entry> list) {
            this.list = list;
        }

        public static class Entry {

            private String key;
            private Object value;

            public String getKey() {
                return key;
            }

            public void setKey(String key) {
                this.key = key;
            }

            public Object getValue() {
                return value;
            }

            public void setValue(Object value) {
                this.value = value;
            }
        }
    }
}

我們寫類一個 StringObjectMapAdapter 類,讓它繼承 XmlAdapter,只需實現兩個方法即可:

  1. unmarshal:反序列化,將 Data 對象轉為 Map 對象。

  2. marshal:序列化,將 Map 對象轉為 Data 對象。

注意,這里的 Data 可作為 StringObjectMapAdapter 的靜態內部類,當然也可獨立存在。在 Data 類中還有另一個靜態內部類 Entry,它實際上就是 Map 中的若干條目,可將 Map 看做是用一個 List 對 Entry 的包裝,這是我們上面看到的 Data 類。

隨后,我們需要將 StringObjectMapAdapter 作用在 Map<String, Object> 上,只需在方法的參數中使用一個 @XmlJavaTypeAdapter 注解即可實現。

@WebService(value = "/soap/ProductService", type = WebService.Type.SOAP)
public interface ProductService {

    boolean createProduct(@XmlJavaTypeAdapter(StringObjectMapAdapter.class) Map<String, Object> productFieldMap);
}

這樣,再次調用 WebService,就會看到運行成功的信息!

在這個解決方案中比較復雜的就是 StringObjectMapAdapter 了,而且我們要知道,它僅僅能處理 Map<String, Object> 類型的數據而已,對於其它不同泛型的 Map 對象還無能為力,我們只能編寫其它對應的 XxxMapAdapter,確實夠折騰的!

對於 REST 而言,以上這一切都似乎不算什么了,不相信您就往下看把。

先寫一個 REST 服務端:

@Bean
@WebService(value = "/rest/ProductService", type = WebService.Type.REST)
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class ProductService extends BaseService {

    @POST
    @Path("/product")
    @Transaction
    public boolean createProduct(Map<String, Object> productFieldMap) {
        return DataSet.insert(Product.class, productFieldMap);
    }
}

接口免了,直接為 Service 類發布 REST 服務,我們可定義輸入與輸出的數據類型,不妨都為 JSON 吧,當然也可以為 XML。

再寫一個 REST 客戶端:

public class ProductServiceRESTTest {

    private String wadl = "http://localhost:8080/smart-sample/ws/rest/ProductService";
    private ProductService productService = RESTHelper.createClient(wadl, ProductService.class);

    @Test
    public void createProductTest() {
        Map<String, Object> productFieldMap = new HashMap<String, Object>();
        productFieldMap.put("productTypeId", 1);
        productFieldMap.put("name", "1");
        productFieldMap.put("code", "1");
        productFieldMap.put("price", 1);
        productFieldMap.put("description", "1");

        boolean result = productService.createProduct(productFieldMap);
        Assert.assertTrue(result);
    }
}

注意,這里使用的是 RESTHelper 獲取 REST 客戶端代理對象的,而不是 SOAPHelper。此外,我們使用的 WADL,而不是 WSDL。

運行一下,完全正確!

看來在對象序列化方面,REST 確實比 SOAP 要優秀一些。如果實際應用場景中,只能使用 SOAP 那么我們應該盡可能回避 Map 對象,實在不行的話,就只能使用 XmlAdapter 的解決方案了。如果條件允許的話,推薦盡量使用 REST。

或許有些朋友提出質疑,在 Security 方面,REST 也提供了類似 SOAP 那樣的 WS-Security 解決方案嗎?將來有機會再與大家討論這方面的問題吧!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM