value="mypackage.MyServiceInterface"/>
通过Spring的魔法,客户端代码不需要改变,而远程方法的激活就像以前的本地调用一样。
除了HTTP远程服务外,Spring还支持其他的远程协议,如基于HTTP的解决方案(Webservices,Hessian,andBurlap)和重量级的如RMI。
配置和部署基于URL的远程服务
通过基于HTTP远程服务来部署应用服务有几个明显的优点,其中一个是相对于RMI或EJB方案,你不需要担心更多的配置问题。
任何尝试过使用JNDI配置(来自不同厂家的J2EE容器或者同一厂家容器的不同版本的负载均衡及群集)的人都这样认为。
URL是无格式的文本串,而这是最方便的。
但同时,通过URL定义服务使得定义有些脆弱。
在前面章节列举的URL的不同部分都会按照自己的方式进行变化。
网络拓朴变化,负载均衡服务器代替普通服务器,应用被布署到不同机器的不同容器中,网络防火墙间的商品被打开或关闭等等。
此外,这些不稳定的URL必须被存储在每一个可能访问服务的客户端的Spring上下文文件中。
当变化发生时,所有的客户端必须更新。
还有从开发阶段到产品阶段的服务进程,指向服务的URL必须反映服务所在的环境。
最后我们到达了问题的关键:
Spring的暴露各部分受管理的bean作为远程访问服务的能力是非常棒的。
甚至在我们需要定义一个服务为服务名时,对客户端隐藏所有有关服务定位的问题。
自动发现和容错的缓存服务
这个问题最简单解决方法是使用某些命名服务来动态实时的转换服务名与服务位置。
实际上,我只需要构建一次这样的系统通过使用JmDNS类库注册Spring远程服务在Zeroconf命名空间中。
基于DNS方案的问题在于更新服务定义是不可能做到实时或事务的。
一个失败的服务器在各类超时前还是出现在服务列表中。
而我们需要的是快速发布并更新URL列表来实现服务并在整个网络中同步的表现所有变化。
满足这些需求的系统才是可用的。
这包含各种分布式缓存的实现。
对Java开发人员来说最简单的想像缓存的方式是认为缓存是一个java.util.Map接口的实现。
你可以通过键值来放入一引起对象,然后你可以用同一键值取得这个对象。
一个分布式缓存系统需要确保相同的键/值映射会存在于每一个参与这个缓存的服务器中的相同Map中并且步伐一致的更新缓存。
一个好的分布式缓存可以解决我们的问题。
我们在实现了服务的网络中关联一个服务名和一个或多个URL。
然后,我们在分布式缓存中存储name=(URL列表)关联并随着网络状态的变化(服务器的加入/移除/当机等)而相应更新。
客户端访问参与分布式缓存的服务就像访问私有的服务一样。
作为附加的奖励,我们会在这里介绍一个简单的负载均衡/容错的解决方案。
如果客户端知道一个服务与几个服务URL关联,他可以随机地使用其中的一个并且通过为这些URL服务的几个服务来提供自然的但也有效的负载均衡。
而且,在一个远程调用失败时,客户端简单地标识那个URL不可用并且使用下一个。
因为服务URL列表存储在分布式缓存中,服务器A不可用的情况也会立刻通知给别的客户端。
分布式缓存在常规的J2EE应用中非常有用,是群集服务的基础。
例如,如果你有一个分布式的群集应用,分布式缓存可以在你的群集成员中提供会话复制。
虽然这种方式提供了高可用性,但也存在严重的瓶颈。
会话数据变化的很快,更新所有群集成员和容错的代价非常高。
带有会话复制的群集应用效率通常比基于负载均衡的非会话复制的方案低很多。
在我们的案例中使用分布式缓存是因为缓存的数据很少。
相对于通常有上千会话对象的分布式系统来说,我们只有少量的服务列表和对应其实现的URL。
此外,我们的列表更新并不频繁。
使用这样一个小列表的分布式缓存可以服务于大量的服务器和客户端。
在本文的剩余部分,我们来看一下“服务描述缓存算法”的实际实现
使用Spring和Jboss缓存来实现服务描述缓存
Jboss应用服务器可能是今天最成功的开源J2EE项目了。
不管是爱是恨,Jboss应用服务器在布署服务器排行榜上占据应得的位置,而且他的模块天性使得布署更加友好。
JBoss发布包包含了很服务。
其中一个是JBoss缓存。
他实现的缓存提供了无论本地或远程的Java对象的高性能缓存。
JBoss缓存有许多配置选项和特性,我希望你更深入的研究使得他更好的适合你的下一个项目。
对我们最有吸引的特性如下:
1、 提供了高质量的Java对象的事务复制。
2、 可以独立运行或者作为Jboss的一部分。
3、 已经是Jboss的一部分
4、 可以使用UDP多播的方式和TCP连接的方式。
JBoss缓存的网络基础是JGroups类库。
JGroups提供了群体成员间的网络通讯并且可以工作于UDP或TCP方式。
在本文中,我会演示如何使用JBoss缓存来存储服务的定义和提供动态的自动服务发现。
刚开始,我们先引入一个自定义类,AutoDiscoveredServiceExporter扩展Spring的标准HttpInvokerServiceExporter类来暴露我们的TestService给远程调用:
这个在没有什么可说的。
我们主要是使用他来标识Spring远程服务作为我们自己的方式来暴露。
接下来是服务端的缓存配置。
Jboss包含了缓存实现,我们可以用Spring内建的JMX代理将缓存引入Spring上下文:
jboss.cache:
service=CustomTreeCache
org.jboss.cache.TreeCacheMBean
这创建一个CustomTreeCacheMBean在服务端的Spring上下文中。
通过自动代理的特性,这个bean实现了org.jboss.cache.TreeCacheMBean接口的方法。
在这里,布署到Jboss服务器只需要将已经提供的custom-cache-service.xml放到服务器的布署目录下。
为了简化代码,我们引入简单的CacheServiceInterface接口:
publicvoidput(Stringpath,Objectkey,Objectvalue)throwsException;
publicObjectget(Stringpath,Objectkey)throwsException;
JBossCache是一种树状结构,这也是为什么我们需要path参数。
这个接口的服务端实现如下引用缓存Mbean:
在最后,我们需要ServicePublisher来观察Spring容器的生命周期,并且在我们的缓存中发布或移除服务定义:
这段代码显示ServicePublisher在Spring上下文刷新时(如应用补布署时)如何处理:
privatevoidcontextRefreshed()throwsException{
logger.info("contextrefreshed");
String[]names=context
.getBeanNamesForType(AutoDiscoveredServiceExporter.class);
logger.info("exportingservices:
"+names.length);
for(inti=0;i StringserviceUrl=makeUrl(names[i]);
try{
Setservices=(Set)cache.get(SERVICE_PREFIX+names[i],
SERVICE_KEY);
if(services==null)
services=newHashSet();
services.add(serviceUrl);
cache.put(SERVICE_PREFIX+names[i],SERVICE_KEY,services);
logger.info("added:
"+serviceUrl);
}catch(Exceptionex){
logger.error("exceptionaddingservice:
",ex);
}
}
如你所见,发布器简单的遍历通过缓存服务描述导出的服务列表并增加定义到缓存中。
我们的缓存设计成路径包含服务名,他的URL列表存储在一个Set对象中。
将服务名作为路径的一部分对JBossCache实现来说是重要的因为他是基于路径来创建和释放事务锁。
这种方式下,对服务A的更新不会干扰对服务B的更新因为他们被映射到不同的路径:
/some/prefix/serviceA/key=(listofURLs)and/some/prefix/serviceB/key=(listofURLs)。
移除服务定义的代码是类似的。
现在我们转到客户端。
我们需要一个缓存实现来与服务端共享:
LocalJBossCacheServiceImpl保存着来自与服务端相同的custom-cache-service.xml配置的JBossCache引用:
publicLocalJBossCacheServiceImpl()throwsException{
super();
cache=newTreeCache();
PropertyConfiguratorconfig=newPropertyConfigurator();
config.configure(cache,"app/context/custom-cache-service.xml");
}
这个缓存定义文件包含了Jgroups层的配置,允许所有缓存成员通过UDP多播来定位彼此。
LocalJBossCacheServiceImpl还实现了接口并且为我们的AutoDiscoveredService提供了缓存服务。
这个bean扩展了标准的HttpInvokerProxyFactoryBean类但配置上有些不同:
class="app.auto.AutoDiscoveredService">
value="app.service.TestServiceInterface"/>
最初,没有URL存在。
自动在网络上寻找在TestService名字上暴露的Spring远程服务。
当服务发现时,他就获得了来自分布式缓存的URL列表:
privateListgetServiceUrls()throwsException{
Setservices=(Set)cache.get(ServicePublisher.SERVICE_PREFIX
+beanName,ServicePublisher.SERVICE_KEY);
if(services==null)
returnnull;
ArrayListresults=newArrayList(services);
Collections.shuffle(results);
logger.info("shuffled:
"+results);
returnresults;
}
Collections.shuffle随机地重排与服务关联的URL列表因此客户端的方法调用在他们之间是负载均衡的。
实际的远程调用如下:
publicObjectinvoke(MethodInvocationarg0)throwsThrowable{
Listurls=getServiceUrls();
if(urls!
=null)
for(IteratorallUrls=urls.iterator();allUrls.hasNext();){
StringserviceUrl=null;
try{
serviceUrl=(String)allUrls.next();
super.setServiceUrl(serviceUrl);
logger.info("goingto:
"+serviceUrl);
returnsuper.invoke(arg0);
}catch(Throwableproblem){
if(probleminstanceofIOException
||probleminstanceofRemoteAccessException){
logger.warn("goterroraccessing:
"
+super.getServiceUrl(),problem);
removeFailedService(serviceUrl);
}else{
throwproblem;
}
}
}
thrownewIllegalStateException("Noservicesconfiguredforname:
"
+beanName);
}
如你所见,如果远程调用抛出异常,客户端代码可以处理这个问题而且可以从列表中取下一个URL,因此也就提供了透明的容错性。
如果调用因为某些异常失败了,他为重新抛出异常给客户端处理。
下面的removeFailedService()方法简单的从列表中移除了失败的URL并更新分布式缓存,使这个信息同步地通知所有其他客户端:
privatevoidremoveFailedService(Stringurl){
try{
logger.info("removingfailedservice:
"+url);
Setservices=(Set)cache.get(ServicePublisher.SERVICE_PREFIX
+beanName