JSP 生命週期完整解析
JSP 本質是 Servlet。容器將 .jsp 翻譯為 _jspService() 方法的 Servlet 類別,生命週期遵循 Servlet 規格,增加翻譯/編譯階段。
四大階段
- 翻譯與編譯:
.jsp → .java → .class
- 初始化:載入類別、實例化、呼叫
jspInit()
- 請求處理:每次請求呼叫
_jspService(req, resp)
- 銷毀:呼叫
jspDestroy()、卸載類別
階段一:翻譯與編譯
觸發時機
- 首次請求 JSP
- JSP 檔案修改後 (時間戳更新)
- 容器啟動時預編譯 (配置
precompilation)
過程
- 解析 JSP 模板文字、指令、動作、Scriptlet、EL
- 生成 Servlet 原始碼 (
_jsp.java)
- 編譯為位元組碼 (
_jsp.class)
生成的 Servlet 結構
public class MyJsp extends HttpJspBase {
// 1. 靜態初始化區塊 (JSP 宣告 -> 欄位/方法)
private static final String CONST = "value";
public void jspInit() { ... }
public void jspDestroy() { ... }
// 2. _jspService 處理每個請求
public void _jspService(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 隱含物件初始化
PageContext pageContext = ...;
HttpSession session = ...;
ServletContext application = ...;
JspWriter out = ...;
// 模板文字 -> out.write("...")
// Scriptlet -> 直接插入 Java 程式碼
// Expression -> out.print(...)
// 動作元素 -> 對應 API 呼叫
}
}
階段二:初始化
- 容器載入 Servlet 類別
- 建立單一實例 (預設單例模式,
isThreadSafe=true)
- 呼叫
jspInit() — 只執行一次
自訂初始化
<%!
private DataSource dataSource;
public void jspInit() {
// 從 JNDI 取得連線池
try {
Context ctx = new InitialContext();
dataSource = (DataSource) ctx.lookup("java:comp/env/jdbc/mydb");
} catch (NamingException e) {
throw new RuntimeException(e);
}
}
public void jspDestroy() {
// 清理資源 (DataSource 通常由容器管理,此處僅示意)
}
%>
階段三:請求處理 (_jspService)
- 每個 HTTP 請求對應一次呼叫
- 多執行緒並發執行 (同一實例)
- 參數:
HttpServletRequest, HttpServletResponse
- 處理所有 HTTP 方法:GET, POST, PUT, DELETE 等
隱含物件在 _jspService 中的初始化順序
PageContext pageContext = JspFactory.getDefaultFactory().getPageContext(
this, request, response, null, true, 8192, true);
HttpSession session = pageContext.getSession();
ServletContext application = pageContext.getServletContext();
ServletConfig config = pageContext.getServletConfig();
JspWriter out = pageContext.getOut();
// page = this
// exception = null (除非是錯誤頁面)
階段四:銷毀
- 容器關閉、應用重新部署、JSP 移除時觸發
- 呼叫
jspDestroy() — 只執行一次
- 釋放資源:關閉連線、線程池、暫存檔案
- Servlet 實例失效,等待 GC
完整生命週期示範程式
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%!
private int initCount = 0;
private int serviceCount = 0;
private int destroyCount = 0;
public void jspInit() {
initCount++;
System.out.println("jspInit() 執行,第 " + initCount + " 次初始化");
}
public void jspDestroy() {
destroyCount++;
System.out.println("jspDestroy() 執行,第 " + destroyCount + " 次銷毀");
}
%>
<%
serviceCount++;
System.out.println("_jspService() 執行,第 " + serviceCount + " 次請求處理");
%>
<!DOCTYPE html>
<html><head><meta charset="UTF-8"><title>JSP 生命週期示範</title></head>
<body>
<h2>JSP 生命週期計數器</h2>
<table border="1">
<tr><th>階段</th><th>次數</th></tr>
<tr><td>初始化 (jspInit)</td><td><%= initCount %></td></tr>
<tr><td>請求處理 (_jspService)</td><td><%= serviceCount %></td></tr>
<tr><td>銷毀 (jspDestroy)</td><td><%= destroyCount %></td></tr>
</table>
<p>重新整理頁面觀察 serviceCount 增加,重新部署觀察 init/destroy 變化。</p>
</body></html>
關鍵特性總結
| 特性 | 說明 |
| 單例多執行緒 | 一個 JSP 類別對應一個實例,並發請求共享,需注意執行緒安全 |
| 翻譯延遲 | 首次請求較慢 (編譯),後續請求快 (直接呼叫 _jspService) |
| 自動重載 | 修改 .jsp 檔案後,容器偵測時間戳變化自動重新翻譯編譯 |
| 預編譯 | 生產環境建議建置時預編譯 (Maven jspc plugin),避免首次請求延遲 |
| 錯誤頁面 | isErrorPage="true" 的 JSP 會在 _jspService 捕獲例外並設定 exception 隱含物件 |
與 Servlet 生命週期對照
| JSP | Servlet | 備註 |
| 翻譯/編譯 | 編譯 .java | JSP 多一步翻譯 |
| jspInit() | init(ServletConfig) | JSP 隱含 ServletConfig |
| _jspService(req,resp) | service(req,resp) | JSP 固定方法名 |
| jspDestroy() | destroy() | 對應 |
最佳實踐
- 生產環境啟用預編譯:Maven
<plugin><groupId>org.apache.tomcat.maven</groupId><artifactId>tomcat7-maven-plugin</artifactId>...</plugin> 或 Ant jspc。
- 避免在 jspInit/jspDestroy 做耗時操作:影響啟動/關閉速度,改用
ServletContextListener。
- 靜態資源分離:CSS/JS/圖片勿放在 JSP 內,由 Nginx/CDN 直接服務。
- 監控編譯錯誤:CI/CD 階段驗證 JSP 編譯通過。
- 理解單例模式:請勿在 Declaration 定義非最終欄位,改用範圍屬性。