webx—Session框架详解



webx—Session框架详解

这个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框架的入口:

 

 

 

  1. <service name=”RunDataService” class=”com.alibaba.webx.service.rundata.DefaultRunDataService”>
  2.     <!–
  3.       - 将response.getWriter()和response.getOutputStream()的内容缓存起来。
  4.      –>
  5.     <property name=”request.buffered.class”
  6.         value=”com.alibaba.webx.request.context.buffered.BufferedRequestContextFactory”/>
  7.     <!–
  8.       - 拦截sendRedirect等可能导致response提交的操作,延后至请求结束才做。
  9.      –>
  10.     <property name=”request.lazycommit.class”
  11.         value=”com.alibaba.webx.request.context.lazycommit.LazyCommitRequestContextFactory”/>
  12.     <!–
  13.       - 自动parse参数(包括upload form)。
  14.      –>
  15.     <property name=”request.parser.class”
  16.         value=”com.alibaba.webx.request.context.parser.ParserRequestContextFactory”/>
  17.     <!–
  18.       - Session框架的配置。
  19.      –>
  20.     <property name=”request.session”>
  21.         <property name=”class” value=”com.alibaba.webx.session.request.SessionRequestContextFactory”/>
  22.     (…未完待续)
  23. </property>
  24.     <!–
  25.       - 设置请求的locale/charset。
  26.      –>
  27.     <property name=”request.locale”>
  28.         <property name=”class” value=”com.alibaba.webx.request.context.locale.SetLocaleRequestContextFactory”/>
  29.         <property name=”defaultLocale” value=”zh_CN”/>
  30.         <property name=”defaultCharset” value=”GBK”/>
  31.     </property>
  32. </service>

 

<service name="RunDataService">
    <!--
      - 将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响应:

 

  1. HTTP/1.1 200 OK
  2. Server: Apache-Coyote/1.1
  3. Set-Cookie: JSESSIONID=AywiPrQKPEzfF9OZ; Path=/
  4. Content-Type: text/html;charset=GBK
  5. Content-Language: zh-CN
  6. Content-Length: 48
  7. Date: Mon, 06 Nov 2006 07:59:38 GMT
  8. <html>
  9. <body>
  10. ……

 

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的配置,也就是标记“…未完待续”的那部分配置:

 

  1. <service name=”RunDataService” class=”com.alibaba.webx.service.rundata.DefaultRunDataService”>
  2.     ……
  3.     <!–
  4.       - Session框架的配置。
  5.      –>
  6.     <property name=”request.session”>
  7.         <property name=”class” value=”com.alibaba.webx.session.request.SessionRequestContextFactory”/>
  8.     (…未完待续)
  9. </property>
  10.     ……
  11. </service>

 

<service name="RunDataService">
    ……

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

    ……
</service>


三、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开关。

  1. <property name=”session.cookie.enabled” value=”true”/>
<property name="session.cookie.enabled" value="true"/>

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

  1. <property name=”session.cookie.name” value=”JSESSIONID”/>
<property name="session.cookie.name" value="JSESSIONID"/>

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

  1. <property name=”session.cookie.domain” value=”alibaba.com”/>
<property name="session.cookie.domain" value="alibaba.com"/>

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

  1. <property name=”session.cookie.path” value=”/”/>
<property name="session.cookie.path" value="/"/>

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

  1. <property name=”session.cookie.maxAge” value=”0″/>
<property name="session.cookie.maxAge" value="0"/>

 

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

  1. <property name=”session.urlencode.enabled” value=”false”/>
<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″ 

  1. <property name=”session.urlencode.name” value=”JSESSIONID”/>
<property name="session.urlencode.name" value="JSESSIONID"/>

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

 

  1. <property name=”session.idbroker.class” value=”com.alibaba.webx.session.idbroker.random.RandomSessionIDBroker”/>
  2. 或:
  3. <property name=”session.idbroker”>
  4. <property name=”class” value=”com.alibaba.webx.session.idbroker.random.RandomSessionIDBroker”/>
  5. <property name=”xyz” value=”123″/>
  6. </property>
<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>

四、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框架中最核心的部分:SessionStore。Session框架最灵活的部分就在于此。我们可以定义很多个session store,让不同的session对象分别存放到不同的Session Store中。前面提到有一个特殊的对象:SESSION_MODEL也必须保存在某个session store中。


 

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

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

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

基本配置方法如下:

  1. <service name=”RunDataService” class=”com.alibaba.webx.service.rundata.DefaultRunDataService”>
  2.     (……其它RequestContext的配置)
  3.     <!–
  4.       - Session框架的配置。
  5.      –>
  6.     <property name=”request.session”>
  7.         <property name=”class” value=”com.alibaba.webx.session.request.SessionRequestContextFactory”/>
  8.         (……前述关于JSESSIONID和生命期的配置)
  9.         <!– Session store 1 –>
  10.         <property name=”session.store.Store名称1″>
  11.             <property name=”class” value=”Store类名”/>
  12.             <!– 匹配方案 –>
  13.             <property name=”match” value=”精确匹配,或*代表匹配所有”/>
  14.             <property name=”matchRegex” value=”匹配正则表达式”/>
  15.             <!– 其它参数,取决于具体的store实现 –>
  16.         </property>
  17.         <!– Session store 2 –>
  18.         <property name=”session.store.Store名称2″>
  19.             <!– 完全类似store 1 –>
  20.         </property>
  21.         <!– 更多session stores –>
  22. </property>
  23.     (……其它RequestContext的配置)
  24. </service>
<service name="RunDataService">
    (……其它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配置的范例:

  1. <!–
  2.   - temporary cookie store
  3.  –>
  4. <property name=”session.store.temporary”>
  5.     <property name=”class” value=”com.alibaba.webx.session.store.cookie.CookieStore”/>
  6.     <property name=”match” value=”*”/>
  7.     <property name=”cookie.name” value=”tmp”/>
  8.     <property name=”cookie.permanent” value=”false”/>
  9. </property>
  10. <!–
  11.   - permanent cookie store
  12.  –>
  13. <property name=”session.store.permanent”>
  14.     <property name=”class” value=”com.alibaba.webx.session.store.cookie.CookieStore”/>
  15.     <property name=”match” value=”userId”/>
  16.     <property name=”matchRegexp” value=”login_\w+”/>
  17.     <property name=”matchRegexp” value=”history_\w+”/>
  18.     <property name=”cookie.name” value=”pmt”/>
  19.     <property name=”cookie.permanent” value=”true”/>
  20. </property>
<!--
  - 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名称不能重复。 

  1. <property name=”cookie.name” value=”…”/>
<property name="cookie.name" value="…"/>

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

 

  1. <property name=”cookie.domain” value=”alibaba.com”/>
  2. <property name=”cookie.path” value=”/”/>
<property name="cookie.domain" value="alibaba.com"/>
<property name="cookie.path" value="/"/>

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

  1. <property name=”cookie.maxAge” value=”0″/>
<property name="cookie.maxAge" value="0"/>

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

 

  1. <property name=”cookie.permanent” value=”false”/>
<property name="cookie.permanent" value="false"/>

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

 

  1. <property name=”cookie.maxLength” value=”3993″/>
<property name="cookie.maxLength" value="3993"/>

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

  1. <property name=”cookie.maxCount” value=”5″/>
<property name="cookie.maxCount" value="5"/>

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

  1. <property name=”cookie.summary” value=”false”/>
<property name="cookie.summary" value="false"/>

h)cookie.encoder。默认的cookie encoder为EncryptCookieEncoderImpl。如果你扩展的cookie encoder有额外的配置,请使用下面第二种形式 

 

  1. <property name=”cookie.encoder.class”
  2. value=”com.alibaba.webx.session.store.cookie.encoder.EncryptCookieEncoderImpl”/>
  3. 或:
  4. <property name=”cookie.encoder”>
  5. <property name=”class” value=”com.alibaba.webx.session.store.cookie.encoder.EncryptCookieEncoderImpl”/>
  6. <property name=”xyz” value=”123″/>
  7. </property>
<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可以接受的字符串,或反之。这个过程通常会包括序列化、加密、压缩、Base64转换等操作。你完全可以实现一种更有效的序列化算法,来替换默认的实现。目前cookie encoder包含了4个实现:

 

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