Servlet 是 Java Web 開發的基石,理解 Servlet 規範是深入學習 Web 容器的前提。本章涵蓋 HTTP 協定基礎、Servlet 規範、生命週期管理、Filter 與 Listener 機制,以及 Session 管理。
HTTP 協定基礎#
HTTP 的本質#
HTTP 協定的本質是一種通信格式
HTTP 是信封,HTML 是信的內容。沒有信封,信也寄不出去。
HTTP 協定是瀏覽器與伺服器之間的資料傳送協定。作為應用層協定,HTTP 基於 TCP/IP 來傳遞資料,主要規定了用戶端和伺服器之間的通信格式。
HTTP 請求處理流程#
sequenceDiagram
participant B as 瀏覽器
participant S as 伺服器
B->>S: 1. 建立 TCP 連線
B->>S: 2. 發送 HTTP 請求<br/>(請求行 + 請求頭 + 請求體)
S-->>B: 3. 回傳 HTTP 回應<br/>(狀態行 + 回應頭 + 回應體)
B->>S: 4. 關閉連線(或保持連線)HTTP 請求格式#
POST /login HTTP/1.1 ← 請求行:方法 + URL + 協定版本
Host: www.example.com ← 請求頭
Content-Type: application/json
Content-Length: 42
{"username":"admin","password":"123"} ← 請求體HTTP 回應格式#
HTTP/1.1 200 OK ← 狀態行:協定版本 + 狀態碼 + 原因短語
Content-Type: application/json ← 回應頭
Content-Length: 28
{"status":"success","token":"xxx"} ← 回應體Servlet 規範#
為什麼需要 Servlet 規範?#
早期的 Web 應用主要用於瀏覽靜態頁面。隨著需求發展,我們需要動態生成內容,這就需要一種機制讓 HTTP 伺服器能呼叫服務端程序。
問題:HTTP 伺服器怎麼知道要呼叫哪個 Java 類的哪個方法?
解決方案:面向介面編程
flowchart LR
HTTP["HTTP 伺服器"] --> SC["Servlet 容器<br/><i>(解耦)</i>"]
SC --> S["Servlet<br/>(業務邏輯)"]
style HTTP fill:#ffccbc
style SC fill:#fff9c4
style S fill:#c8e6c9Servlet 介面是 Servlet 容器與具體業務類之間的契約
這個設計讓 HTTP 伺服器與業務邏輯解耦,你只需要實現 Servlet 介面,容器負責管理和呼叫。
Servlet 介面定義#
public interface Servlet {
// 初始化方法,容器加載 Servlet 時呼叫
void init(ServletConfig config) throws ServletException;
// 獲取 Servlet 組態
ServletConfig getServletConfig();
// 核心方法:處理請求
void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
// 獲取 Servlet 資訊
String getServletInfo();
// 銷毀方法,容器卸載 Servlet 時呼叫
void destroy();
}Servlet 繼承體系#
graph TD
S["Servlet<br/><i>(介面)</i>"]
S --> GS["GenericServlet<br/><i>(抽象類,協定無關)</i>"]
GS --> HS["HttpServlet<br/><i>(抽象類,HTTP 特化)</i>"]
HS --> YS["你的 Servlet<br/><i>(繼承 HttpServlet)</i>"]
style S fill:#e3f2fd
style GS fill:#fff9c4
style HS fill:#ffccbc
style YS fill:#c8e6c9實際開發中,我們只需繼承 HttpServlet 並重寫 doGet、doPost 等方法:
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
out.println("<h1>Hello, Servlet!</h1>");
}
@Override
protected void doPost(HttpServletRequest request,
HttpServletResponse response)
throws ServletException, IOException {
// 處理 POST 請求
}
}Servlet 生命週期#
生命週期階段#
flowchart LR
L["加載類<br/>(類加載器)"] --> N["實體化<br/>(new)"]
N --> I["初始化<br/>(init)"]
I --> S["服務<br/>(service)"]
S -->|多次呼叫| S
S -->|容器關閉| D["銷毀<br/>(destroy)"]
style L fill:#e3f2fd
style I fill:#fff9c4
style S fill:#c8e6c9
style D fill:#ffcdd2| 階段 | 時機 | 說明 |
|---|---|---|
| 加載 | 首次請求或啟動時 | 容器加載 Servlet 類到 JVM |
| 實體化 | 加載後 | 容器創建 Servlet 實體(通常是單例) |
| 初始化 | 實體化後 | 呼叫 init() 方法,只呼叫一次 |
| 服務 | 每次請求 | 呼叫 service() 方法處理請求 |
| 銷毀 | 容器關閉時 | 呼叫 destroy() 方法,釋放資源 |
Servlet 默認是單例的
一個 Servlet 類在容器中通常只有一個實體,多個請求由多個線程共享這個實體。因此,Servlet 中不應該使用實體變數保存請求相關的資料,否則會有線程安全問題。
延遲加載與預加載#
默認情況下,Servlet 採用延遲加載策略,即第一次請求時才創建實體。
可以通過 loadOnStartup 參數組態預加載:
<servlet>
<servlet-name>myServlet</servlet-name>
<servlet-class>com.example.MyServlet</servlet-class>
<load-on-startup>1</load-on-startup> <!-- 數字越小,優先級越高 -->
</servlet>或使用註解:
@WebServlet(urlPatterns = "/my", loadOnStartup = 1)
public class MyServlet extends HttpServlet {
// ...
}Filter 與 Listener#
Filter(過濾器)#
Filter 是 Servlet 規範提供的擴展機制,用於對請求和回應進行統一的預處理和後處理。
典型應用場景:
- 字符編碼設置
- 登錄驗證
- 權限檢查
- 日誌記錄
- 請求限流
Filter 鏈的工作原理#
flowchart LR
subgraph Request["請求流程"]
direction LR
R[請求] --> F1[Filter1] --> F2[Filter2] --> F3[Filter3] --> S[Servlet]
end
subgraph Response["回應流程"]
direction RL
S2[Servlet] --> F3R[Filter3] --> F2R[Filter2] --> F1R[Filter1] --> Res[回應]
end
style S fill:#c8e6c9
style S2 fill:#c8e6c9public interface Filter {
void init(FilterConfig filterConfig) throws ServletException;
void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain) throws IOException, ServletException;
void destroy();
}Filter 實現示例#
@WebFilter("/*")
public class EncodingFilter implements Filter {
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
// 前置處理
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
// 呼叫下一個 Filter 或 Servlet
chain.doFilter(request, response);
// 後置處理(可選)
}
}Filter 鏈的執行順序
- web.xml 組態:按
<filter-mapping>的聲明順序- 註解組態:按 Filter 類名的字母順序
Listener(監聽器)#
Listener 用於監聽容器內部發生的事件,主要分為兩類:
| 類型 | 監聽器介面 | 監聯事件 |
|---|---|---|
| 生命週期 | ServletContextListener | Context 啟動/停止 |
| HttpSessionListener | Session 創建/銷毀 | |
| ServletRequestListener | 請求到達/處理完成 | |
| 屬性變化 | ServletContextAttributeListener | Context 屬性變化 |
| HttpSessionAttributeListener | Session 屬性變化 | |
| ServletRequestAttributeListener | Request 屬性變化 |
Listener 實現示例#
@WebListener
public class AppContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
// Web 應用啟動時執行
System.out.println("Web 應用正在啟動...");
// 初始化全局資源,如資料庫連線池
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
// Web 應用關閉時執行
System.out.println("Web 應用正在關閉...");
// 釋放全局資源
}
}Spring 就是通過 ServletContextListener 來啟動的
Spring 實現了
ContextLoaderListener,監聽ServletContext的啟動事件,在 Web 應用啟動時創建並初始化 Spring 容器。
Filter vs Listener 的本質區別#
| 特性 | Filter | Listener |
|---|---|---|
| 工作方式 | 干預過程 | 監聯狀態 |
| 觸發時機 | 每次請求 | 事件發生時 |
| 作用物件 | 請求/回應 | 容器/Session/請求的狀態 |
| 是否能修改 | 可以修改請求和回應 | 只能回應事件,不能修改 |
Session 管理#
為什麼需要 Session?#
HTTP 協定是無狀態的
每次請求之間沒有關聯,伺服器不知道請求來自哪個用戶。Session 機制解決了用戶身份識別問題。
Cookie 與 Session 的關係#
flowchart LR
subgraph Browser["瀏覽器"]
Cookie["Cookie:<br/>JSESSIONID=ABC123"]
end
subgraph Server["伺服器"]
Session["Session:<br/>用戶資料<br/>購物車<br/>登錄狀態"]
end
Cookie <-->|"對應關係"| Session
style Browser fill:#e3f2fd
style Server fill:#c8e6c9工作流程:
- 用戶首次訪問,伺服器創建 Session,生成唯一的 Session ID
- Session ID 通過 Cookie 發送給瀏覽器
- 瀏覽器後續請求攜帶 Cookie(包含 Session ID)
- 伺服器根據 Session ID 找到對應的 Session 資料
Session 的創建與使用#
// 獲取 Session,不存在則創建
HttpSession session = request.getSession(); // 等同於 getSession(true)
// 獲取 Session,不存在回傳 null
HttpSession session = request.getSession(false);
// 存儲資料
session.setAttribute("user", userObject);
// 讀取資料
User user = (User) session.getAttribute("user");
// 移除資料
session.removeAttribute("user");
// 銷毀 Session
session.invalidate();Session 組態#
web.xml 組態 Session 逾時時間
<session-config>
<!-- 逾時時間,單位:分鐘 -->
<session-timeout>30</session-timeout>
<!-- Cookie 組態 -->
<cookie-config>
<name>JSESSIONID</name>
<http-only>true</http-only>
<secure>true</secure>
<max-age>1800</max-age>
</cookie-config>
<!-- Session 追蹤模式 -->
<tracking-mode>COOKIE</tracking-mode>
</session-config>Tomcat 的 Session 管理#
Tomcat 提供了多種 Session 持久化方案:
| 方案 | 說明 | 適用場景 |
|---|---|---|
| StandardManager | 默認,內存存儲,重啟丟失 | 開發環境 |
| PersistentManager | 持久化到文件系統 | 單機生產 |
| DeltaManager | 集群 Session 複製 | 小規模集群 |
| BackupManager | 集群 Session 備份 | 大規模集群 |
| Redis Session | 使用 Redis 存儲 | 微服務、高可用 |
現代應用的 Session 管理趨勢
在微服務架構下,建議使用 Redis 等外部存儲來管理 Session,實現 Session 共享和高可用。Spring Session 是一個很好的選擇。
Web 應用目錄結構#
Servlet 規範定義了標準的 Web 應用目錄結構:
MyWebApp/
├── WEB-INF/
│ ├── web.xml ← 部署描述符(可選,Servlet 3.0+)
│ ├── classes/ ← 編譯後的類文件
│ │ └── com/
│ │ └── example/
│ │ └── MyServlet.class
│ └── lib/ ← 依賴的 JAR 包
│ └── xxx.jar
├── META-INF/ ← 元資訊目錄
│ └── MANIFEST.MF
├── index.html ← 靜態資源
├── css/
├── js/
└── images/WEB-INF 目錄的特殊性
WEB-INF 目錄下的內容不能被用戶端直接訪問,只能通過 Servlet 轉發。這是保護組態文件和類文件安全的機制。
總結#
| 概念 | 作用 | 關鍵點 |
|---|---|---|
| HTTP 協定 | 通信格式 | 請求/回應格式、無狀態 |
| Servlet | 業務處理 | 生命週期、線程安全 |
| Filter | 請求過濾 | 責任鏈模式、可組合 |
| Listener | 事件監聽 | 生命週期事件、屬性變化 |
| Session | 狀態管理 | Cookie 關聯、分布式存儲 |