- 浏览: 420150 次
- 性别:
- 来自: 广州
-
文章分类
最新评论
-
liyuanhoa_:
...
struts2.0中struts.xml配置文件详解 -
chenmingde:
...
Velocity应用(一) -
weizhikai_ai:
第二十六,当一个线程进入一个对象的一个synchronized ...
Java常见面试题(含答案) -
Aurora_lr:
...
Spring宠物商店学习笔记(一) - -
zs911zs:
all copy from http://www.iteye ...
Mule入门文档
在本文中,作者通过一个Web Service访问的实例,具体描述了SOA应用中所遇到的一系列具体问题,并描述如何利用IoC和AOP等技术进行代码重构,从而构建结构更加良好、灵活的SOA应用。
1.引言
SOA是一种构造分布式系统的方法,它将业务应用功能以服务的形式提供出来,以便更好的复用、组装和与外部系统集成,从而降低开发成本,提高开发效率。SOA的目标是为企业构建一个灵活,可扩展的IT基础架构来更好地支持随需应变的商务应用。
随着SOA技术和产品的不断成熟,现在越来越多的用户开始了解并认同SOA的理念,但对SOA项目的实施还缺乏信心。其主要原因是:SOA应用开发还相对比较复杂。
一年多来,本文作者所在的部门已经从事了许多国内外的SOA项目的实施和支持工作,积累了许多SOA应用开发经验。我们希望能够通过一系列的文章与读者分享这些想法,帮助您更好地构建SOA应用。
本文将从Web Service调用入手,在解决一系列具体问题的过程中,使用IoC (Inversion of Control) 和AOP (Aspect- Oriented Programming) 等方法重构Web Service的访问代码,使得业务逻辑与Web Service访问解耦,为您提供一个更加灵活和易于扩展的访问模式。
Spring是一个流行的轻量级容器,对IoC和AOP提供了良好的支持。本文为您提供了一个基于Spring的实现供您下载学习。示例代码工程使用Eclipse3.1/3.02和JDK1.4开发, 您还需要Spring 1.2.5和Axis1.3提供的支持。详细的下载信息请参见参考资源部分。
回页首
2.Web Service调用
Web Service是目前实现SOA应用的一项基本的,适用的技术,它为服务的访问提供了一个被广泛接受的开放标准。为了便于说明问题,我们将使用XMethods 网站(http://www.xmethods.net/)发布的货币兑换服务作为示例。并针对JAX-RPC 1.1,说明如何编写Web Service 的调用代码。
2.1 示例说明
http://xmethods.net 作为最早推出Web Service实际示例的网站,提供了很多优秀的Web Service 样例。其中有一个汇率计算服务,可以返回两个国家之间的货币兑换比例。获取该服务的详细信息,请参考该服务的服务描述文档(获取WSDL 文档) 。在此就不具体解析该服务描述文档了。读者可以从WSDL2Java生成的接口中了解该服务的用法:
public interface CurrencyExchangePortType extends java.rmi.Remote {
public float getRate(String country1, String country2) throws java.rmi.RemoteException;
}
2.2 客户端调用方法
JAX-RPC作为Java平台的RPC服务调用标准接口,为Web Service客户端调用提供了3种方法,分别是DII,动态代理,和静态Stub。 DII(Dynamic Invocation Interface)采用直接调用方式,可以在程序中设置诸多的调用属性,使用较为灵活,但是调用过程却相对繁琐复杂,易造成代码膨胀且可重用性低,每次调用不同的Web Service都要重复进行大量编码。
JAX-RPC中动态代理(Dynamic Proxy)的方法实现对Web Service的动态调用,可以在运行时根据用户定义的Client端接口创建适配对象。从而避免了直接操作底层的接口,减少了客户端的冗余,屏蔽了调用相关的复杂性。
使用静态Stub和Service Locator是目前最常用的调用方式。JAX-RPC使用静态的Stub方式包装对底层接口的调用,从而提供一种更为简便的调用方式。使用该方式需要利用支持环境(比如Axis)所提供的工具根据WSDL预生成Web Service客户端的实现代码。因此如果服务的WSDL发生变化,就必须重新生成新的客户端代码并进行重新部署。
为了更详细的了解静态Stub的调用方式,您可以将示例代码的WebServiceClient.jar导入到您现有Eclipse工作区之中。
客户端生成代码包括如下4个类:如图 1 所示:
在上图中包括的几个类中:
CurrencyExchangePortType:服务端点接口,定义了Web Service的方法签名。
CurrencyExchangeService:Service接口,定义了获取服务端点接口的方法。
CurrencyExchangeServiceLocator:ServiceLocator类,实现了Service接口。
CurrencyExchangeBindingStub: Stub实现类,实现了服务端点接口,封装了对Web Service访问的底层逻辑。
使用Stub调用Web Service的过程也非常简单,读者可以参考清单 1:
清单 1:Web Service 调用代码示例
try {
//创建ServiceLocator
CurrencyExchangeServiceLocator locator = new
CurrencyExchangeServiceLocator();
//设定端点地址
URL endPointAddress = new URL("http://services.xmethods.net:80/soap");
//创建Stub实例
CurrencyExchangePortType stub =
locator.getCurrencyExchangePort(endPointAddress);
//设定超时为120秒
((CurrencyExchangeBindingStub)stub).setTimeout(120000);
//调用Web Service计算人民币与美元的汇率
float newPrice = stub.getRate("China", "USA") * 100;
} catch (MalformedURLException mex) {
//...
} catch (ServiceException sex) {
//...
} catch (RemoteException rex) {
//...
}
回页首
3.重构Web Service调用代码
3.1 实例代码中的"坏味道"
上面的基于Service Locator的Web Service访问代码虽然简单但暴露出以下几个问题:
1.访问Web Service所需的配置代码被嵌入应用逻辑之中
在Web Service调用中,我们需要设定一系列必要的参数。比如:服务端点地址、用户名/密码、超时设定等等。这些参数在开发和运行环境中都有可能发生变化。我们必须提供一种机制:在环境变化时,不必修改源代码就可以改变Web Service的访问配置。
2 客户端代码与Web Service访问代码绑定
在上面的代码中,业务逻辑与Web Service的Stub创建和配置代码绑定在一起。这也不是一种良好的编程方式。客户端代码只应关心服务的接口,而不应关心服务的实现和访问细节。比如,我们既可以通过Web Service的方式访问远程服务,也可以通过EJB的方式进行访问。访问方式对业务逻辑应该是透明的。
这种分离客户端代码与服务访问代码的方式也有利于测试。这样在开发过程中,负责集成的程序员就可能在远程服务还未完全实现的情况下,基于服务接口编写集成代码,并通过编写POJO(Plain Old Java Object)构建伪服务实现来进行单元测试和模拟运行。这种开发方式对于保证分布式系统代码质量具有重要意义。
因此,为了解决上面的问题我们需要:
1、将Web Service访问的配置管理与代码分离;
2、解除客户端代码与远程服务之间的依赖关系;
3.2 利用IoC模式进行重构代码
我们先介绍在Core J2EE Patterns一书中提到的一种业务层模式:Business Delegate。它所要解决的问题是屏蔽远程服务访问的复杂性。它的主要思想就是将Business Delegate作为远程服务的客户端抽象,隐藏服务访问细节。Business Delegate还可以封装并改变服务调用过程,比如将远程服务调用抛出的异常(例如RemoteException)转换为应用级别的异常类型。
其类图如图 2 所示:
图 2:Business Delegate 模式的类图图解
Business Delegate模式实现很好地实现了客户端与远程访问代码的解耦,但它并不关注Delegate与远程服务之间的解耦。为了更好解决Business Delegate和远程服务之间的依赖关系,并更好地进行配置管理,我们可以用IoC模式来加以解决。
IoC(Inversion of Contro)l意为控制反转,其背后的概念常被表述为"好莱坞法则":"Don't call me, I'll call you." IoC将一部分责任从应用代码交给framework(或者控制器)来做。通过IoC可以实现接口和具体实现的高度分离,降低对象之间的耦合程度。Spring是一个非常流行的IoC容器,它通过配置文件来定义对象的生命周期和依赖关系,并提供了良好的配置管理能力。
现在我们来重构我们的Web Service应用程序,我们首先为Business Delegate定义一个接口类型,它提供了一个应用级组件接口,所有客户端都应通过它来执行汇率计算,而不必关心实现细节,如清单 2 所示:
清单 2:接口定义的代码示例
Public interface CurrencyExchangeManager {
//货币兑换计算
//新价格 = 汇率 * 价格
public float calculate(String country1, String country2, float price)
throws CurrencyExchangeException;
}
Business Delegate的实现非常简单,主要工作是包装汇率计算 Web Service的调用,如清单 3 所示。
清单 3:Business Delegate的代码示例
public class CurrencyExchangeManagerImpl implements CurrencyExchangeManager {
//服务实例
private CurrencyExchangePortType stub;
//获取服务实例
public CurrencyExchangePortType getStub() {
return stub;
}
//设定服务实例
public void setStub(CurrencyExchangePortType stub) {
this.stub = stub;
}
//实现货币兑换
public float calculate(String country1, String country2, float price)
throws CurrencyExchangeException {
try {
//通过Stub调用WebService
float rate = stub.getRate(country1, country2);
return rate * price;
} catch (RemoteException rex) {
throw new CurrencyExchangeException(
"Failed to get exchange rate!", rex);
}
}
}
下面我们需要讨论如何利用Spring的IoC机制,来创建和配置对象,并定义它们的依赖关系。
Spring利用类工厂来创建和配置对象。在Spring框架中,已经为基于JAX-RPC的Web Service调用提供了一个客户端代理的类工厂实现:JaxRpcPortProxyFactoryBean。在配置文件bean.xml中,我们将使用JaxRpcPortProxyFactoryBean来创建和配置Web Service的客户端代理"CurrencyExchangeService",如清单 5 所示。我们还将定义一个名为"CurrencyExchangeManager"的CurrencyExchangeManagerImpl实例,并建立它与CurrencyExchangeService之间的依赖关系。有关Spring 配置和JaxRpcPortProxyFactoryBean的使用细节请参见参考资料。
清单 5:bean.xml的配置文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="CurrencyExchangeService"
class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="serviceInterface">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.
CurrencyExchangePortType</value>
</property>
<property name="wsdlDocumentUrl">
<value>http://www.xmethods.net/sd/2001/CurrencyExchangeService.
wsdl</value>
</property>
<property name="namespaceUri">
<value>http://www.xmethods.net/sd/CurrencyExchangeService.
wsdl</value>
</property>
<property name="serviceName">
<value>CurrencyExchangeService</value>
</property>
<property name="portName">
<value>CurrencyExchangePort</value>
</property>
<property name="endpointAddress">
<value>http://services.xmethods.net:80/soap</value>
</property>
</bean>
<bean id="CurrencyExchangeManager"
class="test.ws.CurrencyExchangeManagerImpl">
<property name="stub">
<ref bean="CurrencyExchangeService"/>
</property>
</bean>
</beans>
最后我们创建一个测试程序来验证我们的代码,如清单6 所示:
清单 6:测试代码
public class Main {
// For test only
public static void main(String[] args) {
// Spring Framework将根据配置文件创建并配置CurrencyExchangeManager实例
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
// 获取CurrencyExchangeManager实例
CurrencyExchangeManager manager = (CurrencyExchangeManager) ctx
.getBean("CurrencyExchangeManager");
try {
System.out.println(manager.calculate("China", "USA", 100));
System.out.println(manager.calculate("China", "Japan", 200));
System.out.println(manager.calculate("China", "USA", 200));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
此时运行测试客户端,等待片刻将会看见测试结果,如清单 7 所示:
清单 7:测试结果。
12.34
2853.26
24.68
注:该结果会随着汇率的变化而出现不同的值。
该程序的类图和顺序图如图3及图4所示:
图 3:示例程序的类图
从上面的类图我们可以看到,我们的测试程序(Main.java)通过Spring框架获取了BusinessDelegate的实例。而且Spring 框架还会根据配置中的依赖关系,在运行时将Web Service的客户端代理" 注射"到CurrencyExchangeManagerImpl实例中,这就是依赖注入(Dependency Injection)。通过这种方式解决了应用逻辑和BusinessDelegate之间的依赖关系,以及BusinessDelegate的实现与远程服务之间的依赖关系,如图 4 所示。
图 4: 示例程序的顺序图
Spring框架提供的ApplicationContext实现会根据配置文件中的描述信息来实现对象生命周期管理,配置管理以及依赖管理等功能。这一切对于应用程序是透明的,应用程序代码只依赖接口进行编程,而无需考虑其它复杂问题。无论是Web Service的配置发生变化,或是改用不同的服务实现时,都不会对客户端应用代码的产生影响。这很好地实现了业务逻辑与Web Service调用之间的解耦。
3.3 构建自己的 Web Service代理工厂
Spring所提供的JaxRpcPortProxyFactoryBean封装了构造Web Service客户端代理的细节,可以通过参数配置来创建Dynamic Proxy和DII类型的Web Service客户端代理。(如果您希望深入了解其实现细节可以参考org.springframework.remoting.jaxrpc包下的源代码。)但由于JaxRpcPortProxyFactoryBean需要使用者对WSDL中Port,Service,名空间等概念有深入的了解;而且如果Web Service使用了复杂数据类型,开发人员需要手工定义类型映射代码。所以JaxRpcPortProxyFactoryBean并不适合Web Service的初学者来使用。
为了进一步简化Web Service代理的创建,并帮助读者更好地理解类工厂在Spring框架下的作用。我们提供了一个基于静态Stub的Web Service客户端代理工厂实现。其核心代码非常简单,就是通过ServiceLocator提供的方法来创建Web Service客户端代理。
其主要代码如清单8所示:
清单8:静态代理工厂的代码
public class WebServiceStubFactoryBean implements FactoryBean,
InitializingBean {
private Class serviceInterface;
private Class serviceLocator;
private Object stub;
…
public void afterPropertiesSet() throws Exception {
//利用serviceLocator和服务接口创建Web Service客户端代理
stub = ((javax.xml.rpc.Service)
serviceLocator.newInstance()).getPort(serviceInterface);
//为Stub设定endpointAddress,usernam, 超时等参数
preparePortStub((javax.xml.rpc.Stub) stub);
}
public Object getObject() {
// 返回客户端代理
return stub;
}
public Class getObjectType() {
// 返回服务接口
return serviceInterface;
}
public boolean isSingleton() {
return true;
}
}
我们需要修改配置文件bean.xml中有关Web Service代理创建的部分,让新的Web Service 代理工厂发挥作用。如清单9所示:
清单9:修改后的bean.xml的配置文件
<bean id="CurrencyExchangeService" class="test.ws.WebServiceStubFactoryBean">
<property name="serviceInterface">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.CurrencyExchangePortType</value>
</property>
<property name="serviceLocator">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.CurrencyExchangeServiceLocator</value>
</property>
<property name="endpointAddress2">
<value>http://services.xmethods.net:80/soap</value>
</property>
<property name="timeout">
<value>120000</value>
</property>
</bean>
得益于Spring框架,虽然我们已经替换了对象的类工厂,却并不需要更改应用代码。通过Spring框架的IoC机制,我们可以完全使用面向接口的编程方式,而将实现的创建、配置和依赖管理交由Spring在运行时完成。即使实现发生了变化,也不需要改变应用程序结构。
回页首
4.新的思考
故事并没有结束,在开发过程中,我们又遇到了一系列关于Web Service调用的问题。
4.1性能
系统性能是分布式应用中的一个重要问题。许多用户都担心由Web Service技术所引入的额外开销是否会影响到产品的性能。随着技术的不断发展,Web Service引擎性能已经有了很大提高,一般来说使用Web Service的系统的性能可以满足绝大部分应用的需求。但在特定情况下,如果系统性能无法满足客户需求,我们首先需要对系统性能进行科学地分析和测定才能定位真正的性能瓶颈。这个问题在上文简单的示例中并不难解决,只需要在Web Service调用前后加入日志代码记录调用时间即可实现。但在实际系统中,比如一个产品目录的Web Service可能提供数十种查询方法,而程序中很多组件都会依赖于该服务提供的查询功能。如果在系统中所有的地方加入性能测定代码,这个工作就变得非常繁琐和困难。我们需要用一种更加优雅的解决方式,在增添新功能的同时并不影响系统代码或结构。
4.2缓存
在项目实践中,一个有效的改善Web Service系统性能的方法就是利用缓存来减少Web Service的重复调用。在具体实现中我们可以采用客户端缓存和服务器端缓存等不同方式,他们具有不同的特点和适用范围。在本文例子中,我们希望实现客户端缓存来提高系统性能。但由于Web Service业务逻辑的差别,我们希望能够为特定的Web Service提供特定的缓存策略,而且这些策略应该是能够被灵活配置的,它们不应于应用程序的逻辑代码耦合在一起。
4.3故障恢复:
对于Web Service应用,系统的可用性也是一个需要考虑的重要问题。在运行时由于网络运行环境的复杂性和不确定性,用户希望能够对Web Service访问提供一定的故障恢复机制:比如重试或者访问备份服务(当系统在调用Web Service失败后,使用备份Web Service的服务地址来继续访问)。这些故障恢复策略应该是可配置的,对应用逻辑透明的。
回页首
5.使用AOP解决SOA应用中的Crosscutting Concern
通过对上边一系列问题的分析,读者也许会发现这些问题并不是Web Service访问的核心问题,但会影响系统中许多不同的组件。而且其中一些问题需要我们能够灵活配置不同的实现策略,因此我们不应该将处理这些问题的代码与应用代码混合。
下面我们将利用AOP(Aspect-Oriented Programming)提供的方法来解决上述的问题。AOP是一种新兴的方法学,它最基本的概念就是关注隔离(Separation of Concern)。AOP提供了一系列的技术使得我们能够从代码中分离那些影响到许多系统模块的crosscutting concerns,并将他们模块化为Aspects。AOP的主要目的仍然是解耦,在分离关注点后,才能将关注点的变更控制一定范围内,增加程序的灵活性,才能使得关注能够根据需求和环境作出随时调整。
我们将利用Spring所提供的AOP功能支持来解决以上问题。这里我们只简单地介绍涉及到的AOP基本概念以及实现,如果您希望更好地了解AOP的概念以及Spring AOP支持的细节请参见参考资料。
Joinpoint 是程序的运行点。在Spring AOP中,一个Joinpoint对应着一个方法调用。
Advice 定义了AOP框架在特定的Joinpoint的处理逻辑。Spring AOP框架通过interceptor方式实现了advice,并且提供了多种advice类型。其中最基本的"around advice"会在一个方法调用之前和之后被执行。
下面我们将利用Spring提供的MethodInterceptor来为Web Service调用实现我们的定义的处理逻辑。
5.1 PerformanceMonitorInterceptor
性能测量是AOP最简单的例子之一,我们可以直接利用Spring提供的实现在bean.xml中声明我们的WebServicePerformanceMonitorInterceptor。
5.2 CacheInterceptor
为了不引入缓存策略的复杂性,我们只提供了一个利用HashMap的简单实现:它利用 Web Service的调用参数列表作为HashMap键值。在Web Service调用之前,首先检查缓存中是否拥有与现在参数列表相同的项,如果有则返回缓存的结果,否则调用Web Service并将<参数列表,结果>记录在HashMap中。在实际应用中,您应该根据具体情况来选择、构造适合Web Service的业务特性的Cache实现,也可以采用成熟的Cache实现。
在下面代码实现中有一个生成Web Service调用主键的小技巧。因为Web Service引擎要求所有调用参数必须是可序列化的,所以我们可以利用Java提供的序列化功能来实现对象的克隆。如清单10所示:
清单10:SimpleCacheInterceptor的代码示例
public class SimpleCacheInterceptor implements MethodInterceptor {
private Map cache = new HashMap();
private Object cloneObject(Object obj) throws Exception {
Object newObj = null;
if (obj != null) {
// 通过序列化/反序列化来克隆对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(obj);
out.flush();
out.close();
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
newObj = in.readObject();
}
return newObj;
}
//基于参数列表数组,生成用于HashMap的键值
public Object generateKey(Object[] args) throws Exception {
Object[] newArgs = (Object[]) cloneObject(args);
List key = Arrays.asList(newArgs);
return key;
}
//实现使用缓存技术的invoke方法
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object result = null;
Object data = null;
Object key = null;
try {
key = generateKey(methodInvocation.getArguments());
data = cache.get(key);
} catch (Exception ex) {
logger.error("Failed to find from the cache", ex);
}
if (data == null) {
//如果Cache中没有缓存结果,调用服务执行生成用于HashMap的键值
result = methodInvocation.proceed();
try {
data = cloneObject(result);
cache.put(key, data);
} catch (Exception ex) {
logger.error("Failed to cache the result!", ex);
}
} else {
result = data;
}
return result;
}
}
5.3 FailoverInterceptor
下面代码提供了一个基于服务备份切换的故障恢复实现,在运行时,如果Interceptor检测到服务调用由于网络故障抛出异常时,它将使用备份服务的端点地址并重新调用。如清单11所示:
清单 11: SimpleFailoverInterceptor的代码示例
public class SimpleFailoverInterceptor implements MethodInterceptor { …
…
//实现支持端点运行时切换的invoke方法
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object result = null;
try {
result = methodInvocation.proceed();
} catch (Throwable ex) {
if (isNetworkFailure(ex)) {
//切换服务端点地址
switchEndPointAddress((Stub) methodInvocation.getThis());
result = methodInvocation.proceed();
} else {
throw ex;
}
}
return result;
}
}
为了支持备份服务切换的功能,我们在WebServicePortProxyFactoryBean中为填加了配置参数"endpointAddress2",它会在创建的Web Service客户端代理对象中记录备份URL。
我们可以在CurrencyExchangeService加入下列参数来试验SimpleFailoverInterceptor的功能。其中第一个端点地址为一个错误的URL。在第一次调用服务时,SimpleFailoverInterceptor会侦测到网络故障的发生,并自动切换使用第二个端点地址继续访问。如清单12所示:
清单12:配置文件种增加的属性
<property name="endpointAddress">
<value>http://localhost/wrong_endpoint_address</value>
</property>
<property name="endpointAddress2">
<value>http://services.xmethods.net:80/soap</value>
</property>
5.4配置文件和运行结果
现在我们需要在Spring配置文件中,为所有interceptor添加定义,并描述如何为CurrencyExchangeService构建AOP Proxy。需要指出的是,我们要在interceptorName列表中声明interceptor链的调用顺序,还要将原有CurrencyExchangeManager引用的stub对象替换为新AOP Proxy。如清单13所示:
清单13:修改后的配置文件片段
<bean id="WebServicePerformanceMonitorInterceptor"
class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor">
<property name="prefix">
<value>Web Service </value>
</property>
<property name="suffix">
<value></value>
</property>
</bean>
<bean id="CacheInterceptor" class="test.ws.SimpleCacheInterceptor"/>
<bean id="FailoverInterceptor" class="test.ws.SimpleFailoverInterceptor"/>
<bean id="CurrencyExchangeProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.
CurrencyExchangePortType</value>
</property>
<property name="target">
<ref local="CurrencyExchangeService"/>
</property>
<property name="interceptorNames">
<list>
<value>WebServicePerformanceMonitorInterceptor</value>
<value>CacheInterceptor</value>
<value>FailoverInterceptor</value>
</list>
</property>
</bean>
<bean id="CurrencyExchangeManager"
class="test.ws.CurrencyExchangeManagerImpl">
<property name="stub">
<ref bean="CurrencyExchangeProxy"/>
</property>
</bean>
这里我们通过为AOP 的ProxyFactoryBean为 Web Service Stub创建了一个AOP代理,并且建立了一个Interceptor链。这样在调用Web Service时,Spring框架会依次调用Interceptor执行。实例执行的顺序图将如图5所示:
图5系统运行顺序图
5.5 Interceptor与JAX-RPC Handler的关系与区别
SOAP Message Handler是JAX-RPC为用户自定义Web Service处理过程提供的一种扩展机制。在处理Web Service请求/响应过程中,Web Service 引擎会根据部署描述中的定义,按照一定的次序调用Handler的处理代码。用户编写的Handler实现可以截获并修改Web Service消息和处理流程,从而实现对Web Service引擎处理行为的定制和增强。
比如,我们可以实现一个服务器端Handler,记录Web Service在受到请求消息和发出响应消息之间的时间间隔来实现对服务器端业务性能的测定。而且我们只需在部署描述中增加Handler声明即可,无需修改任何服务器端代码。
从此可以看出,JAX-RPC Handler与我们在上文中所提供的AOP Interceptor都可以帮助我们的SOA应用程序实现关注分离(Separate Concern)的目标,在不改变应用代码的同时,增强或改变Web Service服务访问的功能。虽然我们可以利用它们实现一些类似的功能,但它们具有着不同的特点和适用范围。
JAX-RPC Handler是Web Service引擎的扩展机制。如果我们需要实现对SOAP消息进行的修改和处理,加入自定义的SOAP Header或对消息内容进行加密,Handler是我们的最佳选择。而AOP是针对对象级别的扩展机制,它更适合对应用层逻辑进行操作。
比如,我们在上文展示的利用AOP实现的CacheInterceptor,它缓存的是Web Service调用参数和结果。而我们也可以通过JAX-RPC Handler实现一个面向SOAP消息的实现,它将缓存Web Service的请求消息和响应消息。这两个实现相比,基于AOP的实现更加简单、直观、快速、对资源消耗也比较小。而面向SOAP消息的实现则更加灵活,对于不采用RPC方式的Web Service访问也能提供支持。
所以在具体的实践过程中,开发人员应该根据具体的需求选择合适的技术,也可以将这两种技术结合使用。
回页首
6.总结
"分而治之"的方法是人们解决复杂问题的一种常见做法。而IoC、AOP等技术都体现了这种思想。通过更好的切分程序逻辑,使得程序结构更加良好,更加富有弹性,易于变化。也使得开发人员可以更加专注于业务逻辑本身,而将一部分其他逻辑交给容器和框架进行处理。
在本文中,我们通过一个Web Service访问的实例,具体描述了SOA应用中所遇到的一系列具体问题,并描述如何利用IoC和AOP等技术进行代码重构,构建更加结构良好、灵活的SOA应用。综上所述,我们可以看到:
1使用IoC框架来实现对象的生命周期管理、配置管理和依赖管理,可以解除业务逻辑对服务调用的依赖关系;
2 使用AOP方法来解决Web Service调用中的crosscutting concerns,将为系统增加新的功能而不必更改应用程序。
3通过IoC和AOP来屏蔽Web Service访问的复杂性,使得开发人员可以更加专注于业务逻辑本身,也使得系统更加稳定和富有弹性。
回页首
下载
描述 名字 大小 下载方法
code sample code.zip 27 KB HTTP
关于下载方法的信息
参考资料
JAX-RPC: http://java.sun.com/xml/jaxrpc/index.html
Spring 参考文档 : http://www.springframework.org/docs/reference/remoting.html
参考文章: SOA adventures, Part 1: Ease Web services invocation with dynamic decoupling
关于AOP:请参考IBM developerWorks上的更多资源。
J2EE核心模式: Core J2EE™ Patterns: Best Practices and Design Strategies, Second Edition By Deepak Alur, John Crupi, Dan Malks
1.引言
SOA是一种构造分布式系统的方法,它将业务应用功能以服务的形式提供出来,以便更好的复用、组装和与外部系统集成,从而降低开发成本,提高开发效率。SOA的目标是为企业构建一个灵活,可扩展的IT基础架构来更好地支持随需应变的商务应用。
随着SOA技术和产品的不断成熟,现在越来越多的用户开始了解并认同SOA的理念,但对SOA项目的实施还缺乏信心。其主要原因是:SOA应用开发还相对比较复杂。
一年多来,本文作者所在的部门已经从事了许多国内外的SOA项目的实施和支持工作,积累了许多SOA应用开发经验。我们希望能够通过一系列的文章与读者分享这些想法,帮助您更好地构建SOA应用。
本文将从Web Service调用入手,在解决一系列具体问题的过程中,使用IoC (Inversion of Control) 和AOP (Aspect- Oriented Programming) 等方法重构Web Service的访问代码,使得业务逻辑与Web Service访问解耦,为您提供一个更加灵活和易于扩展的访问模式。
Spring是一个流行的轻量级容器,对IoC和AOP提供了良好的支持。本文为您提供了一个基于Spring的实现供您下载学习。示例代码工程使用Eclipse3.1/3.02和JDK1.4开发, 您还需要Spring 1.2.5和Axis1.3提供的支持。详细的下载信息请参见参考资源部分。
回页首
2.Web Service调用
Web Service是目前实现SOA应用的一项基本的,适用的技术,它为服务的访问提供了一个被广泛接受的开放标准。为了便于说明问题,我们将使用XMethods 网站(http://www.xmethods.net/)发布的货币兑换服务作为示例。并针对JAX-RPC 1.1,说明如何编写Web Service 的调用代码。
2.1 示例说明
http://xmethods.net 作为最早推出Web Service实际示例的网站,提供了很多优秀的Web Service 样例。其中有一个汇率计算服务,可以返回两个国家之间的货币兑换比例。获取该服务的详细信息,请参考该服务的服务描述文档(获取WSDL 文档) 。在此就不具体解析该服务描述文档了。读者可以从WSDL2Java生成的接口中了解该服务的用法:
public interface CurrencyExchangePortType extends java.rmi.Remote {
public float getRate(String country1, String country2) throws java.rmi.RemoteException;
}
2.2 客户端调用方法
JAX-RPC作为Java平台的RPC服务调用标准接口,为Web Service客户端调用提供了3种方法,分别是DII,动态代理,和静态Stub。 DII(Dynamic Invocation Interface)采用直接调用方式,可以在程序中设置诸多的调用属性,使用较为灵活,但是调用过程却相对繁琐复杂,易造成代码膨胀且可重用性低,每次调用不同的Web Service都要重复进行大量编码。
JAX-RPC中动态代理(Dynamic Proxy)的方法实现对Web Service的动态调用,可以在运行时根据用户定义的Client端接口创建适配对象。从而避免了直接操作底层的接口,减少了客户端的冗余,屏蔽了调用相关的复杂性。
使用静态Stub和Service Locator是目前最常用的调用方式。JAX-RPC使用静态的Stub方式包装对底层接口的调用,从而提供一种更为简便的调用方式。使用该方式需要利用支持环境(比如Axis)所提供的工具根据WSDL预生成Web Service客户端的实现代码。因此如果服务的WSDL发生变化,就必须重新生成新的客户端代码并进行重新部署。
为了更详细的了解静态Stub的调用方式,您可以将示例代码的WebServiceClient.jar导入到您现有Eclipse工作区之中。
客户端生成代码包括如下4个类:如图 1 所示:

在上图中包括的几个类中:
CurrencyExchangePortType:服务端点接口,定义了Web Service的方法签名。
CurrencyExchangeService:Service接口,定义了获取服务端点接口的方法。
CurrencyExchangeServiceLocator:ServiceLocator类,实现了Service接口。
CurrencyExchangeBindingStub: Stub实现类,实现了服务端点接口,封装了对Web Service访问的底层逻辑。
使用Stub调用Web Service的过程也非常简单,读者可以参考清单 1:
清单 1:Web Service 调用代码示例
try {
//创建ServiceLocator
CurrencyExchangeServiceLocator locator = new
CurrencyExchangeServiceLocator();
//设定端点地址
URL endPointAddress = new URL("http://services.xmethods.net:80/soap");
//创建Stub实例
CurrencyExchangePortType stub =
locator.getCurrencyExchangePort(endPointAddress);
//设定超时为120秒
((CurrencyExchangeBindingStub)stub).setTimeout(120000);
//调用Web Service计算人民币与美元的汇率
float newPrice = stub.getRate("China", "USA") * 100;
} catch (MalformedURLException mex) {
//...
} catch (ServiceException sex) {
//...
} catch (RemoteException rex) {
//...
}
回页首
3.重构Web Service调用代码
3.1 实例代码中的"坏味道"
上面的基于Service Locator的Web Service访问代码虽然简单但暴露出以下几个问题:
1.访问Web Service所需的配置代码被嵌入应用逻辑之中
在Web Service调用中,我们需要设定一系列必要的参数。比如:服务端点地址、用户名/密码、超时设定等等。这些参数在开发和运行环境中都有可能发生变化。我们必须提供一种机制:在环境变化时,不必修改源代码就可以改变Web Service的访问配置。
2 客户端代码与Web Service访问代码绑定
在上面的代码中,业务逻辑与Web Service的Stub创建和配置代码绑定在一起。这也不是一种良好的编程方式。客户端代码只应关心服务的接口,而不应关心服务的实现和访问细节。比如,我们既可以通过Web Service的方式访问远程服务,也可以通过EJB的方式进行访问。访问方式对业务逻辑应该是透明的。
这种分离客户端代码与服务访问代码的方式也有利于测试。这样在开发过程中,负责集成的程序员就可能在远程服务还未完全实现的情况下,基于服务接口编写集成代码,并通过编写POJO(Plain Old Java Object)构建伪服务实现来进行单元测试和模拟运行。这种开发方式对于保证分布式系统代码质量具有重要意义。
因此,为了解决上面的问题我们需要:
1、将Web Service访问的配置管理与代码分离;
2、解除客户端代码与远程服务之间的依赖关系;
3.2 利用IoC模式进行重构代码
我们先介绍在Core J2EE Patterns一书中提到的一种业务层模式:Business Delegate。它所要解决的问题是屏蔽远程服务访问的复杂性。它的主要思想就是将Business Delegate作为远程服务的客户端抽象,隐藏服务访问细节。Business Delegate还可以封装并改变服务调用过程,比如将远程服务调用抛出的异常(例如RemoteException)转换为应用级别的异常类型。
其类图如图 2 所示:
图 2:Business Delegate 模式的类图图解

Business Delegate模式实现很好地实现了客户端与远程访问代码的解耦,但它并不关注Delegate与远程服务之间的解耦。为了更好解决Business Delegate和远程服务之间的依赖关系,并更好地进行配置管理,我们可以用IoC模式来加以解决。
IoC(Inversion of Contro)l意为控制反转,其背后的概念常被表述为"好莱坞法则":"Don't call me, I'll call you." IoC将一部分责任从应用代码交给framework(或者控制器)来做。通过IoC可以实现接口和具体实现的高度分离,降低对象之间的耦合程度。Spring是一个非常流行的IoC容器,它通过配置文件来定义对象的生命周期和依赖关系,并提供了良好的配置管理能力。
现在我们来重构我们的Web Service应用程序,我们首先为Business Delegate定义一个接口类型,它提供了一个应用级组件接口,所有客户端都应通过它来执行汇率计算,而不必关心实现细节,如清单 2 所示:
清单 2:接口定义的代码示例
Public interface CurrencyExchangeManager {
//货币兑换计算
//新价格 = 汇率 * 价格
public float calculate(String country1, String country2, float price)
throws CurrencyExchangeException;
}
Business Delegate的实现非常简单,主要工作是包装汇率计算 Web Service的调用,如清单 3 所示。
清单 3:Business Delegate的代码示例
public class CurrencyExchangeManagerImpl implements CurrencyExchangeManager {
//服务实例
private CurrencyExchangePortType stub;
//获取服务实例
public CurrencyExchangePortType getStub() {
return stub;
}
//设定服务实例
public void setStub(CurrencyExchangePortType stub) {
this.stub = stub;
}
//实现货币兑换
public float calculate(String country1, String country2, float price)
throws CurrencyExchangeException {
try {
//通过Stub调用WebService
float rate = stub.getRate(country1, country2);
return rate * price;
} catch (RemoteException rex) {
throw new CurrencyExchangeException(
"Failed to get exchange rate!", rex);
}
}
}
下面我们需要讨论如何利用Spring的IoC机制,来创建和配置对象,并定义它们的依赖关系。
Spring利用类工厂来创建和配置对象。在Spring框架中,已经为基于JAX-RPC的Web Service调用提供了一个客户端代理的类工厂实现:JaxRpcPortProxyFactoryBean。在配置文件bean.xml中,我们将使用JaxRpcPortProxyFactoryBean来创建和配置Web Service的客户端代理"CurrencyExchangeService",如清单 5 所示。我们还将定义一个名为"CurrencyExchangeManager"的CurrencyExchangeManagerImpl实例,并建立它与CurrencyExchangeService之间的依赖关系。有关Spring 配置和JaxRpcPortProxyFactoryBean的使用细节请参见参考资料。
清单 5:bean.xml的配置文件
<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN"
"http://www.springframework.org/dtd/spring-beans.dtd">
<beans>
<bean id="CurrencyExchangeService"
class="org.springframework.remoting.jaxrpc.JaxRpcPortProxyFactoryBean">
<property name="serviceInterface">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.
CurrencyExchangePortType</value>
</property>
<property name="wsdlDocumentUrl">
<value>http://www.xmethods.net/sd/2001/CurrencyExchangeService.
wsdl</value>
</property>
<property name="namespaceUri">
<value>http://www.xmethods.net/sd/CurrencyExchangeService.
wsdl</value>
</property>
<property name="serviceName">
<value>CurrencyExchangeService</value>
</property>
<property name="portName">
<value>CurrencyExchangePort</value>
</property>
<property name="endpointAddress">
<value>http://services.xmethods.net:80/soap</value>
</property>
</bean>
<bean id="CurrencyExchangeManager"
class="test.ws.CurrencyExchangeManagerImpl">
<property name="stub">
<ref bean="CurrencyExchangeService"/>
</property>
</bean>
</beans>
最后我们创建一个测试程序来验证我们的代码,如清单6 所示:
清单 6:测试代码
public class Main {
// For test only
public static void main(String[] args) {
// Spring Framework将根据配置文件创建并配置CurrencyExchangeManager实例
ApplicationContext ctx = new FileSystemXmlApplicationContext("bean.xml");
// 获取CurrencyExchangeManager实例
CurrencyExchangeManager manager = (CurrencyExchangeManager) ctx
.getBean("CurrencyExchangeManager");
try {
System.out.println(manager.calculate("China", "USA", 100));
System.out.println(manager.calculate("China", "Japan", 200));
System.out.println(manager.calculate("China", "USA", 200));
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
此时运行测试客户端,等待片刻将会看见测试结果,如清单 7 所示:
清单 7:测试结果。
12.34
2853.26
24.68
注:该结果会随着汇率的变化而出现不同的值。
该程序的类图和顺序图如图3及图4所示:
图 3:示例程序的类图

从上面的类图我们可以看到,我们的测试程序(Main.java)通过Spring框架获取了BusinessDelegate的实例。而且Spring 框架还会根据配置中的依赖关系,在运行时将Web Service的客户端代理" 注射"到CurrencyExchangeManagerImpl实例中,这就是依赖注入(Dependency Injection)。通过这种方式解决了应用逻辑和BusinessDelegate之间的依赖关系,以及BusinessDelegate的实现与远程服务之间的依赖关系,如图 4 所示。
图 4: 示例程序的顺序图

Spring框架提供的ApplicationContext实现会根据配置文件中的描述信息来实现对象生命周期管理,配置管理以及依赖管理等功能。这一切对于应用程序是透明的,应用程序代码只依赖接口进行编程,而无需考虑其它复杂问题。无论是Web Service的配置发生变化,或是改用不同的服务实现时,都不会对客户端应用代码的产生影响。这很好地实现了业务逻辑与Web Service调用之间的解耦。
3.3 构建自己的 Web Service代理工厂
Spring所提供的JaxRpcPortProxyFactoryBean封装了构造Web Service客户端代理的细节,可以通过参数配置来创建Dynamic Proxy和DII类型的Web Service客户端代理。(如果您希望深入了解其实现细节可以参考org.springframework.remoting.jaxrpc包下的源代码。)但由于JaxRpcPortProxyFactoryBean需要使用者对WSDL中Port,Service,名空间等概念有深入的了解;而且如果Web Service使用了复杂数据类型,开发人员需要手工定义类型映射代码。所以JaxRpcPortProxyFactoryBean并不适合Web Service的初学者来使用。
为了进一步简化Web Service代理的创建,并帮助读者更好地理解类工厂在Spring框架下的作用。我们提供了一个基于静态Stub的Web Service客户端代理工厂实现。其核心代码非常简单,就是通过ServiceLocator提供的方法来创建Web Service客户端代理。
其主要代码如清单8所示:
清单8:静态代理工厂的代码
public class WebServiceStubFactoryBean implements FactoryBean,
InitializingBean {
private Class serviceInterface;
private Class serviceLocator;
private Object stub;
…
public void afterPropertiesSet() throws Exception {
//利用serviceLocator和服务接口创建Web Service客户端代理
stub = ((javax.xml.rpc.Service)
serviceLocator.newInstance()).getPort(serviceInterface);
//为Stub设定endpointAddress,usernam, 超时等参数
preparePortStub((javax.xml.rpc.Stub) stub);
}
public Object getObject() {
// 返回客户端代理
return stub;
}
public Class getObjectType() {
// 返回服务接口
return serviceInterface;
}
public boolean isSingleton() {
return true;
}
}
我们需要修改配置文件bean.xml中有关Web Service代理创建的部分,让新的Web Service 代理工厂发挥作用。如清单9所示:
清单9:修改后的bean.xml的配置文件
<bean id="CurrencyExchangeService" class="test.ws.WebServiceStubFactoryBean">
<property name="serviceInterface">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.CurrencyExchangePortType</value>
</property>
<property name="serviceLocator">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.CurrencyExchangeServiceLocator</value>
</property>
<property name="endpointAddress2">
<value>http://services.xmethods.net:80/soap</value>
</property>
<property name="timeout">
<value>120000</value>
</property>
</bean>
得益于Spring框架,虽然我们已经替换了对象的类工厂,却并不需要更改应用代码。通过Spring框架的IoC机制,我们可以完全使用面向接口的编程方式,而将实现的创建、配置和依赖管理交由Spring在运行时完成。即使实现发生了变化,也不需要改变应用程序结构。
回页首
4.新的思考
故事并没有结束,在开发过程中,我们又遇到了一系列关于Web Service调用的问题。
4.1性能
系统性能是分布式应用中的一个重要问题。许多用户都担心由Web Service技术所引入的额外开销是否会影响到产品的性能。随着技术的不断发展,Web Service引擎性能已经有了很大提高,一般来说使用Web Service的系统的性能可以满足绝大部分应用的需求。但在特定情况下,如果系统性能无法满足客户需求,我们首先需要对系统性能进行科学地分析和测定才能定位真正的性能瓶颈。这个问题在上文简单的示例中并不难解决,只需要在Web Service调用前后加入日志代码记录调用时间即可实现。但在实际系统中,比如一个产品目录的Web Service可能提供数十种查询方法,而程序中很多组件都会依赖于该服务提供的查询功能。如果在系统中所有的地方加入性能测定代码,这个工作就变得非常繁琐和困难。我们需要用一种更加优雅的解决方式,在增添新功能的同时并不影响系统代码或结构。
4.2缓存
在项目实践中,一个有效的改善Web Service系统性能的方法就是利用缓存来减少Web Service的重复调用。在具体实现中我们可以采用客户端缓存和服务器端缓存等不同方式,他们具有不同的特点和适用范围。在本文例子中,我们希望实现客户端缓存来提高系统性能。但由于Web Service业务逻辑的差别,我们希望能够为特定的Web Service提供特定的缓存策略,而且这些策略应该是能够被灵活配置的,它们不应于应用程序的逻辑代码耦合在一起。
4.3故障恢复:
对于Web Service应用,系统的可用性也是一个需要考虑的重要问题。在运行时由于网络运行环境的复杂性和不确定性,用户希望能够对Web Service访问提供一定的故障恢复机制:比如重试或者访问备份服务(当系统在调用Web Service失败后,使用备份Web Service的服务地址来继续访问)。这些故障恢复策略应该是可配置的,对应用逻辑透明的。
回页首
5.使用AOP解决SOA应用中的Crosscutting Concern
通过对上边一系列问题的分析,读者也许会发现这些问题并不是Web Service访问的核心问题,但会影响系统中许多不同的组件。而且其中一些问题需要我们能够灵活配置不同的实现策略,因此我们不应该将处理这些问题的代码与应用代码混合。
下面我们将利用AOP(Aspect-Oriented Programming)提供的方法来解决上述的问题。AOP是一种新兴的方法学,它最基本的概念就是关注隔离(Separation of Concern)。AOP提供了一系列的技术使得我们能够从代码中分离那些影响到许多系统模块的crosscutting concerns,并将他们模块化为Aspects。AOP的主要目的仍然是解耦,在分离关注点后,才能将关注点的变更控制一定范围内,增加程序的灵活性,才能使得关注能够根据需求和环境作出随时调整。
我们将利用Spring所提供的AOP功能支持来解决以上问题。这里我们只简单地介绍涉及到的AOP基本概念以及实现,如果您希望更好地了解AOP的概念以及Spring AOP支持的细节请参见参考资料。
Joinpoint 是程序的运行点。在Spring AOP中,一个Joinpoint对应着一个方法调用。
Advice 定义了AOP框架在特定的Joinpoint的处理逻辑。Spring AOP框架通过interceptor方式实现了advice,并且提供了多种advice类型。其中最基本的"around advice"会在一个方法调用之前和之后被执行。
下面我们将利用Spring提供的MethodInterceptor来为Web Service调用实现我们的定义的处理逻辑。
5.1 PerformanceMonitorInterceptor
性能测量是AOP最简单的例子之一,我们可以直接利用Spring提供的实现在bean.xml中声明我们的WebServicePerformanceMonitorInterceptor。
5.2 CacheInterceptor
为了不引入缓存策略的复杂性,我们只提供了一个利用HashMap的简单实现:它利用 Web Service的调用参数列表作为HashMap键值。在Web Service调用之前,首先检查缓存中是否拥有与现在参数列表相同的项,如果有则返回缓存的结果,否则调用Web Service并将<参数列表,结果>记录在HashMap中。在实际应用中,您应该根据具体情况来选择、构造适合Web Service的业务特性的Cache实现,也可以采用成熟的Cache实现。
在下面代码实现中有一个生成Web Service调用主键的小技巧。因为Web Service引擎要求所有调用参数必须是可序列化的,所以我们可以利用Java提供的序列化功能来实现对象的克隆。如清单10所示:
清单10:SimpleCacheInterceptor的代码示例
public class SimpleCacheInterceptor implements MethodInterceptor {
private Map cache = new HashMap();
private Object cloneObject(Object obj) throws Exception {
Object newObj = null;
if (obj != null) {
// 通过序列化/反序列化来克隆对象
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream out = new ObjectOutputStream(bos);
out.writeObject(obj);
out.flush();
out.close();
ObjectInputStream in = new ObjectInputStream(
new ByteArrayInputStream(bos.toByteArray()));
newObj = in.readObject();
}
return newObj;
}
//基于参数列表数组,生成用于HashMap的键值
public Object generateKey(Object[] args) throws Exception {
Object[] newArgs = (Object[]) cloneObject(args);
List key = Arrays.asList(newArgs);
return key;
}
//实现使用缓存技术的invoke方法
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object result = null;
Object data = null;
Object key = null;
try {
key = generateKey(methodInvocation.getArguments());
data = cache.get(key);
} catch (Exception ex) {
logger.error("Failed to find from the cache", ex);
}
if (data == null) {
//如果Cache中没有缓存结果,调用服务执行生成用于HashMap的键值
result = methodInvocation.proceed();
try {
data = cloneObject(result);
cache.put(key, data);
} catch (Exception ex) {
logger.error("Failed to cache the result!", ex);
}
} else {
result = data;
}
return result;
}
}
5.3 FailoverInterceptor
下面代码提供了一个基于服务备份切换的故障恢复实现,在运行时,如果Interceptor检测到服务调用由于网络故障抛出异常时,它将使用备份服务的端点地址并重新调用。如清单11所示:
清单 11: SimpleFailoverInterceptor的代码示例
public class SimpleFailoverInterceptor implements MethodInterceptor { …
…
//实现支持端点运行时切换的invoke方法
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
Object result = null;
try {
result = methodInvocation.proceed();
} catch (Throwable ex) {
if (isNetworkFailure(ex)) {
//切换服务端点地址
switchEndPointAddress((Stub) methodInvocation.getThis());
result = methodInvocation.proceed();
} else {
throw ex;
}
}
return result;
}
}
为了支持备份服务切换的功能,我们在WebServicePortProxyFactoryBean中为填加了配置参数"endpointAddress2",它会在创建的Web Service客户端代理对象中记录备份URL。
我们可以在CurrencyExchangeService加入下列参数来试验SimpleFailoverInterceptor的功能。其中第一个端点地址为一个错误的URL。在第一次调用服务时,SimpleFailoverInterceptor会侦测到网络故障的发生,并自动切换使用第二个端点地址继续访问。如清单12所示:
清单12:配置文件种增加的属性
<property name="endpointAddress">
<value>http://localhost/wrong_endpoint_address</value>
</property>
<property name="endpointAddress2">
<value>http://services.xmethods.net:80/soap</value>
</property>
5.4配置文件和运行结果
现在我们需要在Spring配置文件中,为所有interceptor添加定义,并描述如何为CurrencyExchangeService构建AOP Proxy。需要指出的是,我们要在interceptorName列表中声明interceptor链的调用顺序,还要将原有CurrencyExchangeManager引用的stub对象替换为新AOP Proxy。如清单13所示:
清单13:修改后的配置文件片段
<bean id="WebServicePerformanceMonitorInterceptor"
class="org.springframework.aop.interceptor.PerformanceMonitorInterceptor">
<property name="prefix">
<value>Web Service </value>
</property>
<property name="suffix">
<value></value>
</property>
</bean>
<bean id="CacheInterceptor" class="test.ws.SimpleCacheInterceptor"/>
<bean id="FailoverInterceptor" class="test.ws.SimpleFailoverInterceptor"/>
<bean id="CurrencyExchangeProxy"
class="org.springframework.aop.framework.ProxyFactoryBean">
<property name="proxyInterfaces">
<value>net.xmethods.www.sd.CurrencyExchangeService_wsdl.
CurrencyExchangePortType</value>
</property>
<property name="target">
<ref local="CurrencyExchangeService"/>
</property>
<property name="interceptorNames">
<list>
<value>WebServicePerformanceMonitorInterceptor</value>
<value>CacheInterceptor</value>
<value>FailoverInterceptor</value>
</list>
</property>
</bean>
<bean id="CurrencyExchangeManager"
class="test.ws.CurrencyExchangeManagerImpl">
<property name="stub">
<ref bean="CurrencyExchangeProxy"/>
</property>
</bean>
这里我们通过为AOP 的ProxyFactoryBean为 Web Service Stub创建了一个AOP代理,并且建立了一个Interceptor链。这样在调用Web Service时,Spring框架会依次调用Interceptor执行。实例执行的顺序图将如图5所示:
图5系统运行顺序图

5.5 Interceptor与JAX-RPC Handler的关系与区别
SOAP Message Handler是JAX-RPC为用户自定义Web Service处理过程提供的一种扩展机制。在处理Web Service请求/响应过程中,Web Service 引擎会根据部署描述中的定义,按照一定的次序调用Handler的处理代码。用户编写的Handler实现可以截获并修改Web Service消息和处理流程,从而实现对Web Service引擎处理行为的定制和增强。
比如,我们可以实现一个服务器端Handler,记录Web Service在受到请求消息和发出响应消息之间的时间间隔来实现对服务器端业务性能的测定。而且我们只需在部署描述中增加Handler声明即可,无需修改任何服务器端代码。
从此可以看出,JAX-RPC Handler与我们在上文中所提供的AOP Interceptor都可以帮助我们的SOA应用程序实现关注分离(Separate Concern)的目标,在不改变应用代码的同时,增强或改变Web Service服务访问的功能。虽然我们可以利用它们实现一些类似的功能,但它们具有着不同的特点和适用范围。
JAX-RPC Handler是Web Service引擎的扩展机制。如果我们需要实现对SOAP消息进行的修改和处理,加入自定义的SOAP Header或对消息内容进行加密,Handler是我们的最佳选择。而AOP是针对对象级别的扩展机制,它更适合对应用层逻辑进行操作。
比如,我们在上文展示的利用AOP实现的CacheInterceptor,它缓存的是Web Service调用参数和结果。而我们也可以通过JAX-RPC Handler实现一个面向SOAP消息的实现,它将缓存Web Service的请求消息和响应消息。这两个实现相比,基于AOP的实现更加简单、直观、快速、对资源消耗也比较小。而面向SOAP消息的实现则更加灵活,对于不采用RPC方式的Web Service访问也能提供支持。
所以在具体的实践过程中,开发人员应该根据具体的需求选择合适的技术,也可以将这两种技术结合使用。
回页首
6.总结
"分而治之"的方法是人们解决复杂问题的一种常见做法。而IoC、AOP等技术都体现了这种思想。通过更好的切分程序逻辑,使得程序结构更加良好,更加富有弹性,易于变化。也使得开发人员可以更加专注于业务逻辑本身,而将一部分其他逻辑交给容器和框架进行处理。
在本文中,我们通过一个Web Service访问的实例,具体描述了SOA应用中所遇到的一系列具体问题,并描述如何利用IoC和AOP等技术进行代码重构,构建更加结构良好、灵活的SOA应用。综上所述,我们可以看到:
1使用IoC框架来实现对象的生命周期管理、配置管理和依赖管理,可以解除业务逻辑对服务调用的依赖关系;
2 使用AOP方法来解决Web Service调用中的crosscutting concerns,将为系统增加新的功能而不必更改应用程序。
3通过IoC和AOP来屏蔽Web Service访问的复杂性,使得开发人员可以更加专注于业务逻辑本身,也使得系统更加稳定和富有弹性。
回页首
下载
描述 名字 大小 下载方法
code sample code.zip 27 KB HTTP
关于下载方法的信息
参考资料
JAX-RPC: http://java.sun.com/xml/jaxrpc/index.html
Spring 参考文档 : http://www.springframework.org/docs/reference/remoting.html
参考文章: SOA adventures, Part 1: Ease Web services invocation with dynamic decoupling
关于AOP:请参考IBM developerWorks上的更多资源。
J2EE核心模式: Core J2EE™ Patterns: Best Practices and Design Strategies, Second Edition By Deepak Alur, John Crupi, Dan Malks
发表评论
-
利用动态代理的 Java 验证
2008-12-18 17:41 848从业务对象实现中去耦 ... -
Java 理论与实践: 用动态代理进行修饰
2008-12-18 17:41 804动态代理是构建 Decorator 和 Adapter 的方便 ... -
分布式软件系统
2008-12-18 15:49 1468分布式软件系统(Distributed Software Sy ... -
SOA 案例研究:SOA 设计
2008-12-17 15:00 1063http://www.ibm.com/developerwor ... -
RUP
2008-12-12 16:53 716RUP(Rational Unified Proces ... -
什么是敏捷开发?
2008-12-05 11:17 2456敏捷开发(agile development)是一种以人为核心 ... -
深入理解敏捷开发的常见九大误区
2008-12-05 11:16 1496任人、开发者和用户应 ... -
对领域模型实现的总结性观点
2008-12-04 17:14 1155陶文发起的对领域模型 ... -
领域模型的价值与困境
2008-12-04 17:02 970很久以前大家就关于这 ... -
OO设计原则----依赖倒置原则(DIP)
2008-12-02 01:52 1112这是一个类与类之间调用规则,术语上解释就是对于组合之间的规范。 ... -
工厂模式与OO设计原则
2008-12-02 01:50 1165如果把创建看作一个职 ... -
设计模式之--动态代理
2008-11-26 18:03 1634动态代理类是一个在运行时由开发人员所指定的一列接口的实现。动态 ... -
Facade外观模式
2008-11-26 16:06 1003GOF《设计模式》一书对Facade模式是这样描述的: ... -
Adapter适配器模式
2008-11-26 16:05 851GOF《设计模式》一书对Adapter模式是这样描述的: ... -
Strategy策略模式
2008-11-26 16:05 1088GOF《设计模式》一书对Strategy模式是这样描述的: ... -
Bridge桥接模式
2008-11-26 16:04 918设计模式》一书对Bridge是这样描述的: 将抽象与其实现解 ... -
Abstract Factory抽象工厂模式
2008-11-26 16:03 772GOF《设计模式》一书对Abstract Factory模式是 ... -
Decorator装饰模式
2008-11-26 16:01 889《设计模式》一书对Decorator是这样描述的: 动态地给 ... -
Observer观察者模式
2008-11-26 15:59 936《设计模式》一书对Observer是这样描述的: 定义对象间的 ... -
Template Method模式
2008-11-26 15:58 989factory模式(包括简单工厂和抽象工厂),Strategy ...
相关推荐
在第十章中,作者探讨了如何博采其他设计技术,例如上下文为王的概念、服务导向架构(SOA)、控制反转(IoC)和依赖注入(DI)、面向方面编程(AOP)等。这些技术能够帮助开发者构建更为灵活和可维护的软件架构。 ...
内容概要:本文介绍了如何使用Python识别图片和扫描PDF中的文字。首先,文章讲解了使用Spire.OCR for Python库来识别图片中的文字,包括安装库、配置OCR模型路径和语言设置、扫描图片以及保存识别后的文本。其次,详细描述了从图片中提取文字及其坐标位置的方法,使用户不仅能够获取文本内容,还能知道文本在图片中的具体位置。最后,文章还介绍了如何结合Spire.PDF for Python将PDF文件转换为图片格式,再通过OCR技术从中提取文字,适用于处理扫描版PDF文件。文中提供了完整的代码示例,帮助读者理解和实践。 适合人群:对Python编程有一定基础,希望学习或提高光学字符识别(OCR)技术的应用开发者,尤其是需要处理大量图片或PDF文档中文字信息的工作人员。 使用场景及目标:① 开发者可以利用这些方法自动化处理图片或PDF文档中的文字信息,提高工作效率;② 实现从非结构化数据(如图片、扫描件)到结构化数据(如文本文件)的转换,便于后续的数据分析和处理;③ 提供了一种解决纸质文档数字化的有效途径,特别是对于历史档案、书籍等资料的电子化保存。 其他说明:需要注意的是,OCR的准确性很大程度上取决于图片的质量,清晰度高、对比度好的图片可以获得更好的识别效果。此外,不同OCR库可能对特定语言或字体的支持程度不同,选择合适的库和配置参数能显著提升识别精度。在实际应用中,建议先进行小规模测试,优化参数后再大规模应用。
2025中小企业数字化转型指南
1.版本:matlab2014/2019a/2024a 2.附赠案例数据可直接运行matlab程序。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。
2025-04-10 兜兜\(^o^)/~
内容概要:本文详细介绍了永磁同步电机(PMSM)无位置传感器控制中滑模观测器(SMO)的设计与仿真实现。首先阐述了系统的整体架构,包括速度环和电流环的工作机制。接着深入探讨了滑模观测器的关键实现步骤,如滑模增益选择、符号函数的应用以及低通滤波器的作用。文中还涉及了坐标变换(Clarke变换和Park变换)、PI参数整定、SVPWM调制等重要环节的具体实现方法和技术难点。此外,作者分享了多个调试过程中遇到的问题及解决方案,如波形抖振、电流采样延时、扇区判断错误等。最后展示了仿真结果,证明了所提方案的有效性和可行性。 适合人群:从事电机控制系统研究与开发的技术人员,尤其是对无位置传感器控制感兴趣的工程师。 使用场景及目标:适用于希望深入了解并掌握永磁同步电机无位置传感器控制技术的研究人员和工程师。目标是帮助读者理解滑模观测器的工作原理,掌握其设计与调试技巧,从而应用于实际项目中。 其他说明:文中提供了丰富的代码片段和实践经验,有助于读者更好地理解和应用理论知识。同时提醒读者注意离散化步长的选择和滑模增益的调整,这些都是确保系统稳定运行的重要因素。
内容概要:本文详细介绍了航天器姿态控制中的关键技术,包括时变滑模控制、自适应控制、执行器饱和处理及推力器安装偏差补偿。针对航天器在复杂环境中面临的不确定性,提出了时变滑模面设计、自适应参数更新、饱和控制和安装偏差补偿的具体实现方法,并提供了相应的MATLAB代码。通过合理的控制策略设计,确保航天器在面对多种干扰因素时仍能保持高精度的姿态控制。 适合人群:航空航天领域的研究人员、控制系统工程师、高校相关专业师生。 使用场景及目标:适用于航天器姿态控制系统的开发与优化,旨在提升系统的鲁棒性、稳定性和控制精度。具体应用场景包括卫星姿态调整、深空探测器姿态控制等。 其他说明:文中提供的MATLAB代码可以直接用于实验验证和教学演示,帮助读者深入理解各控制环节的工作原理和技术细节。此外,文中还分享了许多实用的工程经验,如参数整定技巧、常见问题处理等,有助于指导实际项目开发。
内容概要:本文详细介绍了基于Simulink-Simscape平台构建的四轮转向汽车模型预测控制(MPC)路径跟踪系统。首先,文章阐述了车辆动力学模型的核心结构,包括前轮和后轮转向角的计算方法以及魔术轮胎模型的配置。接着,深入探讨了MPC控制器的设计,特别是代价函数的配置和QP优化的具体实现。此外,还讨论了路面切换模块的设计,确保不同路况下的稳定性和响应性。最后,文章分享了一些调参经验和仿真参数设置的技巧,如积分限幅、随机质量块的应用以及解算器的选择。 适合人群:从事自动驾驶、车辆控制系统研究的工程师和技术人员,尤其是对路径跟踪算法和MPC有浓厚兴趣的研究者。 使用场景及目标:适用于开发和优化四轮转向汽车的路径跟踪系统,旨在提高车辆在各种复杂路况下的行驶稳定性和精确度。具体目标包括减小方向盘转角误差、控制横向位移偏差、优化轮胎力计算等。 其他说明:文中提供了大量MATLAB代码片段和具体的参数配置,帮助读者更好地理解和复现实验结果。同时,作者分享了许多实际调试过程中遇到的问题及其解决方案,使读者能够避开常见陷阱并高效完成模型搭建。
全国银行代码和支行代城市地区mysql
内容概要:本文详细介绍了如何利用动态规划(Dynamic Programming, DP)在MATLAB/SIMULINK环境中实现自动驾驶车辆的动态避障功能。首先,文章解释了动态规划的核心思想及其在路径规划中的应用,特别是通过状态转移方程来解决避障问题。接着,讨论了运动学模型(如自行车模型)的建立方法,以及如何通过PID和MPC控制算法进行路径跟踪和避障。此外,文章还探讨了联合仿真平台(MATLAB + Carsim + Prescan)的搭建和配置,展示了如何将理论转化为实际的仿真效果。最后,提供了完整的代码实现和调试技巧,帮助读者快速上手并优化性能。 适合人群:对自动驾驶技术和路径规划感兴趣的科研人员、工程师和技术爱好者。 使用场景及目标:适用于研究和开发自动驾驶系统,特别是在复杂环境下实现高效的动态避障功能。目标是提高车辆的安全性和智能化水平,减少人为干预。 其他说明:文中提供的代码已在GitHub上开源,读者可以直接下载并运行。需要注意的是,某些高级功能(如深度强化学习)将在后续版本中继续探索。
内容概要:本文档是《拼多多Python面试题目.pdf》,涵盖了Python编程语言的面试题目及其参考答案。文档分为四个主要部分:选择题、填空题、代码编程题和综合题。选择题涉及Python基础知识、多线程、Redis使用场景、字符编码转换、协程特性、深拷贝操作等;填空题考察了列表推导式、对象序列化、文件操作、字符串处理等知识点;代码编程题包括统计热门商品、订单时间窗口校验、字符串压缩、异步批量请求、最长递增子序列等实际编程任务;综合题则要求设计一个高并发秒杀系统,涵盖缓存、数据库、消息队列的选择,库存扣减、分布式锁、限流策略等关键模块的实现思路,以及解决缓存相关问题的方法。 适合人群:有一定Python编程基础,准备求职或希望提升技术能力的开发者,尤其是对Python高级特性和实际应用感兴趣的工程师。 使用场景及目标:①帮助面试者熟悉Python常见知识点和技巧,提高面试成功率;②通过实际编程题目的练习,加深对Python语法和标准库的理解;③掌握高并发系统的架构设计思路,培养解决复杂业务问题的能力。 阅读建议:此文档不仅包含理论知识的选择题和填空题,还有实际编程题目的训练,建议读者在学习过程中不仅要关注正确答案,更要理解每个选项背后的原理,同时动手实践编程题目,结合实际案例进行思考和总结。
内容概要:本文档《Java 算法面试题目.pdf》涵盖了 Java 编程语言的核心知识点和常见面试题,分为选择题、填空题、代码编程题和综合题四个部分。选择题涉及 Java 内存模型、字符串处理、集合类、垃圾回收、线程池、设计模式、Spring 事务、流操作、类加载机制、锁机制、Netty 和 Redis 持久化等主题。填空题考察了线程同步、HashMap 负载因子、序列化、数学运算、Spring 注入、字符串对象创建、位移运算、字符串方法、动态代理和 JVM 参数配置等细节。代码编程题要求实现链表反转、线程安全单例模式、快速排序、二叉树层序遍历和 LRU 缓存等功能。综合题则要求设计一个无人机实时控制系统,涵盖并发控制、可靠传输、路径规划、高可用性和低延迟等方面。; 适合人群:具有 Java 编程基础,尤其是准备面试的中高级开发人员,以及对 Java 核心技术和框架有深入理解需求的技术人员。; 使用场景及目标:①帮助开发者复习和巩固 Java 基础知识和常见面试考点;②通过编程题提升实际编码能力;③通过综合题锻炼系统设计和架构思维,掌握分布式系统的设计要点。; 阅读建议:建议读者先复习相关知识点,再尝试解答题目,最后对照参考答案进行查漏补缺。对于综合题,应结合实际项目经验思考解决方案,重点理解系统设计的关键点和技术选型的理由。
内容概要:该专利提出了一种节能高效的双螺杆压缩机转子型线设计方案,属于动力传动与控制技术领域。核心创新点在于阴阳转子的齿曲线采用抛物线、圆弧、椭圆及其共轭包络线组合而成,形成二次曲线与二次曲线包络线的组合型线。设计特点包括非对称齿面(前齿面更宽,b/a>1.8)、各段曲线光滑连接无尖点、七段曲线组合结构。该设计旨在提高密封性、改善动力特性、降低损耗、提升效率,从而提高双螺杆压缩机的整体性能。文中提供了详细的Python代码实现,包括转子型线的计算和可视化,以及改进后的代码,以更好地反映专利的具体参数和技术细节。 适合人群:机械工程专业人员、从事压缩机设计与制造的技术人员、对双螺杆压缩机转子型线设计感兴趣的科研人员。 使用场景及目标:①用于研究和开发新型双螺杆压缩机,特别是在提高压缩机效率和性能方面;②作为教学案例,帮助学生和工程师理解双螺杆压缩机转子型线的设计原理和技术实现;③为企业提供参考,优化现有产品的设计和制造工艺。 其他说明:文中提供的代码基于专利描述进行了合理的假设和简化,实际应用中可能需要根据具体性能要求进行优化调整。专利技术通过非对称设计、多段曲线组合等方式,实现了高效的密封性和优良的动力特性,显著提升了双螺杆压缩机的性能。
内容概要:本文详细介绍了利用PFC3D5.0进行滑坡冲击建筑物仿真的全流程,涵盖滑坡体和建筑物的建模、参数设定、监测系统的构建以及灾后损伤评估。文中不仅提供了完整的Python代码示例,还解释了各个关键参数的选择依据及其对仿真结果的影响。通过实例展示了如何设置滑坡体的尺寸、密度、阻尼系数等属性,以及建筑物的材料属性、粘结强度等参数。同时,文章强调了实时监测系统的重要性,包括冲击力传感器的布置和预警机制的设计。此外,还探讨了不同条件下建筑物的易损性曲线生成方法及其应用价值。 适合人群:从事地质灾害研究、结构工程分析的专业人士,尤其是那些希望深入了解滑坡冲击建筑物过程并掌握相关数值模拟技术的研究人员和技术人员。 使用场景及目标:适用于需要进行滑坡灾害风险评估、建筑设计优化以及应急响应规划的项目。主要目标是帮助用户理解和预测滑坡对建筑物造成的潜在损害,从而提高建筑物的安全性和耐久性。 其他说明:文中提供的代码和参数设置均经过多次实验验证,确保了较高的准确性。对于想要进一步探索这一领域的读者来说,本文提供了一条从理论到实践的学习路径。
修复版本,改了短信接口到阿里云,现在好像国内短信接口管的最松的反而是阿里了,支付也已经接好了派特了。可以直接开户直接用了,带非常非常非常完整的搭建教程,萌新都可以随便装起来了。
内容概要:本文探讨了在自动驾驶领域中,利用Matlab/Simulink 2018b和Carsim 2020构建ACC(自适应巡航控制)系统的分层PID控制模型的方法和技术要点。首先介绍了软件选择的原因及其版本要求,强调了版本兼容性的重要性。接着详细讲解了模块化建模方法的应用,包括单独的Carsim配置文件、电机驱动模块、车辆巡航模块、车辆跟踪模块、切换逻辑、速度跟踪模块以及联合仿真模块等七个关键组成部分的作用和实现方式。文中提供了具体的MATLAB代码片段展示上层和下层PID控制器的工作机制,并分享了一些实用的小技巧,如安全距离动态补偿、积分限幅处理、死区设置等。此外,还讨论了联合仿真的注意事项,如接口版本匹配、远程调试设置等问题。最后提到模型验证的有效测试案例,并指出整套源码中最值得关注的部分是状态机实现。 适合人群:对自动驾驶技术感兴趣的科研人员、工程师以及相关专业的学生,尤其是那些希望深入了解ACC系统内部工作机制的人群。 使用场景及目标:①帮助读者掌握如何使用Matlab/Simulink和Carsim建立并优化ACC巡航控制系统;②提高读者对PID控制理论的理解及其在实际工程项目中的应用能力;③为从事智能交通系统研究的专业人士提供有价值的参考资料。 其他说明:本文不仅涵盖了理论知识,还包括大量实践经验分享,有助于初学者快速入门的同时也为资深从业者带来新的思考角度。
内容概要:本文详细介绍了基于西门子S7-1200 PLC和TIA Portal V16平台的五层电梯仿真系统。该系统利用博图V16的仿真环境,无需实体PLC即可实现电梯的动态动画运行。核心内容涵盖PLC程序的状态机设计、呼叫处理模块、HMI组态画面的设计以及安全回路处理等方面。文中展示了具体的代码片段,如电梯状态机的嵌套状态切换逻辑、呼叫信号的位操作处理、HMI动画的位置计算等。此外,还提到了一些实用技巧,如优化响应速度的方法、动画效果的实现细节以及仿真过程中可能遇到的问题及其解决方案。 适合人群:工控领域的工程师、培训讲师、学生及其他对电梯控制系统感兴趣的人员。 使用场景及目标:①作为教学工具,帮助学员理解和掌握电梯控制系统的逻辑设计;②作为验证工具,用于测试和优化电梯控制系统的性能;③为实际工程项目提供参考,减少硬件调试的时间和成本。 其他说明:该仿真系统不仅提供了完整的PLC程序和HMI项目文件,还包括详细的仿真参数配置指南和故障模拟功能,使得教学和实验更加生动有趣。