UML软件工程组织

JAXM开发Web服务的构架和模式
陈亚强 (cyqcims@mail.tsinghua.edu.cn)
北京华园天一科技有限公司高级软件工程师  (选自IBM中国)

本文是本系列的第二篇,前一篇我们介绍了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和他联系。

 

 

版权所有:UML软件工程组织