JSP 生命週期


JSP 與 Servlet 是一體的兩面。基本上 Servlet 作的到的功能,使用 JSP 也都作的到,因為 JSP 最後還是會被容器轉譯為 Servlet 原始碼、自動編譯為 .class 檔案、載入 .class 檔案然後生成 Servlet 物件。

在〈第一個 JSP〉中曾經略為介紹過 JSP 與 Servlet 的關係,並舉了一個 JSP 網頁作為範例:

<!DOCTYPE html>
<html>
    <head>
        <title>Hello! World!</title>
    </head>
    <body>
        <h1>Hello! World!</h1>
    </body>
</html>

JSP 網頁最後還是成爲 Servlet,在第一次請求 JSP 時,容器會進行轉譯、編譯與載入的動作(所以第一次請求JSP頁面 會慢許多才得到回應)。以上面這個 JSP 為例,若使用 Tomcat 9 作為 Web 容器,最後由容器轉譯後的 Servlet 類別如下所示:

package org.apache.jsp;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;

public final class helloworld_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements org.apache.jasper.runtime.JspSourceDependent,
                 org.apache.jasper.runtime.JspSourceImports {

  ...略

  public void _jspInit() {
    // 略...
  }

  public void _jspDestroy() {
  }


  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {

    final java.lang.String _jspx_method = request.getMethod();
    if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET POST or HEAD");
      return;
    }

    final javax.servlet.jsp.PageContext pageContext;
    javax.servlet.http.HttpSession session = null;
    final javax.servlet.ServletContext application;
    final javax.servlet.ServletConfig config;
    javax.servlet.jsp.JspWriter out = null;
    final java.lang.Object page = this;
    javax.servlet.jsp.JspWriter _jspx_out = null;
    javax.servlet.jsp.PageContext _jspx_page_context = null;


    try {
      response.setContentType("text/html");
      pageContext = _jspxFactory.getPageContext(this, request, response,
                null, true, 8192, true);
      _jspx_page_context = pageContext;
      application = pageContext.getServletContext();
      config = pageContext.getServletConfig();
      session = pageContext.getSession();
      out = pageContext.getOut();
      _jspx_out = out;

      out.write("<!DOCTYPE html>\r\n");
      out.write("<html>\r\n");
      out.write("    <head>\r\n");
      out.write("        <title>Hello! World!</title>\r\n");
      out.write("    </head>\r\n");
      out.write("    <body>\r\n");
      out.write("        <h1>Hello! World!</h1>\r\n");
      out.write("    </body>\r\n");
      out.write("</html>");
    } catch (java.lang.Throwable t) {

      ...略

    } finally {

      ...略

    }
  }
}

這邊列出的原始碼,比〈第一個 JSP〉多了一些內容,請將目光集中在 _jspInit()_jspDestroy()_jspService() 三個方法。

從 Java EE 7 的 JSP 2.3 開始,JSP 只接受 GETPOSTHEAD 請求,這可以在 _jspService() 一開頭就看到:

final java.lang.String _jspx_method = request.getMethod();
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
  response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSPs only permit GET POST or HEAD");
  return;
}

(我找不到為何規格書上增加了這個限制的討論,猜想是為了讓 JSP 更專門用在呈現資料,因而限縮了可用的 HTTP 語義。)

在撰寫 Servlet 時,你可以重新定義 init() 方法作 Servlet 的初始化,重新定義 destroy() 進行 Servlet 銷毀前的收尾工作。JSP 在轉譯為 Servlet 並載入容器生成物件之後,會呼叫 _jspInit() 方法進行初始化工作,而銷毀前則是呼叫 _jspDestroy() 方法進行善後工作 在 Servlet 中,每個請求到來時,容器會呼叫 service() 方法,而在 JSP 轉譯為 Servlet 後,請求的到來則是呼叫 _jspService() 方法。

至於為何是分別呼叫 _jspInit()_jspDestroy()_jspService() 三個方法,如果是在 Tomcat 9 中,由於轉譯後的 Servlet 是繼承自 HttpJspBase 類別,所以開啟該類別的原始碼,你就可以發現為什麼。

package org.apache.jasper.runtime;

import java.io.IOException;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.HttpJspPage;

import org.apache.jasper.compiler.Localizer;

public abstract class HttpJspBase extends HttpServlet implements HttpJspPage {

    private static final long serialVersionUID = 1L;

    protected HttpJspBase() {
    }

    @Override
    public final void init(ServletConfig config)
        throws ServletException
    {
        super.init(config);
        jspInit();
        _jspInit();
    }

    @Override
    public String getServletInfo() {
        return Localizer.getMessage("jsp.engine.info");
    }

    @Override
    public final void destroy() {
        jspDestroy();
        _jspDestroy();
    }

    @Override
    public final void service(HttpServletRequest request, HttpServletResponse response)
        throws ServletException, IOException
    {
        _jspService(request, response);
    }

    @Override
    public void jspInit() {
    }

    public void _jspInit() {
    }

    @Override
    public void jspDestroy() {
    }

    protected void _jspDestroy() {
    }

    @Override
    public abstract void _jspService(HttpServletRequest request,
                                     HttpServletResponse response)
        throws ServletException, IOException;
}

從原始碼中可以看到,Servlet 的 init() 中呼叫了 jspInit()_jspInit(),其中 _jspInit() 是轉譯後的 Servlet 會重新定義,之後會學到如何在 JSP 中定義方法,如果你想要在 JSP 網頁載入執行時作些初始動作,則可以重新定義 jspInit() 方法。

同樣地,Servlet 的 destroy() 中呼叫了 jspDestroy()_jspDestroy() 方法,其中 _jspDestroy() 方法是轉譯後的 Servlet 會重新定義,如果在想要作一些收尾動作,則可以重新定義 jspDestroy() 方法。

當請求到來而容器呼叫 service() 方法時,當中又呼叫了 _jspService() 方法,也因此你在 JSP 轉譯後的 Servlet 原始碼中,會看到你所定義的程式碼是轉譯在 _jspService() 之中。

之後就會學到如何於 JSP 中定義方法。注意到 _jspInit()_jspDestroy()_jspService() 方法名稱上有個底線,表示這些方法是由容器轉譯時維護,你不應該重新定義這些方法。如果想要作些 JSP 初始化或收尾動作,則應定義 jspInit()jspDestroy() 方法。