理解 SOAP
現在已經安裝了軟件,接下來可以開始著手處理實際的 Web 服務了。正如我在其他類型的 Web 服務中提到的,您可以選擇多種格式。在本系列中,將使用 SOAP。
進行傳遞的所有這些消息都基于可擴展標記語言(Extensible Markup Language,XML)。如果完全不熟悉 XML,在深入了解各個 Web 服務主題前,真的應該進行一些相關研究。不過,以下提供了繼續學習本教程所需的基本知識。
XML 是一種“標記語言”,即給出了一種提供實際內容的附加信息的方式。此信息以“標記”的形式提供,這些標記用于指示“元素”。例如,考慮一下清單 5 中所示的簡單 XML 文檔。
清單 5. 包含基本內容的 XML 文件
<article articleId="88271" categoryId="classifieds" subcategoryId="forsale"> <articleHeadline>Fun, fun, fun</articleHeadline> <articleText>Vintage 1963 T-Bird. Less than 300 miles. Driven by my daughter until I took it away. Serious inquires only. 555-3264 after 7 PM.</articleText> </article> |
請留意此文本中的幾個值得注意的地方。首先,這是文本。這就使其可以供任何人閱讀,或在其中包含關于任何事物的內容。其次,標記使用 > 和 < 指示,開始標記具有一個名稱,并可能帶有各種屬性(如文章 ID),而結束標記以反斜杠 (/) 表示。元素必須為自包含的,并進行了恰當嵌套。也就是說,不能使用與清單 6 所示類似的 XML 文檔。
清單 6. 無效 XML 文檔示例
<article articleId="88271" categoryId="classifieds" subcategoryId="forsale">
<articleHeadline>Fun, fun, fun
<articleText></articleHeadline>Vintage 1963 T-Bird.
Less than 300 miles. Driven by my daughter until I
took it away. Serious inquires only. 555-3264 after
7 PM.</articleText>
</article>
|
XML 還提供了將內容劃分為不同“命名空間”的方法,以便由應用程序對其進行不同的處理。例如,SOAP 消息可能與以下的清單 7 類似。
清單 7. 示例 SOAP 消息
<?xml version='1.0' ?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> <env:Header> </env:Header> <env:Body> <cms:getNumberOfArticles xmlns:cms="http://www.daily-moon.com/cms"> <cms:category>classifieds</cms:category> <cms:subcategory>forsale</cms:subcategory> </cms:getNumberOfArticles> </env:Body> </env:Envelope> |
不要擔心消息的實際結構,但要注意存在兩種不同的“前綴”,每個前綴與特定的命名空間對應。在這種情況下,我們是為了將 SOAP“信封”與實際的有效負載進行區分。
再次說明,關于 XML 有很多需要學習,但這些只是本教程需要了解的基礎知識。
![]() ![]() |
![]()
|
Web 服務消息的基本單元是實際的 SOAP 信封。這是包含處理消息所必需的所有信息的 XML 文檔(請參見清單 8)。
清單 8. 示例 SOAP 信封
<?xml version='1.0' ?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> <env:Header> </env:Header> <env:Body> </env:Body> </env:Envelope> |
在本例中,獲得了一個簡單的 Envelope
,其命名空間指定為 SOAP 1.2 版本。其中包含兩個子元素 Header
和 Body
。讓我們了解一下這兩個子元素所起的作用。
![]() ![]() |
![]()
|
SOAP 消息中的 Header
用于提供有關消息本身的信息,與用于應用程序的信息相對。例如,Header
可以包括路由信息,像清單 9 中的示例類似。
清單 9. Header 中的路由信息
<?xml version='1.0' ?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> <env:Header> <wsa:ReplyTo xmlns:wsa= "http://schemas.xmlSOAP.org/ws/2004/08/addressing"> <wsa:Address> http://schemas.xmlSOAP.org/ws/2004/08/addressing/role/anonymous </wsa:Address> </wsa:ReplyTo> <wsa:From> <wsa:Address> http://localhost:8080/axis2/services/MyService</wsa:Address> </wsa:From> <wsa:MessageID>ECE5B3F187F29D28BC11433905662036</wsa:MessageID> </env:Header> <env:Body> </env:Body> </env:Envelope> |
本例中有一個 WS-Addressing 元素,其中包含有關消息將送達何處以及應將應答送達何處的信息。Header
可包含關于消息本身的所有類型的消息。事實上,SOAP 規范中使用了大量篇幅說明哪些元素可以放入 Header
以及應由“SOAP 中間層”如何對其進行處理。也就是說,SOAP 規范并不假定消息將直接從一個點傳遞到另一個點(從客戶機到服務器)。規范考慮了 SOAP 消息在送達最終目的地的過程中可能實際由多個中間層處理的情況,很清楚地說明了中間層應如何對待在 Header
中找到的信息。不過,對此的討論不在本教程的范圍之內。因此,目前只要知道 Header
可以提供許許多多的功能(如果您需要)即可。
接下來讓我們看看實際的有效負載。
![]() ![]() |
![]()
|
發送 SOAP 消息時,都是有目的性的。您在嘗試告訴接收者執行某種操作,或嘗試向服務器傳遞相關信息。此信息稱為“有效負載”。有效負載位于 Envelope
的 Body
中。它還具有自己的命名空間,在本例中其命名空間與內容管理系統對應。在此情況下,可以完全隨意地選擇命名空間。只需要與 SOAP 命名空間相異即可(請參見清單 10)。
清單 10. 有效負載示例
<?xml version='1.0' ?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> <env:Header> ... </env:Header> <env:Body> <cms:addArticle xmlns:cms="http://www.daily-moon.com/cms"> <cms:category>classifieds</category> <cms:subcategory>forsale</cms:subcategory> <cms:articleHeadline></cms:articleHeadline> <cms:articleText>Vintage 1963 T-Bird. Less than 300 miles. Driven by my daughter until I took it away. Serious inquires only. 555-3264 after 7 PM.</cms:articleText> </cms:addArticle> </env:Body> </env:Envelope> |
在此例中,有效負載很簡單,其中包含將文章添加到 Daily Moon 的內容管理系統的指令。
如何設計有效負載的選擇過程將涉及到樣式和編碼的內容。
![]() ![]() |
![]()
|
本系列教程的第 2 部分將更深入地了解此主題的內容(該部分討論 Web 服務描述語言——WSDL),但在創建應用程序時,您將需要確定要發送和接收的實際有效負載的結構。為此,讓我們花點時間討論一下編程樣式和編碼。
簡單來說,有兩種不同的主流 SOA 消息編程樣式。第一種是 RPC 樣式,基于使用 SOAP 消息創建遠程過程調用(Remote Procedure Call)的概念。在此樣式中,基本思路是在向服務器發送命令(如“添加文章”),并將該命令的參數(如要添加的文章和應該添加到的類別)作為整個方法的子元素包含在其中,如清單 11 中所示。
清單 11. RPC 樣式
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> <env:Header> |
RPC 樣式的替代方法將數據直接作為 SOAP 體的內容處理,并在應用服務器對消息進行路由的信息中包含有關其所屬的過程或函數的信息。(請參見清單 12)。
清單 12. 將數據作為 SOAP 體中的內容
<env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> <env:Header> </env:Header> <env:Body> <cms:addArticle xmlns:cms="http://www.daily-moon.com/cms"> <cms:category>classifieds</category> <cms:subcategory>forsale</cms:subcategory> <cms:articleHeadline></cms:articleHeadline> <cms:articleText>Vintage 1963 T-Bird. Less than 300 miles. Driven by my daughter until I took it away. Serious inquires only. 555-3264 after 7 PM.</cms:articleText> </cms:addArticle> </env:Body> </env:Envelope> |
RPC 樣式的一個變體就是與上面看到的 RPC/literal 相對的 RPC/encoded。在這種情況下,消息中包含類型信息,如清單 13 中所示。
清單 13. RPC/encoded 示例
<?xml version='1.0' ?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> <env:Header> </env:Header> <env:Body> <cms:addArticle xmlns:cms="http://www.daily-moon.com/cms"> <cms:category xsi:type="xsd:string">classifieds</category> <cms:subcategory xsi:type="xsd:string">forsale </cms:subcategory> <cms:articleHeadline xsi:type="xsd:string" /> <cms:articleText xsi:type="xsd:string">Vintage 1963 T-Bird. Less than 300 miles. Driven by my daughter until I took it away. Serious inquires only. 555-3264 after 7 PM.</cms:articleText> </cms:addArticle> </env:Body> </env:Envelope> |
第二個樣式稱為 document/literal 樣式,即將相應的數據直接添加到消息中,如清單 14 中所示。
清單 14. Document/literal 樣式示例
<?xml version='1.0' ?> <env:Envelope xmlns:env="http://www.w3.org/2003/05/SOAP-envelope"> <env:Header> </env:Header> <env:Body> <category>classifieds</category> <subcategory>forsale</subcategory> <articleHeadline></articleHeadline> <articleText>Vintage 1963 T-Bird. Less than 300 miles. Driven by my daughter until I took it away. Serious inquires only. 555-3264 after 7 PM.</articleText> </env:Body> </env:Envelope> |
在這種情況下,消息本身并不包含有關數據所提交到的進程的信息,此工作由路由軟件進行。例如,所有對特定 URL 或端點的調用都可能指向特定的操作。另外,從技術上講,可以使用 document/encoded 樣式,但目前還沒有人這樣做,因此可以將其忽略。
在每個樣式中都涉及到不同的折衷,本系列的第 2 部分將進一步對此進行詳細討論。不過,務必知道還有第三種樣式“document wrapped”,并未正式地確定此樣式,但由于各種互操作性原因而大受歡迎。在此情況下,有效負載的內容包裝為單個元素,但元素并不據數據所屬的過程或函數進行命名。從肉眼來看,這些消息幾乎與 RPC/literal 消息完全相同。
![]() ![]() |
![]()
|
談到發送消息,您有很多選擇,可以發送請求并等待響應,發送請求但不等待響應,發送請求并在到達最終的目的地前通過多個中間層。但就實質而言,只有兩個選擇:
- 請求/響應:在請求/響應模式種,以 SOAP 消息的形式發送請求,然后直接等待發送回響應。請求可以為同步的,也可以是異步的。
- 單向消息傳遞:這種情況也稱為“Fire and Forget”方法,發送請求但并不等待響應。可以在僅傳遞信息時或并不關心接收者對此如何響應時使用此方法。
現在,請注意并沒有使用術語“客戶機”和“服務器”。之所以這樣,是因為這些消息交換模式幾乎可以用于創建與上面提到的方法類似的任意數量的不同備選方法。例如,可以發送一條請求,然后依靠接收者對其進行處理,并在將來完成應完成的工作時發送一條消息。為此,將使用多個單向消息的組合,因此談“客戶機”和“服務器”并不合理,因為每個消息都有其接收方和發送方,所謂的客戶機和服務器的位置會發生對換。
![]() ![]() |
![]()
|
構建 SOAP 客戶機
上面已經對理論進行了介紹,現在讓我們開始構建實際的實現。首先從客戶機開始。
最初出現用于使用 SOAP 消息的 Java API 時,其用途非常特定化。它們的用途相當直接,用于創建 SOAP 消息。需要創建消息、Envelope
、Header
、Body
等等。例如,可以構建“舊式”客戶機來訪問前面安裝的 MyService
服務的 echo
函數(請參見清單 15)。
注意:為了編譯并運行此客戶機,將需要 SAAJ 實現(如原始 Axis 軟件)。可以從 http://ws.apache.org/axis/ 下載 Axis。據說 Axis2 0.95 也包含此實現,但本教程未針對其進行測試。
清單 15. 舊式 SOAP 客戶機
import javax.xml.SOAP.*; import javax.xml.transform.*; import java.io.FileInputStream; import javax.xml.transform.stream.*; import org.w3c.dom.*; public class SendSOAP { public static void main(String args[]) { try { MessageFactory messageFactory = MessageFactory.newInstance(); SOAPMessage message = messageFactory.createMessage(); //Create objects for the message parts SOAPPart SOAPPart = message.getSOAPPart(); SOAPEnvelope envelope = SOAPPart.getEnvelope(); SOAPBody body = envelope.getBody(); SOAPElement bodyElement = body.addChildElement(envelope.createName("echo", "req", "http://localhost:8080/axis2/services/MyService/")); bodyElement.addChildElement("category") .addTextNode("classifieds"); message.saveChanges(); SOAPPart SOAPpartbefore = message.getSOAPPart(); SOAPEnvelope reqenv = SOAPpartbefore.getEnvelope(); System.out.println("REQUEST:"); System.out.println(reqenv.toString()); //Now create the connection SOAPConnectionFactory SOAPConnFactory = SOAPConnectionFactory.newInstance(); SOAPConnection connection = SOAPConnFactory.createConnection(); SOAPMessage reply = connection.call(message, "http://localhost:8080/axis2/services/MyService"); SOAPPart SOAPpart = reply.getSOAPPart(); SOAPEnvelope replyenv = SOAPpart.getEnvelope(); System.out.println("\nRESPONSE:"); System.out.println(replyenv.toString()); connection.close(); } catch (Exception e){ System.out.println(e.getMessage()); } } } |
請注意,您要直接創建 SOAPEnvelope
、SOAPBody
等內容。可以向消息體添加 echo
和 category
等元素。將從其中創建連接,進行調用,同樣也能遍歷 SOAP 消息的結構來獲取實際的內容。如果要運行此客戶機,應該看到與清單 16 中所示類似的響應。
清單 16. 當時的客戶機
REQUEST: <SOAPenv:Envelope xmlns:SOAPenv= "http://schemas.xmlSOAP.org/SOAP/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> <SOAPenv:Body> <req:echo xmlns:req= "http://localhost:8080/axis2/services/MyService/"> <req:category>classifieds</req:category> </req:echo> </SOAPenv:Body> </SOAPenv:Envelope> RESPONSE: <SOAPenv:Envelope xmlns:SOAPenv= "http://schemas.xmlSOAP.org/SOAP/envelope/" xmlns:wsa= "http://schemas.xmlSOAP.org/ws/2004/08/addressing"> <SOAPenv:Header> <wsa:ReplyTo> <wsa:Address> http://schemas.xmlSOAP.org/ws/2004/08/addressing/role/anonymous </wsa:Address> </wsa:ReplyTo> <wsa:From> <wsa:Address> http://localhost:8080/axis2/services/MyService</wsa:Address> </wsa:From> <wsa:MessageID>ECE5B3F187F29D28BC11433905662036</wsa:MessageID> </SOAPenv:Header> <SOAPenv:Body> <req:echo xmlns:req= "http://localhost:8080/axis2/services/MyService/"> <req:category>classifieds</req:category> </req:echo> </SOAPenv:Body> </SOAPenv:Envelope> |
echo
服務所進行的所有工作實際就是對其所接收到的請求進行應答,通過這一點可以很好地了解舊式處理方法與新式處理方法間的差別。接下來讓我們看看二者的差異。
![]() ![]() |
![]()
|
現在越來越趨向于對程序員隱藏使用基于 XML 的 Web 服務消息的復雜性。為此進行了大量的工作,其中大部分的目標都是希望盡可能讓 Web 服務編程與任何其他體系結構的編程工作一樣進行。
在 Axis2 中,實際上并不僅限于此。Axis2 引入了一種全新的方式來使用表示 SOAP 消息的 XML(盡管表面看來與使用文檔對象模型類似)。AxIs 對象模型(Axis Object Model,AXIOM)進行了一系列更改,但我暫時將僅提一下其對消息的信息集的關注,消息集是元素和屬性中包含的實際信息,而不是通常看到的序列化版本。不過,更為重要的是,Axis2 將為您處理 SOAP 信封,從而可以將精力放在構建有效負載上(或者,如果是實際的服務,則是分析有效負載和創建響應)。接下來讓我們看看如何進行此工作。
![]() ![]() |
![]()
|
要開始創建客戶機,請確保 Axis2 lib 目錄中的所有 *.jar 文件——指 Standard 分發版,而不是 War 分發版——都在您的 CLASSPATH 上,并創建名為 ClassifiedClient
的新類。創建有效負載,如清單 17 中所示。
17. 有效負載
import org.apache.axis2.addressing.EndpointReference;
import org.apache.axis2.client.Options;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.om.OMElement;
import javax.xml.stream.XMLOutputFactory;
import javax.xml.stream.XMLStreamException;
import java.io.StringWriter;
import org.apache.axis2.om.OMAbstractFactory;
import org.apache.axis2.SOAP.SOAPFactory;
import org.apache.axis2.om.OMFactory;
import org.apache.axis2.om.OMNamespace;
public class ClassifiedClient {
public static OMElement getEchoOMElement() {
SOAPFactory fac = OMAbstractFactory.getSOAP12Factory();
OMNamespace omNs = fac.createOMNamespace(
"http://daily-moon.com/cms", "cms");
OMElement method = fac.createOMElement("echo", omNs);
OMElement value = fac.createOMElement("category", omNs);
value.addChild(fac.createText(value, "classifieds"));
method.addChild(value);
return method;
}
public static void main(String[] args) {
try {
OMElement payload = ClassifiedClient.getEchoOMElement();
} catch (Exception e) { //(XMLStreamException e) {
System.out.println(e.toString());
}
}
}
|
首先,創建工廠和命名空間,并使用其創建各個元素。在本例中,您將創建與前面示例完全相同的元素,因為將再次使用此客戶機訪問 echo
函數。(稍后將對其進行更改,以訪問真正的服務。)
接下來,將創建請求。
![]() ![]() |
![]()
|
下一步將創建實際的請求。同樣,這方面也體現出技術隨時間的發展。這里不會直接將請求發送到 URL,而要設置“端點引用”,如清單 18 中所示。
清單 18. 請求中的端點引用
... public class ClassifiedClient { private static EndpointReference targetEPR = new EndpointReference( "http://localhost:8080/axis2/services/MyService"); public static OMElement getEchoOMElement() { ... } public static void main(String[] args) { try { OMElement payload = ClassifiedClient.getEchoOMElement(); Options options = new Options(); options.setTo(targetEPR); options.setTransportInProtocol(Constants.TRANSPORT_HTTP); } catch (Exception e) { //(XMLStreamException e) { System.out.println(e.toString()); } } } |
對 WS-Addressing 的全面討論不在本教程的范疇之內,但完全可以說端點引用包含消息將定向到的 URL,還可以有選擇地包含其他信息,如應答地址和其他資源屬性等。
首先,為請求創建 Options,以便為請求設置 EPR 和其他信息(如打算使用的傳輸協議)。完成了這些后,就可以實際發送請求了。
![]() ![]() |
![]()
|
完成了設置后,就可以發送請求了。對于 Axis 的 0.94 版,首選的方式是通過 ServiceClient
類發送消息,如清單 19 中所示。
清單 19. 發送請求
... public static void main(String[] args) { try { OMElement payload = ClassifiedClient.getEchoOMElement(); Options options = new Options(); options.setTo(targetEPR); options.setTransportInProtocol(Constants.TRANSPORT_HTTP); ServiceClient sender = new ServiceClient(); sender.setOptions(options); OMElement result = sender.sendReceive(payload); } catch (Exception e) { //(XMLStreamException e) { System.out.println(e.toString()); } } } |
將創建 ServiceClient
對象,并為其設置前面創建的 Options
。然后就可以直接發送消息了。由于希望得到響應,因此將使用 sendReceive()
方法(該方法用于 in/out 類型的消息)。將在本教程的單向服務部分討論單向消息的信息。sendReceive()
方法將在客戶機接收到實際響應時將其返回。
![]() ![]() |
![]()
|
實際上,sendReceive()
并不會返回整個響應,而僅返回有效負載。如果將結果輸出到命令行,應該清楚地看到清單 20 中的內容。
清單 20. sendReceive 輸出
... sender.setOptions(options); OMElement result = sender.sendReceive(payload); System.out.println(result.toString()); } catch (Exception e) { //(XMLStreamException e) { System.out.println(e.toString()); } } } |
運行此客戶機將獲得清單 21 中所示的響應。
清單 21. sendReceive 響應
<cms:echo xmlns:cms="http://daily-moon.com/cms"><cms:catego ry>classifieds</cms:category></cms:echo> |
當然,接收到此數據后,可以使用其進行任何工作。接下來,我們將構建實際的 getNumberofArticles()
服務。
![]() ![]() |
![]()
|