这个session框架是依赖于我们的通用service框架的。由于service框架是Webx框架的基础,所以Webx自然可以方便地使用这个session框架。
对于webx之外的WEB应用 —— 例如:独立运行的JSP、由其它应用框架如webwork制作的应用 —— 我们提供了一个filter。这样所有的应用都可以使用我们的session框架,从而支持cookie-based session、berkeleyDB-based session以及扩展出任意类型的session实现。
一、 装配session系统
最常见的情况下,session系统是由Webx框架装载的。为了在Webx框架中使用session框架,需要修改WEB-INF/webx.xml文件中的RunDataService。RunDataService是前面所述的RequestContext框架的入口:
<service name="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"/> <property name="defaultLocale" value="zh_CN"/> <property name="defaultCharset" value="GBK"/> </property> </service>
二、 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 OK Server: Apache-Coyote/1.1 Set-Cookie: JSESSIONID=AywiPrQKPEzfF9OZ; Path=/ Content-Type: text/html;charset=GBK Content-Language: zh-CN Content-Length: 48 Date: Mon, 06 Nov 2006 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的配置,也就是标记“…未完待续”的那部分配置:
<service name="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>
在配置文件中,我们指定了: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开关。
<property name="session.cookie.enabled" value="true"/>
指定保存session ID的cookie的名字,默认为"JSESSIONID"。
<property name="session.cookie.name" value="JSESSIONID"/>
指定session ID cookie的domain,如果不设置,则不发送domain。这意味着浏览器认为你的cookie属于当前域名。如果你的应用包含多个子域名,例如:www.alibaba.com、china.alibaba.com,而你又希望它们能共享session的话,请把域名设置成“alibaba.com”。
<property name="session.cookie.domain" value="alibaba.com"/>指定session ID cookie的path,默认为"/"根目录。通常不需要修改这个默认值
<property name="session.cookie.path" value="/"/>指定session ID cookie的寿命(过期时间),单位是秒。默认为0,意味着cookie持续到浏览器被关闭(或称临时cookie)。有效值必须大于0,否则均被认为是临时cookie
<property name="session.cookie.maxAge" value="0"/>
是否允许将session ID编码到URL中,默认为false。注意,如果打开该选项,必须关闭session.cookie.enabled。
<property name="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"
<property name="session.urlencode.name" value="JSESSIONID"/>指定用来生成Session ID的类。默认为RandomSessionIDBroker。如果你扩展的新的SessionIDBroker需要一些额外的参数,请使用下面的第二种配置形式
<property name="session.idbroker.class" value="com.alibaba.webx.session.idbroker.random.RandomSessionIDBroker"/> 或: <property name="session.idbroker"> <property name="class" value="com.alibaba.webx.session.idbroker.random.RandomSessionIDBroker"/> <property name="xyz" value="123"/> </property>
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框架中最核心的部分:SessionStore。Session框架最灵活的部分就在于此。我们可以定义很多个session store,让不同的session对象分别存放到不同的Session Store中。前面提到有一个特殊的对象:SESSION_MODEL也必须保存在某个session store中。
Session store的配置包含两部分内容:
1. 如何创建Session store?配置session store的实现类名、初始化参数等。
2. 将指定key的对象保存在哪个store中?配置匹配方案。
基本配置方法如下:
<service name="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 --> <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> <!-- - permanent cookie store --> <property name="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"/> <property name="cookie.permanent" value="true"/> </property>
需要注意以下几点:
1 你可以配置任意多个session store,只要名字不重复。
上例中,temporary和permanent分别是两个session store的名称。
2)可以包含若干个match属性,用来精确匹配session key。一个特别的值是“*”,它代表默认匹配所有的key。在整个session配置中,只能有一个store拥有默认的匹配。
上例中,如果我的程序调用session.setAttribute("userId",user.getId()),那么这个ID值将被保存到permanent store里;
而session.setAttribute("someKey",something)将被默认匹配到temporarystore中。
3) 可以包含若干个matchRegexp属性,用正则表达式来匹配session key。
上例中,login_a、login_b、history_1等key都将被保存到permanent store里;
4)匹配遵循最大匹配的原则,假如有两个以上的表达式被同时匹配,匹配长度最长的胜出。默认匹配总是在所有的匹配都失败以后才会激活。
6. cookie store详解
前面的例子中,已经多次出现了cookie store的身影。确实,实现cookie store是我们的session框架的最重要的设计目标之一。上文已经给出了一段cookie store配置的范例,下面我们将比较详细地解释一下cookie store的配置方法。
a) 指定cookie的名称。假设名称为“tmp”,那么将生成tmp0、tmp1、tmp2等cookie。多个cookie store的cookie名称不能重复。<property name="cookie.name" value="…"/>b)指定cookie的域名和路径。默认值为JSESSIONID cookie的域名和路径。因此一般不需要特别设置这两个值
<property name="cookie.domain" value="alibaba.com"/> <property name="cookie.path" value="/"/>c) 指定cookie的寿命(过期时间),单位是秒。默认为0,意味着cookie持续到浏览器被关闭(或称临时cookie)。有效值必须大于0,否则均被认为是临时cookie。
<property name="cookie.maxAge" value="0"/>d)是否在多个session中共享当前store中的对象?默认为false。
<property name="cookie.permanent" value="false"/>e)指定每个cookie的最大长度。默认为3993,即3.9K。
<property name="cookie.maxLength" value="3993"/>f)指定cookie的最大个数。默认为5。
<property name="cookie.maxCount" value="5"/>g)是否创建概要cookie。默认为false。
<property name="cookie.summary" value="false"/>h)cookie.encoder。默认的cookie encoder为EncryptCookieEncoderImpl。如果你扩展的cookie encoder有额外的配置,请使用下面第二种形式
<property name="cookie.encoder.class" value="com.alibaba.webx.session.store.cookie.encoder.EncryptCookieEncoderImpl"/> 或: <property name="cookie.encoder"> <property name="class" value="com.alibaba.webx.session.store.cookie.encoder.EncryptCookieEncoderImpl"/> <property name="xyz" value="123"/> </property>
你可以任选其中之一种cookie encoder来配置你的cookie store。当然不同的cookie store完全可以配置不同的cookie encoder