前言
JSP 和 Servlet 是 Java Web 開發的基石,理解其工作原理對排查問題、優化性能、設計架構至關重要。本文系統梳理 Servlet 生命周期、JSP 編譯執行流程、容器管理機制,並釐清二者在 MVC 中的職責分工。
一、Servlet 生命周期(核心考點)
Servlet 由容器(Tomcat/Jetty/Undertow)全權管理,開發者無法直接實例化。
四大階段
| 階段 | 觸發時機 | 核心方法 | 執行次數 | 典型工作 |
| 1. 加載實例化 | 容器啟動或首次請求 | 構造器 | 1 次 | 創建 Servlet 實例,僅執行一次 |
| 2. 初始化 | 實例化後,服務前 | init(ServletConfig) | 1 次 | 讀取配置、建立連接池、載入資源 |
| 3. 處理請求 | 每次客戶端請求 | service(req, resp) → doGet/doPost | N 次 | 業務邏輯處理,多線程並發執行 |
| 4. 銷毀 | 容器關閉/重載/移除 | destroy() | 1 次 | 釋放資源、關閉連接、持久化狀態 |
關鍵特性
- 單實例多線程:容器默認為每個 Servlet 類創建唯一實例,每請求分配一條線程調用 service()
- 線程安全責任在開發者:實例變量非線程安全,請勿在 Servlet 類中定義非 final 實例變量存儲請求狀態
- init/destroy 只執行一次:適合放置昂貴資源初始化/清理邏輯
流程圖解
客戶端請求
↓
容器檢查 Servlet 實例是否存在
├─ 不存在 → 加載類 → 實例化 → init() → service()
└─ 存在 → 直接 service()
↓
service() 根據 HTTP 方法分發:
doGet() / doPost() / doPut() / doDelete() ...
↓
生成響應返回客戶端
二、JSP 工作原理:本質是 Servlet
核心真相
JSP 最終會被容器翻譯為一個 Servlet 類,然後按 Servlet 生命周期執行。
翻譯編譯流程
| 階段 | 動作 | 產出 | 觸發條件 |
| 1. 翻譯 | JSP 引擎解析 .jsp 文件 | Servlet Java 源文件 (*_jsp.java) | 首次訪問 或 JSP 文件修改後 |
| 2. 編譯 | Java 編譯器編譯源文件 | 字節碼文件 (*_jsp.class) | 翻譯後自動觸發 |
| 3. 載入執行 | 類加載器載入字節碼 | Servlet 實例 | 編譯成功後 |
| 4. 執行 | 調用 service() 處理請求 | HTML 響應 | 每次請求 |
Tomcat 實際文件位置
%TOMCAT_HOME%/work/Catalina/localhost/你的項目目錄/org/apache/jsp/
├── index_jsp.java // 翻譯後的 Servlet 源碼
├── index_jsp.class // 編譯後的字節碼
└── ...
可直接查看生成的 Java 代碼理解 JSP 標簽/表達式/腳本片段如何轉換為 Servlet 邏輯。
關鍵差異:JSP vs 純 Servlet
| 維度 | JSP | Servlet |
| 本質 | 模板引擎(HTML 為主,嵌入 Java) | Java 類(代碼為主,輸出 HTML) |
| 擅長 | 視圖渲染、頁面展示、表單生成 | 請求分發、業務邏輯控制、數據處理 |
| 編寫體驗 | 前端友好,設計師可協作 | 純代碼,重構/測試友好 |
| 性能 | 首次訪問慢(翻譯編譯),後續同 Servlet | 啟動即加載,無首次延遲 |
三、MVC 架構中的職責分工
經典 Model 2 架構 (Struts/Spring MVC 思想雛形)
客戶端請求
↓
Controller (Servlet / DispatcherServlet)
├─ 接收參數、驗證、綁定
├─ 調用 Service 業務邏輯
├─ 操作 Model (POJO/DTO/VO)
└─ 選擇 View (JSP / Thymeleaf / FreeMarker)
↓
View 渲染 Model 數據生成 HTML
↓
響應客戶端
職責邊界
| 層 | 技術實現 | 核心職責 | 禁止事項 |
| Controller (C) | Servlet / @Controller | 請求接收、參數校驗、流程控制、視圖選擇 | 業務邏輯、SQL、複雜計算、直接輸出 HTML |
| Model (M) | JavaBean / Entity / DTO / Service / DAO | 業務邏輯、數據訪問、事務管理、規則引擎 | HTTP API 依賴、視圖邏輯、請求/響應操作 |
| View (V) | JSP / Thymeleaf / Vue/React (前後端分離) | 數據展示、頁面結構、樣式、用戶交互 | 業務邏輯、數據庫查詢、複雜 Java 代碼 |
四、容器管理細節(進階)
1. Servlet 加載時機控制
<servlet>
<servlet-name>MyServlet</servlet-name>
<servlet-class>com.demo.MyServlet</servlet-class>
<load-on-startup>1</load-on-startup> <!-- 數字越小越優先啟動 -->
</servlet>
>=0:容器啟動時實例化並 init()
<0 或缺省:首次請求時實例化
2. 多線程並發模型
- 默認:單實例,請求線程池(Tomcat 默認 200 線程)
- 實現
SingleThreadModel(已棄用):每請求創建實例,極其低效
- 現代替代:無狀態 Servlet + ThreadLocal / 請求作用域屬性存儲線程隔離數據
3. JSP 預編譯優化
// Maven 插件預編譯 JSP,構建階段發現語法錯誤,部署時無首次延遲
<plugin>
<groupId>org.apache.tomcat.maven</groupId>
<artifactId>tomcat7-maven-plugin</artifactId>
<configuration>
<compileJsp>true</compileJsp>
</configuration>
</plugin>
五、常見問題與最佳實踐
| 問題 | 原因 | 解決方案 |
| Servlet 實例變量線程安全 | 多線程共享實例變量 | 無狀態設計:只用局部變量、請求屬性、ThreadLocal |
| JSP 首次訪問極慢 | 翻譯+編譯耗時 | 預編譯、預熱腳本訪問關鍵頁面 |
| JSP 中大量 Java 代碼 | 違反 MVC 分層 | 遷移到 Controller/Service,JSP 僅用 EL/JSTL/標簽 |
| Servlet 3.0+ 註解不生效 | web.xml metadata-complete=true | 設置 metadata-complete="false" 或移除屬性 |
| 中文亂碼 | 編碼不一致 | 統一 UTF-8:Filter 設置編碼、JSP pageEncoding、DB 連接串 |
六、總結
- Servlet:容器管理單實例多線程生命周期,核心是 service() 分發 doXxx()
- JSP:本質是 Servlet,首次訪問經歷翻譯→編譯→載入→執行
- MVC 分工:Servlet 做 Controller(流程控制),JSP 做 View(視圖渲染)
- 現代開發:Spring MVC @Controller 替代原生 Servlet,Thymeleaf/Vue 替代 JSP
- 理解底層原理是排查類加載、線程安全、性能抖動等疑難雜癥的前提