標籤:
ServletContext
提兩個問題:
1.大家在訪問某個網站的時候,往往都會看到網站的首頁面顯示您是第幾位瀏覽者(網站計數器),這是怎麼實現的?
2.我們在訪問某個bbs網站的時候,往往會顯示有多少人線上,這是怎麼實現的?
可能我們會想到的常規實現思路:資料庫或者檔案。這種做法比較簡單,但是卻會對資料庫或者檔案訪問過於頻繁,開銷比較大。
解決之道是用ServletContext
什麼是ServletContext
要理解ServletContext就必須和Cookie、Session做一個對比,如:
你可以把它想象成一個公用的空間,可以被所有的客戶訪問,也就是說A用戶端可以訪問D,B用戶端可以訪問D,C用戶端也可以訪問D。
-
WEB容器在啟動時,它會為每個Web應用程式都建立一個對應的ServletContext,它代表當前Web應用。並且它被所有用戶端共用。
-
ServletContext對象可以通過ServletConfig.getServletContext()方法獲得對ServletContext對象的引用,也可以通過this.getServletContext()方法獲得其對象的引用。
-
由於一個WEB應用中的所有Servlet共用同一個ServletContext對象,因此Servlet對象之間可以通過ServletContext對象來實現通訊。ServletContext對象通常也被稱之為context域對象。公開的聊天室就會用到它。
-
當web應用關閉、Tomcat關閉或者Web應用reload的時候,ServletContext對象會被銷毀
怎麼使用ServletContext
使用ServletContext:
(1) 如何得到ServletContext對象
this.getServletContext();
this.getServletConfig().getServletContext();
(2) 你可以把它想象成一張表,這個和Session非常相似:每一行就是一個屬性,如下:
- 添加屬性:
setAttribute(String name, Object obj);
- 得到值:
getAttribute(String name),這個方法返回Object
- 刪除屬性:
removeAttribute(String name)
(3) 生命週期
ServletContext中的屬性的生命週期從建立開始,到伺服器關閉結束。
一個快速入門的案例:
我們建立Servlet1和Servlet2,Servlet1用於在ServletContext中建立屬性,Servlet2用於從ServletContext讀取屬性:
Servlet1的doGet方法為:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); PrintWriter out = response.getWriter(); // 擷取ServletContext對象的引用 // 第一種方法 ServletContext servletContext = this.getServletContext(); // 第二種方法 // ServletContext servletContext2 = this.getServletConfig().getServletContext(); servletContext.setAttribute("name", "小明"); out.println("將 name=小明 寫入了ServletContext");}
Servlet2的doGet方法為:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); PrintWriter out = response.getWriter(); // 取出ServletContext的某個屬性 //1.首先擷取到ServletContext ServletContext servletContext = this.getServletContext(); //2.取出屬性 String name = (String)servletContext.getAttribute("name"); out.println("name="+name);}
以此訪問Servlet1和Servlet2,我們可以分別看到輸出如下:
運行結果似乎和Session,Cookie的應用沒什麼不同。其實看似相同,實則完全不一樣。只要我們不關閉Tomcat或者reload該應用,這時候我們關閉當前的瀏覽器,或者是換一個瀏覽器,假設我們從Chrome換到IE再次訪問Servlet2,依然可以看到結果!這就是它們最大的不同了,因為ServletContext是存在於伺服器記憶體中的一個公用空間,它可以供所有的使用者用戶端訪問。
ServletContext應用
(1)多個Servlet通過ServletContext對象實現資料共用
這個很好理解,類似於Session,我們也可以通過ServletContext對象來共用資料,但要注意的是,Session只能在一個用戶端共用資料,它獨佔一個用戶端。而ServletContext中的資料是可以供所有用戶端共用的。
(2)實現Servlet的請求轉寄
之前我們學過的請求轉寄是通過request對象的:
request.getRequestDispatcher("/url").forward(request, response);
這裡要說明的是,ServletContext也可以實現請求轉寄:
this.getServletContext().getRequestDispatcher("/url").forward(request, response);
這兩個轉寄效果是一樣的。
(3)擷取Web應用的初始化參數
在【Servlet——開發細節+ServletConfig對象】中,我們介紹過在Servlet部署的時候,我們可以使用一個或多個<init-param>標籤為servlet配置一些初始化參數,然後我們通過ServletConfig對象擷取這些參數,假如有如下的MyServlet,它的配置為:
<servlet> <servlet-name>MyServlet</servlet-name> <servlet-class>com.gavin.servlet.MyServlet</servlet-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </servlet>
可以看到它配置了一個初始化參數:encoding=utf-8,那麼我們在MyServlet的原始碼中需要這樣去得到這個參數:
String encoding = this.getServletConfig().getInitParameter("encoding");
上述的參數配置方法只針對一個特定的Servlet有效,現在我們可以通過ServletContext來擷取全域的、整個Web應用的初始化參數,全域的初始化參數是這樣配置在web.xml檔案中的:
<!-- 如果希望所有的Servlet都可以使用該配置,則必須這麼做 --><context-param> <param-name>name</param-name> <param-value>gavin</param-value></context-param>
然後我們可以在任意一個Servlet中使用ServletContext擷取這個參數:
String name = this.getServletContext().getInitParameter("name");
(4)利用ServletContext對象讀取資源檔(比如properties檔案)
讀取資源檔要根據資源檔所在的位置分為兩種情況:
(1)檔案在WebRoot檔案夾下,即我們的Web應用的根目錄下。這時候我們可以使用ServletContext來讀取該資源檔。
假設我們Web根目錄下有一個設定資料庫資訊的dbinfo.properties檔案,裡面配置了name和password屬性,這時候可以通過ServletContext去讀取這個檔案:
// 這種方法的預設讀取路徑就是Web應用的根目錄InputStream stream = this.getServletContext().getResourceAsStream("dbinfo.properties");// 建立屬性對象Properties properties = new Properties();properties.load(stream);String name = properties.getProperty("name");String password = properties.getProperty("password");out.println("name="+name+";password="+password);
(2)但是如果這個檔案放在了src目錄下,通過ServletContext是讀不到的,必須要使用類載入器去讀取。
// 類載入器的預設讀取路徑是src根目錄InputStream stream = MyServlet.class.getClassLoader().getResourceAsStream("dbinfo.properties")
但是如果這個檔案此時還沒有直接在src目錄下,而是在src目錄下的某個包下,比如在com.gavin包下,此時類載入器要加上包的路徑,如下:
InputStream stream = MyServlet.class.getClassLoader().getResourceAsStream("com/gavin/dbinfo.properties")
另外,補充一點,ServletContext可以擷取檔案的全路徑,當然這個也是在Web應用根目錄下的檔案。比如我們在WebRoot檔案夾下有一個images檔案夾,images檔案夾下有一個Servlet.jpg圖片,為了得到這個圖片的全路徑,如下:
// 如何讀取到一個檔案的全路徑,這裡會得到在Tomcat的全路徑String path = this.getServletContext().getRealPath("/images/Servlet.jpg");
在網站開發中,有很多功能要使用ServletContext,比如
1. 網站計數器
2. 網站的線上使用者顯示
3. 簡單的聊天系統
總之,如果是涉及到不同使用者共用資料,而這些資料量不大,同時又不希望寫入資料庫中,我們就可以考慮使用ServletContext實現。
實際案例-網站計數器
在網站建設中,經常會統計某個網頁被瀏覽的次數,那麼這個網站計數器是怎麼實現的?
怎樣才算是一次有效點擊?各個網站有不同的標準:
1. 只要訪問過該網頁,就算是一次,重新整理一次也算,當然這是最簡單的。不過這有點虛假的成分。
2. 不同的IP訪問該網頁,算一次有效點擊;如果是同一個IP在一定時間(比如一天),不管瀏覽該網頁多少次都算一次
3. 使用者退出網站,再次訪問也算一次
現在我們採用第3種來實現這個簡單的案例。
首先,我們編寫3個Servlet,Login、LoginCl和Manager,分別是登入表單,登入處理以及管理首頁面,我們在使用者登入成功之後將存在於ServletContext中的計數器加1,然後請求重新導向到Manager頁面,Manager頁面顯示網站的總訪問次數。
Login頁面的代碼很簡單,這裡不再展示。LoginCl中我們根據使用者的密碼是否是“123”來判斷,如下:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); String passwd = request.getParameter("password"); if ("123".equals(passwd)) { // 合法 // 向ServletContext添加屬性 ServletContext servletContext = this.getServletContext(); int nums = 0; try { nums = Integer.parseInt((String) servletContext.getAttribute("nums")); } catch (Exception e) { nums = 0; } nums += 1; servletContext.setAttribute("nums", String.valueOf(nums)); // request.getRequestDispatcher("/Manager").forward(request,response); response.sendRedirect("/Counter/Manager"); } else { // 非法 }}
在Manager頁面中取出nums這個計數值,如下:
public void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { response.setContentType("text/html;charset=utf-8"); PrintWriter out = response.getWriter(); out.println("<h1>管理頁面</h1>"); ServletContext servletContext = this.getServletContext(); String nums = (String) servletContext.getAttribute("nums"); out.println("該管理頁面被訪問了"+nums+"次");}
【1.注意點】
我們為什麼從LoginCl到Manager要用請求重新導向sendRedirect而不用請求轉寄,這是因為當請求轉寄到Manager頁
面之後,如果我們再次重新整理Manger頁面,相應的表單依然會提交一次。也就是說,我們沒有退出重新登入,而只是在
Manager頁面重新整理就會讓網站計數器nums的值加1。這顯然是不符合要求的。
【2.存在問題】
問題:當伺服器關閉後,我們的計數器就被清空了,那麼如何才能夠保證計數器的穩定增長呢?
解決:在伺服器關閉之前,將存在於ServletContext中的計數值儲存於檔案之中,等下次伺服器開啟的時候再從檔案中讀出這個計數值,放入ServletContext中。
那麼很自然的,我們想到了怎麼才能在伺服器開啟和伺服器關閉的時候去做這些工作呢?—> 要記得Servlet的生命週期,記得兩個自動調用的方法init()和destroy()。
其中init()在這個Servlet初始化的時候調用,而destroy()方法在這個Servlet被銷毀之前調用。那麼我們就可以利用這兩個方法來讀取和儲存我們的計數值。假設這個計數值儲存在Web應用根目錄下的record.txt檔案中。
於是我們編寫InitServlet代碼如下:
public class InitServlet extends HttpServlet { public void destroy() { // 把ServletContext的值重新儲存在檔案中 ServletContext servletContext = this.getServletContext(); String nums = (String) servletContext.getAttribute("nums"); System.out.println("destroy.nums:"+nums); String path = servletContext.getRealPath("record.txt"); FileWriter fileWriter = null; BufferedWriter bw = null; try { fileWriter = new FileWriter(path); bw = new BufferedWriter(fileWriter); bw.write(nums); bw.flush(); } catch (Exception e) { e.printStackTrace(); }finally{ try { bw.close(); fileWriter.close(); } catch (IOException e) { e.printStackTrace(); } } } // 初始化方法 public void init() throws ServletException { ServletContext servletContext = this.getServletContext(); // 1.首先得到該檔案真實路徑 String path = servletContext.getRealPath("record.txt"); // 2.開啟檔案 FileReader fileReader = null; BufferedReader br = null; try { fileReader = new FileReader(path); br = new BufferedReader(fileReader); String nums = br.readLine(); // 把nums添加到ServletContext servletContext.setAttribute("nums", nums); } catch (Exception e) { e.printStackTrace(); } finally { // 關閉流,後開啟先關閉 try { br.close(); fileReader.close(); } catch (IOException e) { e.printStackTrace(); } } }}
然後,我們還要讓這個InitServlet自動裝載到伺服器的記憶體,所以要配置load-on-startup元素,讓其自啟動:
<servlet> <servlet-name>InitServlet</servlet-name> <servlet-class>com.gavin.InitServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet>
最後,因為我們在每次伺服器啟動的時候自動裝載了ServletContext,添加了nums屬性,所以相應的LoginCl的代碼也要修改,很簡單,這裡就不再贅述
【最後的使用原則】
因為存在ServletContext中的資料會長時間儲存在伺服器,會佔用記憶體,因此我們建議不要向ServletContext中添加過大的資料!
Servlet——ServletContext 對象