本文通过设计和实现一个网上商品查询系统,来展示如何利用缓冲管理、业务代理等设计模式加速Web服务的调用效率。
概要
时至今日,SOAP (Simple Object Access Protocol)已由最初的实验性协议,走进了现实应用中。但是,当在分布式Java应用程序中引入SOAP作为底层通讯协议时,它所带来的繁重的网络开销成为令人头痛的问题。本文将通过设计和实现一个网上商品查询系统,来展示如何利用缓冲管理、业务代理等设计模式加速Web服务的调用效率。
引子
众所周知,Web服务的革命早已开始。SOAP(简单对象访问协议),一种XML RPC消息协议成为了Web服务的底层通信协议。从它的名字就可以知道,它是一种易于定义、易于使用的轻量级协议。SOAP使发送和接收XML,并把它转化为异构语言对象变得容易。
但是,在拥有易用性和灵活性的同时也是要付出效率上的代价的。协议的设计者把效率方面的复杂性留给了应用程序开发者。一个简单的SOAP实现往往会给企业级应用程序带来无状态、运行效率低下等大量的问题。大数据量,频繁的SOAP调用甚至可能会引起网络阻塞。
解决Web服务效率问题的方法,一般分为两个层次:
- 容器层。Java Web服务的服务端一般都是一个Servlet应用程序,我们所说的容器层就是指运行Servlet的Web Server。例如,Apache Axis的服务端就可以运行在Tomcat这个Web Server上。
- 应用程序层。一般是指用户自己编写的代码。这里可以是客户端的代码,也可以是服务器端提供Web服务的代码。解决的方法,一般包括缓冲数据、压缩信息量等等。
下文将会介绍一种基于应用程序层的解决方案。通过缓冲模式,代理模式构造一个对商业逻辑透明的,提高Web服务效率的解决方案。本解决方案同时还支持自动同步缓冲区信息和UI自动更新。
在这里假设读者对SOAP已有一定的认识,并且懂得如何使用Apache Axis创建、发布Web服务。本文将不会描述SOAP的工作原理,但是会提供相关链接,在那里可以得到更多的信息。
Web Service在应用程序中的问题
当开发基于SOAP通信协议的胖客户端Java应用程序时,必须注意三个问题:性能、性能、还是性能。如果客户端程序会经常的访问服务端的相同信息,然而实现方法采取每次访问都要进行Web服务的调用,这样效率肯定很低。但现实是成百上千的客户端应用程序会为了这相同数据同时访问服务器,并且它们的操作仅仅限于浏览,服务器性能下降的会更加明显,甚至瘫痪。初步估计,一次SOAP调用的代价与执行一次关系型数据库的SQL操作相当甚至更大(如果考虑到网络的因素)。如下图:
但是,事实上,通常一个SOAP调用总是伴随着一次SQL操作。所以,一个现实中的SOAP调用的代价包括:网络的延时、Server端CPU操作SOAP的时间和数据库服务器的SQL操作延时。
由上图可见,一次Web服务调用如果不考虑网络的因素,就已经需要将近两倍于数据库SQL操作的时间。这样的效率是肯定无法接受的。因此,有一种解决方法就是尽量减少进行Web服务调用的次数了。正统的解决方案,就是采取缓冲机制。其中,缓冲机制还可以分为3种:在Server端缓冲,在Client端缓冲和不缓冲(有些实际应用中,数据量很小。维护缓冲区比不维护的代价更大)。下文中的例子程序将会根据缓冲管理模式,设计一个简单的应用程序。
具体问题举例 —— 网上商品价格查询系统
简介
这是一个常见的程序,在很多的WebService开发包中,都有类似的例子。如Apache Axis、IBM的ETTK等。本例子,仅仅包括一个提供商品报价的服务类,它只提供了一些很简单的操作,如增加和删除商品信息,获取商品信息等等方法。通过WSDL描述这些操作,并且暴露给用户,使其可以方便的开发自己的客户端程序。本程序有一个特点,支持自动同步数据,准确的说应该是自动同步缓冲区数据。事实上,缓冲技术和自动同步技术并没有任何关系,可以分开使用。最后,本程序还提供了一个简单的GUI客户端程序,可以执行部分Web服务方法。
整体架构
客户端架构
通过代理模式,很好的分离了GUI层和业务层逻辑。又利用监听器模式实现了透明的缓冲机制。该实例程序客户端包括两个主面板(panel)。上方面版(panel)显示所有可用的商品信息,而下方面板让用户可以添加和更新商品的信息。当客户端开始运行,它会利用线程阻塞机制在服务端注册一个数据变更监听器,监听所有的数据更新事件。然后,调用Get All Quotes 服务获取所有的商品信息。当任意一个客户机提交和更新了数据,该客户端的监视线程就会返回,于是一次SOAP调用结束。这同时使到服务器端的数据改变事件被触发,从而主动激活所有被阻塞的客户端监视线程,并且触发它们的缓冲区数据更新的SOAP调用,然后每个客户端又会在服务器端注册新的数据改变监视线程,如此反复。这样,只有更新数据的时候,才会发送SOAP调用,一般的读数据实际上是读缓冲区内的数据,而不会进行代价颇高的Web服务调用,从而提高了速度,又实现了数据自动同步功能。
服务器端架构
在服务端设计中,充分利用接口(Interface)带来的便利。全部用接口来描述功能,彻底的隐藏了具体实现细节。下面介绍,Web服务端设计的小技巧:
根据接口(Interface)来产生你的WSDL文件
之所以要这样做,主要有两个原因:
- 将接口发布为Web服务可以避免将一些不需要的方法发布为Web服务。例如main(String [ ] args)
- 对接口编程可以保证灵活性。你可以提供不同的服务实现类,并且无缝的切换。
Apache Axis为此提供了方便的工具(Java2WSDL、 WSDL2Java),可以通过这些工具迅速的产生整个服务端架构。下面是整个Server端的类关系图。
下面列出了整个演示程序所有的包以及每个包的重要组成类。
demo.productinfo.clientside
demo.productinfo.clientside.businessdelegates
- DataChangeListener: 定义数据改变监听器的行为。
- NotificationServiceDelegate: 调用Web服务的代理类。
- ProductInfoServiceDelegate: 用于调用Web服务的代理类。
demo.productinfo.clientside.cache
- CacheExpiredListener:定义了缓冲过期监听器的行为。
- SimpleCache: 简单缓冲模式的实现类。
demo.productinfo.clientside.soapstub
这个包里面的所有类均由Apache Axis的内置工具Java2WSDL和WSDL2Java产生,这里就不对之进行说明了。
demo.productinfo.exception
- ProductInfoException: 自定义的异常类。通常在Java程序开发过程中,不要把系统底层的异常抛给用户,而应该以更加有意义的异常类取而代之,这就是为什么要有这个类的原因。
demo.productinfo.serverside
在这个包中,通过接口类定义了服务器端所有的行为。其下面的子包都是这些接口的具体实现。
- INotificationService: 定义了通告服务的服务方法。
- IProduct: 定义了商品类的基本行为。
- IProductInfoService: 定义了商品信息服务的方法。
- NotificationServiceSoapBindingSkeleton: 该类是由Apache Axis工具产生的服务端的框架类。
- ProductInfoServiceSoapBindingSkeleton: 该类是由Apache Axis工具产生的服务端的框架类。
demo.productinfo.serverside.product
在这个包内全部是具体的商品类。
- ProductTemplate: 该类是实现了IProduct接口的模板类。它包含了一些最基本的参数和方法。提供给其他的具体商品类继承之用。还有许多的商品类,这里就不一一罗列了。
demo.productinfo.serverside.serviceimplement
在这个包内是所有Web服务的具体实现类。
- NotificationServiceImp: 该类是数据同步服务的具体实现。
- ProductInfoImp: 商品查询服务的具体实现。
- ServerNotificationHandlerImp: 服务端数据同步处理器。
业务逻辑代理模式的实现
根据J2EE系统设计模式,业务逻辑代理通过向表现层隐藏了低层的实现,有效的减低表现层和商务逻辑层的耦合度,例如查找和调用EJB的细节。然而在本程序中,这种业务逻辑代理类对外隐藏了查找、调用Web服务和缓冲数据的复杂细节。
IProductInfoService接口类为客户端提供了关于商品信息的所有服务方法的描述。每个客户端必须自己处理SOAP 调用的查找、调用和缓冲。ProductInfoServiceDelegate类,从它的名字可知,它就是客户端的代理类。(在例子中,这个代理类并没有实现所有的Web服务方法调用,有兴趣的朋友可以自己补充完整)。
ProductInfoServiceDelegate类首先初始化一个IProductInfoService接口的实例,并且通过它来处理一切与底层SOAP相关的操作。然后,实例化一个SimpleCache类的静态实例,通过在所有的ProductInfoServiceDelegate类实例间共享这个实例来处理所有缓冲信息的操作。同时ProductInfoServiceDelegate类还包含一个DataChangeListener引用,用于触发注册的UI控件的界面更新事件,这里只是一个简单的实现,因此,一个ProductInfoServiceDelegate仅仅对应一个UI 控件,可以扩展为一个EventListenerList类,从而一个ProductInfoServiceDelegate可以拥有多个UI监听(但是更新UI又是一件危险的事,要注意只能在UI事件处理线程上刷新UI)。最后,ProductInfoServiceDelegate还实现了CacheExpiredListener接口,使它可以作为ClientNotificationHandler类的监听者,监听缓冲更新事件,以及时触发DataChange事件。
下面是ProductInfoServiceDelegate类的一些关键代码:
/**
*<p>
*Add a staff to server database and update the local cache.
*</p>
*
*@param staff <code>IProdcut</code>
*@return true if this operation is succeed or false for fail.
*/
public boolean addStaff(IProduct staff) {
boolean ret = false;
if(staff != null) {
try {
synchronized(cache) {
log.info("ProductInfo Service Delegate : Add Quote -> ["
+ staff.getName() + "]");
ret = soap.addStaff(staff);
if(ret == true) {
log.info("ProductInfo Service Delegate : Updating Cache for [" +
staff.getName() + "] ...");
cache.addObject(String.valueOf(staff.getSN()), staff);
}
}
}catch(ProductInfoException e) {
log.error("ProductInfoException in addStaff: " + e.getMessage());
}catch(RemoteException e) {
log.error("RemoteException in addStaff: " + e.getMessage());
}
}
return ret;
}
/**
* <p>
* Request the staff object. If we can find it from cache,then we got it and return. If
* not we get it from the remote server and update the local cache.
* </p>
*
* @param sn <code>String</code> - the staff's SN Number
* @return The staff if we got it, or null for fail.
*/
public IProduct getStaff(String sn) {
IProduct staff = null;
log.info(new Date() + "Prodcut Service Delegate : Getting Staff for [" + sn + "] ...");
if(sn != null) {
//Try to get this staff from the cache firstly.
staff = (IProduct)cache.fetchObject(sn);
//If we can not get it from local cache, do it again from the remote server.
if(staff == null) {
try {
synchronized(cache) {
//After the synchronized, we try it again to see if we can get it from cache.
staff = (IProduct)cache.fetchObject(sn);
if (staff != null) {
return (staff);
}
staff = soap.getStaff(sn);
if(staff != null) {
cache.addObject(sn, staff);
log.info("ProductInfo Service Delegate : Updating Cache for ["
+staff.getName() + "] ...");
}
}
}catch(RemoteException e) {
log.error("RemoteException in getStaff: " + e.getMessage());
}
}
}
return staff;
}
/**
* <p>
* Get all of the staffs information.
* </p>
*
* @return <code>java.util.Map</code> the map of all these staffs info.
*/
public Map getAllStaffs() {
log.info( new Date() + "ProductInfo Service Delegate : Getting All staffs...");
if(cache == null || cache.isEmpty()) {
try {
synchronized (cache){
if ( (cache != null) && (!cache.isEmpty())) {
return cache.getAllObjectsHashtable();
}
Map tempMap = soap.getAllStaff();
if(tempMap != null) {
for(Iterator i = tempMap.keySet().iterator(); i.hasNext(); ) {
String sn = (String)i.next();
Object o = tempMap.get(sn);
cache.addObject(sn, o);
}
}
}
}catch(RemoteException e) {
log.error("RemoteException in getAllStaffs: " + e.getMessage());
}
}
return cache.getAllObjectsHashtable();
}
|
下面是操作流程图:
由上可以清楚的看到,使用业务代理模式可以使程序员不必去关心底层的通信细节,而把注意力完全集中在实现商业逻辑上来。
缓冲管理模式的实现
当调用业务代理类的getStaff ()方法时,会首先试图从缓冲区中读取,如果无法找到,再向服务器端发出SOAP请求。当一个客户端程序添加或者更新了商品信息,它会首先调用Web服务方法更新服务器上的信息。这会触发服务端的dataChanged事件,导致所有的客户端都要更新本地缓冲区。然后,它在自己的本地缓冲区添加或更新该记录。其它的服务方法,也是一样的。目标就是将发送SOAP请求的次数减到最少。
根据缓冲管理模式,SimpleCache类应该包含两个方法,分别是addObject()和fetchObject()。一个CacheManager类通过ObjectKey来管理缓冲的信息。在本实现中,业务代理类就相当于CacheManager,这是由于每一个业务代理管理属于自己的SimpleCache。而ProductTemplate类实例就相当于ObjectKey。至于Cache的内部数据结构,出于简单的原因,采用了Hashtable。
现实中的程序里,可以通过扩展SimpleCache类,用更加严谨的内存管理机制轻易的替换这种基于Hashtable的简单实现(例如每次仅仅过期那些很少使用的缓冲信息),可以通过统计信息的点击次数或者复杂度来决定将要清除那些缓冲信息。可以利用Java的特性,如弱引用等来提高JVM的垃圾回收机制对被缓冲对象的清理效率。还可以通过创建一个全局的缓冲实例来统一管理整个客户端应用程序的缓冲信息。
总之,优化的方法是有很多很多的。
下面是,Cache类的简单实现代码:
/**
* Empty constructor
*/
public SimpleCache(){
cache = new Hashtable();
}
/**
* Constructor
* @param size int the nitial size of the cache
*/
public SimpleCache(int size){
cache = new Hashtable(size);
}
/**
* puts the key value pair in the cache based on the storage mechanism.
*
* @param key Object representing the key against which the value will be stored.
* @param Value Object
*/
public synchronized void addObject(Object key, Object value){
if (key != null && value != null){
cache.put(key, value);
}
}
/**
* fetchs the value from the cache based on the key.
*
* @param key Object representing the key against which the value will be found.
* @returns Object
*/
public Object fetchObject(Object key){
if (key == null) {
return null;
}
return cache.get(key);
}
/**
* expire the value object from cache
*
* @param key Object representing the key against which the value will be found.
*/
public synchronized void expire(Object key){
if (key != null) {
cache.remove(key);
}
}
/**
* expire the entire cache
*/
public void expire(){
cache.clear();
}
|
下面的时序图演示了数据是如何被缓冲和缓冲是如何被更新的:
自动数据同步的实现
在这个例子程序中,所有的客户端都是自动同步数据的,也就是说一个客户端程序更新或添加了数据,马上就会在其它所有的客户端中体现出来,这个功能是通过通告处理机制实现的。在这个示例程序中,有两个数据同步处理类,分别是运行在服务器端的ServerNotificationHandlerImp和运行在客户端的ClientNotificationHandler。
下面将详细介绍,在这个程序中是怎样实现数据同步的。
众所周知,基于HTTP协议的SOAP是无法支持双向传播(Bi-directional Communication)的,可是又必须当服务端发生变化时,让所有的客户端程序接收到通告(例如,当其中一个客户端向服务端数据库添加了一条商品信息后,所有其它的客户端都应该收到通告,以更新自己的数据视图)。
根据SOAP 绑定协议可以轻易的将底层的通信协议换成双向传播协议,如BEEP (Blocks Extensible Exchange Protocol)。但是这样将失去HTTP协议的简单性,可扩展性和防火墙可穿透性。
因此,这里采用了阻塞的Web服务调用。为此专门提供了一个Web服务--NotificationService。所有调用该服务的客户端线程都会被服务端的ServerNotificationHandler Singleton类实例的isDataChanged方法阻塞,直到数据改变事件触发,该Web服务调用才会返回。在本例中,DataChanged是由另一个Web服务 --- ProductInfoImp触发的。
下面是ServerNotificationHandler类的两个关键方法:
public synchronized boolean isDataChanged() {
while(true) {
try {
wait();
break;
}
catch (Exception ignored) {
}
}
return true;
}
public synchronized void dataChanged() {
notifyAll();
}
|
在客户端,一个Singleton的ClientNotificationHandler单独运行在一个名为ClientNotificationHandlerThread的线程上。每个客户端都会创建这样一个线程,在这个线程上通过NotificationServiceDelegate代理类调用服务端的INotificationService服务的isDataChanged方法,并且被阻塞,只有当ServerNotificationHandlerImp的dataChanged方法被调用时,执行了Java线程的notifyAll操作才能返回。当该调用返回时,ClientNotificationHandler将注册的本地Cache全部过期,也就是触发了缓冲更新的事件。
下面ClientNotificationHandlerThread类的run()方法:
public void run() {
while (true) {
if ((nsbd != null) && (nsbd.isDataChanged())) {
expireAllCaches();
}
else
{
// Sleep for some time before creating the
// NotificationServiceDelegate.
gotoSleep();
initNotificationServiceBusinessDelegate();
}
}
}
|
下面是通过Apache Axis自带的TCP Monitor监视SOAP Call的截图:
由上图可以清晰的看到,有一个SOAP调用一直没有结束,总是保持Active的状态,这就是对NotificationService的调用。当它返回的时候,必然会触发一个getAllQuotes的SOAP调用,然后又会有一个Active的连接。
这种方法也是有一定代价的。主要体现在它要求每一个客户端都必须起一个线程来等待接收通告,而在服务器端对于每一个注册的客户端连结都有一个线程阻塞的等待发出通告。绝大多数的企业级应用程序服务器都可以游刃有余的对应几百个客户端。但当同时有上千个客户端连结的时候,那又是另一回事了。这时候,只有采用非阻塞调用,ServerNotificationHandler将通知事件存储到队列中去,同时客户端的ClientNotificationHandler周期性的调用NotificationService服务。并且出于演示的原因,这个例子的数据同步模块已经被尽量的简化。真正的实现并不会总是使所有客户端的Cache全部过期。一个更具鲁棒性的设计可以让服务端的ServerNotificationHandlerImp传递需要被更新Cache数据的信息,甚至可以详细到哪一个对象实例需要被更新。这样一来,ClientNotificationHandler可以调用更加有效的代理类方法(如expiredOneCache),来更新局部的Cache信息。
下图展示了数据同步模块的工作流程图:
客户端界面
客户端主要两个功能组件,第一个是StaffInfoViewPanel用于显示最新的商品信息。另一个是DataEditPanel用于更新和添加商品信息。这些GUI组件都是通过封装的很好的ProductInfoServiceDelegate代理类来与后台通信的。
下面是运行时界面截图:
可以清楚的看出,当一个客户端程序更新后台数据库的时候,其它的客户端也会实时的更新自己的数据视图。
具体实现中遇到的问题
本文所带的例子,是在Eclipse 2.1IDE上开发完成的,SOAP引擎选用了Apache Axis1.1 Final。这些工具提供了十分快捷的开发方式。下面仅对开发过程中遇到的一些代表性的问题进行简单分析。
-
如何用SOAP传递Hashtable
这个问题在Apache Axis的官方 Mail List上,一直是一个非常火的问题。由于本文的例子也采用Hashtable这种数据结构,所以在此描述一下如何解决这个问题。首先,本例中传送的Hashtable的key为字符串型,value为StockQuote类型,其中StockQuote类是符合标准JavaBean规则的。因此,可以使用Axis自带的BeanSerializer和BeanDeserializer,但是在发布WebService的时候,必须配置服务端的序列化器,如下所示:
<deployment xmlns="http://xml.apache.org/axis/wsdd/"
xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
<service name="ProductInfoService" provider="java:RPC" style="rpc" use="encoded">
<parameter name="wsdlTargetNamespace" value="http://serverside.productinfo.demo"/>
<parameter name="wsdlServiceElement" value="IProductInfoServiceService"/>
<parameter name="wsdlServicePort" value="ProductInfoService"/>
<parameter name="className" value="demo.productinfo.serverside.ProductInfoServiceSoapBindingSkeleton"/>
<parameter name="wsdlPortType" value="IProductInfoService"/>
<parameter name="allowedMethods" value="*"/>
<parameter name="scope" value="Application"/>
<typeMapping
xmlns:ns="http://product.serverside.productinfo.demo"
qname="ns: ProductTemplate"
type="java:demo.productinfo.serverside.product.ProductTemplate"
serializer="org.apache.axis.encoding.ser.BeanSerializerFactory"
deserializer="org.apache.axis.encoding.ser.BeanDeserializerFactory"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/" />
<typeMapping
xmlns:ns="http://exception.productinfo.demo"
qname="ns:ProductInfoException"
type="java:demo.productinfo.exception.ProductInfoException"
serializer="org.apache.axis.encoding.ser.BeanSerializerFactory"
deserializer="org.apache.axis.encoding.ser.BeanDeserializerFactory"
encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
/>
</service>
</deployment>
|
请注意其中的<typeMapping/>节点,必须显示的为ProductTemplate类指定序列化器和反序列化器。否则,服务端会抛出org.xml.sax.SAXException异常。
在客户端,可以直接使用通过Axis自带的WSDL2JAVA工具产生的可序列化的ProductTemplate类,也可以使用自己的ProductTemplate类,只要严格符合JavaBean标准规范。
-
如何调整后台服务的生存周期
由于本例子程序,没有采用数据库,而只是简单的使用Hashtable做为存储商品信息的数据结构。如何保证数据的持久性呢?一般的Web服务只提供无状态的服务,类似于每一次客户端的SOAP调用,服务器端都会创建一个新的类实例来进行处理,你可以把它想象成Stateless Session Bean。为此,Apache Axis支持简单的三个级别的后台服务,分别为:Request、Session和Application。Request级别是默认的选项,它代表每次SOAP请求,都会创建一个新的对象来处理。Session级别代表客户端的每个Session有一个对应的后台对象进行处理。Application级别代表所有客户端的SOAP请求都会共享同一个后台服务实例对象,并且要由程序员自己来保证其数据的同步问题。在本例子中就是采用了Application级的服务。
实施的过程如下:
服务端:在WSDD文件中的每个服务的描述下,添加这样一行:
<parameter name="scope" value="Application"/>
|
-
如何避免Session Timeout
Apache Axis1.1 Final的一个极具争议性的改变就是将原本的无Session Timeout时间限制,默认设置为60秒。这对例子程序有很大的影响,因为例子中每个客户端都需要与Server保持一个长连接,如果有超时设定,那么这个假设将会被破坏。因此,必须取消超时限制。取消方法如下:
FooServiceLocator loc = new FooServiceLocator();
FooService binding = loc.getFooService();
org.apache.axis.client.Stub s = (Stub) binding;
s.setTimeout(0);
// 1 second, in miliseconds
|
对于这个问题,Axis的 Mail List上也处在激烈的争论中,也许在1.2版中就会取消默认60秒的设定。让我们拭目以待吧!
|
|
总结
Web服务虽然已经取得了长足的发展,涌现出大量设计优良的SOAP引擎,如Apache Axis等,它们采用最新的技术在底层效率上已经做出了很大提高,但是由于SOAP协议的一些固有的特性,例如序列化和反序列化效率,传输效率等等问题,整体的效果仍然不尽如人意。但也不需担心,利用设计模式,可以解决大部分的问题。我们有理由相信它美好的未来。
本文中,在设计模式的帮助下,实现了一个透明的客户端缓冲SOAP调用的机制。现实中的程序还必须充分考虑到数据库持久性,全局Cache管理,资源回收,与其他Web服务平台的互操作性,安全性和更加有效的异常处理等问题。
posted on 2009-05-18 18:36
郭鹏 阅读(298)
评论(0) 编辑 收藏 所属分类:
JAVA