從本篇開始學習Servlet技術中的Cookie專題。
首先來了解什么是“會話”。會話是web技術中的一個術語,可以簡單的理解為:用戶打開一個瀏覽器,點擊多個超鏈接,訪問服務器多個web資源,然后關閉瀏覽器,這個過程稱為一個會話。
如果在打開一個瀏覽器訪問一個頁面后,再打開一個瀏覽器訪問同一個頁面,那這就是有兩個會話;而打開一個瀏覽器訪問一個頁面后,通過這個頁面上的某個超鏈接是從新的瀏覽器打開的,那依然只算一個會話。
每個用戶在使用瀏覽器與服務器進行會話的過程中,各自不可避免地會產生一些數據,而程序要想辦法為每個用戶保存這些數據。比如,用戶點擊超鏈接通過一個產品Servlet購買了一個商品,程序應該想辦法保存這個商品,以便於用戶在點擊付款超鏈接時能再從付款Servlet中看到這個商品並為其買單。
使用Request對象是無法保存數據的,因為在點擊商品和付款各自的Servlet是發送兩個不同的Request請求對象,而使用ServletContext對象則會發生多個用戶的線程安全問題,使用轉發功能理論上可行,但是用戶體驗將會大打折扣,每次點擊一個商品就會被要求付款。所以根據以上的需求,有兩種技術來保存會話過程中產生的數據:一個是Cookie,一個是Session,Session技術將會在之后的篇章中介紹學習。
本篇主要先講述Servlet中的Cookie技術。Cookie技術是客戶端技術,程序把每個用戶的數據以cookie的形式寫給用戶各自的瀏覽器。當用戶使用瀏覽器再去訪問服務器時,就會帶着各自的數據過去,這樣web服務器處理的就是用戶各自的數據了。
下圖是一個會話過程中設置上一次訪問時間的Cookie的簡單過程:
創建一個Cookie對象就像平常創建一個Java對象一樣簡單:
在使用構造器時傳入Cookie的名和值這樣的鍵值對即可,我們在服務器端要獲取從瀏覽器發來的Cookie數據可以使用請求對象的request.getCookies方法獲得一個Cookie數組,而我們想向瀏覽器輸出Cookie時可以使用響應對象的response.addCookie(Cookie)方法。
同時Cookie對象還有如下一些方法:
getName方法用來獲取某個Cookie對象的名稱。
setValue方法和getValue方法分別用來設置和獲取某個Cookie對象的值。
setMaxAge(int expires)方法是設置Cookie的有效期,如果沒有這句代碼,Cookie的有效期就是一個會話時間(即關閉瀏覽器該Cookie就不存在了),當設置了Cookie的有效期后,Cookie會保存在瀏覽器指定的硬盤文件中,同時在這段時間內,每次訪問服務器都會帶着Cookie過去。如果將該方法參數置為“0”,則服務器會指示瀏覽器刪除該Cookie。
setPath方法是設置Cookie的有效路徑。表示在訪問某些特定URL時才會帶Cookie過去。假設某個web應用為【myservlet】,如果我們將setPath方法中的參數設置為“/myservlet”,那么訪問該web應用下所有的資源都會使瀏覽器發送Cookie過去;而如果我們將setPath方法中的參數設置為“/myservlet/pages”,那么只有訪問該web應用中的【pages】下的資源才會帶Cookie過去,而訪問該web應用中的其他資源則不會帶Cookie給服務器。如果我們沒有設置setPath方法,那么該Cookie的有效路徑默認為創建Cookie對象的當前程序所在目錄。注意,Cookie的路徑是給瀏覽器使用的(詳見《Servlet的學習之web路徑問題》)
setDomain方法是設置Cookie的有效域名,如:.sina.com(注意最前面有一個點)。表示當瀏覽器訪問該域名時才會帶Cookie過去。但是現在瀏覽器基本全面阻止了這個可能作為不安全的功能,所以幾乎已經被棄用。
舉例:我們訪問某個Servlet,而在訪問這個Servlet時會將當前訪問時間作為Cookie中的值返回給客戶端,同時下次再次訪問該Servlet時,會顯示上一次客戶端來訪問的時間:
1 public void doGet(HttpServletRequest request, HttpServletResponse response) 2 throws ServletException, IOException { 3 4 response.setCharacterEncoding("UTF-8"); 5 response.setContentType("text/html;charset=utf-8"); 6 7 PrintWriter writer = response.getWriter(); 8 writer.write("您上次訪問的時間是:"); 9 //獲取用戶上一次的訪問時間並顯示 10 Cookie[] cookies = request.getCookies(); //從請求中獲取客戶端發來的Cookie 11 for(int i=0;cookies!=null && i<cookies.length;i++) { 12 if(cookies[i].getName().equals("lastAccessTime")) { //獲取最后訪問時間的Cookie 13 Long mTime = Long.parseLong(cookies[i].getValue()); 14 String lastAccessTime = new Date(mTime).toLocaleString(); 15 writer.write(lastAccessTime); 16 } 17 } 18 //將本次登錄時間重新裝載進Cookie中並返回給客戶端 19 Cookie timeCookie = new Cookie("lastAccessTime", System.currentTimeMillis()+""); 20 timeCookie.setMaxAge(1*24*60*60); //將Cookie有效期置為一天 21 response.addCookie(timeCookie); //將Cookie傳回客戶端 22 }
第一次訪問是沒有Cookie的,所以看不到訪問時間:
但是我們通過HttpWatch觀察Response響應包中的內容已經有了“Set-Cookie”響應頭:
刷新后的第二次訪問就可以看到了:
同時觀察HttpWatch中Request請求包的“Cookie”請求頭可以發現:
============================神奇的案例分割線===========================
現在我們再來通過一個案例來學習Cookie,這是一個很常見的案例,比如我們在訪問購物網站的時候經常會發現當瀏覽了這個網站內的某個商品的時候,下次繼續來訪問這個網站,會有一個上次瀏覽物品的顯示。
如果我們不是用登錄后將記錄保存在服務器端,而是使用Cookie技術來將記錄保存在客戶端的瀏覽器中(現實生活中當然很少這樣使用,這里只是作為案例學習),那么我們應該怎么做呢?
首先我們必須在服務器要有兩個Servlet,一個在用戶眼中是用來顯示所有商品的,一個是用來顯示點擊某個商品之后詳細信息的。
⑴.用來顯示所有商品的Servlet需要完成如下功能:
① 在一個部分以超鏈接形式將數據庫中所有的商品顯示在該Servlet上。
② 在另一個部分獲取用戶請求中的Cookie將之前瀏覽過的商品(通過Cookie中的商品id)顯示在該Servlet上。
⑵. 用來顯示點擊某個商品之后詳細信息的Servlet需要完成如下功能:
① 在頁面上通過超鏈接的URL跟隨的參數(即商品id)來獲取該商品對象,同時將該商品對象的詳細信息輸出到Servlet頁面上。
② 如果是用戶首次訪問,將用戶瀏覽商品的id作為Cookie直接返回,而如果是用戶再次訪問,則需要根據一定的條件來將這些Cookie的值進行調整,以便易於顯示和滿足用戶體驗。
當然,在這之前我們還需要做些准備工作,我們需要建立商品對象,這里簡單的以書為商品建立對象:
1 public class Product { 2 3 private String id; 4 private String name; 5 private String author; 6 7 public Product() { 8 super(); 9 10 } 11 public Product(String id, String name, String author) { 12 super(); 13 this.id = id; 14 this.name = name; 15 this.author = author; 16 } 17 18 public String getId() { 19 return id; 20 } 21 public void setId(String id) { 22 this.id = id; 23 } 24 public String getName() { 25 return name; 26 } 27 public void setName(String name) { 28 this.name = name; 29 } 30 public String getAuthor() { 31 return author; 32 } 33 public void setAuthor(String author) { 34 this.author = author; 35 } 36 }
我們還需要一個數據庫來保存商品,這里我們先用一個類來來保存(數據庫還沒學嘛T_T!),保存數據采用Map集合,這是因為如果有檢索會方便:
1 public class ProductDatabase { 2 3 private static Map<String,Product> map = new HashMap<String, Product>(); 4 5 static{ 6 map.put("1", new Product("1","《Java編程思想》","JB")); 7 map.put("2", new Product("2","《Java核心技術》","fdaf")); 8 map.put("3", new Product("3","《Java並發編程》","什么鬼")); 9 map.put("4", new Product("4","《Head first 設計模式》","老王")); 10 map.put("5", new Product("5","《HTML5權威手冊》","hhaa")); 11 } 12 13 public static Map<String,Product> getMap() { 14 15 return map; 16 } 17 18 }
做完了這兩步,那么我們可以安心的去搞Servlet了,首先是在顯示所有商品的Servlet:
1 response.setCharacterEncoding("UTF-8"); 2 response.setContentType("text/html;charset=utf-8"); 3 4 PrintWriter writer = response.getWriter(); 5 //從數據庫中取出要顯示在購物網站首頁的商品 6 Map<String,Product> map = ProductDatabase.getMap(); 7 if(map == null) { 8 writer.print("您訪問的寶貝已下架"); 9 return ; 10 } 11 for(Map.Entry<String, Product> en : map.entrySet()) { 12 writer.print("<a href='/CookieProductProject/servlet/DetailGoodServlet?id="+en.getKey()+"' target='_blank' >" 13 +en.getValue().getName()+" <br/>"); 14 } 15 16 17 //顯示用戶之前瀏覽過的商品,要從用戶發送的請求中的Cookie里取得 18 writer.print("<br/><br/>"); 19 writer.print("您最近瀏覽過的商品: <br/>"); 20 21 Cookie[] cookies = request.getCookies(); 22 for(int i=0;cookies!=null && i<cookies.length;i++ ) { 23 if(cookies[i].getName().equals("productHistory")) { 24 Cookie cookie = cookies[i]; 25 String productId = cookie.getValue(); 26 String[] splitId = productId.split("\\_"); 27 for(String sId:splitId) { 28 Product book = ProductDatabase.getMap().get(sId); 29 writer.print(book.getName()+"<br/>"); 30 } 31 } 32 } 33 } 34
最后是點擊某個商品顯示詳細信息的Servlet:
1 response.setCharacterEncoding("UTF-8"); 2 response.setContentType("text/html;charset=UTF-8"); 3 PrintWriter writer = response.getWriter(); 4 5 //通過用戶點擊商品的超鏈接而跟隨URL來的ID參數來獲取商品的詳細信息 6 String productId = request.getParameter("id"); 7 Map<String, Product> map = ProductDatabase.getMap(); 8 Product book = map.get(productId); 9 writer.print("商品名:"+book.getName()+"<br />"); 10 writer.print("作者:"+book.getAuthor()); 11 12 //同時通過Cookie將用戶觀看的商品以Cookie的形式回傳給用戶瀏覽器 13 Cookie[] allCookies = request.getCookies(); 14 15 Cookie cookie = creCookie(book.getId(),allCookies); 16 cookie.setMaxAge(24*60*60); 17 response.addCookie(cookie);
其中creCookie(String,Cookie[])是自定義方法,用於獲取用戶的cookie並添加本次瀏覽商品id再作為cookie返回:
1 private Cookie creCookie(String id, Cookie[] cookies) { 2 Cookie cookie = null; 3 4 if(cookies == null) { //如果cookies為空,說明用戶首次訪問 5 cookie = new Cookie("productHistory", id); 6 System.out.println(cookie.getValue()); 7 return cookie; 8 } 9 for(int i=0; i<cookies.length; i++) { 10 if(cookies[i].getName().equals("productHistory")){ 11 cookie = cookies[i]; 12 } 13 } 14 15 String historyStr = cookie.getValue(); //此時獲取到的之前瀏覽過數據的歷史記錄,有多種情況 16 String[] produIds = historyStr.split("\\_"); 17 18 //為了檢測數組中是否有包含當前的id,建議使用集合,而且是使用鏈表結構的集合 19 LinkedList<String> list = new LinkedList<String>(Arrays.asList(produIds)); 20 if(list.contains(id)) { 21 list.remove(id); 22 } 23 else if(list.size()>=3){ 24 list.removeLast(); 25 } 26 27 list.addFirst(id); 28 29 StringBuilder sb = new StringBuilder(); 30 for(String sId :list) { 31 sb.append(sId+"_"); 32 } 33 sb.deleteCharAt(sb.length()-1); 34 cookie.setValue(sb.toString()); 35 System.out.println(cookie.getValue()); 36 return cookie; 37 }
我們在瀏覽器中進行首次訪問:
隨便點擊個連接,可以看到該商品的詳細信息(其實瀏覽器也偷偷將該商品的id以cookie傳回了瀏覽器):
我們訪問商品顯示頁面再次【刷新】就可以看到剛才瀏覽過的商品了: