Servlet&JSP的那些事儿(七)

这篇讨论会话管理。我们一旦发送了响应,web服务立马就会忘了你是谁,下一次你再做请求时,web服务器不会认识你,它不记得你做过什么请求,也不记得给过你什么回应,记忆力比鱼还短。但是对于购物车这类应用,如果要求客户在一个请求中既做出选择又要结账,是不合理的。对此,servlet中该如何解决?

如何跟踪用户的回答?

我们想完成一个这样的功能,在对话中,用户回答一个问题后,web应用能根据上一个回答提出一个新的问题。我们都可以采用哪些做法呢?

做法一:使用一个有状态会话的企业JavaBean

当然了,可以让servlet成为一个有状态会话的bean的客户端,每次请求到来时,就可以找到用户的有状态bean。如果提供商没有一个带EJB容器的完整J2EE服务器怎么办?

做法二:使用一个数据库

这样也行。每次把客户的数据写进数据库,但是这样会导致运行时性能很差。所以,不是一个好的选择。

做法三:使用一个HttpSession

我们可以使用一个HttpSession对象保存跨多个请求的会话状态。也即,保存于该用户的整个会话期间的会话状态。

让我们以一个购买货物的例子来说明会话如何工作。

1)用户A选择了一个物品,容器向servlet的一个新线程发送请求,该servlet线程发现与用户A相关的会话,并把她的选择(“牛奶”)作为一个属性保存到会话中。
2)servlet运行其业务逻辑,并返回一个响应,在这里返回一个问题:“什么品牌?”
3)用户A考虑了一下,选择了“蒙牛”,并点击了提交按钮。容器向servlet的一个新线程发送请求,servlet线程发现与用户A相关的会话,并把他的选择(“蒙牛”),作为一个属性保存到会话中。
4)servlet运行期业务逻辑,并返回一个响应,然后又返回一个问题。

假设,此时用户B也来到购物网站。

5)用户A的会话还是活动的,但是此时用户B选择了“书籍”,并点击了提交按钮。容器把用户B的请求发给servlet的一个新线程,servlet线程为用户B开始一个新的会话,并把他的选择作为一个属性保存到会话中。

此时,我们不希望用户A和用户B的回答混在一起,所以他们需要不同的会话对象。我们先回答一个问题,容器怎么知道用户是谁?

HTTP协议使用的是无状态连接,这点我们在文章HTTP解析中已经提及。用户浏览器与服务器建立连接,发送请求,得到响应,然后关闭连接。也即,连接只针对一个请求/响应。由于连接不会持久保留,所以容器认不出第二个请求的用户与第一个请求的用户是同一个用户。为什么不使用客户的IP地址呢?IP地址不也是请求的一部分么?

对于服务器来说,你的IP地址是路由器的地址,所以你和这个网络中的其他人的IP地址都是一样的,所以靠IP地址是不行的。那么使用HTTPS呢?如果使用HTTPS,那么服务器就能认出用户,并把它与一个会话关联。但是这个条件一般不满足。除非有特定需求,否则网站不会使用HTTPS。所以,客户需要唯一的会话ID。道理很简单:

对于用户的第一个请求,容器会生成一个唯一的会话ID,并通过响应把它返回给用户。用户在以后的每一个请求中发回这个会话ID,容器看到这个ID后,就会找到匹配的会话,并把这个会话与请求关联。

容器与用户如何交换会话ID信息?

容器必须以某种方式把会话ID作为响应的一部分交给用户。而用户必须把会话ID作为请求的一部分发回。最简单且最常用的方法是通过cookie交换这个会话ID信息。容器会做几乎所有的cookie工作,包括生成会话ID,创建新的cookie对象,把会话ID放到cookie中等等。

在响应中发送一个会话cookie,使用如下语句:

HttpSession session = request.getSession();
这个方法不只是创建一个会话,在请求上第一次调用这个方法时,会导致随响应发出一个cookie。还不能保证用户会介绍cookie,不过假定客户支持cookie。

从请求中得到会话ID,使用如下语句:

HttpSession session = request.getSession();
这和之前发送会话cookie的方法完全一样。那么,我怎么知道会话是已经存在,还是刚创建?

可以调用isNew()方法来查看。代码如下:

public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException,IOException {
	response.setContentType("text/html;charset=utf-8");         
	PrintWriter out=response.getWriter();
	
	HttpSession session = request.getSession();
	if(session.isNew()) {
		out.println("This is a new session");
	} else {
		out.println("Weclome back!");
	}
}
我们可以通过以下语句判定来测试是否已经存在一个会话。
//传递false表示,这个方法会返回一个已有的会话,如果没有与客户关联的会话,则返回null
HttpSession session = request.getSession(false);
if(session == null) {
	out.println("No session was available..");
	out.println("making one..");
	session = request.getSession();
} else {
	out.println("There was a session..");
}
如果浏览器没启用cookie呢?我们怎么办?

如果用户不接受cookie,可以把URL重写作为一条后路。结社你的做法正确,URL重写就总能起作用-客户并不关心具体发生了什么。可以使用URL+;jsessionid=1234567890的做法。将会话ID放到请求URL的最后作为额外的信息返回。

//获取一个会话
HttpSession session = request.getSession(false);
//向这个URL添加额外的会话ID信息
response.encodeURL("/test.do");
可能有这样一种情况,你想把请求重定向到另外一个URL,但是还想使用一个会话,为此有一个特殊的URL编码方法:

response.encodeRedirectURL("/test.do");

注意,不能对静态页面完成URL重写,使用URL重写只有在作为会话一部分的所有页面都是动态生成的情况下才可以。不能硬编码会话ID,因为ID在运行时之前并不存在。

如何删除会话?

我们肯定不会希望会话一直保留下去,因为会话对象会占用资源。HTTP协议没有提供任何机制让服务器知道用户是不是已经走了,那容器是如何知道用户什么时候走的呢?如何知道浏览器何时崩溃呢?如何知道何时能安全撤销一个会话呢?

会话有三种死法。白绫,匕首,毒药?No,是1)超时;2)在会话对象上调用invalidate();3)应用结束(崩溃或取消部署)。

超时有两种设置方法:

1)在web.xml中配置会话超时,如下:

<?xml version='1.0' encoding='utf-8'?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
                      http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
  version="3.0"
  metadata-complete="true">
	<servlet>
		<servlet-name>TestServlet</servlet-name>
		<servlet-class>com.shan.web.TestServlet</servlet-class>
	</servlet>

	<servlet-mapping>
		<servlet-name>TestServlet</servlet-name>
		<url-pattern>/Test.do</url-pattern>
	</servlet-mapping>

	<session-config>
		<session-timeout>15</session-timeout>
	</session-config>
</web-app>
注:15是指15分钟。也即,如果用户15分钟没对这个会话做任何请求,就杀死这个会话。

2)设置一个特定会话的会话超时,如下:

session.setMaxInactiveInterval(20*60);
注:只针对session这个特定会话。这个方法的参数以秒为单位。如果这个参数设置为负数,那表示把时间设置为无穷大。

在会话对象上调用invalidate()如下:

session.invalidate();

应用结束就不用再说了。我们在看看关键的HttpSession方法都有哪些,如表1所示。

  它做什么 你用它做什么
getCreationTime() 返回第一次创建会话的时间 得出这个会话有多老,你可能想把某些会话时间限定在一个固定时间内,如必须在10min内填完表单。
getLastAccessTime() 返回容器最后一次得到有此会话ID的请求的时间(毫秒数) 得出用户最后一次访问这个会话是什么时候。用这个方法来确定用户是否已经离开很长时间,然后决定是否调用invalidate()来结束会话。
setMaxInactiveInterval() 对于此会话,指定用户请求的最大间隔时间(秒数) 如果过去了指定时间,而用户未对此会话做任何请求,就会导致会话撤销。
getMaxInactiveInterval() 对于此会话,返回客户请求的最大时间间隔(秒数) 得到这个会话可以保持多久不活动,而且仍然活着。
invalidate() 结束会话。当前存储在这个会话中的所有会话属性页会解除绑定。 如果用户已经不活动,活着会话已结束,可以用这个方法结束会话。

表1 关键的HttpSession方法

cookie是否只能用于会话?

其实尽管设计cookie是为了帮助支持会话状态,不过也可以使用定制cookie来完成其他工作。需要知道,cookie实际上就是在用户和服务器之间交换的一小段数据(一个名-值对。)cookie的一个好处是,用户不必介入,cookie交换是自动的(当然,浏览器必须支持cookie才行)。cookie默认与会话的寿命一样长。但是cookie可以活的更长一些,甚至在浏览器已经关闭后仍存活。

利用servlet API使用cookie

可以从HTTP请求和响应得到与cookie相关的首部,但最好不要这样做。对于cookie,你要做的工作都已经封装在了3个类的servlet API中:HttpServletRequest、HttpServletReponse、Cookie。

创建一个新的cookie:

Cookie cookie = new Cookie("username",name);

设置cookie在客户端上活多久:

cookie.setMaxAge(30*60);
注:参数以秒为单位。如果把参数设置为-1,则浏览器退出时cookie就会消失。

把cookie发送到用户:

response.addCookie(cookie);

从用户请求得到cookie(一个或多个cookie):

Cookie[] cookies = response.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;
	}
}
HttpSessionBindingListener

我们之前讲过的都是在会话生命周期中的关键时刻。如果我的属性想知道自己什么时候增加到了一个会话,怎么办?使用监听者即可。如下:

public class Dog implements HttpSessionBindingListener{
	private String breed = null;
	public Dog(String breed) {
		this.breed = breed;
	}

	public String getBreed(){
		return breed;
	}

	public void valueBound(HttpSessionBindingEvent event){
		//我知道我在一个会话中时要运行的代码
	}

	public void valueUnBound(HttpSessionBindingEvent event){
		//我知道我已经不在一个会话中时要运行的代码
	}
}
如果一个属性类(如Dog类)实现了HttpSessionBindingListener,当这个类的一个实例增加到了一个会话或从会话删除时,容器就会调用时间处理回调方法(valueBound()和valueUnBound())。

会话迁移

在分布式web应用环境中,容器会完成负载平衡,去的客户的请求,并把请求发到多个JVM上(可能在同一个物理主机上,也可能在不同物理主机上,我们不关心)。如果每次同一个客户做请求时,最后这个请求都有可能到达一个servlet的不通事理。也即,指向servlet A的请求A可能在一个VM中,而指向servlet A的请求B可能在另一个不同的VM中。此时,ServletContext、ServletConfig、HttpSession对象会如何呢?

只有HttpSession对象(及其属性)会从一个VM移到另一个VM。每个VM中有一个ServletContext。每个VM上每个servlet有一个ServletConfig。但是对于每个web应用的一个戈定会话ID,只有一个HttpSession对象,而不论应用分布在多少个VM上。

会话迁移过程

会话迁移具体过程如下:

1)用户A选择了“牛奶”,点击提交按钮。负载平衡服务器决定向VM-1中的容器A-1发送请求。容器建立一个新的会话,ID#123。这个“jsessionid”cookie放在响应中发回给用户A。
2)用户A选择了“电器”,点击提交按钮。她的请求包括“jsessionid”#123。这次,负载平衡服务器决定把请求发送给VM-2中的容器A-2。容器得到请求看到会话ID发现会话在另一个VM中,即VM-1。
3)会话#123从VM-1迁移到VM-2(一旦移到VM-2中,VM-1中就没有这个会话了)。
4)容器为servlet A-2建立一个新线程,并把新请求与刚迁移过来的会话#123关联。用户A并不知道新请求发送给这个线程,,只不过在迁移时稍微有点延迟。

HttpSessionActivationListener

因为HttpSession有可能迁移,所以如果有人能告诉会话中的属性它们也可以移动会更好一些。如果你的属性都是直接的Serializable对象,并不关心它们最后会放在那里,那么可能不会用到这个监听者。实际上,大部分web应用都不会使用这个监听者。

session的持久化

如果你在网上买了一本书,放到了购物车里,此时,网店管理员重启了web服务器,当你再次给购物车添加书籍时,发现之前的图书信息没了,你是什么感觉?所以,一个健壮的web服务器会提供session持久化机制。这是通过对象序列化的技术实现的。当需要重新加载session对象时,通过对象反序列化的技术在内存中重新构造session对象。这就要求HttpSession的实现类要实现Serializable接口,同时session中保存的对象所属的类也要实现Serialization接口。

几个问题

1)session和cookie的区别

session和cookie的最大区别是,session在服务端保存信息,cookie在客户端保存信息。

2)会话cookie的共享

有些人可能会有一些误解,认为以不同方式打开浏览器窗口,或者使用其他非IE浏览器就可以在不同的浏览器进程之间共享会话cookie。实际不是的,对于存储在内存中的cookie,是不能被不同的浏览器进程所共享的。共享只能发生在同一个浏览器的不同窗口中(因为这些窗口共享同一个进程地址空间)。对于保存在硬盘上的cookie,因为是在外部的存储设备中存储,所以可以在多个浏览器进程间共享。

3)浏览器关闭后,session就消失?

因为有人发现,关闭浏览器之后,再打开一个浏览器,就开始了一次新的会话。其实主要是因为保存session ID的cookie是存储在浏览器内存中,一旦浏览器关闭,cookie将被删除,session ID也就丢失了。当再次打开浏览器连接服务器时,服务器没有收到session ID,也就无法找到先前的session,所以服务器会创建一个新的session。而此时先前的session仍是存在的,知道设置的session超时时间间隔发生,session才会被清除。如果我们将会话cookie保存到硬盘上,或者改写浏览器发送给服务器的请求报头,将原来的session ID发送给服务器,则再次打开浏览器就能看到原来的session了。

转载请注明出处:http://blog.csdn.net/iAm333

你可能感兴趣的:(Servlet&JSP的那些事儿(七))