當企業級計算進入新的SOA世界時,在尋找描述/發布/和發現服務的方面中開始變得越來越重要?;诰W絡服務的方案不提供自動服務發現而且通常都太繁雜了。現在新的輕量級的開發框架提供了新的輕量級的服務發布方案。
在過去幾年中,SPRing框架已經成為開發簡單、靈活而且輕易配置的J2EE應用的事實標準。Spring的核心是IoC法則。根據IoC,應用必須以一個簡單javaBean的集合來開發,然后用一個輕量級的IoC容器來綁定他們并設置相關的依靠關系。
在Spring中,容器通過一系列bean定義也配置,典型的是用xml文件方式:
<bean id="MyServiceBean" class="mypackage.MyServiceImpl">
<property name="otherService" ref="OtherServiceBean"/>
</bean>
當客戶端代碼需要請求時MyService,你只要如下編碼:
MyServiceInterface service = (MyServiceInterface)context.getBean("MyServiceBean");
service.doSomething();
除了IoC之外,Spring提供了幾百種其他服務,代碼約定,而且通過回調標準API來簡化開發典型的服務端應用。無論應用使用重量級的J2EE API如EJB/JMS/JMX或者使用流行的MVC框架來構建網絡接口,Spring都提供了簡化的效果。
隨著Spring框架的成熟,越來越多的人使用他作為大型企業級項目的基礎。Spring已經通過了伸縮性開發的測試而且可以作為組件粘合劑來聯結復雜的分布式系統。
任何企業級應用都由各種組件組成:如聯結以前的系統和ERP系統,第三方系統,網面/表示層/持久導等等。通常一個電子商務站點都是由簡單的網頁應用逐漸深化成包含上百個子應用和子系統的大項目,而且要面對其中的復雜性會阻礙以后的發展。通常的解決方案是將集成電路般的應用分解成一些粗紋理的服務并將其發布到網絡中。
不管應用是被設計成作為分散服務的集成點或者已經集成為一體,治理所有分布式組件和其配置的任務通常都是耗時和代價高的。但假如你使用了Spring作為應用組件的開發平臺,那么你就可以使用Spring的遠程服務通過一系列的協議來將組件暴露給遠程的客戶端。通過Spring,可以使你的分布式應用就如修改一些配置文件那么簡單。
在Spring中最簡單的java-to-java的遠程通訊方案是使用HTTP遠程服務。例如,在web.xml中注冊了Spring的分發服務件后,下面的上下文片斷就可以將MyService作為公共接口使用了:
<bean name="/MyRemoteService" class="org.springframework.remoting.httpinvoker.HttpInvokerServiceEXPorter">
<property name="service" ref="MyServiceBean"/>
<property name="serviceInterface" value="mypackage.MyServiceInterface"/>
</bean>
如你所見,實際的服務被注入到bean的定義中因此可以被遠程調用:
在客戶端,上下文定義如下:
<bean id="MyServiceBean"
class="org.springframework.remoting.httpinvoker.HttpInvokerProxyFactoryBean">
<property name="serviceUrl"
value="http://somehost:8080/webapp-context/some-mapping-for-spring-servlet/MyRemoteService" />
<property name="serviceInterface"
value="mypackage.MyServiceInterface" />
</bean>
通過Spring的魔法,客戶端代碼不需要改變,而遠程方法的激活就像以前的本地調用一樣。
除了HTTP遠程服務外,Spring還支持其他的遠程協議,如基于HTTP的解決方案(Web services, Hessian, and Burlap)和重量級的如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列表)關聯并隨著網絡狀態的變化(服務器的加入/移除/當機等)而相應更新??蛻舳嗽L問參與分布式緩存的服務就像訪問私有的服務一樣。
作為附加的獎勵,我們會在這里介紹一個簡單的負載均衡/容錯的解決方案。假如客戶端知道一個服務與幾個服務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給遠程調用:
<bean name="/TestService" class="app.service.AutoDiscoveredServiceExporter">
<property name="service" ref="TestService"/>
<property name="serviceInterface" value="app.service.TestServiceInterface"/>
</bean>
這個在沒有什么可說的。我們主要是使用他來標識Spring遠程服務作為我們自己的方式來暴露。
接下來是服務端的緩存配置。Jboss包含了緩存實現,我們可以用Spring內建的JMX代理將緩存引入Spring上下文:
<bean id="CustomTreeCacheMBean" class="org.springframework.jmx.access.MBeanProxyFactoryBean">
<property name="objectName">
<value>jboss.cache:service=CustomTreeCache</value>
</property>
<property name="proxyInterface">
<value>org.jboss.cache.TreeCacheMBean</value>
</property>
</bean>
這創建一個CustomTreeCacheMBean在服務端的Spring上下文中。通過自動代理的特性,這個bean實現了org.jboss.cache.TreeCacheMBean接口的方法。在這里,布署到Jboss服務器只需要將已經提供的custom-cache-service.xml放到服務器的布署目錄下。
為了簡化代碼,我們引入簡單的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="app.service.JBossCacheServiceImpl">
<property name="cacheMBean" ref="CustomTreeCacheMBean"/>
</bean>
在最后,我們需要ServicePublisher來觀察Spring容器的生命周期,并且在我們的緩存中發布或移除服務定義:
<bean id="ServicePublisher" class="app.service.ServicePublisher">
<property name="cache" ref="CacheService"/>
</bean>
這段代碼顯示ServicePublisher在Spring上下文刷新時(如應用補布署時)如何處理:
private void contextRefreshed() throws Exception {
logger.info("context refreshed");
String[] names = context
.getBeanNamesForType(AutoDiscoveredServiceExporter.class);
logger.info("exporting services:" + names.length);
for (int i = 0; i < names.length; i++) {
String serviceUrl = makeUrl(names[i]);
try {
Set services = (Set) cache.get(SERVICE_PREFIX + names[i],
SERVICE_KEY);
if (services == null)
services = new HashSet();
services.add(serviceUrl);
cache.put(SERVICE_PREFIX + names[i], SERVICE_KEY, services);
logger.info("added:" + serviceUrl);
} catch (Exception ex) {
logger.error("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="app.auto.LocalJBossCacheServiceImpl">
</bean>
LocalJBossCacheServiceImpl保存著來自與服務端相同的custom-cache-service.xml配置的JBoss Cache引用:
public LocalJBossCacheServiceImpl() throws Exception {
super();
cache = new TreeCache();
PropertyConfigurator config = new PropertyConfigurator();
config.configure(cache, "app/context/custom-cache-service.xml");
}
這個緩存定義文件包含了Jgroups層的配置,答應所有緩存成員通過UDP多播來定位彼此。
LocalJBossCacheServiceImpl還實現了接口并且為我們的AutoDiscoveredService提供了緩存服務。這個bean擴展了標準的HttpInvokerProxyFactoryBean類但配置上有些不同:
<bean id="TestService"
class="app.auto.AutoDiscoveredService">
<property name="serviceInterface"
value="app.service.TestServiceInterface" />
<property name="cache" ref="LocalCacheService"/>
</bean>
最初,沒有URL存在。自動在網絡上尋找在TestService名字上暴露的Spring遠程服務。當服務發現時,他就獲得了來自分布式緩存的URL列表:
private List getServiceUrls() throws Exception {
Set services = (Set) cache.get(ServicePublisher.SERVICE_PREFIX
+ beanName, ServicePublisher.SERVICE_KEY);
if (services == null)
return null;
ArrayList results = new ArrayList(services);
Collections.shuffle(results);
logger.info("shuffled:" + results);
return results;
}
Collections.shuffle隨機地重排與服務關聯的URL列表因此客戶端的方法調用在他們之間是負載均衡的。實際的遠程調用如下:
public Object invoke(MethodInvocation arg0) throws Throwable {
List urls = getServiceUrls();
if (urls != null)
for (Iterator allUrls = urls.iterator(); allUrls.hasNext();) {
String serviceUrl = null;
try {
serviceUrl = (String) allUrls.next();
super.setServiceUrl(serviceUrl);
logger.info("going to:" + serviceUrl);
return super.invoke(arg0);
} catch (Throwable problem) {
if (problem instanceof IOException
problem instanceof RemoteAccessException) {
logger.warn("got error accessing:"
+ super.getServiceUrl(), problem);
removeFailedService(serviceUrl);
} else {
throw problem;
}
}
}
throw new IllegalStateException("No services configured for name:"
+ beanName);
}
如你所見,假如遠程調用拋出異常,客戶端代碼可以處理這個問題而且可以從列表中取下一個URL,因此也就提供了透明的容錯性。假如調用因為某些異常失敗了,他為重新拋出異常給客戶端處理。
下面的removeFailedService()方法簡單的從列表中移除了失敗的URL并更新分布式緩存,使這個信息同步地通知所有其他客戶端:
private void removeFailedService(String url) {
try {
logger.info("removing failed service:" + url);
Set services = (Set) cache.get(ServicePublisher.SERVICE_PREFIX
+ beanName, ServicePublisher.SERVICE_KEY);
if (services != null) {
services.remove(url);
cache.put(ServicePublisher.SERVICE_PREFIX + beanName, ServicePublisher.SERVICE_KEY,
services);
logger.info("removed failed service at:" + url);
}
} catch (Exception e) {
logger.warn("failed to remove failed service:" + url, e);
}
}
假如你構建并布署一個樣例應用在多個Jboss服務器上而且運行提供的LoopingAutoDiscoveredRemoteServiceTest,你可以看到請求是如何在Spring群集中負載均衡的。你也可以停止和重啟任何的服務器,而調用會動態地路由到其他的服務器上。假如你當掉一臺服務器,你會看到一個異常被輸出到客戶端的控制臺上,但所有的請求依舊無停頓的傳遞給其他服務器。
小結
在本文中,我們了解了如何通過Spring的遠程服務來群集網絡服務。此外,你可以學到如何通過只使用名字來定義私有的服務及依靠自動發現來綁定服務到相應的URL,從而簡化布署一個復雜的多層應用
新聞熱點
疑難解答