会话状态
Web服务器没有短期记忆。在Servlet API中可以找到一种极其简单的解决方法。
会话管理
4.1 编写servlet代码,将对象保存到一个会话对象中,以及从会话对象获得对象。
4.2 给定一个场景,描述访问会话对象使用的API,解释何时创建会话对象,并描述撤销会话对象使用的机制,以及何时撤销会话对象。
4.3 使用会话监听者,编写代码对会话的有关时间做出响应,包括向会话增加一个对象,以及会话对象从VM迁移到另一个VM。
4.4 给定一个场景,说明Web容器可以采用哪些会话管理机制,如何使用cookie来管理会话,以及如何使用URL重写管理会话,并编写servlet代码完成URL重写。
容器怎么知道客户是谁?
对容器而言,每个请求都来自于一个新的客户。
解决:客户需要一个唯一的会话ID。容器会生成一个唯一的会话ID,并通过响应把它返回给客户。客户在以后的每一个请求中发回这个会话ID给服务器。如下图:
容器机会做cookie的所有工作
在响应中发送一个会话cookie与从请求得到会话ID的代码一样,如下:
HttpSession session = request.getSession();
就这么简单。在你的服务方法中请求一个会话,余下的所有事情都会自动完成。
自动完成的事:
- 建立新的HttpSession
- 生成唯一的会话ID
- 建立新的cookie对象
- 会话ID与cookie关联
- 响应中设置cookie(set-cookie首部下)
- cookie的所有工作都在后台进行
怎么知道会话已经存在,还是刚刚创建?
请求的无参方法getSession会返回一个会话,而不论是否已经有一个会话。因为我们总能从这个方法得到返回的一个HttpSession实例。所以,要知道会话是不是新创建的,只有去问会话。
HttpSession session = request.getSession();
if(session.isNew()){
//客户还没用过这个会话,或者说是新创建的
}else{
//客户已经使用过这个会话
}
这里指出:得到会话的方式不止request.getSession()一种。
如果我只想要一个已经有的会话呢?
某些情况下,servlet可能只想使用一个原来创建的会话。例如,让结账servlet再开始一个新的会话可能就不太合适。
所以,针对这个目的,专门有一个重载的getSession(boolean)方法。
//传递一个false表示,这个方法会返回一个已经有的会话,如果没有与此客户关联的会话,则会返回null
HttpSession session = request.getSession(false);
if(session==null){
//会话不存在,创建一个
session = request.getSession();
}else{
//会话以存在
}
比较上面2种获取session的方法,它们其实是一样的。除非你并不想创建一个session就可以直接用getSession(false),当不存在一个session的时候,你判断为空,并不去创建一个新的session。
如果不能用cookie
如果客户禁用掉cookie,那么客户就会忽略“Set-Cookie”响应首部。这样的话,那又该如何处理呢?(session的机制就是靠cookie实现的,所以cookie不能用就等于session不能用)
就算客户不接受cookie,我们也能完成会话,但是必须稍微多做点工作。
解决:URL重写(一条后路),如下:
URL+;jsessionid=1234567
如图:
还差最后一步:需要告诉响应要对URL编码,URL重写才能奏效。
PrintWriter out = response.getWriter();
HttpSession session = request.getSession();
//向这个"/BeerTest.do"这个URL增加额外的会话ID信息
out.println(response.encodeURL("/BeerTest.do"));
这里还有一个问题,容器怎么知道客户端cookie不能正常工作?
方法很简单:返回第一个响应时,它会同时尝试cookie和URL重写这两种方式。
使用sendRedirect()的URL重写
可能会有这种情况,你想把请求重定向到另外一个URL,但是还想使用一个会话。为此,有一个特殊的URL编码方法:response.encodeRedirectURL("/BeerTest.do")
。
会话注意点
URL重写要点
- 在写至响应的HTML中,URL重写会把会话ID增加到其中所有的URL的最后。(例如url;jsessionid=1234567)
- 会话ID作为请求URL最后的“额外”信息再通过请求返回。
- 如果客户不接受cookie,URL重写会自动发生,但是必须显示的对所有URL编码。
- 要对一个URL编码,需要调用response.encodeURL(String url)。
- 没有办法对静态页面完成自动的URL重写,所以,如果你依赖于会话,就必须使用动态生成的页面。
删除会话
由于客户可能会因为各种原因导致会话结束,可是我们没将Session销毁,这个时候容器又如何知道什么时候能安全地撤销一个会话?
HttpSession接口的UML
方法名 | 作用 | 一般用途 |
---|---|---|
getCreationTime() | 返回第一次创建会话的时间 | 得出这个会话有多“老”。你可能想要把某些会话的寿命限制为一个固定的值。例如,你可能会说“一旦登录,就必须在10分钟之内完成这个表单” |
getLastAccesssedTime() | 返回容器最后一次得到包含这个会话ID的请求后过去了多长时间(毫秒数) | 得出客户最后一次访问这个会话是什么时候。可以用这个方法来确定客户是否已经离开很长时间,这样就可以像客户发出一份email,询问他们是否还回来。或者可以调用invalidate()结束会话 |
setMaxInactiveInterval() | 指定对于这个会话客户请求的最大间隔时间(秒数) | 如果已经过去了指定的时间,而客户未对这个会话做任何请求,就会导致会话被撤销。可以用这个方法减少服务器中无用的会话 |
getMaxInactiveInterval() | 返回对于这个会话客户请求的最大时间间隔 | 得出这个会话可以保持多长时间不活动当仍“存活”。可以使用这个方法来判断一个会话不活动的客户在会话撤销之前还有多长的“寿命” |
invalidate() | 结束会话。当前存储在这个会话中的所有会话属性也会解除绑定 | 如果客户已经不活动,或者你知道会话已经结束(例如,客户完成了购物结账,或已经注销),可以用这个方法杀死(撤销)会话。会话实例本身可能会被容器回收,但是这一点我们并不关心。置无效(Invaldate)意味着会话ID不再存在,而且属性会从会话对象删除 |
由UML提供给我们的信息,我们知道Session销毁可以通过设置时长,和手动删除(invalidate())。
还有一种死法:应用结束(奔溃或取消部署)
(1)在DD中配置会话超时
<web-app ...>
<session-config>
<session-timeout>15</session-time>
</session-config>
</web-app>
(2)设置一个特定会话的会话超时
session.setMaxInactiveInterval(20*60);
//表示20分钟生存时间
cookie可以另作其他用处吗?cookie是不是只能用于会话?
可以使用cookie在服务器和客户之间交换名/值String对。
服务器把cookie发送给客户,客户再在以后的每个请求中发回这个cookie。
客户的浏览器退出时,会话cookie就会消失,但是你可以告诉cookie在客户端上待得更久一些,甚至在浏览器关闭之后还持久保存。
利用servlet API使用cookie
代码如下:
//创建一个新Cookie
Cookie cookie = new Cookie("username",name);
//设置cookie在客户端上存活多久
cookie.setMaxAge(30*60);
//把cookie发送到客户
response.addCookie(cookie);
//从客户请求得到cookie(或多个cookie)
Cookie[] cookies = request.getCookies();
for(int i=0;i<cookies.length;i++){
Cookie cookie = cookies[i];
if(cookie.getName().equals("username")){
String userName = cookie.getValue();
out.println("Hello "+userName);
break;
}
}
cookie注意点:
HttpSession的重要里程碑
- 创建或撤销会话。
- 由应用的其他部分增加、删除或替换会话属性。
- 会话在一个VM中钝化,并在一个分布式应用中的另一个VM中激活。
会话生命周期时间
里程碑 | 事件和监听者类型 |
---|---|
生命周期:创建(此时会话是新的)会话、撤销(invalidate方法)会话 | HttpSessionEvent、HttpSessionListener |
属性:增加、删除、替换一个属性 | HttpSessionBindingEvent、HttpSessionAttributeListener |
迁移:会话准备钝化、会话已经激活 | HttpSessionEvent、HttpSessionActivationListener |
HttpSessionBindingListener实例:
import javax.servlet.http.*;
public class Dog implements HttpSessionBindingListener {
private String breed;
public Dog(String breed){
this.breed = breed;
}
public String getBreed(){
return breed;
}
public void valueBound(HttpSessionBindingEvent event){
//我知道我在一个会话中时要运行的代码
}
public void valueUnbound(HttpSessionBindingEvent event){
//我知道已经不在一个会话中时要运行的代码
}
}
啤酒Web应用分布在两个VM上
HttpSessionActivationListener:
会话迁移和串行化。
容器需要迁移Serializable属性(类的属性要么为null要么是Serializable)。
但是容器不一定非要使用串行化来迁移HttpSession对象(属性类是Serializable)。
这里指出:tomcat会把输出(System.out.println())放在tomcat的logs/catalina.log文件中。
监听者例子:
会话计数器:这个监听者允许你跟踪这个Web应用中活动会话的个数。
public class BeerSessionCounter implements HttpSessionListener {
private static int activeSession;
public static int getActiveSession(){
return activeSession;
}
public void sessionCreated(HttpSessionEvent event){
activeSession++;
}
public void sessionDestroyed(HttpSessionEvent event){
activeSession--;
}
}
属性监听者:利用这个监听者,每一次向会话增加属性、删除属性或替换属性时你都能跟踪到。
public class BeerAttributeListener implements HttpSessionAttributeListener {
public void attributeAdded(HttpSessionBindingEvent event){
String name = event.getName();
Object value = event.getValue();
System.out.println("Attribute added: " + name + ": " + value)
}
public void attributeRemove(HttpSessionBindingEvent event){
String name = event.getName();
Object value = event.getValue();
System.out.println("Attribute removed: " + name + ": " + value)
}
public void attributeReplaced(HttpSessionBindingEvent event){
String name = event.getName();
Object value = event.getValue();
System.out.println("Attribute replaced: " + name + ": " + value)
}
}
属性类的监听者:这个监听者允许属性跟踪可能对属性本身很重要事情,如增加到会话,或从会话删除,已经会话从一个VM迁移到另一个VM。
public class Dog implements HttpSessionBindingListener,
HttpSessionActivationListener,Serializable {
private String breed;
//假设这里还有更多实例变量,包括一些非Serializable的实例变量
//假设这里是构造函数已经其他get/set方法
public void valueBound(HttpSessionBindingEvent event){
//我知道我在一个会话中时要运行的代码
}
public void valueUnbound(HttpSessionBindingEvent event){
//我知道已经不在一个会话中时要运行的代码
}
public void sessionWillPassivate(HttpSessionEvent event){
//这些代码将非Serializable字段置为某种状态,以便顺利地迁移到一个新VM
}
public void sessionDidActivate(HttpSessionEvent event){
//这些代码用于回复字段...取消在sessionWillPassivate()中做的动作
}
}
DD文件配置
<web-app ...>
<listener>
<listener-class>****.*Listener</listener-class>
</listener>
</web-app>
与会话相关的监听者
场景 | 监听者接口/方法 | 事件类型 | 通常由谁实现 |
---|---|---|---|
你想知道有多少个并发用户。也就是说,你想跟踪活动的会话 | HttpSessionListener(javax.servlet.http) sessionCreated、sessionDestroyed | HttpSessionEvent | 其他类 |
你想知道会话何时从一个VM移到另一个VM | HttpSessionActivateListener(javax.servlet.http) sessionDidActivate、sessionWillPassivate | HttpSessionEvent | 属性类、其他类 |
有一个属性类(这个类的对象要用作为一个属性值),而且你希望此类对象绑定到会话或从会话删除时得到通知 | HttpSessionBindingListener(javax.servlet.http) valueBound、valueUnbound | HttpSessionBindingEvent | 属性类 |
你想知道会话中什么时候增加、删除、替换会话属性 | HttpSessionAttributeListener(javax.servlet.http) attributeAdded、attributeRemoved、attributeReplaced | HttpSessionBindingEvent | 其他类 |