HttpSession框架指南

来源:互联网 发布:虚拟专用网络是免费的 编辑:程序博客网 时间:2024/06/05 16:10

HttpSession框架指南

1.问题

众所周知,标准的Servlet API中,有一个HttpSession的接口。本来HTTP协议是无状态的,通过session机制,就能把无状态的变成有状态的。有了session的支持,WEB应该就能够跟踪一个用户的操作状态。在一个WEB应用中,你可以这样使用session:

// 取得session对象

HttpSessionsession = request.getSession();

 

// 在session中保存用户状态

session.setAttribute(“loginId”,“myName”);

 

// 在另一个请求中,取出session的状态

String myName =(String) session.getAttribute(“loginId”);

如何保存session中的状态?一般的做法,是将session对象保存在内存里,然后用一Session ID来索引。同一时间,会有很多session被保存在服务器的内存里,但每个session的ID都是不一样的。对于JavaServlet,常见的做法是把这个SessionID保存在cookie里。这样一来,凡是cookie值相同的所有的请求,就被看作是在同一个session中的请求。由于内存是有限的,较先进的服务器会把session对象交换到文件中,以确保内存中的session数保持在一个合理的范围内。

为了提高系统扩展性和可用性,我们会使用集群技术—— 就是一组独立的机器共同运行同一个应用。对用户来讲,集群相当于一台服务器。而实际上,同一用户的两次请求可能被分配到两台不同的服务器上来处理。这样一来,怎样保证两次请求中存取的session值一致呢?

多数的服务器会使用session复制的方法:当session的值被改变时,将它复制到其它机器上。这个方案又有两种具体的实现,一种是广播的方式。这种方式下,任何一台服务器都保存着所有服务器所接受到的session对象。服务器之间随时保持着同步,因而所有服务器都是等同的。可想而知,当访问量增大的时候,这种方式花费在广播session上的带宽有多大,而且随着机器增加,网络负担成指数级上升,不具备高度可扩展性。另一种是TCP-ring的方式,也就是把集群中所有的服务器看成一个环,A->B->C->D->A,首尾相接。把A的session复制到B,B的session复制到C,……,以此类推,最后一台服务器的session复制到A。这样,万一A宕机,还有B可以顶上来,用户的session数据不会轻易丢失。但这种方案也有缺点:一是配置复杂;二是每增添/减少一台机器时,ring都需要重新调整,这将成为性能瓶颈;三是要求前端的LoadBalancer具有相当强的智能,才能将用户请求分发到正确的机器上。

另一种保存session的思路是,将session保存在单一的数据源中。这个数据源可被集群中所有的机器所共享。这样一来,复制就不是问题了。然而性能成了问题。每个用户请求,都需要访问后端的数据源(很可能是数据库)来存取用户的数据。这种思路的第二个问题是:缺少应用服务厂商的支持—— 很少有应用服务器支持这种方案,更不用说数据源有很多种(MySQL、Oracle、Hsqldb等各种数据库、专用的session server等)了。第三个问题是:数据源成了系统的瓶颈,一但这个数据源崩溃,所有的应用都不可能正常运行了。

综上所述,session看起来很简单,实际上是一个大问题!犹其是对一个访问量极高的大型网站而言。

2.解决方案

2.1.将session数据保存在客户端

前面所说的通过复制,或者通过单一数据源,都可以解决集群中分布式session的问题,然而存在前面所说的很多问题。另一种思路,是把session保存在客户端。这样一来,服务器就变成无状态了,就能够做到线性可扩展和极高的可用性。怎么保存呢?目前唯一的方法,恐怕就是cookie了。

Cookie方案也不是完美的,有以下限制:

1.        Cookie数量和长度的限制。每个domain最多只能有20条cookie,每个cookie长度不能超过4KB,否则会被截掉。

2.        安全性问题。如果cookie被人拦截了,那人就可以取得所有的session信息。即使加密也与事无补,因为拦截者并不需要知道cookie的意义,他只要原样转发cookie就可以达到目的了。

3.        有些状态不可能保存在客户端。例如,为了防止重复提交表单,我们需要在服务器端保存一个计数器。如果我们把这个计数器保存在客户端,那么它起不到任何作用。

虽然有上述缺点,但是对于其优点(极高的扩展性和可用性)来说,就显得微不足道。然而我们仍然要设法回避上述的缺点,方法是:

1.        通过良好的编程,控制保存在cookie中的session对象的大小。

2.        通过加密和安全传输技术(SSL),减少cookie被破解的可能性。

3.        只在cookie中存放不敏感数据,即使被盗也不会有重大损失。

4.        控制cookie的生命期,使之不会永远有效。偷盗者很可能拿到一个过期的cookie。

2.2.将session保存在专用的服务器上

前面提到,由于cookie机制本身的限制,我们不可能将所有的数据都放在cookie中,势必要有服务器端的方案来辅助。相比复制的方法,单一的数据源更简单更可靠。我们可以使用数据库来保存这部分session。然而我倾向于使用更廉价、更轻量的存储。事实证明,BerkeleyDB是一种很好的选择。

由于我们只把少量关键的信息保存在服务端,因而这个数据源的压力不会非常大。事实证明,BerkeleyDB在保持良好的可用性的前提下,具有相当高的系统容量,可以高效处理大量的数据请求。

2.3.创建通用的session框架

不是Java Servlet API的session机制不够灵活,而是多数应用服务器并没有留出足够的余地,来让你自定义session的存储方案。根据上面的讨论,任何一种session方案都有其优缺点。最好的方法是把它们结合起来。例如:结合cookie-based session和berkeleyDB-based session,就可以解决我们的绝大部分问题。

纵使某个应用服务器提供了对外扩展的接口,可以自定义session的方案,我们也不大可能使用它。为什么呢?因为我们希望保留选择应用服务器软件的自由。

因此,最好的方案,不是在应用服务器上增加什么新功能,而是在WEB应用框架上做手术。一但我们在WEB应用框架中实现了这种灵活的session框架,那么我们的应用可以跑在任何标准的JavaEE应用服务器上。

除此之外,一个好的session框架还应该做到对应用程序透明。具体表现在:

1.        使用标准的HttpSession接口,而不是增加新的API。这样任何WEB应用,都可以轻易在两种不同的session机制之间切换。

2.        应用程序不需要知道session中的对象是被保存到了cookie中还是别的什么地方。

3.        Session框架可以把同一个session中的不同的对象分别保存到不同的地方去,应用程序同样不需要关心这些。例如,把一般信息放到cookie中,关键信息放到berkeleyDB中。甚至同是cookie,也有持久和临时之分,有生命期长短之分。

3.Session框架详解

我们实现了这个session框架。

目前,这个session框架是依赖于我们的通用service框架的。由于service框架是Webx框架的基础,所以Webx自然可以方便地使用这个session框架。

对于webx之外的WEB应用 —— 例如:独立运行的JSP、由其它应用框架如webwork制作的应用 —— 我们提供了一个filter。这样所有的应用都可以使用我们的session框架,从而支持cookie-based session、berkeleyDB-based session以及扩展出任意类型的session实现。

你可以从Subversion中checkout框架的源码:

svn co http://svn.alibaba-inc.com/repos/opentech/toolkit/trunk/webx/session/

或二进制包:

cd repository.project\toolkit\webx

svn co http://svn.alibaba-inc.com/repos/binary/repository.project/toolkit/webx/session/

不过这个框架还会直接或接间地依赖很多其它的包(有Antx来管理其依赖关系,你不必惊慌)。

3.1.开始装配session系统

从现在起,我们开始演示如何配置一个实际的session系统。并在配置的过程中,进一步解说session框架的详情。

最常见的情况下,session系统是由Webx框架装载的。为了在Webx框架中使用session框架,需要修改WEB-INF/webx.xml文件中的RunDataService。RunDataService是前面所述的RequestContext框架的入口:

<servicename="RunDataService"class="com.alibaba.webx.service.rundata.DefaultRunDataService">    <!--      - 将response.getWriter()和response.getOutputStream()的内容缓存起来。     -->    <property name="request.buffered.class"       value="com.alibaba.webx.request.context.buffered.BufferedRequestContextFactory"/>     <!--      - 拦截sendRedirect等可能导致response提交的操作,延后至请求结束才做。     -->    <property name="request.lazycommit.class"       value="com.alibaba.webx.request.context.lazycommit.LazyCommitRequestContextFactory"/>     <!--      - 自动parse参数(包括upload form)。     -->    <property name="request.parser.class"        value="com.alibaba.webx.request.context.parser.ParserRequestContextFactory"/>     <!--      - Session框架的配置。     -->    <property name="request.session">        <property name="class"value="com.alibaba.webx.session.request.SessionRequestContextFactory"/>    (…未完待续)</property>     <!--      - 设置请求的locale/charset。     -->    <property name="request.locale">        <property name="class"value="com.alibaba.webx.request.context.locale.SetLocaleRequestContextFactory"/>        <propertyname="defaultLocale" value="zh_CN"/>        <propertyname="defaultCharset" value="GBK"/>    </property></service>


上面的配置文件还没有完成。我们先来看看这段配置是什么意思。

3.2.Session的总体结构

从表面上看,实现一个session框架很简单,但实际上,它同时设及到对request和response两个对象的修改。好在Java Servlet API提供了HttpServletRequestWrapper和HttpServletResponseWrapper类来包装request和response。为此,我们设计了一个接口:RequestContext。该接口同时包装了request和response对象。不仅如此,多个RequestContext还能串接起来,形成一条链,以创造出不同的功能组合。RequestContext是另一个独立的框架,请参见相关的文档。我们的session框架只不过实现了RequestContext接口,并由该框架来引导session的创建过程。

下图简述了Session框架的整体静态结构。

很明显前面RunDataService的配置被划分成很多“段”:request.buffered.class、request.lazycommit.class、request.parser.class、request.session.class、request.locale.class等。其实这就是所谓的RequestContext链。每一“段”都是一个RequestContext的实现,都会对request和(或)response进行一层包装,以便实现一种特定的功能。这个RequestContext框架不是专为session框架设计的,因此上面配置中有几个request context的实现和session框架是没有关系的。例如:request.parser.class的功能是解析request parameters,透明处理multipart-form格式的请求;request.locale.class的功能是设置当前请求的输入/输出locale和charset。我们将在另文中对这些RequestContext进行详细讨论。

值得注意的是剩下的三个request context:request.buffered.class、request.lazycommit.class和request.session.class。前两个request context —— request.buffered.class和request.lazycommit.class ——并不是session框架的一部分,但是没有它们,就不能实现cookie-basedsession。为什么呢?这要从HTTP协议谈起。下面是一个标准的HTTP响应的文本。无论你的服务器使用了何种平台(Apache HTTPDServer、JavaServlet/JSP、Microsoft IIS,……),只要你通过浏览器来访问,必须返回类似下面的HTTP响应:

HTTP/1.1 200 OKServer:Apache-Coyote/1.1Set-Cookie:JSESSIONID=AywiPrQKPEzfF9OZ;Path=/Content-Type:text/html;charset=GBKContent-Language:zh-CNContent-Length:48Date: Mon, 06 Nov2006 07:59:38 GMT  <html><body>


……

我们注意到,HTTP响应分为Header和Content两部分。从“HTTP/1.1 200 OK”开始,到“<html>”之前,都是HTTP Header,后面则为HTTP Content。而cookie是在header中指定的。一但应用服务器开始向浏览器输出content,那就再也没有机会修改header了。问题就出在这里。作为session的cookie可以在应用程序的任何时间被修改,甚至可能在content开始输出之后被修改。但是此后修改的session将不能被保存到cookie中。

JavaServlet API的术语称“应用服务器开始输出content”为“response被提交”。你可以通过response.isCommitted()方法来判断这一点。那么,哪些操作会导致response被提交呢?

1.        向response.getWriter()或getOutputStream()所返回的流中输出,累计达到服务器所设定的一个chunk的大小,通常为8K。

2.        用户程序或系统调用response.flushBuffer()。

3.        用户程序或系统调用response.sendError()转到错误页面。

4.        用户程序或系统调用response.sendRedirect()重定向。

只要避免上述情形的出现,就可以确保cookie可以被随时写入。

前两个request context —— request.buffered.class和request.lazycommit.class正好解决了上面的问题。第一个RequestContext(request.buffered.class)将所有的输出到response.getWriter()或getOutputStream()的内容缓存在内存里,直到最后一刻才真正输出到浏览器;第二个RequestContext(request.lazycommit.class)拦截了response对象中引起提交的方法,将它们延迟到最后才执行。这样就保证了在cookie被完整写入之前,response绝不会被任何因素提交。

此外,request.buffered.class不是专为session框架而设计的。Webx的页面布局系统也依赖这个RequestContext。

下面,我们进一步讨论session request context的配置,也就是标记“…未完待续”的那部分配置:

<servicename="RunDataService"class="com.alibaba.webx.service.rundata.DefaultRunDataService">    ……     <!--      - Session框架的配置。     -->    <property name="request.session">        <property name="class"value="com.alibaba.webx.session.request.SessionRequestContextFactory"/>    (…未完待续)</property>     ……</service>


3.3.Session的创建过程

在配置文件中,我们指定了:request.session.class = c.a.w.s.r.SessionRequestContextFactory

SessionRequestContextFactory是由RequestContext框架激活的。当它初始化的时候,它就会根据配置文件的内容来创建适当的SessionIDBroker、SessionStore等对象。接下来,它会创建session框架的核心对象:SessionRequestContextImpl。这个对象中包装了原始的request和response,并返回给系统一对修改过的request和response。用户的应用程序最终会通过这个修改过的request对象取得session对象(通过request.getSession()调用),这样一来,就得到了实现标准HttpSession接口的SessionImpl对象。

整个过程如下图:

通常,Java Servlet风格的session系统,会返回一个名叫JSESSIONID的cookie给浏览器,这个cookie中包含了session的主键。系统就是通过这个cookie来跟踪session的。Java Servlet API同时支持将JSESSIONID编码到URL中。这种方式我们的session框架同样支持。现在的问题是,如何生成这个ID?

为了达到最大的兼容性,我们分两种情况来处理JSESSIONID:当一个新session到达时,假如cookie或URL中已然包含了JSESSIONID,那么我们将直接利用这个值。为什么这样做呢?因为这个JSESSIONID可能是由同一域名下的另一个不相关应用生成的。如果我们不由分说地将这个cookie覆盖掉,那么另一个应用的session就会丢失。理想的情况下,对于一个新session,应该是不包含JSESSIONID的。这时,我们需要利用SessionIDBroker来生成一个唯一的字符串,作为JSESSIONID。SessionIDBroker是一个接口,其实现是可被替换的。如果不加指定,默认的实现为RandomSessionIDBroker。

下面罗列出一系列和JSESSIONID相关的配置,所有配置均置身于前例标记“…未完待续”的那部分之中。

²  是否将JSESSIONID保存在cookie中,默认为true。如果为false,应用必须调用response.encodeURL()或response.encodeRedirectURL()来将JSESSIONID保存在URL中。参见:session.urlencode.enabled开关。

<propertyname="session.cookie.enabled" value="true"/>

²  指定保存session ID的cookie的名字,默认为"JSESSIONID"。

<propertyname="session.cookie.name" value="JSESSIONID"/>

²  指定session ID cookie的domain,如果不设置,则不发送domain。这意味着浏览器认为你的cookie属于当前域名。如果你的应用包含多个子域名,例如:www.alibaba.com、china.alibaba.com,而你又希望它们能共享session的话,请把域名设置成“alibaba.com”。

<propertyname="session.cookie.domain" value="alibaba.com"/>

²  指定session ID cookie的path,默认为"/"根目录。通常不需要修改这个默认值。

<propertyname="session.cookie.path" value="/"/>

²  指定session ID cookie的寿命(过期时间),单位是秒。默认为0,意味着cookie持续到浏览器被关闭(或称临时cookie)。有效值必须大于0,否则均被认为是临时cookie。

<propertyname="session.cookie.maxAge" value="0"/>

²  是否允许将session ID编码到URL中,默认为false。注意,如果打开该选项,必须关闭session.cookie.enabled。

<propertyname="session.urlencode.enabled" value="false"/>

²  指定在URL中表示session ID的名字,默认和cookie名相同:"JSESSIONID"。此时,如果session.urlencode.enabled为true的话,调用response.encodeURL("http://localhost:8080/test.jsp?id=1")将得到类似这样的结果:"http://localhost:8080/test.jsp;JSESSIONID=xxxyyyzzz?id=1"。

<propertyname="session.urlencode.name" value="JSESSIONID"/>

²  指定用来生成Session ID的类。默认为RandomSessionIDBroker。如果你扩展的新的SessionIDBroker需要一些额外的参数,请使用下面的第二种配置形式。

<propertyname="session.idbroker.class" value="com.alibaba.webx.session.idbroker.random.RandomSessionIDBroker"/>

或:

<propertyname="session.idbroker">

<property name="class" value="com.alibaba.webx.session.idbroker.random.RandomSessionIDBroker"/>

<property name="xyz" value="123"/>

</property>

3.4.Session的生命期

所谓生命期,就是session从创建到失效的整个过程。如下图所示:

总结一下,其实很简单:

1.        第一次打开浏览器时,JSESSIONID还不存在,或者存在由同一域名下的其它应用所设置的无效的JSESSIONID。这种情况下,session.isNew()返回true。

2.        随后,只要在规定的时间间隔内,以及cookie过期之前,每一次访问系统,都会使session得到更新。此时session.isNew总是返回false。Session中的数据得到保持。

3.        如果用户有一段时间不访问系统了,超过指定的时间,那么系统会清除所有的session内容,并将session看作是新的session。

4.        用户可以调用session.invalidate()方法,直接清除所有的session内容。此后所有试图session.getAttribute()或session.setAttribute()等操作,都会失败,得到IllegalStateException异常,直到下一个请求到来。

在session框架中,有一个重要的特殊对象,用来保存session生命期的状态。这个对象叫作session model。它被当作一个普通的对象存放在session中,但是通过HttpSession接口不能直接看到它。

下面是有关session生命期控制的配置文件项。这些配置也是置身于前例标记“…未完待续”的那部分之中

²  指定session不活动而失效的期限,单位是秒。默认为0,也就是永不失效(除非cookie失效)。例如,设置3600秒,表示用户离开浏览器1小时以后再回来,session将重新开始,老数据将被丢弃。

<propertyname="session.maxInactiveInterval" value="0"/>

²  指定用于保存session状态的对象的名称。默认为"SESSION_MODEL"。一般不需要修改这个值。

<propertyname="session.model.name" value="SESSION_MODEL"/>

²  是否每次都touch session(即更新最近访问时间)。如果是false,那么只在session值有改变时touch。当将session model保存在cookie中时,设为false可以减少网络流量。但如果session值长期不改变,由于最近访问时间一直无法更新,将会使session超过maxInactiveInterval所设定的秒数而失效。默认为false。

<propertyname="session.touch.always" value="false"/>

3.5.Session的保存

至此,我们开始讨论session框架中最核心的部分:SessionStore。Session框架最灵活的部分就在于此。我们可以定义很多个session store,让不同的session对象分别存放到不同的Session Store中。前面提到有一个特殊的对象:SESSION_MODEL也必须保存在某个session store中。

Session store的配置包含两部分内容:

1.        如何创建Session store?配置session store的实现类名、初始化参数等。

2.        将指定key的对象保存在哪个store中?配置匹配方案。

基本配置方法如下:

<servicename="RunDataService" class="com.alibaba.webx.service.rundata.DefaultRunDataService">    (……其它RequestContext的配置)     <!--      - Session框架的配置。     -->    <property name="request.session">        <property name="class"value="com.alibaba.webx.session.request.SessionRequestContextFactory"/>        (……前述关于JSESSIONID和生命期的配置)         <!--Session store 1 -->        <property name="session.store.Store名称1">            <property name="class"value="Store类名"/>             <!-- 匹配方案 -->            <property name="match"value="精确匹配,或*代表匹配所有"/>           <property name="matchRegex" value="匹配正则表达式"/>             <!-- 其它参数,取决于具体的store实现 -->        </property>         <!-- Session store 2 -->        <property name="session.store.Store名称2">            <!-- 完全类似store 1 -->        </property>        <!-- 更多session stores --></property>     (……其它RequestContext的配置)</service>


下面是一段cookie store配置的范例:

<!--  - temporary cookie store --><propertyname="session.store.temporary">    <property name="class"value="com.alibaba.webx.session.store.cookie.CookieStore"/>    <property name="match"value="*"/>    <property name="cookie.name"value="tmp"/>    <propertyname="cookie.permanent" value="false"/></property><!--  - permanent cookie store --><propertyname="session.store.permanent">    <property name="class"value="com.alibaba.webx.session.store.cookie.CookieStore"/>    <property name="match"value="userId"/>    <property name="matchRegexp"value="login_\w+"/>    <property name="matchRegexp"value="history_\w+"/>    <property name="cookie.name"value="pmt"/>    <propertyname="cookie.permanent" value="true"/></property>


需要注意以下几点:

²  你可以配置任意多个session store,只要名字不重复。
上例中,temporary和permanent分别是两个session store的名称。

²  可以包含若干个match属性,用来精确匹配session key。一个特别的值是“*”,它代表默认匹配所有的key。在整个session配置中,只能有一个store拥有默认的匹配。
上例中,如果我的程序调用session.setAttribute("userId",user.getId()),那么这个ID值将被保存到permanent store里;
而session.setAttribute("someKey",something)将被默认匹配到temporarystore中。

²  可以包含若干个matchRegexp属性,用正则表达式来匹配session key。
上例中,login_a、login_b、history_1等key都将被保存到permanent store里;

²  匹配遵循最大匹配的原则,假如有两个以上的表达式被同时匹配,匹配长度最长的胜出。默认匹配总是在所有的匹配都失败以后才会激活。

²  必须有一个session store能够匹配session model —— 用来存放session的生命期状态。你可以用<property name="match" value="*"/>来匹配session model,也可以用精确匹配:<propertyname="match" value="SESSION_MODEL"/>。其中session model的名字是必须和前述session.model.name的配置相同,默认为“SESSION_MODEL”。

3.6.CookieStore详解

前面的例子中,已经多次出现了cookie store的身影。确实,实现cookie store是我们的session框架的最重要的设计目标之一。上文已经给出了一段cookie store配置的范例,下面我们将比较详细地解释一下cookie store的配置方法。

²  指定cookie的名称。假设名称为“tmp”,那么将生成tmp0、tmp1、tmp2等cookie。多个cookie store的cookie名称不能重复。

<propertyname="cookie.name" value="…"/>

²  指定cookie的域名和路径。默认值为JSESSIONID cookie的域名和路径。因此一般不需要特别设置这两个值。

<propertyname="cookie.domain" value="alibaba.com"/>

<propertyname="cookie.path" value="/"/>

²  指定cookie的寿命(过期时间),单位是秒。默认为0,意味着cookie持续到浏览器被关闭(或称临时cookie)。有效值必须大于0,否则均被认为是临时cookie。

<propertyname="cookie.maxAge" value="0"/>

²  是否在多个session中共享当前store中的对象?默认为false。
如果这个值为true,必须同时设置一个大于0的cookie.maxAge。反之,即使maxAge大于0,但permanent为false,那么对象的值也不可能在多个session之间共享。

<propertyname="cookie.permanent" value="false"/>

²  指定每个cookie的最大长度。默认为3993,即3.9K。
Cookie store会把所有对象序列化到cookie中。但是前面讲过cookie的长度是不能超过4K的。因此我们必须限制cookie的长度,如果超过这个长度,就把数据分发到新的cookie中去。因此一个cookie store实际可能产生好几个cookie。假设cookie.name为tmp,那么所生成的cookie的名称将分别为:tmp0、tmp1、tmp2,以此类推。

<propertyname="cookie.maxLength" value="3993"/>

²  指定cookie的最大个数。默认为5。
因此,实际cookiestore可生成的cookie总长度为:cookie.maxLength * cookie.maxCount。如果超过这个长度,cookie store将会在日志里面发出警告(WARN级别),并忽略store中的所有对象。

<propertyname="cookie.maxCount" value="5"/>

²  是否创建概要cookie。默认为false。
有时由于域名/路径等设置的问题,会导致cookie紊乱。例如:发现同名的cookie、cookie缺失等错误。这些问题很难跟踪。概要cookie就是为检查这类问题提供一个线索。如果将些开关打开,将会产生一个概要性的cookie。假如cookie.name为tmp,那么概要cookie的名字将是tmpsum。概要cookie会指出当前store共有几个cookie,每个cookie的前缀等内容。当cookie的总数和内容与概要cookie不符时,系统将会在日志中提出详细的警告信息(DEBUG级别)。请尽量不要在生产系统中使用这个功能。

<propertyname="cookie.summary" value="false"/>

²  最后,还有一个非常重要的配置项:cookie.encoder。默认的cookie encoder为EncryptCookieEncoderImpl。如果你扩展的cookie encoder有额外的配置,请使用下面第二种形式。

<propertyname="cookie.encoder.class"value="com.alibaba.webx.session.store.cookie.encoder.EncryptCookieEncoderImpl"/>或:<propertyname="cookie.encoder"><property name="class" value="com.alibaba.webx.session.store.cookie.encoder.EncryptCookieEncoderImpl"/><property name="xyz" value="123"/></property>


Cookie encoder是做什么的呢?它的功能就是把一个对象序列化成一个cookie可以接受的字符串,或反之。这个过程通常会包括序列化、加密、压缩、Base64转换等操作。你完全可以实现一种更有效的序列化算法,来替换默认的实现。目前cookie encoder包含了4个实现:

你可以任选其中之一种cookie encoder来配置你的cookie store。当然不同的cookie store完全可以配置不同的cookie encoder。

3.7.其它Session Store

3.7.1.SimpleMemoryStore

SimpleMemoryStore是最简单的session store。它将所有的session对象都保存在内存里面。这种store不支持多台机器的session同步,而且也不关心内存是否被用尽。因此这种简单的store一般只应使用于测试环境。

配置的方法极简单:

<!-- Simplememory store --><propertyname="session.store.mySimpleStore">    <property name="class"value="com.alibaba.webx.session.store.simple.SimpleMemoryStore"/>     <!-- 匹配方案 -->    <property name="match"value="…"/>    <property name="matchRegex"value="…"/></property>


3.7.2.MultiplexSessionStore

有时候为了兼容的原因,或其它的原因,我们需要将一个attribute同时写入到两个或以上的session store中。MultiplexSessionStore就是为了解决这个问题。它本身不实现任何实际的session store的逻辑,而是将所有的session store请求转发给一个或多个session store。

配置的方法也不复杂:

<!-- store 1--><propertyname="session.store.store1">    <property name="class"value="…"/>     <!-- 请不要配置匹配方案--></property> <!-- store 2--><propertyname="session.store.store1">    <property name="class"value="…"/>     <!-- 请不要配置匹配方案--></property> <!-- multiplexstore --><propertyname="session.store.myMultiplexStore">    <property name="class"value="com.alibaba.webx.session.store.multiplex.MultiplexSessionStore"/>     <!-- 匹配方案 -->    <property name="match"value="…"/><property name="matchRegex" value="…"/>     <!-- 转发到哪些store--><property name="store">        <value>store1</value>        <value>store2</value></property></property>


3.8.实现新的Session Store

要创建一个新的session store并不复杂。你只需要实现一个接口:SessionStore。

public interfaceSessionStore {    void init(String storeName,Configuration configuration, SessionConfig sessionConfig)            throwsServiceInitializationException;     Iterator getAttributeNames(String sessionID,StatusHolder statusHolder);     Object loadAttribute(String attrName,String sessionID, StatusHolder statusHolder);     void invaldiate(StringsessionID, StatusHolder statusHolder);     void commit(Map attrs, StringsessionID, StatusHolder statusHolder);}


3.8.1.初始化session store

    void init(String storeName,Configuration configuration, SessionConfig sessionConfig)

            throwsServiceInitializationException;

当session框架被初始化时,所有在其中的session store的init方法都会被调用。在init方法里面,

1.        你可以取得当前session store的名称;

2.        你可以从configuration对象中取得当前session store的配置;

3.        你还可以用sessionConfig对象取得整个session框架的配置信息。

3.8.2.装载数据

    Object loadAttribute(String attrName,String sessionID, StatusHolder statusHolder);

当下列情况下,相应session store的loadAttribute方法将被调用:

1.        当用户调用:session.getAttribute(attrName)时,系统将从匹配attrName的session store中装载该对象。

2.        在一个请求中,用户第一次调用request.getSession()时,系统将尝试从匹配的session store中装载SESSION_MODEL对象。

在同一个request中,对于同一个session store和同一个attrName,loadAttribute方法至多被调用一次。

3.8.3.保存(提交)数据

    void commit(Map attrs, StringsessionID, StatusHolder statusHolder);

当一个请求结束时,系统将调用commit方法,来通知sessionstore将数据保存起来。

在同一个request中,除非touchAways开关为false,并且没有修改/增加/删除任何session对象,那么该方法一定会,且仅会被调用一次。

被提交的attrs表中,仅包括新增的和修改的对象。如果对象被删除,则值为null。所有的sessionstore均会得到一次机会提交,即使其attrs为空。

3.8.4.遍历数据

    Iterator getAttributeNames(StringsessionID, StatusHolder statusHolder);

当用户调用session.getAttributeNames()方法时,系统会向每个session store提出遍历的请求。

3.8.5.清除所有数据

    void invaldiate(StringsessionID, StatusHolder statusHolder);

当下列情况下,session store的invalidate方法被调用:

1.        当用户调用:session.invalidate()时,系统将请求所有session store清除数据。

2.        在一个请求中,用户第一次调用request.getSession()时,无论是新的session,或是过期的session,系统都会请求所有的session store清除数据。

对于cookie store而言,有一种特殊情况:当cookie store的cookie.permanent为true时,cookie store在invalidate时不会清理数据。这样,这些数据就可以被多个session所共享。

3.8.6.优化session store

Session store在整个系统中被设计成singleton,因此,你不能在session store对象的成员变量中保存任何request scope的状态。然而,有时候这是必须的,例如:

²  对于数据库为基础的session store,为了性能的考虑,你希望第一次loadAttribute时,一次性从数据库中读取所有对象。随后的loadAttribute只需要从内存中读取就可以了。

²  对于cookie store,必须一次性解码所有的cookie。如果每次loadAttribute都去解码cookie就会很低效。

对于这些需求,session框架提供了一个StatusHolder接口。你可能已经留意到,所有的session store的接口方法中,都有一个statusHolder的参数。这就是给你存放状态的地方。除此以外,StatusHolder接口还为你提供了一些访问session系统的方法,例如:getHttpSession ()可以取得当前的session对象,getSessionRequestContext()可以取得当前的request context,从而取得当前session系统的一些配置。

看一下cookiestore中的代码片段,你就会明白了:

public IteratorgetAttributeNames(String sessionID, StatusHolder statusHolder) {    Status status = getStatus(statusHolder);     returnstatus.attributes.keySet().iterator();} public ObjectloadAttribute(String attrName, String sessionID, StatusHolder statusHolder) {    Status status = getStatus(statusHolder);     return status.attributes.get(attrName);} ... private StatusgetStatus(StatusHolder statusHolder) {    Status status = (Status)statusHolder.getStatus();     if (status == null) {        status = new Status();        statusHolder.setStatus(status);    }     ensureCookieLoading(status,statusHolder.getSessionRequestContext().getRequest(),                        statusHolder);    return status;} ... private classStatus {    private boolean cookieLoaded;    private boolean cookieCommitted;    private boolean cookieSummary;    private Map     requestCookies;    private String  mergedCookieValue;    private Map     attributes;}


4.实例示范

4.1.不使用Webx框架

Session框架可以被非webx应用(如JSP)直接使用。

4.1.1.创建项目(取得依赖的jar包)

首先,请创建一个antx项目,修改一下project.xml(也可以不用antx,但这是为了方便你取得依赖的jar包):

<?xmlversion="1.0" encoding="GB2312"?> <projectid="test/session">    <build>        <dependencies>           <include uri="toolkit/webx/filter"version="2.0"/>            <includeuri="toolkit/webx/session" version="2.0"/>        </dependencies>    </build></project>


4.1.2.创建web.xml

对于antx项目,这个文件在src/descriptors/web/目录中。最终这个文件将出现在web应用的WEB-INF/目录下。

<?xmlversion="1.0" encoding="GB2312"?><!DOCTYPEweb-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application2.3//EN"       "http://java.sun.com/dtd/web-app_2_3.dtd"><web-app>    <filter>       <filter-name>rundata</filter-name>       <filter-class>com.alibaba.webx.filter.rundata.RunDataFilter</filter-class>    </filter>    <filter-mapping>       <filter-name>rundata</filter-name>       <url-pattern>*.jsp</url-pattern>    </filter-mapping>    <servlet>       <servlet-name>webx</servlet-name>       <servlet-class>com.alibaba.webx.WebxControllerServlet</servlet-class>       <load-on-startup>1</load-on-startup>    </servlet></web-app>


4.1.3.创建webx.xml

对于antx项目,这个文件在src/webroot/WEB-INF/目录中。最终这个文件将出现在web应用的WEB-INF/目录下。

<?xmlversion="1.0" encoding="GB2312"?><configuration>    <services>        <service name="RunDataService"class="com.alibaba.webx.service.rundata.DefaultRunDataService">            <propertyname="request.buffered.class"               value="com.alibaba.webx.request.context.buffered.BufferedRequestContextFactory"/>            <property name="request.lazycommit.class"               value="com.alibaba.webx.request.context.lazycommit.LazyCommitRequestContextFactory"/>            <propertyname="request.parser.class"               value="com.alibaba.webx.request.context.parser.ParserRequestContextFactory"/>            <property name="request.session">               <property name="class"value="com.alibaba.webx.session.request.SessionRequestContextFactory"/>               <property name="session.cookie.domain"value=""/>                <propertyname="session.cookie.path" value="/"/>               <!--                 - temporary cookie store                -->               <property name="session.store.temporary">                   <property name="class" value="com.alibaba.webx.session.store.cookie.CookieStore"/>                   <property name="match" value="*"/>                   <property name="cookie.name" value="tmp"/>                   <property name="cookie.permanent"value="false"/>               </property>           </property>             <propertyname="request.locale">                <propertyname="class"                          value="com.alibaba.webx.request.context.locale.SetLocaleRequestContextFactory"/>                <property name="defaultLocale"value="zh_CN"/>                <propertyname="defaultCharset" value="GBK"/>            </property>        </service>    </services></configuration>


4.1.4.创建测试用的JSP

在src/webroot/目录下创建一个test.jsp:

<%@ pagecontentType="text/html; charset=GBK" pageEncoding="GBK"%><html><body><%Integer count =(Integer) session.getAttribute("count"); if (count ==null) {    count = new Integer(0);} count = newInteger(count.intValue() + 1); session.setAttribute("count",count);%> Count:<%=count%></body></html>


4.1.5.设置日志(可选、推荐)

设置一下log4j.xml,这样可以查看session系统的日志。为了方便,我们将所有日志打印在屏幕上。

请在src/webroot/WEB-INF/目录下创建log4j.xml:

<?xmlversion="1.0" encoding="GB2312"?><log4j:configurationxmlns:log4j="http://jakarta.apache.org/log4j/">    <!-- =====================================================================-->    <!-- 以下是appender的定义                                               -->    <!--===================================================================== -->    <appender name="PROJECT"class="org.apache.log4j.ConsoleAppender">        <param name="encoding"value="GB2312"/>        <layoutclass="org.apache.log4j.PatternLayout">            <paramname="ConversionPattern" value="%-4r [%t] %-5p %c %x -%m%n"/>        </layout>    </appender>    <!-- =====================================================================-->    <!-- 以下是logger的定义                                                  -->    <!--===================================================================== -->    <logger name="org.apache" additivity="false">        <level value="error"/>    </logger>    <!--===================================================================== -->    <!-- Root logger的定义                                                   -->    <!-- =====================================================================-->    <root>        <level value="debug"/>        <appender-refref="PROJECT"/>    </root></log4j:configuration>


4.1.6.打包、运行

执行antx war打包、发布。在浏览器上输入:http://localhost:8080/session/test.jsp就可以看到结果。当然,域名、端口、context path都得根据你布署的实际情况来定。

4.2.使用Webx框架

使用webx框架是最自然的 ——把上面的web.xml配置中RunDataFilter去掉即可 —— Webx不需要这个filter。

5.总结

Session是个难题,特别是对于要求高扩展性和高可用性的网站来说。

我们在标准的Java Servlet API的基础之上,实现了一套全新的session框架。在此基础上可以进一步实现多种session的技术,例如:基于cookie的session、基于数据库的session、基于berkeley DB的session、基于内存的session,甚至也可以实现基于TCP-ring的session等等。最重要的是,我们能把这些技术结合起来,使每种技术的优点能够互补,缺点可以被避免。

所有这一切,对应用程序是完全透明的—— 应用程序不用知道session是如何实现的、它们的对象被保存到哪个session store中等问题 —— session框架可以妥善地处理好这一切。

 


0 0
原创粉丝点击