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)