本文目的在于分析Jetspeed支持集群的现状。首先介绍了集群计算的背景知识,然后使用tomcat作为例子配置了一个集群,接着分析了jetspeed对集群的支持现状,提出了解决这些问题的办法,最后详细解释了jetspeed保存sesson数据的操作,这将对jetspeed的改造有帮助。
1 、 集群背景介绍 1.1 术语定义
服务软体是b/s或c/s结构的s部分,是为b或c提供服务的服务性软件系统。
服务硬体指提供计算服务的硬件、比如pc机、pc服务器。
服务实体通指服务软体和服务硬体。
客户端指接受服务实体服务的软件或硬件。
1.2 两大关键特性
集群是一组协同工作的服务实体,用以提供比单一服务实体更具扩展性与可用性的服务平台。在客户端看来,一个集群就象是一个服务实体,但事实上集群由一组服务实体组成。与单一服务实体相比较,集群提供了以下两个关键特性:
可扩展性--集群的性能不限于单一的服务实体,新的服务实体可以动态地加入到集群,从而增强集群的性能。
高可用性--集群通过服务实体冗余使客户端免于轻易遇到out of service的警告。在集群中,同样的服务可以由多个服务实体提供。如果一个服务实体失败了,另一个服务实体会接管失败的服务实体。集群提供的从一个出错的服务实体恢复到另一个服务实体的功能增强了应用的可用性。
1.3 两大能力
为了具有可扩展性和高可用性特点,集群的必须具备以下两大能力:
负载均衡--负载均衡能把任务比较均衡地分布到集群环境下的计算和网络资源。
错误恢复--由于某种原因,执行某个任务的资源出现故障,另一服务实体中执行同一任务的资源接着完成任务。这种由于一个实体中的资源不能工作,另一个实体中的资源透明的继续完成任务的过程叫错误恢复。
负载均衡和错误恢复都要求各服务实体中有执行同一任务的资源存在,而且对于同一任务的各个资源来说,执行任务所需的信息视图(信息上下文)必须是一样的。
1.4 两大技术
实现集群务必要有以下两大技术:
集群地址--集群由多个服务实体组成,集群客户端通过访问集群的集群地址获取集群内部各服务实体的功能。具有单一集群地址(也叫单一影像)是集群的一个基本特征。维护集群地址的设置被称为负载均衡器。负载均衡器内部负责管理各个服务实体的加入和退出,外部负责集群地址向内部服务实体地址的转换。有的负载均衡器实现真正的负载均衡算法,有的只支持任务的转换。只实现任务转换的负载均衡器适用于支持ACTIVE-STANDBY的集群环境,在那里,集群中只有一个服务实体工作,当正在工作的服务实体发生故障时,负载均衡器把后来的任务转向另外一个服务实体。
内部通信--为了能协同工作、实现负载均衡和错误恢复,集群各实体间必须时常通信,比如负载均衡器对服务实体心跳测试信息、服务实体间任务执行上下文信息的通信。
具有同一个集群地址使得客户端能访问集群提供的计算服务,一个集群地址下隐藏了各个服务实体的内部地址,使得客户要求的计算服务能在各个服务实体之间分布。内部通信是集群能正常运转的基础,它使得集群具有均衡负载和错误恢复的能力。
2 集群配置 从上图可知,由服务实体1、服务实体2和负载均衡器组成了一个集群。服务实体1和服务实体2参与对客户端的服务支持工作,均衡负载器为客户端维护集群的单一影像。集群实体间通过内部的通信网交流信息,这种交流机制一般采用组播协议。负载均衡器通过内部通信网探测各服务实体的心跳信息,服务实体间通过内部通信网完成任务资源的传播。可以看出,配置集群主要由配置服务实体和配置负载均衡器两部分组成。本文使用tomcat 4.12、apache 2.0.43配置集群环境,相关软件的部署图如下:
服务实体1/2,负载均衡器可以部署在不同的机器上,也可以在同一机器上,本文环境为同一机器。
2.1 准备软件
tomcat是开源servlet \jsp服务器,下载地点http://jakarta.apache.org/ ;
apache 2.0.43 是开源的www服务器,下载地点http://www.apache.org/dist/httpd/binaries/ ;
JavaGroups是一个实现集群服务实体间通信的通信协议,下载地址:http://www.javagroups.com/ ;
Tomcat 会话复制库,基于JavaGroups通信协议,完成集群服务实体间任务执行上下文的复制,下载地址: http://www.filip.net/tomcat/tomcat-javagroups.jar
jk2模块,jk 是mod_jserv的替代者,它是Tomcat-Apache插件,处理Tomcat和Apache之间的通信,在集群配置中充当负载均衡器的作用。JK2是符合apache 2.x系列的新品,下载地址:http://jakarta.apache.org/builds/jakarta-tomcat-connectors/jk2/release/v2.0.2/bin/ 。
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的例子,我们用它来测试集群的错误恢复能力。
测试步骤如下:
关闭tomcat1和tomcat2;
启动tomcat1
在浏览器中输入属性名tomcat1和属性值tomcat1再提交,返回的页面显示session中有刚刚输入的tomcat1属性;
启动tomcat2;
过一会后(等待tomcat2和tomcat1通信并复制信息)关闭tomcat1;
在浏览器中输入属性名tomcat2和属性值tomcat2再提交,返回的页面显示session中有刚刚输入的tomcat2属性,还有先前输入的tomcat1属性;
启动tomcat1;
过一会后(等待tomcat2和tomcat1通信并复制信息)关闭tomcat2;
在浏览器中输入属性名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);
}
3 jetspeed集群 我们现在知道了如何配置、甚至拥有一个集群环境,接下来本文分析Jetspeed的集群现状,主要包括repository和Session数据;为了使分析具有目的,在分析Jetspeed的集群现状之前,先讲述了集群需求和RunData对象。读者可以用集群环境来验证和调试Jetspeed的集群功能。
3.1 集群要求
《Memory Session Replication》一文中讲述了支持集群的应用程序需注意的要点,现在对关于应用系统开发时应注意的事项总结如下:
保存在Session中的对象必须实现java.io.Serializable接口;
从session中获取对象修改后必须用session.setAttribute方法重置session中的属性,因为只有setAttribute能导致session复制。
Java VM不支持类变量的序列化,所以要注意failover不能依赖类变量;
保证各个服务实体的配置完全一样;
保证session状态是唯一决定当前任务状态的东西,临时文件、类变量等会使得错误恢复难以实现、行为可能琢磨不定;
利用request.setAttribute()保存当前请求级的状态,减少服务实体间通信次数。
尽量不要在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文件中实现。
Xreg是jetspeed的注册表,用于登记portlet、control、controller、skin、mediatype等原始资源的定义,jetspeed中缺省地把它实现为文件形式,各种类型有自己的注册表文件;
Psml 是门户结构标记语言的简称,用于组织xreg中的原始资源形成一个对门户视图的定义,当用户使用桌面浏览器访问jetspeed系统时,这个系统根据用户的URL定位一个Psml文档,接着解释这个文档形成HTML代码流返回给浏览器,浏览器展现这个代码流从而形成视窗化的门户视图。Jetspeed中包括了对psml的数据库和文件两种实现方式;
Properties定义了Jetspeed的重要服务及其参数,目前只有文件实现方式。
Jetspeed的启动Repository主要在Properties文件中,运行Repository在xreg和psml中。文件形式的实现大大阻碍了jetspeed支持集群的能力和表现,因为现在很少的应用服务器集群能在一个文件系统上运行,如果Repository需要在运行时改变,就必须同步多个服务实体上的文件,这是一个相当麻烦的问题。如果Repository支持数据库实现形式,Jetspeed可以充分利用数据库的存储和同步机制实现同一个Repository服务于多个Jetspeed。所以要想 jetspeed支持集群、拥有更佳表现,对Repository的数据库化是一个不可忽视的任务。
支持数据库的集群配置如下图:
这个图显示了在数据库集群环境下的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中多放一些数据):
从上面的session快照可以看出,Jetspeed的session数据主要分为两类:BaseJetspeedUser和JetspeedHttpStateManagerService$StateEntry,下面我们就分别来看看这两个类的情况。
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 类图
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的映射如何被填充和清除。
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的映射关系。
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的映射关系。
3.5 修改建议
(1) 实现数据库形式的repository。根据前面的集群需求第五条,必须把repository数据库化才能使得集群下的各个jetspeed的资源视图相同。
(2) StateEntry。根据前面的集群需求第一条,必须让StateEntry实现Serializable接口。目前StateEntry是一个内部类,为了让JVM的Serializer设施能顺利创建StateEntry对象,最好把其public化。
(3) setAttribute要重设session属性。根据前面的集群需求第二条,session对象的setAttribute是导致复制的引子,我们必须在改变session属性后调用session对象的setAttribute方法重置session属性,如下图所示。
虽然Jetspeed中这样模式的代码如下:
更改JetspeedHttpStateManagerService的setAttribute方法。
对下面类中的doXXX方法按照这个模式进行修改。
controllers.MultiColumnControllerAction;
portlets.CustomizeSetAction;
controllers.RowColumnControllerAction;
注意StateEntry中的值的序列性。
4 总结 可以这样说,目前的jetspeed在设计和实现时没有考虑集群环境下的运行情况,本文的分析突出了jetspeed支持集群的主要症结、但不一定完善,甚至有不正确的地方,另外一个主要内容是分析jetspeed保存在session对象中的数据。希望本文有助于大家加深对集群的理解,有助于提醒大家在设计和开发软件系统时"keep clustering in mind"。