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[] 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 地址.
来源: http://www.jianshu.com/p/c904e5116c9b