当企业级计算进入新的SOA世界时在寻找描述/发布/和发现服务的方面中开始变得越来越重要基于网络服务的方案不提供自动服务发现而且通常都太繁杂了现在新的轻量级的开发框架提供了新的轻量级的服务发布方案
在过去几年中Spring框架已经成为开发简单灵活而且容易配置的JEE应用的事实标准Spring的核心是IoC法则根据IoC应用必须以一个简单JavaBean的集合来开发然后用一个轻量级的IoC容器来绑定他们并设置相关的依赖关系
在Spring中容器通过一系列bean定义也配置典型的是用XML文件方式
<bean id=MyServiceBean class=mypackageMyServiceImpl>
<property name=otherService ref=OtherServiceBean/>
</bean>
当客户端代码需要请求时MyService你只要如下编码
MyServiceInterface service = (MyServiceInterface)contextgetBean(MyServiceBean);
servicedoSomething();
除了IoC之外Spring提供了几百种其他服务代码约定而且通过回调标准API来简化开发典型的服务端应用无论应用使用重量级的JEE API如EJB/JMS/JMX或者使用流行的MVC框架来构建网络接口Spring都提供了简化的效果
随着Spring框架的成熟越来越多的人使用他作为大型企业级项目的基础Spring已经通过了伸缩性开发的测试而且可以作为组件粘合剂来联结复杂的分布式系统
任何企业级应用都由各种组件组成如联结以前的系统和ERP系统第三方系统网面/表示层/持久导等等通常一个电子商务站点都是由简单的网页应用逐渐深化成包含上百个子应用和子系统的大项目而且要面对其中的复杂性会阻碍以后的发展通常的解决方案是将集成电路般的应用分解成一些粗纹理的服务并将其发布到网络中
不管应用是被设计成作为分散服务的集成点或者已经集成为一体管理所有分布式组件和其配置的任务通常都是耗时和代价高的但如果你使用了Spring作为应用组件的开发平台那么你就可以使用Spring的远程服务通过一系列的协议来将组件暴露给远程的客户端通过Spring可以使你的分布式应用就如修改一些配置文件那么简单
在Spring中最简单的javatojava的远程通讯方案是使用HTTP远程服务例如在webxml中注册了Spring的分发服务件后下面的上下文片断就可以将MyService作为公共接口使用了
<bean name=/MyRemoteService class=orgspringframeworkremoting>
<property name=service ref=MyServiceBean/>
<property name=serviceInterface value=mypackageMyServiceInterface/>
</bean>
如你所见实际的服务被注入到bean的定义中因此可以被远程调用
在客户端上下文定义如下
<bean id=MyServiceBean
class=orgspringframeworkremoting>
<property name=serviceUrl
value=//somehost:/webappcontext/somemappingforspringservlet/MyRemoteService />
<property name=serviceInterface
value=mypackageMyServiceInterface />
</bean>
通过Spring的魔法客户端代码不需要改变而远程方法的激活就像以前的本地调用一样
除了HTTP远程服务外Spring还支持其他的远程协议如基于HTTP的解决方案(Web services Hessian and Burlap)和重量级的如RMI
配置和部署基于URL的远程服务
通过基于HTTP远程服务来部署应用服务有几个明显的优点其中一个是相对于RMI或EJB方案你不需要担心更多的配置问题任何尝试过使用JNDI配置(来自不同厂家的JEE容器或者同一厂家容器的不同版本的负载均衡及群集)的人都这样认为
URL是无格式的文本串而这是最方便的但同时通过URL定义服务使得定义有些脆弱在前面章节列举的URL的不同部分都会按照自己的方式进行变化网络拓朴变化负载均衡服务器代替普通服务器应用被布署到不同机器的不同容器中网络防火墙间的商品被打开或关闭等等
此外这些不稳定的URL必须被存储在每一个可能访问服务的客户端的Spring上下文文件中当变化发生时所有的客户端必须更新还有从开发阶段到产品阶段的服务进程指向服务的URL必须反映服务所在的环境
最后我们到达了问题的关键Spring的暴露各部分受管理的bean作为远程访问服务的能力是非常棒的甚至在我们需要定义一个服务为服务名时对客户端隐藏所有有关服务定位的问题
自动发现和容错的缓存服务
这个问题最简单解决方法是使用某些命名服务来动态实时的转换服务名与服务位置实际上我只需要构建一次这样的系统通过使用 JmDNS类库注册Spring远程服务在Zeroconf命名空间中
基于DNS方案的问题在于更新服务定义是不可能做到实时或事务的一个失败的服务器在各类超时前还是出现在服务列表中而我们需要的是快速发布并更新URL列表来实现服务并在整个网络中同步的表现所有变化
满足这些需求的系统才是可用的这包含各种分布式缓存的实现对Java开发人员来说最简单的想像缓存的方式是认为缓存是一个javautilMap接口的实现你可以通过键值来放入一引起对象然后你可以用同一键值取得这个对象一个分布式缓存系统需要确保相同的键/值映射会存在于每一个参与这个缓存的服务器中的相同Map中并且步伐一致的更新缓存
一个好的分布式缓存可以解决我们的问题我们在实现了服务的网络中关联一个服务名和一个或多个URL然后我们在分布式缓存中存储name=(URL列表)关联并随着网络状态的变化(服务器的加入/移除/当机等)而相应更新客户端访问参与分布式缓存的服务就像访问私有的服务一样
作为附加的奖励我们会在这里介绍一个简单的负载均衡/容错的解决方案如果客户端知道一个服务与几个服务URL关联他可以随机地使用其中的一个并且通过为这些URL服务的几个服务来提供自然的但也有效的负载均衡而且在一个远程调用失败时客户端简单地标识那个URL不可用并且使用下一个因为服务URL列表存储在分布式缓存中服务器A不可用的情况也会立刻通知给别的客户端
分布式缓存在常规的JEE应用中非常有用是群集服务的基础例如如果你有一个分布式的群集应用分布式缓存可以在你的群集成员中提供会话复制虽然这种方式提供了高可用性但也存在严重的瓶颈会话数据变化的很快更新所有群集成员和容错的代价非常高带有会话复制的群集应用效率通常比基于负载均衡的非会话复制的方案低很多
在我们的案例中使用分布式缓存是因为缓存的数据很少相对于通常有上千会话对象的分布式系统来说我们只有少量的服务列表和对应其实现的URL此外我们的列表更新并不频繁使用这样一个小列表的分布式缓存可以服务于大量的服务器和客户端
在本文的剩余部分我们来看一下服务描述缓存算法的实际实现
使用Spring和Jboss缓存来实现服务描述缓存
Jboss应用服务器可能是今天最成功的开源JEE项目了不管是爱是恨Jboss应用服务器在布署服务器排行榜上占据应得的位置而且他的模块天性使得布署更加友好
JBoss发布包包含了很服务其中一个是JBoss缓存他实现的缓存提供了无论本地或远程的Java对象的高性能缓存JBoss缓存有许多配置选项和特性我希望你更深入的研究使得他更好的适合你的下一个项目
对我们最有吸引的特性如下
提供了高质量的Java对象的事务复制
可以独立运行或者作为Jboss的一部分
已经是Jboss的一部分
可以使用UDP多播的方式和TCP连接的方式
JBoss缓存的网络基础是JGroups类库JGroups提供了群体成员间的网络通讯并且可以工作于UDP或TCP方式
在本文中我会演示如何使用JBoss缓存来存储服务的定义和提供动态的自动服务发现
刚开始我们先引入一个自定义类AutoDiscoveredServiceExporter扩展Spring的标准HttpInvokerServiceExporter类来暴露我们的TestService给远程调用
<bean name=/TestService class=appserviceAutoDiscoveredServiceExporter>
<property name=service ref=TestService/>
<property name=serviceInterface value=appserviceTestServiceInterface/>
</bean>
这个在没有什么可说的我们主要是使用他来标识Spring远程服务作为我们自己的方式来暴露
接下来是服务端的缓存配置Jboss包含了缓存实现我们可以用Spring内建的JMX代理将缓存引入Spring上下文
<bean id=CustomTreeCacheMBean class=orgspringframeworkjmxaccessMBeanProxyFactoryBean>
<property name=objectName>
<value>jbosscache:service=CustomTreeCache</value>
</property>
<property name=proxyInterface>
<value>orgjbosscacheTreeCacheMBean</value>
</property>
</bean>
这创建一个CustomTreeCacheMBean在服务端的Spring上下文中通过自动代理的特性这个bean实现了orgjbosscacheTreeCacheMBean接口的方法在这里布署到Jboss服务器只需要将已经提供的customcacheservicexml放到服务器的布署目录下
为了简化代码我们引入简单的CacheServiceInterface接口
public void put(String path Object key Object value) throws Exception;
public Object get(String path Object key) throws Exception;
JBoss Cache是一种树状结构这也是为什么我们需要path参数
这个接口的服务端实现如下引用缓存Mbean
<bean id=CacheService class=appserviceJBossCacheServiceImpl>
<property name=cacheMBean ref=CustomTreeCacheMBean/>
</bean>
在最后我们需要ServicePublisher来观察Spring容器的生命周期并且在我们的缓存中发布或移除服务定义
<bean id=ServicePublisher class=appserviceServicePublisher>
<property name=cache ref=CacheService/>
</bean>
这段代码显示ServicePublisher在Spring上下文刷新时(如应用补布署时)如何处理
private void contextRefreshed() throws Exception {
(context refreshed);
String[] names = context
getBeanNamesForType(AutoDiscoveredServiceExporterclass);
(exporting services: + nameslength);
for (int i = ; i < nameslength; i++) {
String serviceUrl = makeUrl(names[i]);
try {
Set services = (Set) cacheget(SERVICE_PREFIX + names[i]
SERVICE_KEY);
if (services == null)
services = new HashSet();
servicesadd(serviceUrl);
cacheput(SERVICE_PREFIX + names[i] SERVICE_KEY services);
(added: + serviceUrl);
} catch (Exception ex) {
loggererror(exception adding service: ex);
}
}
如你所见发布器简单的遍历通过缓存服务描述导出的服务列表并增加定义到缓存中我们的缓存设计成路径包含服务名他的URL列表存储在一个Set对象中将服务名作为路径的一部分对JBoss Cache实现来说是重要的因为他是基于路径来创建和释放事务锁这种方式下对服务A的更新不会干扰对服务B的更新因为他们被映射到不同的路径/some/prefix/serviceA/key=(list of URLs) and /some/prefix/serviceB/key=(list of URLs)
移除服务定义的代码是类似的
现在我们转到客户端我们需要一个缓存实现来与服务端共享
<bean id=LocalCacheService class=appautoLocalJBossCacheServiceImpl>
</bean>
LocalJBossCacheServiceImpl保存着来自与服务端相同的customcacheservicexml配置的JBoss Cache引用
public LocalJBossCacheServiceImpl() throws Exception {
super();
cache = new TreeCache();
PropertyConfigurator config = new PropertyConfigurator();
nfigure(cache app/context/customcacheservicexml);
}
这个缓存定义文件包含了Jgroups层的配置允许所有缓存成员通过UDP多播来定位彼此
LocalJBossCacheServiceImpl还实现了接口并且为我们的AutoDiscoveredService提供了缓存服务这个bean扩展了标准的HttpInvokerProxyFactoryBean类但配置上有些不同
<bean id=TestService
class=appautoAutoDiscoveredService>
<property name=serviceInterface
value=appserviceTestServiceInterface />
<property name=cache ref=LocalCacheService/>
</bean>
最初没有URL存在自动在网络上寻找在TestService名字上暴露的Spring远程服务当服务发现时他就获得了来自分布式缓存的URL列表
private List getServiceUrls() throws Exception {
Set services = (Set) cacheget(ServicePublisherSERVICE_PREFIX
+ beanName ServicePublisherSERVICE_KEY);
if (services == null)
return null;
ArrayList results = new ArrayList(services);
Collectionsshuffle(results);
(shuffled: + results);
return results;
}
Collectionsshuffle随机地重排与服务关联的URL列表因此客户端的方法调用在他们之间是负载均衡的实际的远程调用如下
public Object invoke(MethodInvocation arg) throws Throwable {
List urls = getServiceUrls();
if (urls != null)
for (Iterator allUrls = erator(); allUrlshasNext();) {
String serviceUrl = null;
try {
serviceUrl = (String) allUrlsnext();
supersetServiceUrl(serviceUrl);
(going to: + serviceUrl);
return superinvoke(arg);
} catch (Throwable problem) {
if (problem instanceof IOException
|| problem instanceof RemoteAccessException) {
loggerwarn(got error accessing:
+ supergetServiceUrl() problem);
removeFailedService(serviceUrl);
} else {
throw problem;
}
}
}
throw new IllegalStateException(No services configured for name:
+ beanName);
}
如你所见如果远程调用抛出异常客户端代码可以处理这个问题而且可以从列表中取下一个URL因此也就提供了透明的容错性如果调用因为某些异常失败了他为重新抛出异常给客户端处理
下面的removeFailedService()方法简单的从列表中移除了失败的URL并更新分布式缓存使这个信息同步地通知所有其他客户端
private void removeFailedService(String url) {
try {
(removing failed service: + url);
Set services = (Set) cacheget(ServicePublisherSERVICE_PREFIX
+ beanName ServicePublisherSERVICE_KEY);
if (services != null) {
servicesremove(url);
cacheput(ServicePublisherSERVICE_PREFIX + beanName ServicePublisherSERVICE_KEY
services);
(removed failed service at: + url);
}
} catch (Exception e) {
loggerwarn(failed to remove failed service: + url e);
}
}
如果你构建并布署一个样例应用在多个Jboss服务器上而且运行提供的LoopingAutoDiscoveredRemoteServiceTest你可以看到请求是如何在Spring群集中负载均衡的你也可以停止和重启任何的服务器而调用会动态地路由到其他的服务器上如果你当掉一台服务器你会看到一个异常被输出到客户端的控制台上但所有的请求依旧无停顿的传递给其他服务器
小结
在本文中我们了解了如何通过Spring的远程服务来群集网络服务此外你可以学到如何通过只使用名字来定义私有的服务及依赖自动发现来绑定服务到相应的URL从而简化布署一个复杂的多层应用