重学JavaWeb —— Servlet,简单全面一发入魂
文章目錄
- Servlet
 - 概述
 - 基本使用
 - 兩個(gè)重要對象
 - 請求轉(zhuǎn)發(fā)
 - 會(huì)話技術(shù)
 - Cookie
 - Session
 - 對比小結(jié)
 
- 其它相關(guān)對象
 - ServletContext
 - ServletConfig
 
- 過濾器
 - 概述
 - 使用
 - 配置參數(shù)
 - 注意
 - 應(yīng)用場景
 
- 監(jiān)聽器
 
Servlet
概述
狹義地說,Servlet就是定義在JavaEE規(guī)范中的一個(gè)接口,javax.servlet.Servlet,參見JavaEE的API文檔
package javax.servlet;import java.io.IOException;public interface Servlet {void init(ServletConfig config) throws ServletException;ServletConfig getServletConfig();void service(ServletRequest req, ServletResponse res) throws ServletException, IOException;String getServletInfo();void destroy(); }Servlet接口定義的是一套處理網(wǎng)絡(luò)請求的規(guī)范。一個(gè)實(shí)現(xiàn)了Servlet接口的類(下面簡稱Servlet類),表明它是一個(gè)可以處理網(wǎng)絡(luò)請求的類。Servlet類需要實(shí)現(xiàn)Servlet接口中的5個(gè)方法,其中最主要的3個(gè)是:
- init()
 - destroy()
 - service()
 
它們分別定義了,一個(gè)Servlet類,
- 在初始化時(shí)需要干什么
 - 在銷毀時(shí)需要干什么
 - 在每次接收到請求時(shí),要作何處理
 
實(shí)現(xiàn)了Servlet接口的類,就能處理網(wǎng)絡(luò)請求了嗎?
顯然沒這么簡單。
我們只知道,一個(gè)Servlet類在處理請求時(shí),執(zhí)行的是它的service()方法,但service()方法是由誰調(diào)用的,方法入?yún)⒅械膔equest和response對象又是怎么來的,這顯然不是Servlet類自己做的,而且Servlet中也沒有諸如監(jiān)聽網(wǎng)絡(luò)端口等和網(wǎng)絡(luò)請求有關(guān)的操作。
所以,Servlet類要處理網(wǎng)絡(luò)請求,還需要依賴一個(gè)東西——Servlet容器。這個(gè)容器要負(fù)責(zé)監(jiān)聽網(wǎng)絡(luò)端口,封裝網(wǎng)絡(luò)請求數(shù)據(jù),管理眾多的Servlet類,進(jìn)行請求的調(diào)度分發(fā),返回響應(yīng)數(shù)據(jù)等。最典型的Servlet容器就是Tomcat。
Servlet容器和Servlet類是如何協(xié)作處理一個(gè)HTTP請求的?
1. Servlet容器監(jiān)聽網(wǎng)絡(luò)端口,接收到HTTP請求 2. 根據(jù)請求的url, 查找對應(yīng)的Servlet類 3. 若對應(yīng)的Servlet類不在容器中,則檢索并創(chuàng)建該Servlet實(shí)例,并初始化(調(diào)Servlet的init方法) 4. 容器構(gòu)建該次請求的request和response對象, 調(diào)用對應(yīng)Servlet的service方法,傳入這2個(gè)參數(shù) 5. Servlet類執(zhí)行service方法, 對請求進(jìn)行處理,并設(shè)置響應(yīng)數(shù)據(jù)到response對象中 6. 容器取出response對象中的響應(yīng)數(shù)據(jù),封裝好HTTP響應(yīng)報(bào)文,返回給客戶端我們可以看到,在HTTP請求的整個(gè)處理過程中,干活兒最多的,其實(shí)是Servlet容器。Servlet容器把處理請求的整個(gè)流程,框架,都搭好了,只是在中間留了很小的一部分空間,給Servlet類。所以,Servlet類負(fù)責(zé)的,其實(shí)只是整個(gè)流程中很小的一環(huán)。
不過,這也是因?yàn)?#xff0c;在處理網(wǎng)絡(luò)請求時(shí),諸如監(jiān)聽網(wǎng)絡(luò)端口,封裝請求數(shù)據(jù),響應(yīng)請求數(shù)據(jù),這些操作,都是共性的。也就是說,所有的處理網(wǎng)絡(luò)請求的程序,都必須有這些相同的步驟,而唯一不同的地方,只是在于對請求的業(yè)務(wù)處理。于是就把這些相同的地方抽取出來,做成了Servlet容器,不同的地方,交給各個(gè)Servlet類。這也體現(xiàn)了編程領(lǐng)域中,抽取共性,封裝變化的設(shè)計(jì)理念。試想,若沒有Servlet容器,則每開發(fā)一個(gè)網(wǎng)絡(luò)程序,都要自己完整實(shí)現(xiàn)一套處理網(wǎng)絡(luò)請求和響應(yīng)的流程,那開發(fā)的門檻就太高了,開發(fā)量也很大,并且都是重復(fù)的工作。
廣義地講,Servlet是一套規(guī)范體系,不僅僅是javax.servlet.Servlet接口,而是javax.servlet包下定義的全部規(guī)范。主要包括了JavaWeb三大組件,Servlet,Filter,Listener,以及其他一系列相關(guān)的接口。
下面,對Servlet的規(guī)范體系進(jìn)行非常簡要的歸納總結(jié)。
基本使用
構(gòu)建一個(gè)最基本的web程序,只需要有Servlet類(實(shí)現(xiàn)了javax.servlet.Servlet接口的類)即可
Servlet配置參數(shù):
- urlPattern:該Servlet處理的url
 - initParams:name - value,鍵值對形式,可通過ServletConfig獲取
 - loadOnStartup:值為一個(gè)整數(shù)。若是0或正數(shù),容器啟動(dòng)時(shí)就立刻加載Servlet,數(shù)字越小越先加載。若是負(fù)數(shù),容器可自由選擇何時(shí)加載(一般會(huì)懶加載,即用到該Servlet時(shí)才加載)
 
Servlet生命周期:
- init():容器加載Servlet實(shí)例,進(jìn)行初始化時(shí),調(diào)用執(zhí)行Servlet的init()
 - destroy():容器銷毀Servlet實(shí)例時(shí),調(diào)用執(zhí)行Servlet的destroy()
 - service():容器將請求交由一個(gè)Servlet處理時(shí),調(diào)用執(zhí)行Servlet的service()
 
javax.servlet.Servlet只是最原始的接口規(guī)范,javax.servlet包下還定義了一些基本的抽象子類,如javax.servlet.GenericServlet,javax.servlet.http.HttpServlet,類的層次結(jié)構(gòu)圖如下
由于構(gòu)建的web應(yīng)用幾乎都是基于HTTP協(xié)議的,我們創(chuàng)建一個(gè)Servlet時(shí),通常只需要繼承HttpServlet,由于最常用的HTTP方法是GET和POST,通常我們只需要重寫doGet和doPost方法即可。本文下面的內(nèi)容,默認(rèn)基于HTTP協(xié)議。
兩個(gè)重要對象
在Servlet處理請求時(shí),有兩個(gè)最重要的對象——request和response。前者封裝了HTTP請求數(shù)據(jù),后者封裝了HTTP響應(yīng)數(shù)據(jù)。這兩個(gè)對象分別是HttpServletRequest和HttpServletResponse類的實(shí)例。對于request,主要的操作是get,因?yàn)樾枰獜钠渲蝎@取請求數(shù)據(jù),對于response,主要的操作是set,因?yàn)樾枰锩嬖O(shè)置響應(yīng)數(shù)據(jù)
它們中的常用方法下面列舉一二
HttpServletRequest:
- getRequestURL:獲取請求URL
 - getMethod:獲取請求方法
 - getParameter(String s):獲取請求參數(shù)
 - getHeader(String s):獲取請求頭
 - getInputStream() / getReader():獲取請求體
 - …
 
HttpServletResponse
- addHeader(String k, String v):添加響應(yīng)頭
 - getOutputStream() / getWriter():獲取輸出流,以便往響應(yīng)體里寫入數(shù)據(jù)
 - setStatus(int sc):設(shè)置HTTP響應(yīng)狀態(tài)碼
 - sendRedirect(String s):設(shè)置重定向
 - …
 
請求轉(zhuǎn)發(fā)
當(dāng)一個(gè)請求,在一個(gè)Servlet中不能完成處理,需要進(jìn)行請求轉(zhuǎn)發(fā)時(shí),有2種轉(zhuǎn)發(fā)方式
-  
服務(wù)端轉(zhuǎn)發(fā)
通過request.getRequestDispatcher("/xx").forward(request,response)
轉(zhuǎn)發(fā)到能處理/xx這種url的Servlet。發(fā)生在服務(wù)端內(nèi)部,本質(zhì)相當(dāng)于方法調(diào)用,只能轉(zhuǎn)發(fā)到服務(wù)端內(nèi)部的資源。
 -  
客戶端轉(zhuǎn)發(fā)
通過設(shè)置HTTP狀態(tài)碼為3xx(一般設(shè)置為302),并在HTTP響應(yīng)頭添加Location指定重定向的url地址,瀏覽器收到3xx的HTTP響應(yīng),會(huì)自動(dòng)重定向到Location頭部指定的url地址。實(shí)際發(fā)生了2次HTTP請求,可跳轉(zhuǎn)到任意url。可以調(diào)用response對象的sendRedirect方法完成。
 
會(huì)話技術(shù)
眾所周知,HTTP是無狀態(tài)的協(xié)議,即每組HTTP請求/響應(yīng),都是互相獨(dú)立的,HTTP協(xié)議本身不具備記憶能力。但有的場景需要在多次HTTP請求之間維護(hù)一些狀態(tài)信息,此時(shí),就輪到會(huì)話技術(shù)登場了。根據(jù)狀態(tài)信息是保存在服務(wù)端,還是客戶端,會(huì)話技術(shù)分為了2種:Cookie和Session
Cookie
cookie是HTTP協(xié)議的擴(kuò)展標(biāo)準(zhǔn)。本質(zhì)就是一個(gè)簡單的k-v鍵值對。當(dāng)瀏覽器收到的響應(yīng)報(bào)文中,包含了Set-Cookie頭部時(shí),會(huì)將該頭部中的k-v鍵值對,保存在瀏覽器端(默認(rèn)保存在瀏覽器內(nèi)存,在瀏覽器關(guān)閉后失效),下次再發(fā)起請求時(shí),會(huì)自動(dòng)添加Cookie頭部,攜帶先前保存下來的k-v鍵值對。
在Servlet中,通過response對象來向?yàn)g覽器發(fā)送一個(gè)cookie,具體操作如下
Cookie cookie = new Cookie("userId","123"); response.addCookie(cookie);HTTP響應(yīng)報(bào)文如下(簡化)
HTTP/1.1 200 OK Set-Cookie: userId=123下次請求時(shí)的HTTP請求報(bào)文如下(簡化)
GET / HTTP/1.1 Host: www.baidu.com Cookie: userId=123在Servlet中,通過request對象獲取請求中攜帶的cookie,具體操作如下
Cookie[] cookies = request.getCookies(); // 獲取請求攜帶的全部Cookie for(Cookie c : cookies) {String name = c.getName();String value = c.getValue(); }k-v鍵值對,是cookie最基本的信息。除此之外,cookie還可以設(shè)置以下屬性
- domain:設(shè)置允許攜帶cookie的域
 - path:設(shè)置允許攜帶cookie的資源路徑
 - maxAge:設(shè)置cookie最大存活時(shí)間,單位秒(若不設(shè)置,cookie默認(rèn)在瀏覽器關(guān)閉時(shí)失效)
 - httpOnly:設(shè)為true,可避免JS腳本竊取cookie
 - secure:設(shè)為true,則只有使用HTTPS等安全的協(xié)議時(shí),才攜帶Cookie
 
另:Cookie,翻譯過來是小餅干的意思,這樣的命名也意味著,它只能保存少量的信息(簡單的k-v鍵值對),且瀏覽器一般對cookie的大小,數(shù)量等都有所限制,而由于將信息保存在了瀏覽器端,也就意味著信息容易遭到竊取或篡改,不夠安全。
于是,將信息保存在服務(wù)端的Session技術(shù),輪到它登場了。
Session
session是依賴于cookie的。它的大概原理是:一次請求到來,服務(wù)器端在處理請求時(shí),生成一個(gè)session對象(保存在服務(wù)端的內(nèi)存中),以及一個(gè)對應(yīng)的sessionId。當(dāng)服務(wù)端處理完畢,返回響應(yīng)時(shí),在HTTP響應(yīng)報(bào)文添加這樣的頭部Set-Cookie:JSESSIONID=123。瀏覽器端保存下這個(gè)name為JSESSIONID的cookie,在下次請求時(shí),攜帶這個(gè)cookie。服務(wù)端根據(jù)JSESSIONID,找到自己內(nèi)存中對應(yīng)的session對象,這樣,即可在多個(gè)HTTP請求之間共享數(shù)據(jù)。在tomcat的配置中,session的默認(rèn)有效時(shí)間為30分鐘。session對象中也可以添加k-v鍵值對,但值不僅僅是字符串,還可以是任意的Object對象,這就比cookie能攜帶的信息要大得多了。且session對象的大小,理論上只會(huì)受到服務(wù)器內(nèi)存大小的限制。且由于存在服務(wù)端,安全性相對就高了許多。
在Servlet中,一個(gè)session對象是一個(gè)HttpSession類的實(shí)例,通過request對象來生成或獲取一個(gè)session對象,如下
// 1. 若此次請求中沒有攜帶name為JSESSIONID的cookie, 則getSession會(huì)新創(chuàng)建一個(gè)session對象 // 并把對應(yīng)的id添加到響應(yīng)頭 // 2. 若此次請求中有攜帶name為JSESSIONID的cookie, 則getSession會(huì)根據(jù)這個(gè)id的值 // 從內(nèi)存中找到該id對應(yīng)的session對象 HttpSession session = request.getSession();session對象中常用的方法:
- setAttribute(String s, Object o):往session對象中添加一組鍵值對數(shù)據(jù)
 - getAttribue(String s)
 - setMaxInactiveInterval(int i):設(shè)置session對象最大有效時(shí)長,單位秒。(負(fù)數(shù)表示永不失效)
 - invalidate():使該session對象失效
 
另:
對比小結(jié)
| 數(shù)據(jù)存儲(chǔ)位置 | 客戶端(瀏覽器端) | 服務(wù)端 | 
| 限制 | 少量數(shù)據(jù),簡單的字符串鍵值對 | 可存很多數(shù)據(jù),任意Object類型 | 
| 有效性 | 默認(rèn)在瀏覽器關(guān)閉后失效 | 默認(rèn)有效時(shí)長30分鐘(tomcat中) | 
| 安全性 | 低 | 高 | 
其它相關(guān)對象
上面的會(huì)話技術(shù),是在同一用戶的多次請求之間進(jìn)行數(shù)據(jù)共享。若要在整個(gè)web應(yīng)用中共享數(shù)據(jù),則可以通過ServletContext對象實(shí)現(xiàn)。
ServletContext
一個(gè)Web應(yīng)用,對應(yīng)一個(gè)ServletContext對象,根據(jù)命名也能看出,這是整個(gè)web應(yīng)用的上下文環(huán)境。這個(gè)對象中的常用方法,下面列舉一二
- setAttribue(String s, Object o):添加一組鍵值對數(shù)據(jù)
 - getAttribe(String s)
 - getInitParameter(String s):獲取web應(yīng)用的全局初始化參數(shù)(web.xml中的<context-param>標(biāo)簽)
 - …
 
小應(yīng)用:統(tǒng)計(jì)網(wǎng)站訪問量。每收到一個(gè)請求,就對ServletContext中的一個(gè)屬性進(jìn)行累加操作
ServletConfig
ServletConfig對象用于向Servlet傳遞一些參數(shù),以便在Servlet初始化時(shí)使用。它主要包含了如下方法
- getServletContext():獲取web應(yīng)用全局上下文
 - getInitParameter(String s):獲取Servlet初始化參數(shù)(web.xml中的<init-param>標(biāo)簽,也可通過@WebInitParam注解進(jìn)行設(shè)置)
 
過濾器
概述
Servlet,Filter,Listener并稱JavaWeb三大組件。
其中的Filter,指的就是javax.servlet.Filter接口。它可以在請求被處理(請求的資源可以是Servlet,也可以是HTML等靜態(tài)資源)的之前,之后,進(jìn)行一些額外的操作。
它的工作流程如下圖所示
過濾器可以配置不止一個(gè),當(dāng)有多個(gè)過濾器時(shí),它們就形成了一個(gè)鏈,如下圖
與Servlet的生命周期類似,Filter也有如下3個(gè)方法
- init():過濾器被創(chuàng)建時(shí),該方法被調(diào)用
 - destroy():過濾器被銷毀時(shí),該方法被調(diào)用
 - doFilter():過濾器對請求/響應(yīng)進(jìn)行過濾處理時(shí),該方法被調(diào)用
 
Filter相關(guān)的一共有3個(gè)類
- javax.servlet.Filter:核心接口
 - javax.servlet.FilterConfig:過濾器配置接口,可以通過該接口給過濾器傳遞一些初始化參數(shù)
 - javax.servlet.FilterChain:過濾器鏈接口
 
使用
創(chuàng)建一個(gè)過濾器的步驟如下
新建一個(gè)類,實(shí)現(xiàn)javax.servlet.Filter接口
重寫init(),destroy(),doFilter()方法
配置過濾器
-  
xml方式:web.xml
<filter><filter-name>myFilter</filter-name><filter-class>filter.LogFilter</filter-class></filter><filter-mapping><filter-name>myFilter</filter-name><url-pattern>/*</url-pattern></filter-mapping> -  
注解方式:@WebFilter
@WebFilter(servletNames = {"ElServlet"}) public class LogFilter implements Filter {// 代碼略 } 
配置參數(shù)
只需要配置過濾器在何時(shí)起作用即可,可以指定其urlPattern,對滿足某一格式的url進(jìn)行攔截;也可以指定servletNames,對指定的Servlet進(jìn)行攔截(二者選其一即可)
-  
urlPattern
 -  
servletNames
 
如下
@WebFilter(servletNames = {"ElServlet"}) public class LogFilter implements Filter {// 代碼略 } @WebFilter(urlPatterns = "/*") public class LogFilter implements Filter {// 代碼略 }注意
由于攔截某一請求的過濾器,可能有多個(gè),這就形成一條過濾器鏈,當(dāng)一個(gè)過濾器處理完畢后,應(yīng)該調(diào)用鏈上的下一個(gè)過濾器,或者直接進(jìn)入到資源處理(當(dāng)該過濾器是鏈中的最后一個(gè)時(shí))。
所以,在某個(gè)過濾器的doFilter()方法中,完成了處理后,應(yīng)該調(diào)用FilterChain的doFilter()方法,將請求/響應(yīng)在過濾器鏈上傳遞下去(若不調(diào)用,則請求不會(huì)到達(dá)最終的web資源)。在調(diào)用FilterChain的doFilter()方法之前,請求還未被處理,在其后,請求已完成處理。這就對應(yīng)了先前說的,Filter可以在請求被處理的之前,之后,進(jìn)行一些額外的操作。
@WebFilter(urlPatterns = "/*") public class LogFilter implements Filter {public void destroy() {}public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws ServletException, IOException {System.out.println("before servlet");chain.doFilter(req, resp);System.out.println("after servlet");}public void init(FilterConfig config) throws ServletException {} }應(yīng)用場景
Filter的常見應(yīng)用場景列舉如下
-  
進(jìn)行日志記錄(記錄被調(diào)用的接口,請求參數(shù),以便進(jìn)行問題排查,數(shù)據(jù)統(tǒng)計(jì)等工作)
 -  
統(tǒng)一設(shè)置編碼格式(如處理中文亂碼問題等)
 -  
過濾敏感詞匯
 -  
數(shù)據(jù)壓縮
 -  
數(shù)據(jù)加密
 -  
身份認(rèn)證
 
監(jiān)聽器
JavaWeb三大組件的最后一個(gè),Listener。
用于監(jiān)聽一些重要事件的發(fā)生。以便在事件發(fā)生時(shí),能夠做一些額外操作。
Servlet API中針對如下對象,提供了對應(yīng)的監(jiān)聽器接口
-  
ServletContext對象
ServletContextListener,ServletContextAttributeListener
 -  
HttpSession對象
HttpSessionListener,HttpSessionAttributeListener,HttpSessionIdListener
 -  
ServletRequest對象
ServletRequestListener,ServletRequestAttributeListener
 
比如,可以監(jiān)聽HttpSession對象的創(chuàng)建和銷毀,來實(shí)現(xiàn)一個(gè)網(wǎng)站在線人數(shù)統(tǒng)計(jì)的功能(若用戶請求的是一個(gè)JSP頁面,JSP頁面在默認(rèn)情況下,會(huì)為每一個(gè)新的請求創(chuàng)建一個(gè)session對象,這是下面進(jìn)行人數(shù)統(tǒng)計(jì)的基礎(chǔ),這個(gè)默認(rèn)行為,可以通過JSP指令<%@ page session="false" %>來取消)
import javax.servlet.ServletContext; import javax.servlet.annotation.WebListener; import javax.servlet.http.HttpSession; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import java.time.LocalDateTime;/*** 統(tǒng)計(jì)網(wǎng)線在線人數(shù)* Session創(chuàng)建, 就全局人數(shù)+1, Session銷毀, 就全局人數(shù)-1* **/ @WebListener // 加上注解后,就不必在web.xml中進(jìn)行配置 public class OnLineCounter implements HttpSessionListener {private final String COUNT = "count";@Overridepublic void sessionCreated(HttpSessionEvent httpSessionEvent) {HttpSession session = httpSessionEvent.getSession();System.out.println(LocalDateTime.now() + ", 新來了一個(gè)人, id = " + session.getId());ServletContext servletContext = session.getServletContext();Object o = servletContext.getAttribute(COUNT);int count = o == null ? 0 : (int) o;servletContext.setAttribute(COUNT, ++count);}@Overridepublic void sessionDestroyed(HttpSessionEvent httpSessionEvent) {HttpSession session = httpSessionEvent.getSession();System.out.println(LocalDateTime.now() + ", 走了一個(gè)人, id = " + session.getId());ServletContext servletContext = session.getServletContext();int count = (int) servletContext.getAttribute(COUNT);servletContext.setAttribute(COUNT, --count);} }總結(jié)
以上是生活随笔為你收集整理的重学JavaWeb —— Servlet,简单全面一发入魂的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
                            
                        - 上一篇: ssh免密码登录全过程
 - 下一篇: Composer -- PHP依赖管理的