4、Servlet
在 Web 應用中,Servlet 是一項重要的技術。Servlet 是利用 Java 類編寫的服務器端程序,與平台架構、協議無關。JSP 的實質是 Servlet,因為 JSP 在執行第一次後,會被編譯成 Servlet 的類文(即 .class),當再重複調用執行 JSP 時,就直接執行第一次所產生的 Servlet,而不再重新把 JSP 編譯成 Servelt,所以 Servlet 至關重要。
本章主要涉及的知識點有:
(1)Servlet 的基本概念和技術特點
(2)一個 Servlet 的生命週期
(3)如何編寫和部署一個 Servlet 程序
(4)Servlet 與 JSP 之間的關聯與區別
4.1、Servlet 是什麼
本節首先介紹 Servlet 的基本概念,Servlet 是利用 Java 類編寫的服務器端應用程序,顧名思義,它通常是在服務器端運行的程序,打開瀏覽器即可調用一個。它可以被看作位於客户端和服務器端的一箇中間層,負責接收和請求客户端用户的響應。
Servlet 使用了很多 Web 服務器都支持的 API,可以調用和擴展 Java 中提供的大量程序設計接口、類、方法等功能。
Servlet 可以提供以下功能。
1、對客户端發送的數據進行讀取和攔截
客户端在發送一個請求時,一般而言都會攜帶一些數據(例如 URL 中的參數、頁面中的表單、Ajax 提交的參數等),當一個 Servlet 接收到這些請求時,JavaServlet 中的類通過所提供的方法就能得到這些參數(例如,方法 request.getParameterName(name) 用於獲得名為 name 的參數值),也正因為這個原因,Servlet 可以對發送請求起到攔截作用,它在某些請求發出前先做一個預處理分析,從而判斷客户端是否可以做某些請求(例如檢查訪問權限、設定程序的字集、檢查用户角色等),當 Servlet 具有如上功能時,一般可以被稱為攔截器。
2、讀取客户端請求的隱含數據
客户端請求的數據可以分為隱含數據和顯式數據:
(1)隱含數據一般不直接跟隨於 URL 中,它存在於請求的來源、緩存數據(Cookie)、客户端類型中;
(2)顯式數據顯然是用户可以直觀看到的,例如表單數據和 URL 參數。Servlet 不但可以處理顯式數據,而且可以處理隱含數據,是一個“多面手”。
3、運行結果或者生成結果
當一個 web 應用程序對客户端發出的請求做出響應時,一般需要很多中間過程才能得到結果。Servlet 起到這個中間角色的作用,協調各組件、各部分完成相應的功能,根據不同的請求做出相應的響應並顯示結果。
4、發送響應的數據
Servlet 在對客户端做出響應並經過處理得出結果後,會對客户端發送響應的數據,以便讓客户端獲取請求的結果數據。在 Web 應用程序中,Servlet 的這個功能相當突出,無論現有的技術多麼先進,都是基於這個功能出發的。綜上所述,Servlet 的程序運行順序大致如圖 4.1 所示。
圖 4.1 Servlet 的運行順序
4.2、Servlet 的技術特點
本節介紹了 Servlet 的概念和功能,從而讓讀者村 Servlet 有一個整體的印象和認識,並瞭解其運行的順序等,本節將介紹 Servlet 的一些技術特點,讓讀者瞭解 Servlet 的優點。
Servlet 在開發中帶來的優點就是能及時響應和處理 Web 端的請求,使得一個不懂網頁的 Java 開發人員也能編寫出 Web 應用程序,只是在開發/修改一個 Web 程序時比較麻煩,因為代碼的可讀性比較差,也比較難以維護,但是它卻具有以下特點。
1、高效率
Servlet 本身就是一個 Java 類,在運行的時候位於同一個 Java 虛擬機中,可以快速地響應客户端的請求並生成結果。在 Web 服務器中處理一個請求使用的都是線程而非進程,也就是説在性能開銷方面就小很多,無須大量的啓動進程時間,在高併發量訪問時,一個進程可以有多個線程,併發時線程在 CPU 中的開銷代價要遠小於進程的開銷。
2、簡單方便
開發一個 Web 程序,從開發順序上來説比較簡單:
(1)首先定義一個 Servlet 類,然後在系統(web.xml)中配置程序,繼而發佈程序,這樣一個 Web 程序就完成了。
(2)在開發的過程中,系統提供了大量的實用工具和方法,可以處理複雜的 HTML 表單數據、處理 cookie、跟蹤網頁會話等。
4.3、Servlet 的生命週期
每個生命都有特定的生命週期,例如人的生命週期為“嬰兒一少年一青年→壯年一老人”開發項目的生命週期為“立項→開發一運維一消亡”。同樣,Servlet 也不例外,它有 3 個階段,分別是:初始化(包括裝載和初始化)、運行、消亡。
1、初始化階段
初始化階段可以分為裝載和初始化兩個子階段。裝載就是由 Servlet 容器裝載一個 Servlet 類,把它裝載到 Java 內存中,Servlet 容器可創建一個 Servlet 對象並與 web.xml 中的配置對應起來;初始化子階段是調用 Servlet 中的 init() 方法,在整個 Servlet 生命週期中 init() 方法只被調用一次。
2、運行階段
在這個階段中會實際響應客户端的請求,當有請求時 Servlet 會創建 HttpServletRequest 和HttpServletResponse 對象,然後調用 service(HttpServletRequest request, HttpServletResponse response)方法。serivce() 方法通過 request 對象獲得請求對象的信息並加以處理,再由 response 對象對客户端做出響應。
3、消亡階段
當 Servlet 應用被終止後,Servlet 容器會調用 destroy() 方法對 Servlet 對象進行銷燬。在消亡的過程中,Servlet 容器將釋放被它所佔的資源,例如關閉流、關閉數據庫連接等。同樣在整個 Servlet 生命週期中 destroy() 方法也只被調用一次。
4、演示生命週期
(1)HelloServlet
下面通過編寫一個 Servlet 類來説明它的生命週期,完整的代碼如下:
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import static java.lang.System.out;
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 1L; // 序列化
// 1. 初始化階段
public void init(){
out.println("控制器初始化,整個生命週期,只執行一次!");
}
// 2. 運行階段
/**
* 當客户端訪問了綁定該 Servlet 的路徑時,會執行 service 方法
* @param request 客户端請求對象
* @param response 服務端響應對象
* @throws IOException 異常
*/
public void service(ServletRequest request, ServletResponse response) throws IOException {
out.println("運行階段:調用了:service 方法");
response.setContentType("text/html;charset=gbk");
PrintWriter writer = response.getWriter();
writer.println("收到了 service 請求");
}
protected void service(HttpServletRequest request, HttpServletResponse response){
out.println("調用了受保護的 service 方法");
}
protected void doGet(HttpServletRequest request,HttpServletResponse response) throws IOException {
out.println("調用 doGet 方法");
// 設置響應的頁面類別和頁面編碼
response.setContentType("text/html;charset=gbk");
PrintWriter writer = response.getWriter();
writer.println("收到 HelloServlet doGet 請求");
}
protected void doPost(HttpServletRequest request,HttpServletResponse response) throws IOException {
out.println("調用 doPost 方法");
// 設置響應的頁面類別和頁面編碼
response.setContentType("test/html;charset=gbk");
PrintWriter writer = response.getWriter();
out.println("收到 HelloServlet doPost 請求");
}
public void destroy(){
out.println("調用 destroy 方法");
}
}
可以看出,上述實例中的各個方法都只是執行打印功能,它們只是為了説明一個 Servlet 的生命週期執行過程。
(2)web.xml
完成上述 Servlet 編譯後,還需要配置一下 web.xml,具體配置如下:
<servlet>
<servlet-name>helloServlet</servlet-name>
<servlet-class>com.shw.jspservlettomcat.ch4.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>helloServlet</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
注意:配置 servlet-name 時應區分字母的大小寫。
除了在 web.xml 中配置 Servlet 外,在 Serylet3.0 及以後的版本中還可以通過直接注入的方式進行配置,代碼如下:
@WebServlet{
urlPatterns = {"/HelloServlet"},
name = "helloServlet"
}
public class HelloServlet extends HttpServlet {
private static final long serialVersionUID = 1L; // 序列化
// 1. 初始化階段
public void init(){
out.println("控制器初始化,整個生命週期,只執行一次!");
}
// 2. 運行階段
/**
* 當客户端訪問了綁定該 Servlet 的路徑時,會執行 service 方法
* @param request 客户端請求對象
* @param response 服務端響應對象
* @throws IOException 異常
*/
public void service(ServletRequest request, ServletResponse response) throws IOException {
out.println("運行階段:調用了:service 方法");
response.setContentType("text/html;charset=gbk");
PrintWriter writer = response.getWriter();
writer.println("收到了 service 請求");
}
protected void service(HttpServletRequest request, HttpServletResponse response){
out.println("調用了受保護的 service 方法");
}
protected void doGet(HttpServletRequest request,HttpServletResponse response) throws IOException {
out.println("調用 doGet 方法");
// 設置響應的頁面類別和頁面編碼
response.setContentType("text/html;charset=gbk");
PrintWriter writer = response.getWriter();
writer.println("收到 HelloServlet doGet 請求");
}
protected void doPost(HttpServletRequest request,HttpServletResponse response) throws IOException {
out.println("調用 doPost 方法");
// 設置響應的頁面類別和頁面編碼
response.setContentType("test/html;charset=gbk");
PrintWriter writer = response.getWriter();
out.println("收到 HelloServlet doPost 請求");
}
public void destroy(){
out.println("調用 destroy 方法");
}
}
在上述代碼中,第 12~15 行就是利用注入聲明的方式表示這是一個 Servlet 類,@WebServlet 中的參數如表 4.1 所示。從上述配置可以看出,這種方法比較簡單,也是現在主流的開發形式,在隨後的章節中還會繼續介紹和使用這種方法。如果利用注入的方式進行了配置,那麼 web.xml 就不用配置 Servlet。
表 4.1 @WebServlet 中的主要屬性列表
|
name(String name) |
描述指定 Servlet 的 name 屬性,等價於<servlet-name>。如果沒有指定,則該 Servlet 的取值為類的全名 |
|
urlPatterns(Stringll urls) |
指定 Servlet 的 URL 匹配模式,等價於 <url-pattem>標籤 |
配置完 web.xml 之後,可以通過以下步驟査看 Servlet 的生命週期執行過程。
(1)啓動 Tomcat,將項目工程放在 webapps 文件夾下。
(2)在瀏覽器地址欄中輸入與該 Servlet 配置相對應的 URL,在瀏覽器中可以看到“收到 service 請求”內容,在控制枱中會輸出:
控制器初始化,整個生命週期,只執行一次!
運行階段:調用了:service 方法
訪問地址:http://localhost:8080/jsp_servlet_tomcat_war/HelloServlet
(3)再在瀏覽器中輸入 URL,在瀏覽器中看到“收到 service 請求”內容,在控制枱中會輸出:
運行階段:調用了:service 方法
通過上述過程,驗證了 Servlet 的生命週期過程。圖 4.2 進一步説明了生命週期的不同階段。
圖 4.2 Servlet 的生命週期
從程序的運行結果還可以知道,當重寫了 service() 方法之後,doPost() 方法和 doGet() 方法是不會被處理的,由 service() 來管理轉向對應的方法。
5、工作原理
1、Servlet 生命週期管理
- 加載階段:Web 容器啓動時讀取
web.xml,發現 Servlet 配置 - 初始化:容器創建
HelloServlet實例,調用init()方法 - 服務階段:當匹配的 URL 請求到來時,調用
service()方法 - 銷燬階段:容器關閉時調用
destroy()方法
2、URL 映射機制
客户端請求: http://localhost:8080/your-app/HelloServlet
↓
容器解析 URL 路徑: /your-app/HelloServlet
↓
匹配 <url-pattern>/HelloServlet</url-pattern>
↓
找到對應的 <servlet-name>helloServlet</servlet-name>
↓
實例化/調用 com.shw.jspservlettomcat.ch4.HelloServlet
↓
執行相應的 doGet() 或 doPost() 方法
3、請求處理流程
public class HelloServlet extends HttpServlet {
// 初始化方法 - 容器調用
public void init() {
// 初始化資源
}
// 處理 GET 請求
protected void doGet(HttpServletRequest request, HttpServletResponse response) {
// 處理業務邏輯
response.getWriter().write("Hello World!");
}
// 銷燬方法 - 容器調用
public void destroy() {
// 清理資源
}
}
4、核心作用
- 聲明式配置:將 Servlet 類與 URL 路徑解耦
- 集中管理:所有 Servlet 配置集中在
web.xml中 - 動態加載:支持 Servlet 的懶加載和預加載配置
- 訪問控制:通過 URL 模式控制哪些請求由哪個 Servlet 處理
5、@WebServlet 替代方案
雖然 web.xml 配置仍然有效,但現代開發更推薦使用註解:
@WebServlet("/HelloServlet")
public class HelloServlet extends HttpServlet {
// 類內容...
}
註解方式更簡潔,但底層原理完全相同。
4.4、編寫和部署 Servlet
上一節講解了 Servlet 的生命週期過程,本節將介紹如何編寫一個 Servlet 類和部署 Servlet 工程,編寫和部署 Servlet 是開發一個 Web 工程的基礎,也是讀者必須掌握的內容。
4.4.1、編寫 Servlet 類
本小節將講述如何編寫一個簡單的 Servlet 類。本書編寫 Servlet 的開發工具為 Intelli DEA,在 IntelliJ DEA的主界面中依次單擊 File | New | Project 命令,然後在彈出的窗口左側選擇 Java,在右側上方選擇 JDK17.0.4,進入下一步,輸入項目名稱,選擇項目路徑,這樣就完成了初始工程的創建。
由於 Java EE 已正式更名為 Jakarta EE,而目前的 DEA 只能支持 Java EE,因此創建完成後我們需要手動接入 Jakarta EE 的支持。首先在已創建的工程上右擊鼠標,在彈出的快捷菜單中選擇 Add FrameWork Suppont 命令,再在彈出的窗口中選擇 Java EE | Web Application 選項,再單擊 0K 按鈕,如圖 4.3 所示。然後打開項目工程目錄,修改 web-app 的版本為 5.0,代碼如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="https://jakarta.ee/xml/ns/jakartaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_5_0.xsd"
version="5.0">
</web-app>
整個工程的目錄結構如圖 4.4所示。
圖 4.3 接入 Jakarta EE 的支持
圖 4.4 Servlet 工程目錄
為了使目錄結構更加具有層次感,在包 com.shw.ch4,然後右擊該文件,在彈出的快捷菜單中選擇New | Java Class 命令,然後在 Name 文本框中輸入自己定製的 Servlet 程序文件的名字。創建 Servlet 的對話框如圖 4.5 所示。
圖 4.5 創建一個 Servlet 類
單擊 Finish 按鈕後出現主編輯界面,在編寫 Servlet 類時需要繼承 HttpServlet 類,這樣才可以開發具體的功能,假設想在頁面中循環輸出數字 0~10,那麼程序代碼如下:
import jakarta.servlet.annotation.WebServlet;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@WebServlet(
urlPatterns = "/FirstServlet",
name = "firstServlet"
)
public class FirstServlet extends HttpServlet {
private static final long serialVersionUID = 1L;
public void init(){
System.out.println("控制器開始初始化,執行方法:init()");
}
public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException {
System.out.println("控制器訪問:doGet 方法");
// 設置響應的頁面類別和頁面編碼
response.setContentType("text/html;charset=gbk");
// 獲取一個響應的輸出流
PrintWriter writer = response.getWriter();
writer.println("<html>");
writer.println("<head>");
writer.println("<title>測試 0-10 的循環結果</title>");
writer.println("<body>");
writer.println("開始執行。。。。。。"+"<br>");
int count = 0;
for (int i=0;i<=10;i++){
count += i;
}
writer.println("程序執行的結果是:" + count);
writer.println("</body>");
writer.println("</head>");
writer.println("</html>");
writer.flush();
writer.close();
}
public void doPost(HttpServletRequest request,HttpServletResponse response) throws IOException {
doGet(request,response);
System.out.println("調用:doPost 方法");
}
public void destroy(){
System.out.println("調用:destroy 方法");
}
}
這裏採用的是註解 @WebServlet 的方式注入 Servlet。現在分析一下創建 Servlet 的步驟:
(1)引入相應的包。
例如 jakarta.servlet 包或者 jakarta.servlet.http 包(這兩個包的區別在於前者是與協議無關的,後者是與HTTP協議相關的)。
在平時開發的過程中,一般都是繼承自 HttpServlet 類,因為它封裝了很多基於 HTTP 的 Servlet 功能。當然要想自己開發一個協議還可以繼承 GenericServlet 類。
FirstServlet 中有兩個處理請求的方法:一個是 doGet() 方法響應 HTTP Get 請求;另一個是 doPost() 方法,響應 HTTP Post 請求。
這裏之所以使用 jakarta 包。而不是原來的 javax 包,原因在於 Java EE 在 2017 年由甲骨文轉讓給 Eclipse 基金會,2018年3月 Eclipse 基金會正式宣佈 JavaEE 更名為 Jakarta EE。
(2)創建一個擴展類,例如本例中的 FirstServlet 類。
(3)重構 doGet() 或者 doPost() 方法。例如,本例中重構了doGet() 方法,在該方法中完成處理請
求,並輸出到 HTML 頁面中。
(4)配置 web.xml 或添加 @WebServlet 註解。
4.4.2、署Servlet類
在 InteliJ IDEA 中部署一個 Web 工程有多種方法,本書只介紹兩種方法:一種是原始的編譯部署,另一種是直接利用在 ImtelJ IDEA 中配置的 Tomcat 服務器部署。
1、原始的編譯部署
利用 Java 編譯器將具體的 Java 類進行編譯,例如本例中運行編譯命令“javac FirstServlet.java”,在當前目錄中生成 FirstServlet.class 文件,將 FirstServlet.class 文件複製到 WEB-INF 文件夾下的 classes 文件夾中,如圖 4.6 所示。
再將 web.xml 存放在 WEB-INF 目錄下,接着將整個 Servlet 工程文件夾存放在 webapps 目錄下,如圖 4.7 所示,再啓動 Tomcat 服務器。
圖 4.7 Servlet 放置目錄
2、利用在 IntelliJ IDEA 中配置的 Tomcat 服務器部署
操作步驟如下:
(1)首先單擊右上角的按鈕打開項目結構窗口,在窗口中選擇 Artifacts,單擊加號,在彈出菜單中選擇Web Application:Exploded,單擊 OK 按鈕完成配置,如圖 4.8 所示。
圖 4.8 在 IntelliJ IDEA 中部署 Servlet 工程
(2)在項目界面選擇 Run | Edit Confgurations 打開配置界面,或者通過右上角的下拉框選擇 Edit Configurations 打開,找到已配置的 Tomcat,如果不存在已配置的 Tomcat,請參照第1章有關 InteliJ IDEA 工具下載部分配置 Tomcat。單擊對應的 Tomcat,選擇 Server 選項卡配置 URL 和 HTTP port 端口號,單擊 OK 按鈕完成項目部署,如圖 4.9 所示。配置完成後單擊右上角的按鈕啓動項目即可。
(3)打開正瀏覽器,在地址欄中輸入地址“http://localhost:8080/ch4_war_exploded/FirstServlet”然後按回車鍵,如果在頁面中顯示信息“開始執行……程序執行結果:55”,那麼恭喜你,這個 Servlet 成功部署並運行。
4.5、Servlet與JSP 的比較
上一節介紹瞭如何編寫和部署 Servlet 程序,從而使讀者瞭解編寫一個 Servlet 的基本步驟,並能編寫出簡單的 Servlet 類。本節將介紹 Servlet 與 JSP 之間的區別與聯繫,包括它們之間的內在聯繫是什麼。其實從本質上講,Servlet 與 JSP 是一樣的,因為 JSP 頁面最後在運行時會被轉換成一個 servlet,但是從開發者的視角看,運用這兩種技術還是有些區別的,這些區別也決定了在開發中應該如何選擇使用它們。JSP 與 Servlet 的主要區別説明如下。
1、Servlet 是 Java 代碼,JSP 是頁面代碼
編寫 Servlet 就是編寫 Java 代碼,所以應用 java 中的規範去編寫 Servlet 類就可以了,但是若想在客户端中響應結果,就必須在代碼中加入大量的 HTML 代碼。可想而知,當想要得到一個比較美觀、複雜的界面時,HTML 代碼量會相當多而且非常煩瑣。JSP 以 HTML 代碼為主,在頁面中適當嵌入 Java 代碼來處理業務上的邏輯。顯然,JSP 會比 Servlet 較易編寫並更直觀。基於此的差異也是選擇 Servlet 或者 JSP 技術的考量標準之一。如果業務中主要是以頁面為主,就選揚 JSP 技術;反之,則選擇 Servlet 技術(適合服務器端開發)。
2、Servlet 的運行速度快過 JSP
Servlet 本身就是一個 Java 類,編譯的時候會直接被轉換為類文件;JSP 需要先被編譯為 Java 類, 而後再運行,所以 Servlet 的運行速度較快。
3、Servlet 需要手動編譯,JSP 由服務器自動編譯
Servlet 類被編譯成為類文件後,需要手動複製到 Web 應用程序目錄下。JSP 頁面部署到 Web 應用則簡單很多,只需要將 JSP 頁面複製到指定的目錄下即可,當它第一次被訪問時,Web 服務器自動將 JSP 代碼轉換為Servlet(Java 代碼)並自動編譯。
4、編輯 HTML 工具不支持編輯 Servlet
當前有很多製作網頁的工具,例如 Dreamweaver、Golive 等,利用這些工具可以快速地開發網頁、編寫出複雜的界面,而且具備可視化效果,極大地提高了網頁開發的效率。
但對於大多數的網頁工具而言,在 HTML 頁面中加入 Java 代碼是可以的,但是要在 Java 代碼中加入 HTML 代碼卻不行,而且不能及時糾錯。
因此,初學者利用不同的編輯器工具編寫 JSP 和 Servlet 也是可以理解的。
瞭解了 Servlet 與 JSP 的差別之後,在編寫 Web 應用程序時,就要根據當前的需要權衡是使用 JSP 還是 Servlet。開發者應儘量使 JSP 和 Servlet 都發揮出最大的作用,同時又能夠便於日後的代碼維護工作。
一般而言,Servlet 大多用於負責對客户端的請求進行處理和調用 Java Bean,由 Java Bean 負責提供可複用的數據以及訪問數據等;而 JSP 頁面主要負責頁面的展示,將動態數據展現給客户,這就是開發者提出的簡易 MVC 模式,這樣分工大大減少了 JSP 頁面中 Java 程序和 HTML 代碼的耦合度,對維護工作具有重大的意義。
4.6 小結
本章主要介紹了 Servlet 的基礎知識,包括它的基本概念、技術特點等。通過本章的學習,讀者可掌握 Servlet 的基本編寫方法和步驟、生命週期的意義,以及在生命週期各階段調用的不同方法,最後需要了解並掌握Servlet 與 JSP 的相同點和不同點。