1.Cookie和Session
在JavaWeb基础(五)中,我们分享了.Servlet规范
、Servlet生命周期
、Servlet请求流程
、Servlet初始化参数
、Servlet继承体系结构和设计原因
。
今天我主要来分享下Cookie和Session, Cookie和Session使用起来其实很简单, 主要用来解决多个request之间的数据共享问题
。先说下我这篇博客会分享的内容.
-
Servlet3.0的注解
、Http协议存的问题
、Cookie技术
、Session技术
、总结下使用场景
1.1 Servlet3.0新增的注解
之前的编码中,我们每次增加一个Servlet。都需要到WEB-INF
下配置web.xml
.我们需要注册Servlet
并关联资源名
.
而从J2EE6规范起, 我们就可以用注解的方式来配置这些信息.这个版本的Servlet即Servlet3.0
, 对应着J2EE6的规范
和Tomcat7.*
.
为什么引入Servlet配置注解方式
每个新技术的引入都是有一定应用场景, Servlet注解的引入主要是为了解决配置xml的繁琐和臃肿.
因为随着Sevlet的增加, web.xml里的配置会爆炸性的增长, 这时候对于修改和维护该配置文件效率往往会非常低.
web.xml中的metadata-complete属性
该属性表示元数据的完整性
.即xml是用来描述Servlet的
,我们也可以把XML看成是描述Servlet的一种元数据
.
-
如果我们声明其为true
。表示xml描述信息是完整的
,那么这时候tomcat就不会再去解析Servlet的注解信息. -
如果我们声明其为false
。表示xml描述信息是不完整的
,那么这时候tomcat机会再去解析Servlet的注解信息, 这时候我们使用注解替代web.xml才会生效
.
代码演示
我们之前分享的中,提到最多的配置是资源名称映射到Servlet配置
和Servlet初始化参数配置
.我们就来看下如何使用注解来配置.首先这两个配置对应的是@WebServlet注解
和@WebInitParam注解
.以下是其源代码, 我们只列出部分常见属性.
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
String name() default "";
String[] value() default {};
String[] urlPatterns() default {};
int loadOnStartup() default -1;
WebInitParam[] initParams() default {};
}
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebInitParam {
String name();
String value();
}
@WebServlet有name
、value
、urlPatterns
、loadOnStartup
、initParams
五个常用属性.
- value和urlPatterns作用一样, 只是因为value是默认属性,所以我们可以不用写Key
- initParams属性是一个数组, 元素类型是@WebInitParams注解, 其属性为name和value,name确定key值, value确定对应key的值。
- 并且这些注解的生命周期都是能存储到运行时.所以其原理就是利用运行时在确定要
资源名要关联的Servlet
.
如下代码,使用Servlet注解
package com.sweetcs.web.servlet.servlet3_0;
import java.io.IOException;
import java.util.Arrays;
import javax.jws.soap.InitParam;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebInitParam;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(
value={"/annotation_servlet"},
initParams={
@WebInitParam(name = "enocding", value ="UTF-8")
})
public class AnnotationServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 使用反射读取配置初始化参数
Class<AnnotationServlet> clazz = null;
try {
clazz = (Class<AnnotationServlet>) Class.forName("com.sweetcs.web.servlet.servlet3_0.AnnotationServlet");
WebServlet annOfWebServlet = clazz.getAnnotation(WebServlet.class);
System.out.println(Arrays.toString(annOfWebServlet.value()));
System.out.println(Arrays.toString(annOfWebServlet.urlPatterns()));
System.out.println(Arrays.toString(annOfWebServlet.initParams()));
} catch (ClassNotFoundException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
启动浏览器, 输入http://127.0.0.1:8080/annotation_servlet. 运行输出如下.注解能在运行时成功读取到,其中value和urlpattern属性作用是一样,但是如果我们只配置其中一个,其只能读取到其中一个的值.
web.xml和注解的选择
XML和注解区别
- XML使得配置和Java相分离, 维护性较高。注解和Java代码耦合, 维护性低.
- XML维护繁琐, 配置文件臃肿, 开发效率低。注解使得开发效率高, 方便快速定位.
选择
一般在企业级开发中, 我们当然要尽量的选择其优点, 所以我们在xml中做通用配置。个别的Servlet配置才用注解。
1.2 Http协议无状态带来的问题
一次会话
从打开浏览器,到关闭浏览器过程中的操作可以称为一次会话.我们把一次会话也称为Session。而一次会话中我们可以发送多次的请求Request.
Http协议的问题
http协议
是一种无状态连接的协议
.导致了服务端无法让多个请求共享数据
.
说白话就是
服务端不知道上一次是哪个客户端请求了自己
。一次会话中可以发送多次请求
, 但是服务器却不知道这多次请求是来自同一个客户端
。问题:
这也就导致了服务端无法让多个请求共享数据.为了解决这个问题就引入了参数传递机制
、Cookie技术
和Session技术
举个栗子
客户端例子
学过移动端开发的同学都知道, 对于同一个用户, 我们需要在多个页面之前传递数据.而因为Http协议的
无状态连接
问题导致我们无法在多个页面之前传递数据.这只是在客户端之前的一个例子, 并不准确。
服务端例子
在服务端开发中,涉及到网络通信,所以可以理解为
多个请求无法标识
,导致了服务端无法有效的利用之前的数据在多个请求前实现共享
.这也就是导致了你发次请求过来,下次我就不知道是你了,如果有很多页面需要做权限控制,那么每次页面一跳转,由于Http协议的健忘性
.你又得重新登入做权限验证
1.3 参数传递机制
为了解决服务端无法识别请求。我们可以在请求中自己附加参数,用来标记请求,这是最原始的解决方案。如下代码使用参数传递来解决数据共享问题
.
登入界面
LoginServlet
@WebServlet(urlPatterns={"/login"})
public class LoginServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println("username = " + username +"password = " + password);
IUserDAO userDAO = new UserDAOImpl();
User user = userDAO.loginReturnUserOrNull(username, password);
PrintWriter printWriter = resp.getWriter();
if (null == user) {
printWriter.write("login failed");
}else {
resp.sendRedirect("/get?username=" + username);
}
}
}
GetServlet
负责显示邮件箱
@WebServlet(urlPatterns={"/get"})
public class GetServlet extends HttpServlet{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
PrintWriter pw = resp.getWriter();
String username = req.getParameter("username");
pw.write("用户:" + username + "<br />");
for (int i = 0; i < 6; i++) {
pw.write("<a href="+"'/content?username="+ username +"'>第(" + i +")封邮件</a> <br />");
}
}
}
邮箱界面, 显示邮件
ContentServlet
负责显示邮件内容
@WebServlet("/content")
public class ContentServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
String username = req.getParameter("username");
PrintWriter pw = resp.getWriter();
pw.write(username + " 大哥你好!有空聚聚");
}
}
可以看到这种方式,是通过URL携带参数进行数据共享.但是这种方式是不安全的, 而且很繁琐的, 如果要共享用户信息需要在每个连接后携带共享数据.
1.4 Cookie技术
Cookie原理
Cookie是一种客户端技术。
- 1.程序把每个用户的数据以Cookie响应给用户的浏览器.
- 2.用户浏览器接收到Cookie后就会将其保存到本地。
- 3.当用户使用浏览器取访问服务器资源中,就会携带各自的Cookie数据, 这样服务器就可以通过Cookie里知道该请求是哪些用户发送的.
Cookie的使用
创建Cookie对象
@Test
public void testCreateAndReadCookie() {
Cookie cookie = new Cookie("name", "Sweetcs");
String name = cookie.getName();
String value = cookie.getValue();
System.out.println(cookie);
System.out.println("name="+name +" value= " +value);
}
将Cookie放入响应, 响应给浏览器,让浏览器去存储
@WebServlet(urlPatterns={"/cookie/login"})
public class LoginServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println("username = " + username +"password = " + password);
IUserDAO userDAO = new UserDAOImpl();
User user = userDAO.loginReturnUserOrNull(username, password);
PrintWriter printWriter = resp.getWriter();
// 1.创建Cookie
Cookie cookie = new Cookie(username, password);
// 2.setPath设置的共享范围是在同一个web服务器之下.
cookie.setPath("/"); // 该设置表示该浏览器请求的web应用只要是该服务器下都会传递该Cookie给服务器
// 3.设置共享Cookie的域名,通常应用在多个二级域名之间传递数据
// cookie.setDomain(""); // 一般用于设置二级域名,那多个跨域web app能共享Cookie.
// 4. 设置Cookie存活时间为3分钟。如果设置为0表示让浏览器删除Cookie,设置为负数, 表示Cookie只在本次会话中有效.
cookie.setMaxAge(60 * 3);
if (null == user) {
printWriter.write("login failed");
}else {
// 2.将Cookie加入响应头,响应给浏览器,客户端接收到会进行存储
resp.addCookie(cookie);
resp.sendRedirect("/cookie/get");
}
}
}
登入后的Http报文
可以看到如上Http报文,因为我们在LoginServlet的响应给浏览器Cookie,其响应头中有一个字段Set-Cookie:xxx
.浏览器接受到后,判断有这个字段就将Cookie进行解析保存.
获取Cookie中的数据
@WebServlet(urlPatterns={"/cookie/get"})
public class GetServlet extends HttpServlet{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
// 1.客户端接受到Cookie后,当再次发送请求,会携带Cookie数据.
PrintWriter pw = resp.getWriter();
Cookie[] cookies = req.getCookies();
String username = null;
for (Cookie cookie : cookies) {
String key = cookie.getName();
String value = cookie.getValue();
if ("username".equals(key)) {
username = value;
break;
}
}
pw.write("用户:" + username + "<br />");
for (int i = 0; i < 6; i++) {
pw.write("<a href='/cookie/content'>第(" + i +")封邮件</a> <br />");
}
}
}
查看邮件内容页面对应的Http报文.可以看到当我们跳转到该页面,浏览器会携带Cookie数据.
Cookie分类
- 会话Cookie
默认,关闭浏览器之后,就会销毁的Cookie。也就是在一次会话范围内有效的Cookie,我们称为会话Cookie
. - 持久化Cookie
设置Cookie的最大存活时间setMaxAge(seconds)
seconds == 0
: 删除Cookie.(让浏览器删除Cookie)
seconds < 0
: 会话Cookie.(持久化Cookie转换成会话Cookie)
seconds > 0
: 存储指定的秒数.(让浏览器持久化Cookie
cookie主要分为会话Cookie
和持久化Cookie
.持久化Cookie可以通过setMaxAge(-1)转换成会话Cookie.
Cookie的缺陷
-
Cookie不够安全
.一个浏览器的其他用户可以查看其他人的Cookie -
Cookie不能直接支持中文
.需要先将中文进行编码才能保存中文.
// 使用URLEncode和URLDecoder对中文进行编码
@Test
public void testCookieStoreChinese() {
Cookie cookie = null;
try {
cookie = new Cookie("name", URLEncoder.encode("苏轼", "UTF-8"));
System.out.println("encode value = " + cookie.getValue());
System.out.println("decode value = " + URLDecoder.decode(cookie.getValue(), "UTF-8"));
} catch (UnsupportedEncodingException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
运行输出
-
每个Cookie只能存储一个数据
.需要多个数据, 需要多个Cookie -
站点对Cookie大小有限制, 服务器和浏览器对Cookie个数也有限制
。站点对Cookie大小限制在4KB内.服务器在一个客户端最多保存20个,浏览器最多可以保存300个。 -
设计上有问题
。因为Cookie存储在浏览器端,一旦丢失就不能恢复, 不能再次使用。如果是存储在服务器上就没这个问题。
Cookie的增、删、改、查总结
- 增
Cookie cookie = new Cookie(Key, Value);
response对象.addCookie(cookie);
- 删
cookie.setMaxAge(0)
- 改
// 方式一
cookie.setValue(newValue);
// 方式二
cookie = new Cookie(oldKey, newValue);
response对象.addCookie(cookie);
- 查
cookie.getValue()
Cookie使用步骤总结
- 创建Cookie.
cookie = new Cookie(key, value)
- 添加Cookie到响应中(
response.addCookie(cookie)
)- (添加前要先配置)配置Cookie的
path
和domain
(cookie.setPath("\")
同服务器跨app。cookie.setDomain(".baidu.com")
不同服务器, 不同app跨域) - (添加前要先配置)配置Cookie的过期时间(
setMaxAge(seconds)
).过期后浏览器会自动删除
- (添加前要先配置)配置Cookie的
- 获取Cookie.(
Cookie[] cookies = req.getCookies()
)
1.5 Session
Session是服务端共享数据的技术.其作用和Cookie类似, 用于存储数据, 只是其是存储于服务端, 而不是客户端.其主要是为了解决Cookie的缺陷
Session原理
服务器创建一个Session对象, 作为存储共享数据的地方。并将session的地址作为Cookie响应给浏览器.(其key为jsessionid, value为session的地址),浏览器使用Cookie技术存储下该jssesionid,并在下一次访问的时候, 将该Cookie携带给服务器, 服务器通过该session的地址(jsessionid)可以获取到其对应的session, 进行数据的共享。
Session的操作
- 创建和获取Session对象
// 1.创建Session.如果session存在则返回,如果不存在则创建一个.session本质上是一个Map结构
HttpSession session = req.getSession(true);
// 2.创建Session.如果session存在则返回,如果不存在返回null
HttpSession session = req.getSession(false);
- 存储和取数据
session.setAttribute("username", username); // 存的是对象类型
String username = (String)session.getAttribute("username"); // 取出来的时候需要强转
- 删除session
// 1.删除Session中指定属性
session.removeAttribute("username");
// 2.销毁整个session对象
session.invalidate();
- 设置Session超时时间
指定session在客户端没有和服务端交互的最就时间,超过这个时间,服务端会自动销毁Session.sessioin对象.setMaxInactiveInterval(秒)
.
tomcat的web.xml配置,默认是30分钟,但是一般20几分钟就销毁了,并不准.
// 客户端和服务端没有交互的最长时间,超过这个时间,session自动销毁
session.setMaxInactiveInterval(60 * 10);
使用Session技术重构上述代码
LoginServlet
@WebServlet(urlPatterns={"/session/login"})
public class LoginServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
String username = req.getParameter("username");
String password = req.getParameter("password");
System.out.println("username = " + username +" password = " + password);
IUserDAO userDAO = new UserDAOImpl();
User user = userDAO.loginReturnUserOrNull(username, password);
PrintWriter printWriter = resp.getWriter();
// 1.创建Session.如果session存在则返回,如果不存在则创建一个.session本质上是一个Map结构
HttpSession session = req.getSession(true);
// 2.向Session中添加共享数据
session.setAttribute("USERNAME_IN_SESSION", username);
// 3.配置如果没有请求过来,session保存多久.
session.setMaxInactiveInterval(60 * 10);
if (null == user) {
printWriter.write("login failed");
}else {
resp.sendRedirect("/session/get");
}
}
}
GetServlet
@WebServlet(urlPatterns={"/session/get"})
public class GetServlet extends HttpServlet{
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
// 1.客户端接受到Cookie后,当再次发送请求,会携带Cookie数据.
PrintWriter pw = resp.getWriter();
HttpSession session = req.getSession(true);
String username = (String)session.getAttribute("USERNAME_IN_SESSION");
pw.write("用户:" + username + "<br />");
for (int i = 0; i < 6; i++) {
pw.write("<a href='/session/content'>第(" + i +")封邮件</a> <br />");
}
}
}
ContentServlet
@WebServlet("/session/content")
public class ContentServlet extends HttpServlet {
@Override
protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
req.setCharacterEncoding("UTF-8");
resp.setContentType("text/html;charset=UTF-8");
HttpSession session = req.getSession(true);
String username = (String)session.getAttribute("USERNAME_IN_SESSION");
PrintWriter pw = resp.getWriter();
pw.write(username + " 大哥你好!有空聚聚");
}
}
LoginServlet对应的HTTP响应报文
(由登入界面跳转到邮件列表)
可以看到使用Session
的后,会响应一个Cookie
.该Cookie
的key
是JSESSIONID
,value
是Session对象在服务端的地址
ContentServlet对应的HTTP请求报文
(由邮件列表跳转到邮件内容的请求过程)
可以看到,此时的请求会携带Cookie
,Cookie
中存储有JSESSIONID
的值
Session的细节
- 属性名称唯一, 习惯的命名规范是
XXX_IN_SESSION
-
多个数据存储于Session, 一般我们把存储的数据封装成一个对象
.再进行存储. - 多态服务器需要共享Session,那存储的对象需要实现Serializable接口.才能在网络中传输Session
URL重写
开发中一般浏览器我们都是不会禁用Session和Cookie的。如果禁用了Cookie.此时就十分麻烦了.这就使得我们没法利用Cookie技术,在请求之前传递JESSIONID.从而session机制也就失效了.为了解决这个问题,我们可以用最传统的方式,将JESSIONID拼接在每个URL之后。但这样做又十分麻烦。J2EE为我们提供了一个十分便捷的接口, 只要传入要资源的路径,就会自动拼接出带JESSIONID的参数地址。
该接口会自动判断是否需要在资源路径后拼接JESSIONID
String url = response.encodeURL("/session/list");
System.out.println(url);
Cookie和Session的区别
- Cookie是将共享数据存储在浏览器.当访问服务器时候,携带共享数据给服务器。
- Session是将共享数据存储在服务器,让后将Session的地址告诉浏览器,浏览器访问服务器的时候,携带的是Session地址。