级别: 初级
陈亚强, 高级软件工程师
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数据类型
业务代表模型在以上数据转换和业务处理起着重要的作用。