級別: 初級
陳亞強, 高級軟件工程師
2003 年 6 月 01 日
本文是本系列的第二篇,前一篇我們介紹了JAXM的開發技術,在這篇里,我將結合前一篇的案例來討論JAXM Web服務的構架和設計模式。
本文是本系列的第二篇, 前一篇我們介紹了JAXM的開發技術,在這篇里,我將結合前一篇的案例來討論JAXM Web服務的構架和設計模式。
閱讀本文前您需要以下的知識和工具:
JavaTM Web Services Developer Pack 1.1,并且會使用初步使用;
至少會使用一種EJB容器來開發、部署EJB,并且會在客戶端調用EJB組件;
對J2EE平臺有比較全面的了解;
對UML比較熟悉。
本文的參考資料見 參考資料
本文的全部代碼在這里 下載
系統構架
消息傳送方式
JAXM使用SOAP消息在消息客戶端和服務端傳送消息,消息有兩種類型,一種是SOAPConnection,另一種是ProviderConnection。前者是一種點對點的消息發送模型,后者需要通過MessageProvider來把消息傳送到目標。它們的消息傳送路徑如圖1所示。
圖1 單向和雙向的消息傳送方式
不使用MessageProvider,可以帶來一些便利,比如:
客戶端可以是一般的J2SE程序(本文講述的案例就是如此);
不需要額外的配置。
但是,不使用MessageProvider也有以下的限制:
只能發送request-response類型的消息;
客戶端只能是客戶端的角色。
本文的案例采用了點對點的消息傳送方式,調用環境如圖2所示。
圖2 JAXM調用環境
整體構架
系統體系結構如圖3所示。
圖3 系統體系結構
上圖的分層模型和J2EE應用程序的分層模型基本一致,不同的是客戶端和JAXM Servlet數據的通信是封裝成SOAP,但是,它也是HTTP的調用。
用例
本案例共有三個用例,它們分別是按名字查找書,按類別查找書,查找所有的書。如圖4所示。
圖4 用例圖
數據模型
在數據庫里,圖書信息用圖5的格式保存。
圖5 數據庫表
為了傳輸數據的方便,減少遠程調用的次數,特別設計了BookVO來代表圖書的信息,BookVO是一個值對象,如圖6所示。
圖6 BookVO值對象類圖
值對象設計模式在J2EE模式中是非常有名且大量使用的設計模式,相信讀者會很熟悉。我們知道,JAXM Servlet和EJB組件之間傳遞數據是通過對象來傳遞的,這個對象就是包含有BookVO實例的java.util.Collection。但是JAXM和客戶端是通過SOAP消息來傳遞的(當然,也可以使用序列化的對象作為附件發送),為了傳輸圖書信息,我們就要定義對應的DTD(或者schema)。針對以上的模型,定義的DTD如例程1所示。
例程1 傳輸圖書信息的格式(book.dtd)
<!ELEMENT books(book*)>
<!ELEMENT book ( name, publisher, price, author+, category, description ) >
<!ELEMENT name (#PCDATA)>
<!ELEMENT publisher (#PCDATA)>
<!ELEMENT price (#PCDATA)>
<!ELEMENT author (#PCDATA)>
<!ELEMENT category (#PCDATA)>
<!ELEMENT description (#PCDATA)>
<!ATTLIST book id CDATA #REQUIRED>
|
業務邏輯
業務層是EJB組件,這里使用了兩個EJB組件,一個是BookServiceFacadeEJB,它是一個有狀態會話Bean,另一個是BookEntityEJB,它是一個實體Bean,代表了BookEntityTable的持久數據。
BookEntityEJB組件類圖如圖7所示。
圖7 BookEntityEJB組件的類圖
其中,isbn是BookEntityEJB組件的主鍵,BookEntityHome定義了幾個find方法,它們是:
findAllBook(),查找所有的書,返回的是由BookEntity組成的Collection;
findByCategory(String category),按類別查找書,返回的是由BookEntity組成的Collection;
findByName(String name),按書名查找書,返回的是一個BookEntity的遠程引用。
BookServiceFacadeEJB是會話門面,JAXM Servlet通過它來和BookEntityEJB交互。它的類圖如下:
圖8 BookServiceFacadeEJB組件的類圖
同樣,它也定義了幾個find方法,但是和BookEntityHome接口不同的是,它返回的是包含了BookVO值對象的Collection,而不是包含了BookEntity遠程引用的Collection。具體的實現細節請參考源代碼。
EJB組件之間的依賴關系如圖9所示。
圖9 EJB組件之間的依賴關系
JAXM服務端
在JAXM 服務端設計了三個服務JAXM Servlet,分別對應了圖4中的三個用例,它們是:
ListAllBook:查找所有的書;
BookDetail:按書名查找某本特定的圖書;
ListByCategory:按類別查找。
它們的建模關系如圖9所示。
圖9 服務端JAXM Servlet的類圖
每個JAXM Servlet都有一個SOAPMessage onMessage(SOAPMessage message)方法,這個方法在它們接收到SOAP消息時調用,可以說是客戶端調用服務端的入口。
客戶端
最終客戶端是一個叫BookClientGUI的圖形界面程序,它并不直接和SOAP消息打交道,它是通過JAXMDelegate類來和服務端JAXM Servlet進行交互的,這里我們簡單列出它的類圖,在接下來的設計模式里將詳細介紹。
圖10 客戶端類圖
在上圖中,請注意到BookBusiness接口,它是BookClientGUI和JAXMDelegate通信的橋梁,這里也體現了面向接口編程的思想。
設計模式
JAXM進行Web服務開發還不是特別普遍,故對它的設計模式的探討還比較少。但是它也有它自己的特點,基本上來說,它的設計模式和J2EE平臺其它組件設計模式是一致的。我們在使用J2EE設計模式時,基本上有以下幾點的考慮:
- 減少遠程調用的次數(使用值對象、值列表組裝器、值對象組裝器模式)
- 降低組件之間、層(Tier)之間的耦合(使用會話門面、業務代表模式)
- 減少服務查找的復雜度(使用服務定位器模式)
- 數據的一致訪問(使用數據訪問對象模式)
- 進行異步通信(使用服務激發器模式)
JAXM也是J2EE平臺的一種技術,它當然可以使用J2EE核心模式中的任何一種,但是它有自己的特點,比如客戶端和服務端是通過SOAP消息進行通信,這個和J2EE平臺的其它組件之間通信是不同的。在JAXM編程中,為了實現數據(這里是SOAP消息)的一致返回,我們可以使用XML業務代表的模式。
JAXM進行編程時,數據傳遞的特點如圖11所示。
圖11 JAXM數據傳輸的特點
從上圖可以看出,客戶端最終要使用的數據是java對象或者Java的基本數據類型,而客戶端和服務端的通信是通過SOAP消息格式來傳輸的;同樣,在服務端,它要調用業務邏輯,也必須使用java對象或者是java基本數據類型。這樣就存在數據的傳輸和數據的使用的矛盾,為了解決這個矛盾,降低層(Tier)之間的耦合度,使數據易于處理,我們可以使用一個數據轉換器來轉換數據。當客戶端要發送數據時,它使用數據轉換器把請求數據轉換成SOAP消息格式;在服務端,它調用了業務邏輯后,為了使數據能在internet上傳輸,它要使用數據轉換器把調用結果封裝成SOAP消息。接下來我們來看怎么處理這個問題。
客戶端模式--JAXM業務代表
在客戶端,通過使用JAXM業務代表,可以降低最終客戶和SOAP消息的耦合度。系統的結構如下。
圖12 JAXM業務代表
JAXM業務代表使用數據轉換器來轉換數據,業務代表直接和Web服務進行交互,它屏蔽了Web服務請求的復雜過程,為客戶端提供易于使用的接口。
此案例中,具體實現的類圖如下:
圖13 客戶端類圖
圖中的JAXMDelegate為JAXM業務代表,它實現了BookBusiness接口,它是此模式的核心,它實現的方法是客戶端可以直接調用的方法。
BookBusiness接口定義了和最終客戶端(BookClientGUI)交互的方法,BookBusiness接口如例程2所示。
例程2 BookBusiness接口
package com.hellking.webservice;
import java.util.Collection;
public interface BookBusiness
{
/**
* @return Collection,查詢所有的書
*/
public Collection getAllBooks();
/**
* @param name
* @return BookVO
*查詢某本特定的書
*/
public BookVO getTheBookDetail(String name);
/**
* @return Collection
*按類別查詢圖書
*/
public Collection getBookByCategory(String category);
}
|
SOAPToBeanEngine是數據轉換器,它負責把具體的SOAP消息轉換成客戶端可以使用的數據。SOAPToBeanEngine實現了DTOEngine接口,我們看DTOEngine接口的具體代碼,如例程3所示。
例程3 DTOEngine接口
package com.hellking.webservice;
import java.util.Collection;
import javax.xml.soap.SOAPMessage;
public interface DTOEngine
{
public void build();//把SOAP Message轉換成Bean(對象)的具體代碼。
public Collection getResult();//返回轉換結果。
public void init(SOAPMessage msg);//初始化,msg為要轉換的信息。
}
|
以上三個方法是每個把SOAP消息轉換成Java對象的數據轉換器(如SOAPToBeanEngine)都必須實現的方法。實際上,這里的SOAPToBeanEngine只能轉換BookVO相關的信息,如果要把此模式的框架設計得更加完美,還需進一步抽象,比如抽象到只要傳入相關的值對象類(BookVO.class)和SOAP Message就能轉換成對應的Bean結果集。
當客戶端(BookClientGUI)發出一請求時,它調用JAXMDelegate對應的方法,JAXMDelegate根據請求構造對應的SOAP消息,然后把消息發送到服務端(如ListByCategory Servlet),服務端根據客戶的請求做出對應的處理,并把處理結果返回到JAXMDelegate,JAXMDelegate使用SOAPToBeanEngine把返回的SOAP Message轉化成Java對象(如值Bean),最后返回給客戶端(BookClientGUI),BookClientGUI再把獲得的數據進行處理后顯示。
假如客戶端要按類別查詢圖書信息,我們來看下一個順序圖,如圖14所示。
圖14 按類別查詢圖書客戶端順序圖
JAXMDelegate是此模式的核心,我們來看一下它的代碼,如例程4所示。
例程4 JAXMDelegate的部分代碼
package com.hellking.webservice;
import java.net.*;
import java.io.*;
import java.util.*;
import javax.xml.soap.*;
public class JAXMDelegate implements BookBusiness
{
SOAPConnection con =null;//到服務端的連接
EndpointLocator locator=new EndpointLocator();//服務定位器
Collection allbook;//cache
DTOEngine dto;//數據轉換對象
public JAXMDelegate()
{
allbook=new ArrayList();
dto=new SOAPToBeanEngine();
try
{
SOAPConnectionFactory scf = SOAPConnectionFactory.newInstance();
con = scf.createConnection();//生成一個用于SOAP調用的連接
}
catch(Exception e)
{
e.printStackTrace();
}
}
//構造SOAP消息
public SOAPMessage createMessage(String target,String name,String category)
{
try
{
MessageFactory mf = MessageFactory.newInstance();
SOAPMessage msg = mf.createMessage();
SOAPPart sp = msg.getSOAPPart();
SOAPEnvelope envelope = sp.getEnvelope();
//SOAPHeader hdr = envelope.createSOAPHeader();
//AttachmentPart attachment = message.createAttachmentPart();
SOAPBody body = envelope.getBody();
Name bodyName=envelope.createName(
"books",target,"http://hellking.webservice.com");
SOAPBodyElement gpp=body.addBodyElement(bodyName);
//如果category不空,那么構建一個按類別查找的SOAP消息
if(category!=null)
{
gpp.addChildElement("category").addTextNode(category);
}
//如果name不空,那么構建一個按圖書名字查找的SOAP消息
if(name!=null)
{
gpp.addChildElement("name").addTextNode(name);
}
msg.saveChanges();
//msg.writeTo(new FileOutputStream("e://d.msg"));
return msg;
}
catch(Exception e)
{
e.printStackTrace();
return null;//一般要進行錯誤處理,這里省略
}
}
//按類別查找書,業務代表方法
public Collection getBookByCategory(String category)
{
try
{
SOAPMessage msg=createMessage("GetBookByCategory",null,category);
String endpoint=locator.getBookByCategory_Endpoint();
SOAPMessage reply=con.call(msg,new URL(endpoint));
reply.writeTo(System.out);
dto.init(reply);//初始化數據轉換器
return dto.getResult();//返回數據轉換結果
}
catch(Exception ex)
{
ex.printStackTrace();
return null; //一般要進行錯誤處理,這里省略。
}
}
//查詢所有的圖書,業務代表方法
public Collection getAllBooks()
{
/**
*allbook為JAXMDelegate的cache,由于Web服務調用代價比較高,
*故使用它來減少不必要的遠程調用。如果allbook為空,那么調用對應的Web服務
*來獲得數據,并且把調用結果保存在allbook中,如果不為空,那么直接返回allbook
*中的數據。
*/
if(allbook.size==0)
{
try
{
SOAPMessage msg=createMessage("GetAllBooks",null,null);
String endpoint=locator.getAllBooks_Endpoint();
SOAPMessage reply=con.call(msg,new URL(endpoint));
reply.writeTo(new FileOutputStream("e://out.msg"));
dto.init(reply); //初始化數據轉換器
Collection re=dto.getResult(); //獲得轉換結果
allbook=re;
return re; //返回數據轉換結果
}
catch(Exception e)
{
e.printStackTrace();
return null; //一般要進行錯誤處理,這里省略
}
}
else
return allbook;
}
//按圖書名字查找某本圖書,業務代表方法
public BookVO getTheBookDetail(String name)
{
try
{
SOAPMessage msg=createMessage("GetBookDetail",name,null);
String endpoint=locator.getTheBookDetail_Endpoint();
SOAPMessage reply=con.call(msg,new URL(endpoint));
reply.writeTo(System.out);
dto.init(reply); //初始化數據轉換器
Collection ret=dto.getResult();
if(ret.size()==1)
{
return (BookVO)ret.iterator().next();
}
else
return null;
}
catch(Exception e)
{
e.printStackTrace();
return null; //一般要進行錯誤處理,這里省略
}
}
}
|
 |
服務端模式
服務端的模式和客戶端的模式基本一樣,只是處理過程相反。服務端從客戶端接收到SOAP消息后,然后讀取參數,調用對應的業務方法,然后使用SOAPToBeanEngine來把調用的結果轉換成SOAP消息返回。
如圖15所示是相應的數據轉換模型。
圖15所示是相應的數據轉換模型
在服務端,數據轉換器負責把對象轉換成SOAP消息,這里和客戶端是相反的。服務端類圖如下。
圖16 服務端類圖
在圖16中,OTDEngine接口定義了把Bean轉換成SOAP消息的方法,如例程5所示。
例程5 OTDEngine接口定義的方法
package com.hellking.webservice;
import javax.xml.soap.*;
import java.util.Collection;
public interface OTDEngine
{
public void build();//構造SOAP消息
public SOAPMessage getResult();//返回結果
public void init(Collection c,String type);//初始化
}
|
OTDEngine定義了把Bean轉換成SOAP消息需要的方法:build()、init()、getResult()。
XMLBusinessDelegate是此模式的核心,它調用業務邏輯,并且使用BeanToSOAPEngine來轉換結果。我們來看它的部分代碼,如例程6所示。
例程6 XMLBusinessDelegate部分代碼
package com.hellking.webservice;
import javax.naming.*;
import com.hellking.webservice.ejb.*;
import java.util.*;
import java.rmi.*;
import javax.xml.messaging.*;
import javax.xml.soap.*;
public class XMLBusinessDelegate
{
InitialContext init=null;
BookServiceFacadeHome facadeHome;
OTDEngine otd;
public XMLBusinessDelegate()throws NamingException
{
init=this.getInitialContext();
otd=new BeanToSOAPEngine();
}
public static InitialContext getInitialContext()
throws javax.naming.NamingException
{
Properties p = new Properties();
//… p.put(XXX,XXX)
return new javax.naming.InitialContext(p);
}
//查找所有的圖書
public SOAPMessage listAllBook()
{
try
{
//查找業務組件à調用業務邏輯à構造SOAP消息à返回消息。
Object objref = init.lookup("ejb/bookservicefacade");
facadeHome = (BookServiceFacadeHome)
javax.rmi.PortableRemoteObject.narrow(objref, BookServiceFacadeHome.class);
Collection result=facadeHome.create().getAllBook();
System.out.println(result.size());
otd.init(result,"GetAllBooks");//初始化BeanToSOAPEngine
SOAPMessage ret=otd.getResult();//獲得結果
return ret;
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
//按Category查找圖書
public SOAPMessage listByCategory(String category)
{
try
{
//查找業務組件à調用業務邏輯à構造SOAP消息à返回消息。
Object objref = init.lookup("ejb/bookservicefacade");
facadeHome = (BookServiceFacadeHome)
javax.rmi.PortableRemoteObject.narrow(objref, BookServiceFacadeHome.class);
Collection result=facadeHome.create().findByCategory(category);
otd.init(result,"GetBookByCategory");//初始化BeanToSOAPEngine
SOAPMessage ret=otd.getResult();//獲得結果
return ret; //返回結果
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
//查詢某本特定的圖書。
public SOAPMessage getBookDetail(String name)
{
try
{
//查找業務組件à調用業務邏輯à構造SOAP消息à返回消息。
Object objref = init.lookup("ejb/bookservicefacade");
facadeHome = (BookServiceFacadeHome)
javax.rmi.PortableRemoteObject.narrow(objref, BookServiceFacadeHome.class);
Collection result=facadeHome.create().getBookDetail(name);
otd.init(result,"GetBookDetail");
SOAPMessage ret=otd.getResult();
return ret;
}
catch(Exception e)
{
e.printStackTrace();
return null;
}
}
…
}
|
假如客戶端傳來要按類別查詢圖書信息,ListByCategory Servlet將調用XMLBusinessDelegate 的listByCategory(String category)方法,XMLBusinessDelegate查找BookServiceFacadeHome接口,生成BookServiceFacade應用,調用getBookDetail(name);方法,然后初始化OTDEngine,最后調用getResult()方法來返回結果。順序圖如圖17所示。
圖17 按類別查找圖書的服務端順序圖
總結
本篇結合具體的案例介紹了JAXM Web服務開發的體系結構和設計模式。數據轉換在設計中占有很大的分量,總的來說,從客戶端發出的數據要經過以下途徑:
java數據類型àSOAP請求消息àjava數據類型à業務邏輯返回的java數據類型àSOAP相應消息àjava數據類型
業務代表模型在以上數據轉換和業務處理起著重要的作用。