Servlet

标签 : Java与Web


会话跟踪

HTTP本身是“无状态”协议,它不保存连接交互信息,一次响应完成之后即连接断开,下一次请求需要重新建立连接,服务器不记录上次连接的内容.因此如果判断两次连接是否是同一用户, 就需要使用会话跟踪技术来解决.常见的会话跟踪技术有如下几种:

  • URL重写: 在URL结尾附加会话ID标识,服务器通过会话ID识别不同用户.
  • 隐藏表单域: 将会话ID埋入HTML表单隐藏域提交到服务端(会话ID不在浏览器页面显示).
  • Cookie: 第一次请求时服务器主动发一小段信息给浏览器(即Cookie),下次请求时浏览器自动附带该段信息发送给服务器,服务器读取Cookie识别用户.
  • Session: 服务器为每个用户创建一个Session对象保存到内存,并生成一个sessionID放入Cookie发送给浏览器,下次访问时sessionID会随Cookie传回来,服务器再根据sessionID找到对应Session对象(Java领域特有).

Session机制依赖于Cookie,如果Cookie被禁用Session也将失效.


Cookie

Cookie是识别当前用户,实现持久会话的最好方式.最初由网景公司开发,但现在所有主流浏览器都支持.以至于HTTP协议为他定义了一些新的HTTP首部.

URL重写与隐藏表单域两种技术都有一定的局限,细节可参考博客四种会话跟踪技术

  • Cookie规范

    • Cookie通过请求头/响应头在服务器与客户端之间传输, 大小限制为4KB;
    • 一台服务器在一个客户端最多保存20个Cookie;
    • 一个浏览器最多保存300个Cookie;

Cookie的key/value均不能保存中文,如果需要,可以在保存前对中文进行编码, 取出时再对其解码.


Java-Cookie

在Java中使用Cookie, 必须熟悉javax.servlet.http.Cookie类, 以及HttpServletRequest/HttpServletResponse接口提供的几个方法:

Cookie 描述
Cookie(String name, String value) Constructs a cookie with the specified name and value.
String getName() Returns the name of the cookie.
String getValue() Gets the current value of this Cookie.
void setValue(String newValue) Assigns a new value to this Cookie.
void setMaxAge(int expiry) Sets the maximum age in seconds for this Cookie.
int getMaxAge() Gets the maximum age in seconds of this Cookie.
void setPath(String uri) Specifies a path for the cookie to which the client should return the cookie.
void setDomain(String domain) Specifies the domain within which this cookie should be presented.
Request 描述
Cookie[] getCookies() Returns an array containing all of the Cookie objects the client sent with this request.
Response 描述
void addCookie(Cookie cookie) Adds the specified cookie to the response.
  • 示例: 获取上次访问时间

    Request中获取Cookie: last_access_time, 如果没有则新建,否则显示last_access_time内容, 并更新为当前系统时间, 最后放入Response:
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    Cookie[] cookies = request.getCookies();
    Cookie latCookie = null;
    if (cookies != null){
        for (Cookie cookie : cookies){
            if (cookie.getName().equals(L_A_T)){
                latCookie = cookie;
                break;
            }
        }
    }

    // 已经访问过了
    if (latCookie != null){
        printResponse("您上次访问的时间是" + latCookie.getValue(), response);
        latCookie.setValue(new Date().toString());
    } else{
        printResponse("您还是第一次访问", response);
        latCookie = new Cookie(L_A_T, new Date().toString());
    }

    response.addCookie(latCookie);
}

private void printResponse(String data, HttpServletResponse response) throws IOException {
    response.setContentType("text/html; charset=utf-8");
    response.getWriter().print("<H1>" + data + "</H1>");
}

有效期

Cookie的Max-Age决定了Cookie的有效期,单位为秒.Cookie类通过getMaxAge()setMaxAge(int maxAge)方法来读写Max-Age属性:

Max-Age 描述
0 Cookie立即作废(如果原先浏览器已经保存了该Cookie,那么可以通过设置Max-Age为0使其失效)
< 0 默认,表示只在浏览器内存中存活,一旦浏览器关闭则Cookie销毁
> 0 将Cookie持久化到硬盘上,有效期由Max-Age决定

域属性

服务器可向Set-Cookie响应首部添加一个Domain属性来控制哪些站点可以看到该Cookie, 如

Set-Cookie: last_access_time="xxx"; Domain=.fq.com

该响应首部就是在告诉浏览器将Cookie last_access_time="xxx"发送给域”.fq.com”中的所有站点(如www.fq.com, mail.fq.com).

Cookie类通过setDomain()方法设置域属性.

如果没有指定域, 则Domain默认为产生Set-Cookie响应的服务器主机名.


路径属性

Cookie规范允许用户将Cookie与部分Web站点关联起来.该功能可通过向Set-Cookie响应首部添加Path属性来实现:

Set-Cookie:last_access_time="Tue Apr 26 19:35:16 CST 2016"; Path=/servlet/

这样如果访问http://www.example.com/hello_http_servlet.do就不会获得last_access_time,但如果访问http://www.example.com/servlet/index.html, 就会带上这个Cookie.

Cookie类中通过setPath()方法设置路径属性.

如果没有指定路径, Path默认为产生Set-Cookie响应的URL的路径.


Session

在所有的会话跟踪技术中, Session是功能最强大,最多的. 每个用户可以没有或者有一个HttpSession对象, 并且只能访问他自己的Session对象.

与URL重写, 隐藏表单域和Cookie不同, Session是保存在服务器内存中的数据,在达到一定的阈值后, Servlet容器会将Session持久化到辅助存储器中, 因此最好将使保存到Session内的对象实现java.io.Serializable接口.

使用Session, 必须熟悉javax.servlet.http.HttpSession接口, 以及HttpServletRequest接口中提供的几个方法:

HttpSession 描述
void setAttribute(String name, Object value) Binds an object to this session, using the name specified.
Object getAttribute(String name) Returns the object bound with the specified name in this session, or null if no object is bound under the name.
void invalidate() Invalidates this session then unbinds any objects bound to it.
Enumeration<String> getAttributeNames() Returns an Enumeration of String objects containing the names of all the objects bound to this session.
void removeAttribute(String name) Removes the object bound with the specified name from this session.
String getId() Returns a string containing the unique identifier assigned to this session.
boolean isNew() Returns true if the client does not yet know about the session or if the client chooses not to join the session.
Request 描述
HttpSession getSession() Returns the current session associated with this request, or if the request does not have a session, creates one.
HttpSession getSession(boolean create) Returns the current HttpSession associated with this request or, if there is no current session and create is true, returns a new session.
String getRequestedSessionId() Returns the session ID specified by the client.

示例-购物车

  • domain
/**
 * @author jifang.
 * @since 2016/5/1 20:14.
 */
public class Product implements Serializable {

    private int id;
    private String name;
    private String description;
    private double price;

    public Product(int id, String name, String description, double price) {
        this.id = id;
        this.name = name;
        this.description = description;
        this.price = price;
    }

    // ...
}
public class ShoppingItem implements Serializable {
    private Product product;
    private int quantity;

    public ShoppingItem(Product product, int quantity) {
        this.product = product;
        this.quantity = quantity;
    }

    // ...
}
  • 商品列表页面(/jsp/products.jsp)
<%@ page import="com.fq.web.domain.Product" %>
<%@ page import="com.fq.web.util.ProductContainer" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Products</title>
</head>
<body>
<h2>Products</h2>
<ul>
    <%
        for (Product product : ProductContainer.products) {
    %>
    <li><%=product.getName()%>
        ($<%=product.getPrice()%>)
        (<a href="${pageContext.request.contextPath}/jsp/product_details.jsp?id=<%=product.getId()%>">Details</a>)
    </li>
    <%
        }
    %>
</ul>
<a href="${pageContext.request.contextPath}/jsp/shopping_cart.jsp">Shopping Cart</a>
</body>
</html>
  • 商品详情(/jsp/product_details.jsp)
<%@ page import="com.fq.web.domain.Product" %>
<%@ page import="com.fq.web.util.ProductContainer" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Product Details</title>
</head>
<body>
<h2>Product Details</h2>
<%
    int id = Integer.parseInt(request.getParameter("id"));
    Product product = ProductContainer.getProduct(id);
    assert product != null;
%>
<form action="${pageContext.request.contextPath}/session/add_to_card.do" method="post">
    <input type="hidden" name="id" value="<%=id%>"/>
    <table>
        <tr>
            <td>Name:</td>
            <td><%=product.getName()%>
            </td>
        </tr>
        <tr>
            <td>Price:</td>
            <td><%=product.getPrice()%>
            </td>
        </tr>
        <tr>
            <td>Description:</td>
            <td><%=product.getDescription()%>
            </td>
        </tr>
        <tr>
            <td><input type="text" name="quantity"></td>
            <td><input type="submit" value="Buy"></td>
        </tr>
        <tr>
            <td><a href="${pageContext.request.contextPath}/jsp/products.jsp">Products</a></td>
            <td><a href="${pageContext.request.contextPath}/jsp/shopping_cart.jsp">Shopping Cart</a></td>
        </tr>
    </table>
</form>
</body>
</html>
  • 加入购物车(AddCardServlet)
@WebServlet(name = "AddCardServlet", urlPatterns = "/session/add_to_card.do")
public class AddCardServlet extends HttpServlet {

    @SuppressWarnings("All")
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        int id = Integer.parseInt(request.getParameter("id"));
        Product product = ProductContainer.getProduct(id);
        int quantity = Integer.parseInt(request.getParameter("quantity"));

        HttpSession session = request.getSession();
        List<ShoppingItem> items = (List<ShoppingItem>) session.getAttribute(SessionConstant.CART_ATTRIBUTE);
        if (items == null) {
            items = new ArrayList<ShoppingItem>();
            session.setAttribute(SessionConstant.CART_ATTRIBUTE, items);
        }
        items.add(new ShoppingItem(product, quantity));

        request.getRequestDispatcher("/jsp/products.jsp").forward(request, response);
    }

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
        doPost(request, response);
    }
}
  • 购物车(/jsp/shopping_card.jsp)
<%@ page import="com.fq.web.constant.SessionConstant" %>
<%@ page import="com.fq.web.domain.ShoppingItem" %>
<%@ page import="java.util.List" %>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<html>
<head>
    <title>Shopping Cart</title>
</head>
<body>
<h2>Shopping Cart</h2>
<a href="${pageContext.request.contextPath}/jsp/products.jsp">Products</a>
<table>
    <tr>
        <td style="width: 150px">Quantity</td>
        <td style="width: 150px">Product</td>
        <td style="width: 150px">Price</td>
        <td>Amount</td>
    </tr>
    <%
        List<ShoppingItem> items = (List<ShoppingItem>) session.getAttribute(SessionConstant.CART_ATTRIBUTE);
        if (items != null) {
            double total = 0.0;
            for (ShoppingItem item : items) {
                double subtotal = item.getQuantity() * item.getProduct().getPrice();
    %>
    <tr>
        <td><%=item.getQuantity()%>
        </td>
        <td><%=item.getProduct().getName()%>
        </td>
        <td><%=item.getProduct().getPrice()%>
        </td>
        <td><%=subtotal%>
        </td>
    </tr>
    <%
            total += subtotal;
        }%>
    <tr>
        <td>Total: <%=total%>
        </td>
    </tr>
    <%
        }
    %>

</table>
</body>
</html>

有效期

Session有一定的过期时间: 当用户长时间不去访问该Session,就会超时失效,虽然此时sessionID可能还在Cookie中, 只是服务器根据该sessionID已经找不到Session对象了.

Session的超时时间可以在web.xml中配置, 单位为分钟:

<session-config>
    <session-timeout>30</session-timeout>
</session-config>

另外一种情况: 由于sessionID保存在Cookie中且Max-Age-1,因此当用户重新打开浏览器时已经没有sessionID了, 此时服务器会再创建一个Session,此时新的会话又开始了.而原先的Session会因为超时时间到达而被销毁.


字符编码

字符编码就是以二进制的数字来对应字符集的字符,常见字符编码方式有:ISO-8859-1(不支持中文),GB2312,GBK,UTF-8等.在JavaWeb中, 经常遇到的需要编码/解码的场景有响应编码/请求编码/URL编码:


响应编码

服务器发送数据给客户端由Response对象完成,如果响应数据是二进制流,就无需考虑编码问题.如果响应数据为字符流,那么就一定要考虑编码问题:

response.getWriter()默认使用ISO-889-1发送数据,而该字符集不支持中文,因此遇到中文就一定会乱码.

在需要发送中文时, 需要使用:

response.setCharacterEncoding("UTF-8");
// getWriter() ...

设置编码方式,由于在getWriter()输出前已经设置了UTF-8编码,因此输出字符均为UTF-8编码,但我们并未告诉客户端使用什么编码来读取响应数据,因此我们需要在响应头中设置编码信息(使用Content-Type):

response.setContentType("text/html;charset=UTF-8");
// getWriter() ...

注意: 这句代码不只在响应头中添加了编码信息,还相当于调用了一次response.setCharacterEncoding("UTF-8");


请求编码

1. 浏览器地址栏编码

在浏览器地址栏书写字符数据,由浏览器编码后发送给服务器,因此如果在地址栏输入中文,则其编码方式由浏览器决定:

浏览器 编码
IE/FireFox GB2312
Chrome UTF-8

2. 页面请求

如果通过页面的超链接/表单向服务器发送数据,那么其编码方式由当前页面的编码方式确定:

<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">

3. GET

当客户端发送GET请求时,无论客户端发送的数据编码方式为何,服务端均已ISO-8859-1解码(Tomcat8.x之后改用UTF-8),这就需要我们在request.getParameter()获取数据后再转换成正确的编码:

private Map<String, String> convertToParameterMap(HttpServletRequest request) throws UnsupportedEncodingException {
    Enumeration<String> names = request.getParameterNames();
    Map<String, String> parameters = new HashMap<String, String>();
    if (names != null) {
        while (names.hasMoreElements()) {
            String name = names.nextElement();
            String value = request.getParameter(name);
            parameters.put(name, new String(value.getBytes("ISO-8859-1"), "UTF-8"));
        }
    }
    return parameters;
}

4. POST

当客户端发送POST请求时,服务端也是默认使用IOS-8859-1解码,但POST的数据是通过请求体传送过来,因此POST请求可以通过request.setCharacterEncoding()来指定请求体编码方式:

private Map<String, String> convertToParameterMap(HttpServletRequest request) throws IOException {
    Map<String, String> parameters = new HashMap<String, String>();
    if (request.getMethod().equals("POST")) {
        request.setCharacterEncoding("UTF-8");
        Enumeration<String> names = request.getParameterNames();
        while (names.hasMoreElements()) {
            String key = names.nextElement();
            parameters.put(key, request.getParameter(key));
        }
    } else {
        Enumeration<String> names = request.getParameterNames();
        while (names.hasMoreElements()) {
            String key = names.nextElement();
            String value = request.getParameter(key);
            parameters.put(key, new String(value.getBytes("ISO-8859-1"), "UTF-8"));
        }
    }

    return parameters;
}

URL编码

网络标准RFC 1738规定:

“…Only alphanumerics [0-9a-zA-Z], the special characters "$-_.+!*'()," [not including the quotes - ed], and reserved characters used for their reserved purposes may be used unencoded within a URL.”

“只有字母和数字[0-9a-zA-Z]、一些特殊符号"$-_.+!*'(),"[不包括双引号]、以及某些保留字,才可以不经过编码直接用于URL。”

如果URL中有汉字,就必须编码后使用, 而URL编码过程其实很简单:

首先需要指定一种字符编码,把字符串解码后得到byte[],然后把小于0的字节+256,再将其转换成16进制,最后前面再添加一个%.

这个编码过程在Java中已经封装成了现成的库, 可直接使用:

URLEncoder 描述
static String encode(String s, String enc) Translates a string into application/x-www-form-urlencoded format using a specific encoding scheme.
URLDecoder 描述
static String decode(String s, String enc) Decodes a application/x-www-form-urlencoded string using a specific encoding scheme.

注: 在Web中Tomcat容器会自动识别URL是否已经编码并自动解码.


参考

更多有关编码知识, 可以参考:

1. 阮一峰: 关于URL编码

2. Web开发者应知的URL编码知识

3. 字符集和字符编码(Charset & Encoding)


Servlet - 会话跟踪的更多相关文章

  1. 关于Servlet会话跟踪的那些事儿

    关于servlet会话跟踪,一搜都能搜出很多.我也不免落入俗套,也总结了一把.希望我所总结的知识尽量是知识海洋里的一汪清泉.能帮助到我自己和哪怕一个人,那也是值得的. 故事由来: 我们知道,http协 ...

  2. 7、Servlet会话跟踪

    一.会话跟踪: 不管操作多少功能,都是与当前登录用户相关的信息,当前的登录用户始终没有改变,也就是用户名和密码都没有丢失.但HTTP协议是一个无状态的协议,当一个客户向服务器发出请求(request) ...

  3. Servlet会话跟踪和Cookies及HttpSession会话

    会话只是指一段指定的时间间隔. 会话跟踪是维护用户状态(数据)的一种方式.它也被称为servlet中的会话管理. Http协议是一个无状态的,所以我们需要使用会话跟踪技术来维护用户状态. 每次用户请求 ...

  4. IT兄弟连 JavaWeb教程 Servlet会话跟踪 Session常用方法

    ●  public Object getAttribute(String name) 该方法返回在该session会话中具有指定名称的对象,如果没有指定名称的对象,则返回null. ●  public ...

  5. IT兄弟连 JavaWeb教程 Servlet会话跟踪 Session技术

    Servlet提供了HttpSession接口,该接口提供了一种跨多个页面请求或访问网站时识别用户以及存储有关用户信息的方式. Servlet容器使用这个接口来创建一个HTTP客户端和HTTP服务器之 ...

  6. IT兄弟连 JavaWeb教程 Servlet会话跟踪 Cookie常用方法

    以下是在Servlet中操作Cookie时可使用的有用的方法列表 ●  public void setDomain(String pattern) 该方法设置cookie适用的域,例如 itxdl.c ...

  7. IT兄弟连 JavaWeb教程 Servlet会话跟踪 经典面试题

    1.描述Cookie的作用 Cookie是网站保存在浏览器客户端的信息,也就是说保存在访客的机器里的变量,一般随着HTTP头发送到客户端.在Cookie生效之后及失效之前,客户每次发出页面请求的时候, ...

  8. IT兄弟连 JavaWeb教程 Servlet会话跟踪 经典案例

    案例需求:编写一个servlet,可以向session中存放一个消息,再编写一个servlet可以从session取得session中存放的这个消息. 案例实现: package com.xdl.se ...

  9. IT兄弟连 JavaWeb教程 Servlet会话跟踪 Cookie路径问题

    操作Cookie时,需要注意路径问题: 设置操作:任何路径都可以设置Cookie,但是有时我们也是用设置进行替换Cookie和删除Cookie(maxAge=0)! 替换:只能由完全相同的路径来操作! ...

随机推荐

  1. [LeetCode] Remove Boxes 移除盒子

    Given several boxes with different colors represented by different positive numbers. You may experie ...

  2. Java IO(四)

    在文件操作流中,输入输出的目标都是文件,但是有时候,我们并不需要写入文件,只是需要中转一下而已,这样就会显得很麻烦,所以我们就可以使用内存操作流.在内存操作流中,输入输出目标都是内存. 内存输出流:B ...

  3. [PA 2014]Iloczyn

    Description 斐波那契数列的定义为:k=0或1时,F[k]=k:k>1时,F[k]=F[k-1]+F[k-2].数列的开头几项为0,1,1,2,3,5,8,13,21,34,55,…你 ...

  4. codeforces round #419 E. Karen and Supermarket

    On the way home, Karen decided to stop by the supermarket to buy some groceries. She needs to buy a ...

  5. PKUWC2018划水记

    PKUWC2018划水记 Day -1 ​ 从福州出发去长沙,原本是预定Day0当天的航班,后来怕来不及提前到了今天. ​ 由于最近长沙下雪,所以听说飞机取消了很多班次,所以早上起来的时候还特地看了一 ...

  6. ●UVA 11021 tunnello

    题链: https://vjudge.net/problem/UVA-11021题解: 概率DP. 定义dp[i]表示初始1只麻球的情况下,第i天都死完的概率. (因为每只麻球互相独立,那么最后答案为 ...

  7. 网络编程基础API

    1.预备知识 网络字节序 1.TCP/IP协议规定,网络数据流应采用大端字节序 0x12345678 小端存储:78存储在低地址 大端存储:12存储在低地址 网络字节序和主机字节序的转换 #inclu ...

  8. java集合之ArrayList源码解读

    源自:jdk1.8.0_121 ArrayList继承自AbstractList,实现了List.RandomAccess.Cloneable.Serializable. ArrayList内部是通过 ...

  9. Docker学习笔记【一】

    [本篇学习笔记来源于 Docker 从入门到实践] 1.什么事Docker?[What] Docker在容器的基础上,进行了进一步的封装,从文件系统.网络互联到进程隔离等,极大的简化了容器的创建和维护 ...

  10. Mysql锁机制--索引失效导致行锁变表锁

    Mysql 系列文章主页 =============== Tips:在阅读本文前,最好先阅读 这篇(Mysql锁机制--行锁)文章~ 在上篇文章中,我们看到InnoDB默认的行锁可以使得操作不同行时不 ...