目錄
- 前言
- 1. Servlet 簡介
- 2. Servlet 前世今生
- 3. Servlet 執行流程
- 4. Servlet 快速入門
- 5. 兩種配置 Servlet程序 URL的方式
- 5.1 使用 注解來配置 Servlet程序 的 URL
- 5.1.1 urlPattern 的配置規則
- 精確匹配
- 目錄匹配:使用 * 符號代表任意路徑
- 擴展名匹配
- 任意匹配
- 5.1.2 小結
- 5.2 使用 XML文件來配置 Servlet程序 的 URL
- 6. Servlet 接口詳解
- 6.1 ServletConfig 類
- 6.2 ServletRequest 和 ServletResponse 類
- 6.3 Servlet 接口中的方法
- 7. Servlet 體系結構
- 7.1 GenericServlet 抽象類
- 7.2 HttpServlet 抽象類
- 8. Servlet 生命周期
- 8.1 生命周期與三個重要方法的對應
- 8.2 生命周期的演示
- 8.3 Servlet 的初始化時機
- 9. Servlet 線程不安全問題
- 10. 小結一下 Tomcat 和 Servlet的執行流程
前言
博主將用 CSDN 記錄 Java 后端開發學習之路上的經驗,并將自己整理的編程經驗和知識分享出來,希望能幫助到有需要的小伙伴。
博主也希望和一直在堅持努力學習的小伙伴們共勉!唯有努力鉆研,多思考勤動手,方能在編程道路上行至所向。
由于博主技術知識有限,博文中難免會有出錯的地方,還望各位大佬包涵并批評指正,博主會及時改正;如果本文對小伙伴你有幫助的話,求求給博主一個贊支持一下,可以一起交流,一起加油!!
本文是博主在學習B站尚硅谷的JavaWeb網課時整理的學習筆記,在這里感謝其優質網課,如果有興趣的小伙伴也可以去看看。
本文編寫過程中,參考了以下幾位 csdn博主的博客,寫的非常好,有興趣的小伙伴也可以去看看。
Servlet是什么
JavaWeb(專欄)
(尚硅谷)JavaWeb新版教程03-Tomcat-Servlet
1. Servlet 簡介

-
Servlet是 JavaWeb最為核心的內容,它是 Java提供的一門動態Web資源開發技術。
-
使用Servlet就可以實現,根據不同的用戶在頁面上動態顯示不同內容。
-
Servlet是
JavaEE
規范之一,其實本質上就是一個接口,將來我們需要自己定義類來實現Servlet 接口,并由Web服務器運行實現了Servlet接口的類。
那 Servlet到底是干什么的呢?答案很簡單,接口的作用就是用于規范代碼

Servlet 接口定義的是一套處理網絡請求的規范,所有實現了 Servlet接口的類,都需要實現它的五個接口方法。
其中最主要的是兩個聲明周期的方法**init()
和destory()
,還有一個處理請求的方法service()
。也就是說,所有實現 Servlet接口的類,或者說,所有想要處理網絡請求**的類,都需要回答以下三個問題:
你初始化時要做什么?
你接收到請求時要做什么?
你銷毀時要做什么?
剛剛說 Servlet是一個規范,那實現了 Servlet接口的類,就能處理瀏覽器發送的請求了嗎?
- 答案是不能。我們不會在 Servlet實現類中寫監聽 8080端口的代碼,Servlet 實現類也不會直接和瀏覽器打交道。
那瀏覽器發送的請求是怎么來到 Servlet實現類的呢?
- 答案是使用 Servlet 容器,比如最常用的**
Tomcat
**。Servlet 實現類都是要部署在一個容器中的,不然 Servlet實現類根本不能起作用。
所以換而言之,Tomcat
才是直接與瀏覽器打交道的家伙,它負責監聽端口,當瀏覽器發送請求過來后,Tomcat
根據瀏覽器所訪問的URL
等信息,確定要將請求交給哪個 Servlet實現類去處理,然后調用那個 Servlet實現類的service()
方法,service()
方法便會返回一個響應對象,最后再由Tomcat
把這個響應對象返回給瀏覽器。
Servlet 本身在 Tomcat中是“非常被動”的一個角色,處理網絡請求與響應不是它的主要職責,它其實更偏向于業務代碼。所謂的請求和響應對象都是由 Tomcat 傳給 Servlet用來處理請求和響應的工具,但 Servlet本身不處理這些,而是另外調用其他類的方法去處理。
2. Servlet 前世今生
Tomcat
其實是 Web服務器和 Servlet容器的結合體。
-
Web服務器的所做的工作本質上是:
- 將某個主機上的資源映射為一個
URL
供外界(如:瀏覽器)訪問。
- 將某個主機上的資源映射為一個
-
那什么是 Servlet容器呢?
- Servlet容器,顧名思義里面存放的是 Servlet 實現類對象。
我們怎樣才能通過 Web服務器映射的URL
訪問到資源呢?肯定是需要創建一個Web項目來處理瀏覽器所發送的請求的,主要有三個過程:
接收請求(收到請求)
處理請求(處理請求)
響應請求(返回處理結果)
任何一個Web項目,必然包括這三個步驟。其中接收請求和響應請求是共性功能,而且沒有差異性。于是,我們就可以把接收請求和響應請求這兩個步驟抽取出來并封裝進Web服務器中。

但對于不同請求的處理功能邏輯是不同的。沒關系,我們將處理請求的過程抽取出來封裝成一個類,該類必須要實現 Servlet接口,也就是 Servlet實現類,而這個類就交給程序員自己編寫。我們把 Servlet實現類存放在 Web項目中,可以把它看作是一個資源。
當然,隨著互聯網的發展,出現了三層架構,所以一些處理邏輯又從 Servlet實現類中抽取出來,封裝到 Service
層(業務層)和Dao
層(持久層)中。

但是Servlet 并不擅長往瀏覽器輸出 HTML
頁面,所以后來就出現了 Thymeleaf
技術(后面會介紹)。
慢慢的,等Spring 家族出現后,Servlet 開始退居幕后,取而代之的是 SpringMVC
。SpringMVC
的核心組件DispacterServlet
其實本質上就是一個 Servlet接口的實現子類,但是它已經自立門戶,在原來 HttpServlet
(Servlet子類)的基礎上,又封裝了一層邏輯。本處我們不再具體展開,而是專注于講解 Servlet。
3. Servlet 執行流程
Servlet(Server Applet
,服務器小程序,是服務器的一小部分),全稱 Java Servlet
,是用 Java編寫的服務器端程序。其主要功能在于能夠交互式地瀏覽和修改數據,生成動態 Web內容。
狹義的 Servlet是指 Java語言實現的一個接口,而廣義的 Servlet是指任何實現了 Servlet接口的類。一般情況下,我們將 Servlet 理解為后者。在后面的描述中,Servlet 就代指任何實現了 Servlet接口的類。
Servlet 可以運行于所有支持 Java 的應用服務器中。從原理上講,Servlet 可以響應任何類型的請求,但絕大多數情況下Servlet 只用于基于 HTTP協議的Web服務器。
Servlet的工作流程:Servlet 由 Web服務器調用,Web服務器在收到瀏覽器對某個 Servlet的訪問請求后:
- Web服務器首先檢查是否已經裝載并創建了該Servlet的實例對象。如果是,則直接執行第④步,否則,執行第②步。
- 裝載并創建該 Servlet的一個實例對象。
- 調用 Servlet實例對象的
init()
方法。- 創建一個用于封裝 HTTP請求消息的**
HttpServletRequest
對象和一個代表HTTP響應消息的HttpServletResponse
**對象,然后調用 Servlet中的service()
方法,并將請求和響應對象作為參數傳遞進去。- Web服務器被停止或重新啟動之前,Servlet引擎將卸載所有 Servlet,并在卸載之前調用Servlet的
destroy()
方法。
最早支持 Servlet標準的是 JavaSoft
的Java Web Server
,此后,一些其它的基于 Java的Web服務器開始支持標準的 Servlet。執行流程如下圖:
以 Tomcat服務器為例,Servlet 簡單的執行流程如下:
- 瀏覽器向Tomcat服務器請求某個 Servlet 的實例對象;
- Tomcat 加載該 Servlet 類到內存中;
- Tomcat 調用
init()
方法初始化該 Servlet并實例化該Servlet對象;- Tomcat 調用該Servlet 類中的
service()
方法,service()
方法根據不同請求的方式再去調用doGet()
方法或者doPost()
方法,此外還有doHead()
、doPut()
、doTrace()
、doDelete()
、doOptions()
等方法;- 最后Tomcat 調用 Servlet中的
destroy()
方法銷毀該Servlet對象。
詳細的執行流程如下:
- 加載和實例化 Servlet。這項操作一般是動態執行的。然而,Tomcat 通常會提供一個管理的選項,用于在 Tomcat 啟動時強制裝載和初始化特定的 Servlet;
- Tomcat 創建一個 Servlet的實例;
- 第一個瀏覽器的請求到達 Tomcat;
- Tomcat 調用 Servlet 的
init()
方法(可配置為 Tomcat 創建 Servlet 實例時調用:在Web.xml
文件中<servlet>
標簽下配置<load-on-startup>
標簽,配置的值為整型,值越小 Servlet 的啟動優先級越高); - 一個瀏覽器的請求到達 Tomcat;
- Tomcat 創建一個請求對象,用于處理瀏覽器請求;
- Tomcat 創建一個響應對象,用于響應瀏覽器請求;
- Tomcat 調用 Servlet 的
service()
方法,并傳遞請求對象和響應對象作為方法的參數; service()
方法獲得關于請求對象的信息,處理請求,訪問其他資源,獲得需要的信息;service()
方法使用響應對象的方法,將響應對象傳回 Tomcat,最終Tomcat 將響應傳回瀏覽器。service()
方法還可能調用其它方法以處理請求,如doGet()
或doPost()
或者程序員自己開發的新的方法;- 對于更多的瀏覽器的相同請求,Tomcat 創建新的請求對象和響應對象,仍然激活此 Servlet 的
service()
方法,將這兩個對象作為參數傳遞給它,如此重復以上的循環。但無需再調用init()
方法。一般 Servlet 只初始化一次(只有一個對象),當 Tomcat 不再需要該 Servlet 時(一般當 Server 關閉時),Tomcat 調用 Servlet 的destroy()
方法將其銷毀。
小結:
-
Servlet就是一群人來制定 Java應用程序中使用 Web時的各種規范,統一接口,其他內部實現由廠商自己實現,Tomcat、jetty、jboss等等應運而生。
-
Web服務器習慣處理靜態資源,所以需要一個幫助程序來幫忙處理動態請求(如當前時間)。Web服務器會將動態請求轉發給幫助程序,幫助程序處理后,返回處理后的靜態結果給Web服務器。這樣就避免了Web服務器處理動態資源。所以,Servlet 的本質是一個幫助程序。 如下圖:

3.Servlet執行流程分為三個階段:init
(初始化),service
(運行),destroy
(銷毀)
下面我們試試自己創建一個Servlet 程序,并通過瀏覽器訪問該程序。
4. Servlet 快速入門
需求分析: 編寫一個Servlet類,并使用IDEA中 Tomcat插件進行部署,最終通過瀏覽器訪問所編寫的 Servlet程序。 具體的實現步驟為:
第一步:使用 Maven工具創建一個Web項目Web-demo
,并在pom.xml
文件中導入 ServletAPI
的依賴坐標。
<dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>3.1.0</version><!--此處為什么需要添加<scope>標簽?provided 指的是該依賴只在編譯和測試過程中有效,最后生成的war包時不會加入該依賴因為Tomcat的lib目錄中已經有servlet-api這個jar包,如果在生成war包的時候再加入,就會和Tomcat中的jar包沖突,導致報錯--><scope>provided</scope>
</dependency>
第二步:在 Web項目中的 main/src
文件夾下定義一個ServletDemo1
類,實現 Servlet接口,并重寫接口中所有方法,在service()
方法中輸出一句話。
import javax.servlet.*;
import java.io.IOException;public class ServletDemo1 implements Servlet {public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {System.out.println("servlet hello world~");}public void init(ServletConfig servletConfig) throws ServletException {}public ServletConfig getServletConfig() {return null;}public String getServletInfo() {return null;}public void destroy() {}
}
第三步:在ServletDemo1
類上使用@WebServlet
注解,該注解的作用是配置該 Servlet程序的訪問路徑(相當于我們給Tomcat中部署的 Web項目設置訪問路徑URL
,也就是在Application context
屬性中設置的值)。
@WebServlet("/demo1")
public class ServletDemo1 implements Servlet {...
}
第四步:啟動Tomcat,在瀏覽器中輸入URL
地址訪問該Servlet程序。
http://localhost:8080/Web-demo/demo1
第五步:瀏覽器訪問后,在IDEA 控制臺會打印出servlet hello world~
,說明ServletDemo1
程序已經成功運行并成功處理了瀏覽器請求。
我們以快速入門的這個程序為例,簡單看一下 Servlet 程序的執行流程。如下圖:
1. 瀏覽器發出http://localhost:8080/Web-demo/demo1
請求,從請求的UEL
中可以解析出三部分內容,分別是localhost:8080
、Web-demo
、demo1
。
- 根據
localhost:8080
可以找到要訪問的Tomcat Web服務器。 - 根據
Web-demo
可以找到部署在 Tomcat服務器上的Web-demo
項目。 - 根據
demo1
可以找到要訪問的是項目中的哪個 Servlet類,其根據@WebServlet
注解中設置的值進行匹配。
2. 找到ServletDemo1
這個類后,Tomcat Web服務器就會為ServletDemo1
這個類創建一個對象,然后調用對象中的service()
方法。
ServletDemo1
類實現了 Servlet接口,所以類中必然會重寫service()
方法供 Tomcat Web服務器進行調用。service()
方法中有ServletRequest
和ServletResponse
兩個形參,ServletRequest
封裝的是請求數據,ServletResponse
封裝的是響應數據,后期我們可以通過這兩個參數實現前后端的數據交互。(后面我們會詳細講解這幾個參數)
至此,Servlet 的入門案例就已經完成,大家可以按照上面的步驟進行練習了。
5. 兩種配置 Servlet程序 URL的方式
5.1 使用 注解來配置 Servlet程序 的 URL
在快速入門案例中,我們知道,一個 Servlet程序在編寫好后,要想被瀏覽器訪問到,就需要配置其訪問路徑:即urlPattern
。一個Servlet程序,可以配置多個urlPattern
,也就是說,同一個 Servlet 程序可以被映射到多個URL
路徑上,多個URL
路徑可以訪問同一個 Servlet程序。 比如:
@WebServlet(urlPatterns = {"/demo1", "/demo2"})
代碼舉例:
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;/**
* urlPattern: 一個Servlet可以配置多個訪問路徑
*/
@WebServlet(urlPatterns = {"/demo1", "/demo2"})
public class ServletDemo1 implements Servlet {public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {System.out.println("servletdemo1 & servletdemo2");}public void init(ServletConfig servletConfig) throws ServletException {}public ServletConfig getServletConfig() {return null;}public String getServletInfo() {return null;}public void destroy() {}
}
在瀏覽器上輸入:
http://localhost:8080/Web-demo/demo1
,
http://localhost:8080/Web-demo/demo2
這兩個地址都能訪問到ServletDemo1
程序。
5.1.1 urlPattern 的配置規則
精確匹配
/*** UrlPattern:* * 精確匹配*/
@WebServlet(urlPatterns = "/user/select")
public class ServletDemo3 implements Servlet {public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {System.out.println("精確訪問 Servlet");}...// 省略其他幾個方法
}
ServletDemo3
程序訪問路徑為:http://localhost:8080/Web-demo/user/select
目錄匹配:使用 * 符號代表任意路徑
/*** UrlPattern:* * 目錄匹配: /user/**/
@WebServlet(urlPatterns = "/user/*")
public class ServletDemo4 implements Servlet {public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {System.out.println("目錄訪問 Servlet");}...// 省略其他幾個方法
}
ServletDemo4
程序的訪問路徑為:http://localhost:8080/Web-demo/user/任意
思考:
- 訪問路徑
http://localhost:8080/Web-demo/user
是否能訪問到ServletDemo4
程序?- 訪問路徑
http://localhost:8080/Web-demo/user/a/b
是否能訪問到ServletDemo4
程序?- 訪問路徑
http://localhost:8080/Web-demo/user/select
是訪問到ServletDemo3
程序還是ServletDemo4
程序?
答案是:是;是;訪問
ServletDemo3
。因此我們可以得到的結論是
/user/*
中的/*
代表的是零或多個層級訪問目錄,同時精確匹配優先級要高于目錄匹配。
擴展名匹配
/*** UrlPattern:* * 擴展名匹配: *.do*/
@WebServlet(urlPatterns = "*.do")
public class ServletDemo5 implements Servlet {public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {System.out.println("擴展名匹配 Servlet");}...// 省略其他幾個方法
}
ServletDemo5
程序的訪問路徑為:http://localhost:8080/Web-demo/任意.do
注意:
- 如果路徑配置的不是擴展名方式,那么在路徑的前面就必須要加**
/
**,否則會報錯。如下:
- 如果路徑配置的是
*.do
,那么在*.do
的前面不能加上/
,即不能是"/*.do"
,否則會報錯。如下:
任意匹配
/*** UrlPattern:* * 任意匹配: /*/
@WebServlet(urlPatterns = "/")
public class ServletDemo6 implements Servlet {public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {System.out.println("擴展名匹配 Servlet");}...// 省略其他幾個方法
}
ServletDemo6
程序的訪問路徑為:http://localhost:8080/Web-demo/任意
/*** UrlPattern:* * 任意匹配: /**/
@WebServlet(urlPatterns = "/*")
public class ServletDemo7 implements Servlet {public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {System.out.println("擴展名匹配 Servlet");}...// 省略其他幾個方法
}
ServletDemo7
程序的訪問路徑為:http://localhost:8080/Web-demo/任意
注意: 路徑/
和/*
的區別?
-
當我們的項目中的 Servlet 程序的
urlpattern
配置為/
,就會覆蓋掉 Tomcat中的DefaultServlet
類,當其他所有Servlet程序的url-pattern
都匹配不上瀏覽器的某個請求URL
時,最后都會匹配這個urlpattern
配置為/
的 Servlet程序。 -
當我們的項目中配置了 Servlet 程序的
urlpattern
配置為/
,意味著該 Servlet程序可以匹配任意請求URL
,但是精確匹配仍是排在第一位的。 -
DefaultServlet
是用來處理靜態資源,如果配置了/
會把默認的DefaultServlet
類覆蓋掉,就會引發請求靜態資源的時候沒有走默認的路徑而是走了自定義的 Servlet類,最終導致靜態資源不能被訪問。
5.1.2 小結
-
urlPattern
總共有四種配置方式,分別是精確匹配、目錄匹配、擴展名匹配、任意匹配。 -
五種配置的優先級為 精確匹配 > 目錄匹配> 擴展名匹配 >
/*
>/
,無需記,以最終運行結果為準。
5.2 使用 XML文件來配置 Servlet程序 的 URL
前面對應 Servlet程序的URL
配置,我們都是使用 @WebServlet
這個注解,但 Servlet接口 從 3.0版本后才開始支持注解配置,3.0版本前只支持使用 XML文件的配置方法。
由于瀏覽器通過
URL
地址訪問 Web服務器中的 Web項目和資源,所以 Servlet 程序若想被瀏覽器訪問,就必須把 Servlet程序映射到一個URL
地址上,這個工作需要在Web.xml
文件中使用<servlet>
元素和<servlet-mapping>
元素完成。
<servlet>
元素用于注冊 Servlet,它包含有兩個主要的子元素:<servlet-name>
和<servlet-class>
,分別用于設置 Servlet的注冊名稱和 Servlet的完整類名。一個
<servlet-mapping>
元素用于映射一個已注冊的 Servlet的一個對外訪問路徑,它包含有兩個子元素:<servlet-name>
和<url-pattern>
,分別用于指定 Servlet的 注冊名稱和該 Servlet的對外訪問路徑。
對于 Web.xml文件的配置步驟有兩步:
第一步:編寫一個Servlet類(此時類上不需要加上@WebServlet
這個注解了)
public class ServletDemo8 implements Servlet {public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {System.out.println("使用XML配置文件映射 Servlet");}...// 省略其他幾個方法
}
第二步:在web.xml
文件中配置該 Servlet程序的URL
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/Web-app_4_0.xsd"version="4.0"><!-- 下面的元素用于注冊 Servlet,相當于告訴Web服務器存在一個Servlet,名稱為ServletDemo8,完整類名為...--><servlet><!-- Servlet的名稱,名字任意--><servlet-name>demo8</servlet-name><!-- Servlet的類全名--><servlet-class>com.Web.ServletDemo8</servlet-class></servlet><!-- 下面的元素用于映射一個已注冊的Servlet的對外訪問路徑,相當于告訴Web服務器,若想訪問ServletDemo8,就需要通過 /demo8 這個url路徑訪問--><servlet-mapping><!-- Servlet的名稱,要和上面的名稱一致--><servlet-name>demo8</servlet-name><!-- Servlet的訪問路徑--><url-pattern>/demo8</url-pattern></servlet-mapping>
</Web-app>
同一個 Servlet 可以被映射到多個URL
路徑上,即多個<servlet-mapping>
元素的<servlet-name>
的設置值可以是同一個 Servlet的注冊名, 但是映射的<url-pattern>
的值可以不一樣。例如:
<servlet-mapping><servlet-name>HelloServlet</servlet-name><url-pattern>/hello</url-pattern></servlet-mapping><servlet-mapping><servlet-name>HelloServlet</servlet-name><url-pattern>/hello.html</url-pattern>
</servlet-mapping>
通過上面的配置,當瀏覽器想訪問名稱是HelloServle
的 Servlet程序時,可以使用如下的幾個地址去訪問:
-
http://localhost:8080/Web-demo/hello
-
http://localhost:8080/Web-demo/hello.html
顯然,HelloServlet
程序被映射到了多個URL
上。
這種通過 XML文件配置方式和注解比起來,確認麻煩很多,所以建議大家使用注解方式來開發。但是大家也要認識 XML文件的配置方式,因為并不是所有的項目都是基于注解開發的。
6. Servlet 接口詳解
下面我們將介紹Servlet 接口中的方法以及方法中的形參。
首先查看Servlet 接口中的接口方法:
Servlet 接口中包含了五個方法,這些方法最難的地方在于,傳入方法中的形參是什么。不過幸好Tomcat
已經事先幫我們把形參對象封裝好傳入方法中了,不需要我們再去設置。
除此之外,既不需要我們自己寫 TCP連接,也不需要我們解析 HTTP請求,更不需要我們把結果轉換成 HTTP響應,ServletRequest
對象和ServletResponse
對象已經幫我們把這些難事搞定了(注:這兩個對象我們會在后面詳細介紹)。
所以,在 Servlet實現類里面主要寫的代碼都是業務邏輯,我們自己編寫的 Servlet程序和原始的、底層的解析、連接等沒有絲毫關系。最難的這些操作,Tomcat 已經幫我們封裝成形參對象傳入方法中了。
上面我們說過,Servlet 接口的作用是為了保證了方法名的規范性和一致性,若方法名不一致,Tomcat 將無法通過方法名對其實現類的方法進行調用。
因此我們自己寫的 Servlet程序,只需要實現 Servlet接口,并編寫處理瀏覽器所發送的請求的業務邏輯就好了。
總的來說,Tomcat 已經幫我們完成了底層的操作,并且傳入了三個形參,分別是:ServletConfig
、ServletRequest
、ServletResponse
。他們的解析將在下面分別介紹。
6.1 ServletConfig 類
ServletConfig
類即Servlet配置類,也就是我們在 Web.xml
文件中配置的 <servlet>
標簽中的內容。它封裝了 Servlet程序的一些參數信息,如果需要這些信息,我們可以從ServletConfig
對象中獲取。
如下圖為TestServlet
類實例化以及ServletConfig
類實例化過程:

過程解釋:
首先,Tomcat 解析
Web.xml
配置文件,在<servlet-mapping>
標簽中通過<url-pattern>
找到對應的<servlet-name>
中設置的 TestServlet。然后Tomcat再去
<servlet>
標簽中通過<servlet-name>
找到對應的<servlet-class>
中設置的TestServlet
的全類名,最后通過 Java的反射機制便可將 TestServlet 類實例化為對象。在 TestServlet實例化的同時,Tomcat 也會對
ServletConfig
類進行實例化。最后,Tomcat 會將
ServletConfig
的實例對象作為參數傳遞給TestServlet
對象中的init()
方法。
6.2 ServletRequest 和 ServletResponse 類
ServletRequest
類是接收請求和發送請求的類,Tomcat 已經處理并封裝好了,不需要我們編寫的 Servlet程序操心。
當瀏覽器發送的HTTP請求到達 Tomcat之后,Tomcat 通過字符串解析,把各個請求頭(Header)、請求地址(URL)、請求參數等都封裝進ServletRequest
對象中,然后將ServletRequest
對象傳遞給負責處理請求的 Servlet程序。
在 Servlet程序 中通過調用ServletRequest
對象中的方法,就可以得到瀏覽器發送的請求信息。如下:
至于ServletResponse
類,當瀏覽器發送的HTTP請求到達 Tomcat之后,Tomcat 也會將一個ServletResponse
對象傳遞給 Servlet程序,不過此時它還是一個空對象。
在 Servlet程序 處理完請求后會得到處理結果,這時 Servlet程序就會通過ServletResponse
對象的write()
方法,將處理結果寫入ServletResponse
對象內部的緩沖區。
Servlet程序處理請求結束后,會將封裝了處理結果的ServletResponse
對象返回給 Tomcat,Tomcat 遍歷ServletResponse
對象中緩沖區存儲的信息,最后組裝成 HTTP響應發給瀏覽器。
如下圖是ServletRequest/ServletResponse
對象的傳遞流程:

6.3 Servlet 接口中的方法
上面我們介紹完ServletConfig
、ServletRequest
、ServletResponse
類后,對于Servlet 接口中的方法形參有了一些了解。接下來我們就介紹一下Servlet 接口中的5個方法。如下圖:
Servlet接口中包含了5個方法,其中 init()
、service()
、destory()
這3個方法是聲明周期方法。init()
和destory()
方法在 Servlet程序存活期間各自只會執行一次,即分別在 Servlet程序被 Tomcat創建和銷毀時。而service()
方法則在每次有新請求到來時都會被調用。也就是說,我們主要的業務代碼需要寫在service()
方法中。
但是,瀏覽器發送的請求基本只有兩種類型:GET/POST
。所以我們必須在service()
方法中對不同的請求類型進行判斷。如下:
- 其中第一行的
request.getMethod()
方法是獲取瀏覽器發送請求的類型:GET/POST/PUT...
等等,我們需要判斷瀏覽器發送請求的類型,并根據不同的請求類型對請求進行處理。
那能不能將上面的代碼簡化呢?因為每一個 Servlet程序都要寫這些重復的判斷請求類型代碼的話,工作量會很大且沒有意義,我們不想自己寫這些判斷請求類型的代碼。
我們去看看 JavaAPI給我們提供的 Servlet 接口庫是否有抽象類或者接口已經幫我們寫好這些代碼了,如果有,那我們的 Servlet程序直接繼承或者實現它不就行了嗎?
7. Servlet 體系結構
要想解決上面的問題,我們需要先對 Servlet接口的體系結構進行了解。如下圖:
因為我們將來開發B/S架構的Web項目,都是針對 HTTP協議,所以我們自定義的 Servlet程序,其實都繼承自 HttpServlet抽象類。
我們分別來看看 這幾個抽象類都有些什么吧。
7.1 GenericServlet 抽象類
GenericServlet
抽象類:該類實現了 Servlet接口(Generic:通用的)。 GenericServlet
類源碼如下:

GenericServlet
抽象類的作用如下:
- 定義了一個全局變量
ServletConfig
對象,提升了init()
方法中原本是形參的ServletConfig
對象的作用域,使得ServletConfig
對象變成了全局變量,方便其他方法使用。 init()
方法中還另外調用了一個init()
空參方法,如果我們希望 Servlet程序在被創建時做一些特定的初始化操作,可以在繼承GenericServlet
類后,重寫該init()
空參方法。- 為了方便在其他類中也可以獲得
ServletConfig
對象,于是寫了一個getServletConext()
方法。
不過令人沮喪的是,GenericServlet
類中的service()
方法還是一個空方法,這并沒有完成我們最初簡化代碼開發的設想。那繼續看一下HttpServlet
抽象類
7.2 HttpServlet 抽象類
HttpServlet
抽象類,它繼承自GenericServlet
類。HttpServlet
類源碼如下:
HttpServlet
本身是一個抽象類,它定義了很多全局變量,我們看看它里面的service()
方法有沒有實現對瀏覽器發送請求的類型進行判斷。源碼如下:
顯而易見,HttpServlet
類中的service()
方法幫我們完成了對復雜的瀏覽器發送請求的類型判斷,而這正是我們想要的。
在service()
方法中,如果請求類型為 GET類型,則會調用doGet()
方法處理該請求;如果請求類型為 POST類型,則會調用doPost()
方法處理該請求。
問題是HttpServlet
類為什么還要聲明成抽象類呢?它的文檔中注釋了:
可以發現,繼承了HttpServlet
類的 Servlet程序,必須重寫doGet()
、doPost()
、doPut()
、doDelete()
等方法中的至少一個。
補充:一個類聲明成抽象類,一般有兩個原因:
它內部有抽象方法。
它沒有抽象方法,但是該類不希望被實例化。
其實,HTTPServlet
類做成抽象類,是為了不被實例化。
它為什么不希望被實例化,而且要求子類重寫doGet()
、doPost()
等方法呢?我們來看一下源碼:
HttpServlet
中的doGet()
方法,使用了protected
修飾,意思是希望子類能重寫該方法。
那如果我們沒有重寫doGet()
、doPost()
等方法,又會怎么樣?
源碼告訴我們,瀏覽器頁面會顯示405錯誤:(http.method_get_not_supported)
也就是說,HttpServlet
類雖然在service()
方法中幫我們完成了對瀏覽器發送請求的類型的判斷。但是在實際開發中,針對每一種請求類型,具體的業務邏輯代碼都是不同的(換句話說,HttpServlet
只能幫我們把不同類型的請求分類,而并不能幫我們處理請求)。
由于HttpServlet
無法知曉具體的請求想干什么,所以它索性抽象出了 7個方法,并且提供了方法的默認實現:即報 405、400錯誤。這些錯誤提示我們HttpServlet
類并不支持處理具體的請求業務。因此,我們必須重寫這些方法,用來處理具體的請求業務。
以上就是不能讓HttpServlet
類被實例化的原因了,不然如果不小心調用了HttpServlet
對象提供的doXxx()
方法去處理請求,就會出錯。
而這也就是模板方法模式:父類把能寫的邏輯都寫完,把不確定的業務代碼抽象成一個方法,并調用它。當子類重寫了該方法后,整個業務代碼就活了。
我們自己編寫的 Servlet程序繼承了HttpServlet
類,當 Tomcat 以后調用HttpServlet
類的service()
方法時,在該方法內部會調用doXxx()
方法。由于我們的 Servlet程序重寫了HttpServlet
類的doXxx()
方法,因此service()
方法最終調用的是我們自己編寫的doXxx()
方法。
補充:Java子類調用父類的方法的步驟:
- 子類的對象調用方法時,會首先在子類中查找是否存在該方法,如果子類中沒有該方法,再到父類中查找。
- 如果該方法中又調用了其他方法,那么還是按照之前的順序,先在子類中查找,再在父類中查找。
如下圖:
小結:
- 最后我們明白了,我們自己編寫一個 Servlet程序來處理瀏覽器發送的請求,只需要繼承
HttpServlet
類,然后在 Servlet程序中根據不同的請求類型和請求業務,重寫HttpServlet
類中的doGet()
、doPost()
、doPut()
、doDelete()
等方法即可。 - 其他的事就不需要我們管了,方法形參由 Tomcat幫我們封裝好傳遞進來,請求類型的判斷由
HttpServlet
類的service()
方法幫我們完成。
8. Servlet 生命周期
8.1 生命周期與三個重要方法的對應
生命周期:Servlet 生命周期指它從被創建到被銷毀的整個過程。對應 Servlet 中的三個方法:init()
, service()
, destroy()
默認情況下:
- 第一次接收到瀏覽器發送的請求時,負責處理請求的 Servlet程序會進行實例化(調用構造方法)、初始化(調用
init()
方法)、然后處理請求(調用service()
方法)。- 從第二次接收請求開始,Servlet 只會調用
service()
方法處理請求,沒有實例化和初始化的過程了。- 當Tomcat 容器關閉時,容器中所有的 Servlet 實例都會被銷毀,此時調用銷毀方法
destroy()
,實例隨后會被 Java的垃圾收集器所回收。
8.2 生命周期的演示
我么通過重寫 Servlet接口中的這三個方法來看一下 Servlet 的整個生命周期過程,之前我們說過,如果子類中重寫了父類的方法,那么調用的是子類重寫后的方法。
在Web-demo
項目中創建Demo02Servlet
類。如下:
// 演示Servlet的生命周期
@WebServlet(urlPatterns = "/demo2")
public class Demo02Servlet extends HttpServlet {public Demo02Servlet(){System.out.println("正在實例化....");}@Overridepublic void init() throws ServletException {System.out.println("正在初始化.....");}@Overrideprotected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {System.out.println("正在服務.....");}@Overridepublic void destroy() {System.out.println("正在銷毀......");}
}
在瀏覽器中發送第一次請求,訪問:http://localhost:8080/Web-demo/demo2
控制臺輸出如下:

然后刷新網頁,在網頁上每一次刷新,就代表著重新發送一次請求,這個時候控制臺輸出信息只有:正在服務…。如下圖:

最后點擊停止運行按鈕,Tomcat 容器就會對所有的 Servlet實例 進行銷毀,控制臺輸出如下:

通過上面案例我們發現:
-
對于每一個 Servlet 實例,Tomcat 只會創建一個,所有相同的請求都由對應的處理該請求的 Servlet實例去處理。
-
默認情況下,第一次發送請求時,Tomcat 才會去實例化和初始化 Servlet程序,然后由 Servlet程序處理請求。
- 這樣做的好處是: 提高系統的啟動速度。就是說只在有請求時,才創建 Servlet 對象。
- 這樣做的缺點是: 處理第一次發送的請求時,系統耗時較長。
結論:
- 如果需要提高系統的啟動速度,當前默認情況就是這樣。
- 如果需要提高系統的響應速度,我們就應該設置 Servlet 的初始化時機。
8.3 Servlet 的初始化時機
默認是第一次接收請求時,進行實例化,初始化 Servlet。
我們可以通過修改Web.xml
文件中 Servlet程序 的 <load-on-startup>
標簽來設置 Servlet程序啟動的先后順序,數字越小,啟動順序越靠前,最小值為0(這個優先級表示隨著 Tomcat 的啟動,直接對 Servlet程序進行實例化和初始化操作,無需等到有請求到來)。
修改Web.xml
配置文件如下:
<servlet><servlet-name>Demo02Servlet</servlet-name><servlet-class>com.servlets.Demo02Servlet</servlet-class><!--設置初始化優先級--><load-on-startup>1</load-on-startup>
</servlet>
啟動 Tomcat時控制臺展示如下,可以看出,在部署好 Tomcat 之前 Demo02Servlet
就進行了實例化和初始化:
補充:我們也可以使用注解@WebServlet(urlPatterns = "/demo2", loadOnStartup = 1)
來設置 Servlet程序初始化的優先級。loadOnstartup
的取值有兩類情況:
- 負整數:第一次有請求訪問時再初始化 Servlet對象。
- 0 或正整數:服務器啟動時就創建 Servlet對象,數字越小優先級越高。
9. Servlet 線程不安全問題
Servlet 在Tomcat
容器中是:單例的、多線程情況下是線程不安全的。
單例:所有相同的請求都由對應處理該請求的同一個 Servlet實例去響應處理。
線程不安全:假如有兩個瀏覽器線程同時請求同一個 Servlet實例,第一個線程需要根據 Servlet程序中的某個成員變量值去做邏輯判斷。但是在第一個線程進行邏輯判斷的過程中,第二個線程改變了這個成員變量的值,從而導致第一個線程的判斷結果發生了變化。這就是線程不安全問題。如下圖:
解決多線程不安全問題的注意事項:
- 盡量的不要在 Servlet程序中定義成員變量。
- 如果不得不定義成員變量,那么一定不要去:①修改成員變量的值;②根據成員變量的值做一些邏輯判斷。
10. 小結一下 Tomcat 和 Servlet的執行流程
一個 Web 項目,需要部署到 Tomcat 容器中去,并不是把 Web 項目的源代碼部署進去,而是把 Web 項目的部署包artifact
部署到 Tomcat中去。所以我們需要先生成 Web項目的 部署包,在IDEA 2022中,創建 Web 項目時,就會自動生成其對應的部署包了。不過在舊版的 IDEA 中,則需要在 Project Structure
中手動生成 Web項目的部署包。
若是已經生成了 Web項目的部署包之后,又需要在 Web 項目中另外引入新的 jar 包,這個后面引入的 jar 包在已部署的包中不會自動更新。此時在Project Structure
中會出現 Problems
提示,我們根據其提示操作就可以解決該問題。另外,我們也可以直接把第三方 jar 包直接放到已部署的 Web 項目中的 WEB-INF
目錄下,這樣該 jar 包也會自動添加進部署包中,不過這樣做的缺點是該后添 jar 包只能給該Web項目獨享,其他的項目不能再使用此 jar包。
最后就是要在 Tomcat 中部署 Web 項目的部署包了。首先進入 Tomcat 模板中的 Deployment
模塊,添加需要部署的 Web項目的 Artifact
部署包,然后會出現一個Web項目的根目錄Application context
,其內設置的目錄路徑就是 Web 項目在 Tomcat 容器中的根目錄路徑context root
,我們可以對其進行更改。 當Tomcat 啟動后,會自動打開指定的瀏覽器,然后去訪問Web項目的URL
。
1.假設在 Tomcat 中設置的 URL 為
http://localhost:8080/demo
,啟動 Tomcat 后,瀏覽器便會訪問該 URL,此時會向 Tomcat 服務器中部署的 Web 項目中請求資源demo
,于是 Tomcat 便會到 Web 項目中尋找該資源。
- 若通過注解
@WebServlet
沒有直接找到,Tomcat 便解析Web.xml
配置文件,在<servlet-mapping>
標簽中找到了<url-patter>
標簽中值為/demo
所對應的<servlet-name>
標簽中的類名。- 然后去
<servlet>
標簽中找到相匹配的 Servlet 類,然后通過反射生成該類的實例對象,并自動調用其sevice()
方法,最后在使用結束后銷毀該實例對象。
2.假設在 Tomcat 中設置的 URL 為
http://localhost:8080/test.html
,啟動 Tomcat 后,在瀏覽器訪問該 URL,則是向 Tomcat 服務器中部署的 Web 項目中請求資源test.html
,于是 Tomcat 便會到 Web 項目中尋找該資源,找到該資源后,便給瀏覽器返回響應,于是在瀏覽器中便會顯示對應的test.html
靜態界面,
瀏覽器啟動后,可能會報錯 404。404:意味著找不到指定的資源。 假設我們的網址是:http://localhost:8080/pro01/(/pro01
是根目錄,沒有指定要訪問的資源路徑), 那么表明客戶端請求的資源是 Web 項目中的 index.html
,它也是客戶端默認訪問的資源路徑。
我們也可以通過Web.xml
中的<welcome-file-list>
標簽進行設置客戶端默認訪問的資源路徑(在 Tomcat的Web.xml
中設置,或者在自己項目的Web.xml
中設置)。
**瀏覽器啟動后,可能會報錯 405。405:表示當前請求的方法不支持。**比如,我們的 form表單中的method=post
, 那么 Servlet 必須重寫doPost()
方法。否則會報 405 錯誤。
還有可能會出現空指針異常或者是NumberFormatException
。
最后注意:web.xml
文件中的<url-pattern>
標簽中的值中以斜杠開頭。