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 只接受 GET
、POST
、HEAD
請求,這可以在 _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()
方法。