配置 Tomcat 集群

集群背景介紹

1.1 術語定義

服務軟體是b/s或c/s結構的s部分,是為b或c提供服務的服務性軟件系統。

服務硬體指提供計算服務的硬件、比如pc機、pc服務器。

服務實體通指服務軟體和服務硬體。

客戶端指接受服務實體服務的軟件或硬件。

1.2 兩大關鍵特性

集群是一組協同工作的服務實體,用以提供比單一服務實體更具擴展性與可用性的服務平臺。在客戶端看來,一個集群就象是一個服務實體,但事實上集群由一組服務實體組成。與單一服務實體相比較,集群提供了以下兩個關鍵特性:

  • 可擴展性--集群的性能不限于單一的服務實體,新的服務實體可以動態地加入到集群,從而增強集群的性能。
  • 高可用性--集群通過服務實體冗余使客戶端免于輕易遇到out of service的警告。在集群中,同樣的服務可以由多個服務實體提供。如果一個服務實體失敗了,另一個服務實體會接管失敗的服務實體。集群提供的從一個出錯的服務實體恢復到另一個服務實體的功能增強了應用的可用性。

1.3 兩大能力

為了具有可擴展性和高可用性特點,集群的必須具備以下兩大能力:

  • 負載均衡--負載均衡能把任務比較均衡地分布到集群環境下的計算和網絡資源。
  • 錯誤恢復--由于某種原因,執行某個任務的資源出現故障,另一服務實體中執行同一任務的資源接著完成任務。這種由于一個實體中的資源不能工作,另一個實體中的資源透明的繼續完成任務的過程叫錯誤恢復。

負載均衡和錯誤恢復都要求各服務實體中有執行同一任務的資源存在,而且對于同一任務的各個資源來說,執行任務所需的信息視圖(信息上下文)必須是一樣的。

1.4 兩大技術

實現集群務必要有以下兩大技術:

  • 集群地址--集群由多個服務實體組成,集群客戶端通過訪問集群的集群地址獲取集群內部各服務實體的功能。具有單一集群地址(也叫單一影像)是集群的一個基本特征。維護集群地址的設置被稱為負載均衡器。負載均衡器內部負責管理各個服務實體的加入和退出,外部負責集群地址向內部服務實體地址的轉換。有的負載均衡器實現真正的負載均衡算法,有的只支持任務的轉換。只實現任務轉換的負載均衡器適用于支持ACTIVE-STANDBY的集群環境,在那里,集群中只有一個服務實體工作,當正在工作的服務實體發生故障時,負載均衡器把后來的任務轉向另外一個服務實體。
  • 內部通信--為了能協同工作、實現負載均衡和錯誤恢復,集群各實體間必須時常通信,比如負載均衡器對服務實體心跳測試信息、服務實體間任務執行上下文信息的通信。

具有同一個集群地址使得客戶端能訪問集群提供的計算服務,一個集群地址下隱藏了各個服務實體的內部地址,使得客戶要求的計算服務能在各個服務實體之間分布。內部通信是集群能正常運轉的基礎,它使得集群具有均衡負載和錯誤恢復的能力。





回頁首


集群配置


集群配置邏輯圖

從上圖可知,由服務實體1、服務實體2和負載均衡器組成了一個集群。服務實體1和服務實體2參與對客戶端的服務支持工作,均衡負載器為客戶端維護集群的單一影像。集群實體間通過內部的通信網交流信息,這種交流機制一般采用組播協議。負載均衡器通過內部通信網探測各服務實體的心跳信息,服務實體間通過內部通信網完成任務資源的傳播。可以看出,配置集群主要由配置服務實體和配置負載均衡器兩部分組成。本文使用tomcat 4.12、apache 2.0.43配置集群環境,相關軟件的部署圖如下:



服務實體1/2,負載均衡器可以部署在不同的機器上,也可以在同一機器上,本文環境為同一機器。

2.1 準備軟件

2.2 配置負載均衡器

在apache下配置負載均衡器分為三步,注意每次修改httpd.conf和workers2.properties時不要忘了重新啟動apache。

  • 第一步,安裝和調試apache

    負載均衡器jk2模塊是apache www 服務的插件,所以配置負載均衡器就得先安裝apache。本文下載的是windows版本 2.0.43,執行setup.exe并回答一些簡單問題就可完成apache的任務。值得注意的是,安裝并啟動apache后如果apache對http://localhost/ 地址沒反應,你得修改apache安裝路徑下htdocs目錄下的index.html.xx文件,比如把index.html.en改成index.html。

  • 第二步,安裝jk2

    把下載到的 mod_jk2-2.0.43.dll改成mod_jk2.dll 放到apache的modules目錄下,修改apache的httpd.conf,即在LoadModule foo_module modules/mod_foo.so 行下插入mod_jk2模塊的裝載信息:

    
                                                        # Example:
                                                        # LoadModule foo_module modules/mod_foo.so
                                                        #
                                                        LoadModule jk2_module modules/mod_jk2.dll
                                                        

  • 第三步,配置jk2

    jk2的配置全在一個配置文件中,文件名為workers2.properties,和apache 的httpd.conf放在同一個目錄下。以下是這個文件的內容:

    
                                                        #++++++++++++++++++++++++++++++++++++
                                                        # only at beginnin. In production uncomment it out
                                                        [logger.apache2]
                                                        level=DEBUG
                                                        #shm必須配
                                                        [shm]
                                                        file=D:\Program Files\Apache Group\Apache2\logs\shm.file
                                                        size=1048576
                                                        # 第一個tomcat 的地址
                                                        # Example socket channel, override port and host.
                                                        [channel.socket:tomcat1]
                                                        port=11009
                                                        host=127.0.0.1
                                                        # 定義第一個工作者指向第一個tomcat
                                                        # define the worker
                                                        [ajp13:tomcat1]
                                                        channel=channel.socket:tomcat1
                                                        #第二個tomcat 得地址
                                                        # Example socket channel, override port and host.
                                                        [channel.socket:tomcat2]
                                                        port=12009
                                                        host=10.1.36.123
                                                        # 定義第二個工作者指向第二個tomcat
                                                        # define the worker
                                                        [ajp13:tomcat2]
                                                        channel=channel.socket:tomcat2
                                                        #定義負載均衡器,使其包含兩個工作者
                                                        [lb:lb1]
                                                        worker=ajp13:tomcat2
                                                        worker=ajp13:tomcat1
                                                        #指定負載均衡器完成單一地址映射,使得apache 服務所在的uri全部指向 兩個tomcat 上的 root
                                                        # Uri mapping
                                                        [uri:/*]
                                                        group=lb:lb1
                                                        #++++++++++++++++++++++++++++++++++++++++++
                                                        

對于jk2模塊的負載均衡配置可參見相關站點,值得提及的是jk2的負載均衡還支持權重分配等優秀功能。

2.3 配置tomcat

同屬于一個集群下的兩個服務實體,要求功能的同一性,所以我們可先安裝和配置第一個tomcat,接著拷貝形成第二個tomcat,最后配置第二個tomcat。

2.3.1 安裝第一個tomcat

安裝tomcat 非常簡單,本文就不再描述。我們假設第一個tomcat的安裝路徑為d:\tomcat1。

拷貝tomcat-javagroups.jar和javagroups.jar到d:\tomcat1\ server\lib 路徑下。

2.3.2 配置第一個tomcat

2.3.2.1 配置jk2

tomcat 中的jk2 connector缺省端口為8009,為了在一臺機器上運行兩個tomcat,修改D:\Tomcat1\conf\jk2.properties,設置jk2 connector的端口為11009,整個文件內容如下:


                                                #++++++++++++++
                                                channelSocket.port=11009
                                                #++++++++++++++
                                                

2.3.2.2 修改server.conf

首先為了讓一臺機器上運行兩個tomcat,修改server.conf的tomcat 停止指令監聽端口:


                                                <Server port="8005" shutdown="SHUTDOWN" debug="0"> 改為
                                                <Server port="11005" shutdown="SHUTDOWN" debug="0">
                                                

然后打開JK2 AJP connector ,關閉其它connector,下面是JK2 AJP 1.3的樣子,這里已把它的端口改為11009:


                                                <!-- Define a Coyote/JK2 AJP 1.3 Connector on port 8009 -->
                                                <Connector className="org.apache.coyote.tomcat4.CoyoteConnector"
                                                port="11009" minProcessors="5" maxProcessors="75"
                                                enableLookups="true" redirectPort="8443"
                                                acceptCount="10" debug="0" connectionTimeout="20000"
                                                useURIValidationHack="false"
                                                protocolHandlerClassName="org.apache.jk.server.JkCoyoteHandler"/>
                                                

接著配置需要集群支持的webapp(比如examples) 的context,添加如下manager:


                                                	<Manager
                                                className="org.apache.catalina.session.InMemoryReplicationManager"
                                                protocolStack="UDP(mcast_addr=228.1.2.3;mcast_port=45566;ip_ttl=32):PING(timeout=3000;
                                                num_initial_members=6):FD(timeout=5000):VERIFY_SUSPECT(timeout=1500):
                                                pbcast.STABLE(desired_avg_gossip=10000):pbcast.NAKACK(gc_lag=10;
                                                retransmit_timeout=3000):UNICAST(timeout=5000;min_wait_time=2000):
                                                MERGE2:FRAG:pbcast.GMS(join_timeout=5000;join_retry_timeout=2000;
                                                shun=false;print_local_addr=false)">
                                                </Manager>
                                                

注意protocolStack的值必須在一行內寫完。

2.3.3 配置第二個tomcat

我們先把已經配好的第一個tomcat復制一份,形成第二個tomcat,假設路徑為d:\tomcat2。

2.3.3.1 配置jk2

修改D:\Tomcat2\conf\jk2.properties,設置jk2 connector的端口12009,整個文件內容如下:


                                                #++++++++++++++
                                                channelSocket.port=12009
                                                #++++++++++++++
                                                

2.3.3.2 修改server.conf

有了第一個tomcat的配置我們只需修改server.conf的tomcat 停止指令監聽端口:


                                                <Server port="11005" shutdown="SHUTDOWN" debug="0"> 改為
                                                <Server port="12005" shutdown="SHUTDOWN" debug="0">
                                                

然后設置JK2 AJP connector 端口為12009。

2.4 運行測試

啟動apache,tomcat1和tomcat2。

2.4.1 測試負載均衡

我們先準備兩個文件,第一個文件為test.jsp,拷貝到第一個tomcat 的根web應用的目錄即d:\tomcat1\webapps\ROOT 下:


                                                <html>
                                                <body bgcolor="red">
                                                <center>
                                                <%= request.getSession().getId() %>
                                                <h1>Tomcat 1</h1>
                                                </body>
                                                </html>
                                                

第二個文件也為test.jsp,拷貝到第二個tomcat 的根web應用的目錄即d:\tomcat2\webapps\ROOT 下:


                                                <html>
                                                <body bgcolor="blue">
                                                <center>
                                                <%= request.getSession().getId() %>
                                                <h1>Tomcat 2</h1>
                                                </body>
                                                </html>
                                                

從不同的瀏覽器中多次輸入地址http://localhost/test.jsp 會看到不同的顏色,這表明apache中的jk2模塊起到了負載均衡的作用。

2.4.2 測試錯誤恢復

訪問url: http://localhost/examples/servlet/SessionExample 可以得到一個關于session的例子,我們用它來測試集群的錯誤恢復能力。

測試步驟如下:

  1. 關閉tomcat1和tomcat2;
  2. 啟動tomcat1
  3. 在瀏覽器中輸入屬性名tomcat1和屬性值tomcat1再提交,返回的頁面顯示session中有剛剛輸入的tomcat1屬性;
  4. 啟動tomcat2;
  5. 過一會后(等待tomcat2和tomcat1通信并復制信息)關閉tomcat1;
  6. 在瀏覽器中輸入屬性名tomcat2和屬性值tomcat2再提交,返回的頁面顯示session中有剛剛輸入的tomcat2屬性,還有先前輸入的tomcat1屬性;
  7. 啟動tomcat1;
  8. 過一會后(等待tomcat2和tomcat1通信并復制信息)關閉tomcat2;
  9. 在瀏覽器中輸入屬性名tomcat11和屬性值tomcat11再提交,返回的頁面顯示session中有剛剛輸入的tomcat11屬性,還有先前輸入的tomcat1和tomcat2屬性;

……

2.4.3 測試多目傳輸的方法

如果運行測試失敗,可以使用下面的JAVAGROUP方法測試機器的多目傳輸性:

啟動多目接收器:


                                                java org.javagroups.tests.McastReceiverTest -mcast_addr 224.10.10.10 -port 5555
                                                

啟動多目傳輸器:


                                                java org.javagroups.tests.McastSenderTest -mcast_addr 224.10.10.10 -port 5555
                                                

這樣你在McastSenderTest窗口中輸入內容,應該在McastReceiverWindow中可以看到結果。如果看不到結果,在McastSenderTest運行參數中加入-ttl 32,如果還不行,可以修改多目地址再試試(注意避開系統保留用的多目地址);如果還不行,就去問問網管吧!

2.4.4 對tomcat-javagroups的修改

tomcat-javagroups.jar中的org.apache.catalina.session.ReplicatedSession類的removeAttribute方法會導致stackoverflow錯誤,請按下面的代碼對其進行修改:


                                                public void removeAttribute(String name, boolean notify, boolean jgnotify) {
                                                super.removeAttribute(name);
                                                if ( jgnotify )
                                                {
                                                SessionMessage msg =
                                                new SessionMessage(notify?SessionMessage.
                                                EVT_ATTRIBUTE_REMOVED_WNOTIFY:SessionMessage.
                                                EVT_ATTRIBUTE_REMOVED_WONOTIFY,
                                                null,
                                                getId(),
                                                name,
                                                null,
                                                null);
                                                sendMessage(msg);
                                                }
                                                }
                                                public void removeAttribute(String name, boolean notify) {
                                                removeAttribute(name,notify,true);
                                                }
                                                





回頁首


jetspeed集群

我們現在知道了如何配置、甚至擁有一個集群環境,接下來本文分析Jetspeed的集群現狀,主要包括repository和Session數據;為了使分析具有目的,在分析Jetspeed的集群現狀之前,先講述了集群需求和RunData對象。讀者可以用集群環境來驗證和調試Jetspeed的集群功能。

3.1 集群要求

《Memory Session Replication》一文中講述了支持集群的應用程序需注意的要點,現在對關于應用系統開發時應注意的事項總結如下:

  1. 保存在Session中的對象必須實現java.io.Serializable接口;
  2. 從session中獲取對象修改后必須用session.setAttribute方法重置session中的屬性,因為只有setAttribute能導致session復制。
  3. Java VM不支持類變量的序列化,所以要注意failover不能依賴類變量;
  4. 保證各個服務實體的配置完全一樣;
  5. 保證session狀態是唯一決定當前任務狀態的東西,臨時文件、類變量等會使得錯誤恢復難以實現、行為可能琢磨不定;
  6. 利用request.setAttribute()保存當前請求級的狀態,減少服務實體間通信次數。
  7. 盡量不要在session中保存大對象,提高服務實體間通信性能。

3.2 RunData對象

RunData對象概念來自于Turbine,在Jetspeed中RunData對象的類型是DefaultJetspeedRunData,這個類擴展了Turbine中的DefaultTurbineRunData類。Jetspeed系統接到用戶瀏覽器的URL請求,進行計算和信息處理,最后返回給瀏覽器HTTP代碼流的整個過程中的代碼都可以訪問同一個RunData對象。所以RunData對象是Jetspeed系統中各個代碼模塊共享信息的機制。

3.3 Jetspeed的Repository

Repository 一般指一個軟件系統賴以啟動、運行的持久性環境,包括啟動Repository和運行Repository兩部分。啟動Repository用于決定系統啟動時的參數,系統運行時不會改變它,如果改變了這些參數,軟件系統必須重新啟動;運行Repository指實時影響軟件系統業務操作的參數,這些參數可以被用戶或管理員當系統在線時改變。現在的趨勢是:盡量減少啟動Repository,而擴大運行Repository;針對Repository的修改最好能使用管理性框架,比如SNMP和JMX。Jetspeed的repository主要在Xreg、psml和Properties文件中實現。

  1. Xreg是jetspeed的注冊表,用于登記portlet、control、controller、skin、mediatype等原始資源的定義,jetspeed中缺省地把它實現為文件形式,各種類型有自己的注冊表文件;
  2. Psml 是門戶結構標記語言的簡稱,用于組織xreg中的原始資源形成一個對門戶視圖的定義,當用戶使用桌面瀏覽器訪問jetspeed系統時,這個系統根據用戶的URL定位一個Psml文檔,接著解釋這個文檔形成HTML代碼流返回給瀏覽器,瀏覽器展現這個代碼流從而形成視窗化的門戶視圖。Jetspeed中包括了對psml的數據庫和文件兩種實現方式;
  3. Properties定義了Jetspeed的重要服務及其參數,目前只有文件實現方式。

Jetspeed的啟動Repository主要在Properties文件中,運行Repository在xreg和psml中。文件形式的實現大大阻礙了jetspeed支持集群的能力和表現,因為現在很少的應用服務器集群能在一個文件系統上運行,如果Repository需要在運行時改變,就必須同步多個服務實體上的文件,這是一個相當麻煩的問題。如果Repository支持數據庫實現形式,Jetspeed可以充分利用數據庫的存儲和同步機制實現同一個Repository服務于多個Jetspeed。所以要想 jetspeed支持集群、擁有更佳表現,對Repository的數據庫化是一個不可忽視的任務。

支持數據庫的集群配置如下圖:


支持數據庫集群的jetspeed集群配置圖

這個圖顯示了在數據庫集群環境下的jetspeed集群配置,數據庫負載均衡器實現數據庫集群的單一影像,例子有weblogic server中的multipool datasource,sql server 基于的windows 2000集群的單一集群IP,ORACLE RAC 的支持多連接地址的thin jdbc driver。

3.4 Jetspeed的Session數據

支持集群必須使得各個服務實體針對某個任務的執行環境是相同的,對于jetspeed來說就是針對各個URL請求,session的數據能在各個jetspeed上復制。這些session被同一個sessionid所標識,這些標識可能來自瀏覽器的cookies或URL中。我們首先用一個velocityportlet來顯示Jetspeed的session中到底保存了什么數據,這個portlet的注冊名字為SessionPortlet。

3.4.1 SessionPortlet

SessionPortlet是一個velocityPortlet,其類名可以是CustomizerVelocityPortlet或VelocityPortlet,一般情況下沒有必要開發一個新的portlet class。關于如何開發部署portlet的教程可見參考部分,現在我們分注冊、控制助手、portlet模版和運行來講述這個portlet。

3.4.1.1 注冊

SessionPortlet用于顯示目前的session數據。它在xreg中的注冊代碼為:


                                                <portlet-entry name="SessionPortlet" hidden="false" type="ref"
                                                parent="CustomizerVelocity" application="false">
                                                <meta-info>
                                                <title>SessionPortlet</title>
                                                <description>check infomation in session</description>
                                                </meta-info>
                                                <classname>org.apache.jetspeed.portal.portlets.CustomizerVelocityPortlet</classname>
                                                <parameter name="template" value="session" hidden="true"
                                                cachedOnName="true" cachedOnValue="true"/>
                                                <parameter name="action" value="portlets.SessionAction"
                                                hidden="true" cachedOnName="true" cachedOnValue="true"/>
                                                <media-type ref="html"/> 
                                                <url cachedOnURL="true"/>
                                                <category group="Jetspeed">legend</category>
                                                <category group="Jetspeed">velocity.legend</category>
                                                </portlet-entry>
                                                

3.4.1.2 控制助手Action

portlets.SessionAction是Velocityportlet模版portlet的控制助手,在velocity解釋模版前執行:


                                                public class SessionAction extends VelocityPortletAction {
                                                protected void buildNormalContext( VelocityPortlet portlet,
                                                Context context,
                                                RunData rundata )
                                                {
                                                Map map = new HashMap();
                                                Enumeration enumeration = rundata.getSession().getAttributeNames();
                                                while (enumeration.hasMoreElements()) {
                                                Object key = (Object) enumeration.nextElement();
                                                Object value = (Object)rundata.getSession().getAttribute(key.toString());
                                                map.put(key, value);
                                                }
                                                context.put("sessions",map);
                                                }
                                                }
                                                

從上面的代碼可以看出,這個控制助手在模版的模型(MVC中的M)環境中設置了一個保存了session數據的map數據結構。

3.4.1.3 portlet模版

SessionPortlet的模版文件是session.vm(MVC中的V),這個文件的內容如下:


                                                <ul>
                                                #foreach( $key in $sessions.keySet() )
                                                <li>Key: $key -> Value: $sessions.get($key)</li>
                                                #end
                                                </ul>
                                                

3.4.1.4 定制psml和運行SessionPortlet

用admin/jetspeed或turbine/turbine帳號/口令登錄到jetspeed系統后,可以在velocity.legend portlet分類中找到SessionPortlet,把它加入到你的psml中后可以看到SessionPortlet顯示的session數據(你可以多多點擊其它的URL,盡量地使jetspeed在session中多放一些數據):


Jetspeed的session數據快照

從上面的session快照可以看出,Jetspeed的session數據主要分為兩類:BaseJetspeedUser和JetspeedHttpStateManagerService$StateEntry,下面我們就分別來看看這兩個類的情況。


Session數據的類圖(部分)

3.4.2 BaseJetspeedUser

我們從《Session數據類圖(部分)》可以看出BaseJetspeedUser實現了serializable接口。另外分析這個類及其父類的代碼可了解到這個類的成員也實現了serializable接口。所以可以初步得出這個類是集群安全的。

DefaultTurbineRundata實現了這個類型的session數據的操作接口:

  • 保存user對象到session中,這個方法登錄后由TurbineAuthentication的login調用,登錄前由JetspeedSessionValidator的doPerform調用,它們同時會調用DefaultTurbineRundata的setUser方法:
    
                                                        public void save()
                                                        {
                                                        session.putValue(User.SESSION_KEY, (Object) user );
                                                        }
                                                        public void setUser(User user)
                                                        {
                                                        this.user = user;
                                                        }
                                                        


  • 從session中獲取user對象數據,這個方法由JetspeedSessionValidator的doPerform調用:
    
                                                        	public void populate()
                                                        {
                                                        user = getUserFromSession();
                                                        if ( user != null )
                                                        {
                                                        user.setLastAccessDate();
                                                        user.incrementAccessCounter();
                                                        user.incrementAccessCounterForSession();
                                                        }
                                                        }
                                                        public User getUserFromSession()
                                                        {
                                                        return getUserFromSession(session);
                                                        }
                                                          public static User getUserFromSession(HttpSession session)
                                                        {
                                                        try
                                                        {
                                                        return (User) session.getValue(User.SESSION_KEY);
                                                        }
                                                        catch ( ClassCastException e )
                                                        {
                                                        return null;
                                                        }
                                                        }
                                                        


  • 刪除session中的用戶數據,目前沒地方調用:
    
                                                         public boolean removeUserFromSession()
                                                        {
                                                        return removeUserFromSession(session);
                                                        }
                                                        public static boolean removeUserFromSession(HttpSession session)
                                                        {
                                                        try
                                                        {
                                                        session.removeValue(User.SESSION_KEY);
                                                        }
                                                        catch ( Exception e )
                                                        {
                                                        return false;
                                                        }
                                                        return true;
                                                        }
                                                        


3.4.2.1 用戶登錄

用戶在jetspeed的首頁中輸入用戶名和口令,接著點擊登錄(login)按鈕,可以激活JLoginUser.doPerfom->TurbineAuthentication.login->DefaultTurbineRundata.save->JetspeedSessionValidator.doPerform-> DefaultTurbineRundata.populate系列步驟。

如果properties配置中的配置項automatic.logon.enable 的值為true,JLoginUser.doPerfom還會設置瀏覽器cookies:username 和logincookie。username是成功登錄的用戶名, logincookie是一個隨機值,會保存到用戶數據庫中。

當用戶訪問jetspeed的首頁時,JetspeedSessionValidator.doPerform檢查RunData對象中的當前用戶,如果沒有登錄而且automatic.logon.enable 的值為true,它會從cookies中獲取username 和logincookie,再從用戶數據庫中查尋用戶的logincookie,如果它們相等則調用下面的代碼設置RunData的用戶數據:


                                                data.setUser(user);
                                                user.setHasLoggedIn(new Boolean(true));
                                                user.updateLastLogin();
                                                data.save();
                                                

至于針對不同的用戶,首頁中顯示的portlet由缺省screen模版中調用JetspeedTool的方法(有一套PSML定位算法)來決定。

3.4.2.2 當session過期之后顯示匿名用戶的主頁

當session過期,Turbine.doget首先會創建新的session,接著激活JetspeedSessionValidator.doPerform-> JetspeedSecurity.getAnonymousUser->DefaultTurbineRundata.save系列步驟。

JetspeedSessionValidator.doPerform會設置缺省screen模版。

3.4.2.3 用戶登出

當用戶登錄之后,點擊Jetspeed系統右上角的登出(logout)按鈕,可以激活JLogOut.doPerform-> TurbineAuthentication.logout-> TurbineAuthentication.getAnonymousUser-> DefaultTurbineRundata.save系列步驟。

TurbineAuthentication.getAnonymousUser從數據庫中得到匿名用戶的用戶數據(根據properties配置中user.anonymous項)。

如果properties配置中配置項automatic.logon.enable 的值為true,JLogOut.doPerform還會刪除瀏覽器和當前request的cookies:username 和logincookie,防止后面的JetspeedSessionValidator拿著先前的用戶數據自動登錄。JLogOut.doPerform最后設置data的缺省screen模版。

3.4.3 JetspeedHttpStateManagerService$StateEntry

我們從《Session數據的類圖(部分)》可以看出StateEntry沒有實現了Serializable接口。把它放到session的屬性中不是集群安全的。Serializable接口只是個標志接口,它不擁有任何函數和數據成員,

為了使其集群安全化,首先必須讓StateEntry實現Serializable接口。

DefaultJetspeedRunData擁有下列對StateEntry類型的session數據操作接口:

  • 用戶session接口,獲取保存用戶session數據的SessionState。這個SessionState保存的session數據以"org.apache.jetspeed.services.statemanager.JetspeedHttpStateManagerService"+ sessionID為key。
    
                                                        public SessionState getUserSessionState()
                                                        {
                                                        StateManagerService service = (StateManagerService)TurbineServices
                                                        .getInstance().getService(StateManagerService.SERVICE_NAME);
                                                        if (service == null) return null;
                                                        return service.getSessionState(getSession().getId());
                                                        }
                                                        


  • request的Session接口,獲取保存當前request session數據的SessionState。這個SessionState保存的session數據以"org.apache.jetspeed.services.statemanager.JetspeedHttpStateManagerService"+ (sessionID和profileID組成的pageSessionID)為key。
    
                                                         public SessionState getPageSessionState()
                                                        {
                                                        StateManagerService service = (StateManagerService)TurbineServices
                                                        .getInstance().getService(StateManagerService.SERVICE_NAME);
                                                        if (service == null) return null;
                                                        return service.getSessionState(getPageSessionId());
                                                        }
                                                        


  • Portlet的Session接口,獲取保存Portlet session數據的SessionState。這個SessionState保存的session數據以"org.apache.jetspeed.services.statemanager.JetspeedHttpStateManagerService"+ pageSessionID+portletID為key。
    
                                                         public SessionState getPortletSessionState(String id)
                                                        {
                                                        // get the StateManagerService
                                                        StateManagerService service = (StateManagerService)TurbineServices
                                                        .getInstance().getService(StateManagerService.SERVICE_NAME);
                                                        if (service == null) return null;
                                                        String pageInstanceId = getPageSessionId();
                                                        return service.getSessionState(pageInstanceId + id);
                                                        }
                                                        


3.4.3.1 類圖


StateManagement類圖

BaseStateManagerService有一個類型為Map的成員變量m_httpSessions,以Thread對象為key,HttpSession對象為值。HttpSession對象中屬性的key 是前面DefaultJetspeedRunData的StateEntry類型的session數據操作接口的key,屬性的值為StateEntry對象。StateEntry對象的成員變量m_key保存操作接口的key,成員變量m_map是一個Map對象,以后面我們要講的setAttribute方法的name參數為 key,value參數為值。

3.4.3.2 初始化

下面的順序圖是一個簡圖,主要用于解釋BaseStateManagerService的成員變量m_httpSessions的映射如何被填充和清除。


m_httpSessions填充與刪除順序圖

Turbine是一個servlet,其doGet方法是jetspeed系統的入口。

  • 填充

    Turbine請求JetspeedRunDataService生成RunData對象,JetspeedRunDataService調用HttpServiceRequest的getSession(true)方法獲取與當前請求對應的httpSession對象(以true為參數,getSession在當前session無效時會返回一個新的httpSession對象,否則返回先前請求的httpSession對象),JetspeedRunDataService接著調用JetspeedHttpStateManagerService的setCurrentContext(httpSession對象)方法,這個方法會以當前的Thread為key,參數httpSession對象為值填充BaseStateManagerService的成員變量m_httpSessions。

  • 清除

    doGet方法填充了m_httpSessions,并作了好多事情之后,在即將退出之前調用了JetspeedRunDataService的putRunData(data)方法,這個方法再調用JetspeedHttpStateManagerService的clearCurrentContext()方法刪除BaseStateManagerService的成員變量m_httpSessions中以當前Thread為key的Map項。
    下圖顯示了m_httpSessions對象經過初始化后的內存狀態快照,體現了m_httpSessions對象保留的Thread-〉HttpSession的映射關系。


m_httpSessions對象之內存狀態示意圖

3.4.3.3 屬性操作

當DefaultJetspeedRunData通過session操作接口獲取SessionState之后,其它就可以使用SessionState對象的成員方法操作狀態屬性了。這兩個方法是:


                                                public void setAttribute( String name, Object value );
                                                public void removeAttribute( String name );
                                                


屬性操作順序圖

在從RunData對象處獲取sessionState對象后,jetspeed代碼可以調用這個對象的屬性操作方法。

  • setAttribute(name,value)操作的大概步驟是:

    (1) 主要步驟:

    1.1sessionState對象利用自己的key,結合參數name,value調用JetspeedHttpStateManagerService的setAttribute(key,name,value)方法;

    1.1.1JetspeedHttpStateManagerService調用自己的getState(key)方法在參數key的幫助下獲取保存在當前線程session中的StateEntry對象的m_map變量,這個過程由1.1.1.1-1.1.1.4組成;

    1.1.2得到StateEntry對象的m_map變量后,JetspeedHttpStateManagerService接著先處理m_map中的先前的參數name對應的屬性值,再設置參數name對應的屬性值新值為參數value。

    (2) 候選步驟:

    1.1.1a 如果session中沒有相應的StateEntry對象,則先生成并往一個session中加入一個。

  • getAttribute(name)操作的大概步驟是:

    (1) 主要步驟:

    2.1sessionState對象利用自己的key,結合參數name調用JetspeedHttpStateManagerService的getAttribute(key,name)方法;

    2.1.1JetspeedHttpStateManagerService調用自己的getState(key)方法在參數key的幫助下獲取保存在當前線程session中的StateEntry對象的m_map變量;

    2.1.2得到StateEntry對象的m_map變量后,JetspeedHttpStateManagerService接著調用m_map對象的get(name)方法獲取屬性值。

下圖體現了這些方法執行后HttpSession對象保留的key-> StateEntry對象以及StateEntry對象的Name->Value的映射關系。


httpSession對象之內存狀態示意圖

3.5 修改建議

(1) 實現數據庫形式的repository。根據前面的集群需求第五條,必須把repository數據庫化才能使得集群下的各個jetspeed的資源視圖相同。

(2) StateEntry。根據前面的集群需求第一條,必須讓StateEntry實現Serializable接口。目前StateEntry是一個內部類,為了讓JVM的Serializer設施能順利創建StateEntry對象,最好把其public化。

(3) setAttribute要重設session屬性。根據前面的集群需求第二條,session對象的setAttribute是導致復制的引子,我們必須在改變session屬性后調用session對象的setAttribute方法重置session屬性,如下圖所示。


httpSession對象之內存狀態示意圖

雖然Jetspeed中這樣模式的代碼如下:

  • 更改JetspeedHttpStateManagerService的setAttribute方法。

對下面類中的doXXX方法按照這個模式進行修改。

  • controllers.MultiColumnControllerAction;
  • portlets.CustomizeSetAction;
  • controllers.RowColumnControllerAction;
  • 注意StateEntry中的值的序列性。