本文是本系列的第二篇,前一篇我们介绍了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数据类型
业务代表模型在以上数据转换和业务处理起着重要的作用。
下一步
介绍了JAXM的Web服务开发后,下一篇将是使用JAX-RPC来开发Web服务了,到时候我们可以结合两者特点来做一个比较。
参考资料
关于作者
陈亚强:北京华园天一科技有限公司高级软件工程师,擅长J2EE技术,曾参与多个J2EE项目的设计和开发,对Web服务有很大的兴趣并且有一定的项目经验。热爱学习,喜欢新技术。即将由电子工业出版社出版的《J2EE企业应用开发》正在最终定稿阶段,目前正从事J2EE方面的开发和J2EE
Web服务方面的图书写作。您可以通过 cyqcims@mail.tsinghua.edu.cn和他联系。 |
|