Tomcat原始碼解析:Jsp檔案的編譯、實現
1.Jsp簡介
jsp(java server page),其根本是一個簡化的Servlet技術,是一種動態網頁技術標準。
它是在傳統的網頁HTML頁面中插入java程式碼段,從而形成jsp檔案,字尾為.jsp。
jsp同Servlet一樣,是在服務端執行,通常返回給客戶端的是一個HTML檔案。
這種動態網頁技術,主要目的是將邏輯從Servlet中分離,jsp側重於顯示
2.Jsp處理方式
上文說了,Jsp本質就是Servlet,所以java處理Jsp的方式基本同Servlet一樣。
java是一門編譯型語言,因為應用伺服器(tomcat等)首先需要將Jsp頁面轉換為一個標準java類檔案,然後進行編譯、載入並例項化。
編譯後的java類是一個Servlet實現,負責將我們在jsp頁面中編寫的內容輸出到客戶端
1)Jsp頁面採用單獨的類載入器
因此重新編譯不會導致整個應用重新載入,這也是我們可以在執行狀態更新Jsp頁面的原因
2)提升效能方式
應用伺服器會對Jsp類和例項進行快取,並定時檢測Jsp頁面的更新情況,如發生變更,將會重新編譯
3.Jsp編譯(執行時編譯)
所謂執行時編譯:就是tomcat並不會再啟動web應用時自動編譯Jsp檔案,而是在客戶端第一次請求時才編譯需要訪問的Jsp檔案
編譯過程分為:
1)獲取Jsp檔案路徑
預設將HttpServletRequest.getServletPath+HttpServletRequest.getPathInfo作為jsp路徑
注意:還有其他兩種方式,下面會通過原始碼來分析
2)根據Jsp檔案構造JspServletWrapper檔案
JspServletWrapper為Jsp引擎的核心,它負責編譯、載入Jsp檔案並完成請求處理。每個Jsp頁面對應一個JspServletWrapper例項。Tomcat會快取JspServletWrapper物件以提升系統性能
3)呼叫Servlet的方法完成請求處理
JspServletWrapper判斷當前是否首次載入,如果是,則進行編譯;如果不是,則直接呼叫Servlet的方法進行業務處理
4)編譯結果處理
通常預設情況下,會存放在%CATALINA_HOME%/work/Engine/Host(一般為localhost)/Context(應用名稱)目錄下
當然使用者也可以通過配置的方式來自定義目錄:
// 配置scratchdir ,該引數在預設的Server專案中web.xml中可以找到
<context-param>
<param-name>scratchdir</param-name>
<param-value>web-app/tm/jsp/</param-value>
</context-param>
4.通過原始碼來分析一下上述Jsp編譯的過程
Jsp本質上就是Servlet
我們建立的是一個.jsp檔案,但應用伺服器真正使用的是一個Servlet類,是一個.java檔案,那麼在這個過程中究竟發生了什麼呢?
首先有一個預設的知識點:tomcat在預設的web.xml中配置了一個org.apache.jasper.servlet.JspServlet,用於處理所有.jsp或者.jspx結尾的請求,該Servlet實現即為執行時編譯的入口。
下面我們就來看下這個類
5.預設web.xml的觀察
1)建立SpringMVC專案
筆者建立了一個SpringMVC專案,具體過程不表
然後建立一個Controller類,請求路徑為/mvc/hello,返回hello,指向一個jsp檔案(hello.jsp),同時在src/main/webapp/WEB-INF/jsp/下建立hello.jsp。
在當前IDE關聯tomcat,並將該web專案(命名為springweb)新增到tomcat中。
我們可以在IDE中看到一個Server專案,這個是自動建立的,如下所示
2)觀察web.xml檔案
該檔案是tomcat的預設web.xml,我們來看下其主要的幾個項
* DefaultServlet(預設的Servlet,當請求找不到mapping時,就會轉發到這)
<!-- The default servlet for all web applications, that serves static -->
<!-- resources. It processes all requests that are not mapped to other -->
<!-- servlets with servlet mappings (defined either here or in your own -->
<!-- web.xml file). This servlet supports the following initialization -->
<!-- parameters (default values are in square brackets): -->
<servlet>
<servlet-name>default</servlet-name>
<servlet-class>org.apache.catalina.servlets.DefaultServlet</servlet-class>
<init-param>
<param-name>debug</param-name>
<param-value>0</param-value>
</init-param>
<init-param>
<param-name>listings</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
注意:讀者可以仔細閱讀一下相關原始碼,可以發現,裡面基本做了所有的異常處理,403、404...
* JspServlet(處理.jsp)
<!-- The JSP page compiler and execution servlet, which is the mechanism -->
<!-- used by Tomcat to support JSP pages. Traditionally, this servlet -->
<!-- is mapped to the URL pattern "*.jsp". This servlet supports the -->
<!-- following initialization parameters (default values are in square -->
<servlet>
<servlet-name>jsp</servlet-name>
<servlet-class>org.apache.jasper.servlet.JspServlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
* welcome-list(預設的歡迎頁面)
<!-- ==================== Default Welcome File List ===================== -->
<!-- When a request URI refers to a directory, the default servlet looks -->
<!-- for a "welcome file" within that directory and, if present, to the -->
<!-- corresponding resource URI for display. -->
<!-- If no welcome files are present, the default servlet either serves a -->
<!-- directory listing (see default servlet configuration on how to -->
<!-- customize) or returns a 404 status, depending on the value of the -->
<!-- listings setting. -->
<!-- -->
<!-- If you define welcome files in your own application's web.xml -->
<!-- deployment descriptor, that list *replaces* the list configured -->
<!-- here, so be sure to include any of the default values that you wish -->
<!-- to use within your application. -->
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>index.htm</welcome-file>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
6.org.apache.jasper.servlet.JspServlet原始碼分析
1)類結構
// The JSP engine (a.k.a Jasper)
public class JspServlet extends HttpServlet implements PeriodicEventListener {
可以看到,JspServlet本質上也是一個Servlet,也符合Servlet的一系列使用規範。
通過上面預設web.xml的分析可以看到,應用伺服器啟動時就會載入該類,並呼叫其init方法
2)JspServlet.service()方法
主要的業務處理都在這,我們重點來看下這個方法
@Override
public void service (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.jspFile可以通過配置中的init-param來構建(一般來說,我們不配置這個欄位)
String jspUri = jspFile;
if (jspUri == null) {
// 2.判斷請求中的javax.servlet.include.servlet_path屬性是否為空,不為空則設定為jspUri(一般來說,不配置該欄位)
jspUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_SERVLET_PATH);
if (jspUri != null) {
String pathInfo = (String) request.getAttribute(
RequestDispatcher.INCLUDE_PATH_INFO);
if (pathInfo != null) {
jspUri += pathInfo;
}
} else {
// 3.HttpServletRequest.getServletPath+HttpServletRequest.getPathInfo作為jsp路徑
jspUri = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
jspUri += pathInfo;
}
}
}
// 通過上面1-3的分析,我們確認了jsp的路徑
...
try {
// 4.檢查是否預編譯,如果沒有編譯過,則在serviceJSPFile方法會先編譯該Jsp
boolean precompile = preCompile(request);
// 5.呼叫jsp對應的Servlet.service()方法
serviceJspFile(request, response, jspUri, precompile);
} catch (RuntimeException e) {
throw e;
} ...
}
3)serviceJspFile(request, response, jspUri, precompile)
private void serviceJspFile(HttpServletRequest request,
HttpServletResponse response, String jspUri,
boolean precompile)
throws ServletException, IOException {
// 1.判斷是否已經載入過,沒有則載入
// 載入的主要方式也就是包裝一個JspServletWrapper,放入到rctxt中
JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
if (wrapper == null) {
synchronized(this) {
wrapper = rctxt.getWrapper(jspUri);
if (wrapper == null) {
// Check if the requested JSP page exists, to avoid
// creating unnecessary directories and files.
if (null == context.getResource(jspUri)) {
handleMissingResource(request, response, jspUri);
return;
}
wrapper = new JspServletWrapper(config, options, jspUri,
rctxt);
rctxt.addWrapper(jspUri,wrapper);
}
}
}
try {
// 2.業務處理
wrapper.service(request, response, precompile);
} catch (FileNotFoundException fnfe) {
handleMissingResource(request, response, jspUri);
}
}
總結:
我們將Jsp資訊封裝為JspServletWrapper,然後將業務處理交給JspServletWrapper處理,下面我們就來看下JspServletWrapper是如何處理的
7.org.apache.jasper.servlet.JspServletWrapper業務處理
service方法主要內容如下:
// JspServletWrapper.service(request, response, precompile)
public void service(HttpServletRequest request,
HttpServletResponse response,
boolean precompile)
throws ServletException, IOException, FileNotFoundException {
Servlet servlet;
try {
...
// 1.如果是第一次訪問service訪問,則需要先編譯Jsp為Servlet
if (options.getDevelopment() || firstTime ) {
synchronized (this) {
firstTime = false;
ctxt.compile();
}
} else {
if (compileException != null) {
// Throw cached compilation exception
throw compileException;
}
}
// 2.獲取對應的Servlet
servlet = getServlet();
} catch (ServletException ex) {
...
}
try {
// 3.對已經載入的Jsp進行處理,如果長時間不用則刪除之
if (unloadAllowed) {
synchronized(this) {
if (unloadByCount) {
if (unloadHandle == null) {
unloadHandle = ctxt.getRuntimeContext().push(this);
} else if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) {
ctxt.getRuntimeContext().makeYoungest(unloadHandle);
lastUsageTime = System.currentTimeMillis();
}
} else {
if (lastUsageTime < ctxt.getRuntimeContext().getLastJspQueueUpdate()) {
lastUsageTime = System.currentTimeMillis();
}
}
}
}
// 4.真正的業務處理,交由具體的Servlet
if (servlet instanceof SingleThreadModel) {
// sync on the wrapper so that the freshness
// of the page is determined right before servicing
synchronized (this) {
servlet.service(request, response);
}
} else {
servlet.service(request, response);
}
} catch (UnavailableException ex) {
...
}
...
}
下面我們逐步來看下這幾個方法
1)JspCompilationContext.compile(),建立Jsp compile,主要將Jsp轉換為java類,具體過程不表
public void compile() throws JasperException, FileNotFoundException {
// 主要在這裡,建立Compile,預設建立org.apache.jasper.compiler.JDTCompiler
createCompiler();
if (jspCompiler.isOutDated()) {
...
}
}
2)getServlet()獲取jsp對應的Servlet
public Servlet getServlet() throws ServletException {
// 已經載入過的不會再次載入,直接返回即可
if (reload) {
synchronized (this) {
// Synchronizing on jsw enables simultaneous loading
// of different pages, but not the same page.
if (reload) {
// This is to maintain the original protocol.
destroy();
final Servlet servlet;
try {
// 1.使用InstanceManager生成對應的Servlet類
// 本例中的hello.jsp 生成 org.apache.jsp.WEB_002dINF.jsp.hello_jsp
InstanceManager instanceManager = InstanceManagerFactory.getInstanceManager(config);
servlet = (Servlet) instanceManager.newInstance(ctxt.getFQCN(), ctxt.getJspLoader());
} catch (Exception e) {
Throwable t = ExceptionUtils
.unwrapInvocationTargetException(e);
ExceptionUtils.handleThrowable(t);
throw new JasperException(t);
}
// 2.呼叫servlet.init方法初始化
servlet.init(config);
if (!firstTime) {
ctxt.getRuntimeContext().incrementJspReloadCount();
}
theServlet = servlet;
reload = false;
// Volatile 'reload' forces in order write of 'theServlet' and new servlet object
}
}
}
return theServlet;
}
3)servlet.service(request, response)到這裡就將請求轉發給特定的Servlet去處理了
總結:
最終tomcat編譯器將hello.jsp編譯成了hello_jsp.java,該類繼承了HttpServlet。
所以,正驗證了開頭我們說的:Jsp本質上就是Servlet
8.hello_jsp.java展示
最後我們來展示一下hello.jsp以及生成後的hello_jsp.java類
1)hello.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD//XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
<head>
<meta http-equiv="content-type" content="text/html;charset=utf-8"/>
<title>九九乘法表</title>
</head>
<body>
<br/>
<form id="form1" name="form1" method="post" action="result.jsp">
<p align="center">請輸入兩個自然數給您列印乘法表</p>
<p align="center">要求:startNumber <endNumber <br/></p>
<table width="350" border="1" align="center" cellpadding="0"
cellspacing="0" bgcolor="#aaccdd" bordercolor="#cccccc">
<tr>
<td width="101">startNumber:</td>
<td width="113">
<label>
<input name="s" type="text" id="textfield" size="15" maxlength="8" height="20"/>
</label>
</td>
<td width="68"> <br/></td>
</tr>
<tr>
<td>endNumber</td>
<td>
<label>
<input name="e" type="text" id="textfield2" size="15" maxlength="8" height="20"/>
</label>
</td>
<td> <br/></td>
</tr>
<tr>
<td> </td>
<td>
<label>
<input type="submit" name="button" id="button" value="submit"/>
<input name="button2" type="reset" id="button2" value="reset"/>
</label>
</td>
<td> </td>
</tr>
</table>
</form>
</body>
</html>
2)hello_jsp.java(目錄為%CATALINA_HOME%\work\Catalina\localhost\springweb\org\apache\jsp\WEB_002dINF\jsp)
/*
* Generated by the Jasper component of Apache Tomcat
* Version: Apache Tomcat/8.5.31
* Generated at: 2018-11-28 01:27:32 UTC
* Note: The last modified time of this file was set to
* the last modified time of the source file after
* generation to assist with modification tracking.
*/
package org.apache.jsp.WEB_002dINF.jsp;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
public final class hello_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent,
org.apache.jasper.runtime.JspSourceImports {
private static final javax.servlet.jsp.JspFactory _jspxFactory =
javax.servlet.jsp.JspFactory.getDefaultFactory();
private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;
private static final java.util.Set<java.lang.String> _jspx_imports_packages;
private static final java.util.Set<java.lang.String> _jspx_imports_classes;
static {
_jspx_imports_packages = new java.util.HashSet<>();
_jspx_imports_packages.add("javax.servlet");
_jspx_imports_packages.add("javax.servlet.http");
_jspx_imports_packages.add("javax.servlet.jsp");
_jspx_imports_classes = null;
}
private volatile javax.el.ExpressionFactory _el_expressionfactory;
private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;
public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
return _jspx_dependants;
}
public java.util.Set<java.lang.String> getPackageImports() {
return _jspx_imports_packages;
}
public java.util.Set<java.lang.String> getClassImports() {
return _jspx_imports_classes;
}
public javax.el.ExpressionFactory _jsp_getExpressionFactory() {
if (_el_expressionfactory == null) {
synchronized (this) {
if (_el_expressionfactory == null) {
_el_expressionfactory = _jspxFactory.getJspApplicationContext(getServletConfig().getServletContext()).getExpressionFactory();
}
}
}
return _el_expressionfactory;
}
public org.apache.tomcat.InstanceManager _jsp_getInstanceManager() {
if (_jsp_instancemanager == null) {
synchronized (this) {
if (_jsp_instancemanager == null) {
_jsp_instancemanager = org.apache.jasper.runtime.InstanceManagerFactory.getInstanceManager(getServletConfig());
}
}
}
return _jsp_instancemanager;
}
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;charset=UTF-8");
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("\r\n");
out.write("<!DOCTYPE html PUBLIC \"-//W3C//DTD//XHTML 1.0 Transitional//EN\"\r\n");
out.write("\"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\">\r\n");
out.write("<html>\r\n");
out.write("<head>\r\n");
out.write(" <meta http-equiv=\"content-type\" content=\"text/html;charset=utf-8\"/>\r\n");
out.write(" <title>九九乘法表</title>\r\n");
out.write("</head>\r\n");
out.write("<body>\r\n");
out.write("<br/>\r\n");
out.write("<form id=\"form1\" name=\"form1\" method=\"post\" action=\"result.jsp\">\r\n");
out.write(" <p align=\"center\">請輸入兩個自然數給您列印乘法表</p>\r\n");
out.write(" <p align=\"center\">要求:startNumber <endNumber <br/></p>\r\n");
out.write(" <table width=\"350\" border=\"1\" align=\"center\" cellpadding=\"0\"\r\n");
out.write(" cellspacing=\"0\" bgcolor=\"#aaccdd\" bordercolor=\"#cccccc\">\r\n");
out.write(" <tr>\r\n");
out.write(" <td width=\"101\">startNumber:</td>\r\n");
out.write(" <td width=\"113\">\r\n");
out.write(" <label>\r\n");
out.write(" <input name=\"s\" type=\"text\" id=\"textfield\" size=\"15\" maxlength=\"8\" height=\"20\"/>\r\n");
out.write(" </label>\r\n");
out.write(" </td>\r\n");
out.write(" <td width=\"68\"> <br/></td>\r\n");
out.write(" </tr>\r\n");
out.write(" <tr>\r\n");
out.write(" <td>endNumber</td>\r\n");
out.write(" <td>\r\n");
out.write(" <label>\r\n");
out.write(" <input name=\"e\" type=\"text\" id=\"textfield2\" size=\"15\" maxlength=\"8\" height=\"20\"/>\r\n");
out.write(" </label>\r\n");
out.write(" </td>\r\n");
out.write(" <td> <br/></td>\r\n");
out.write(" </tr>\r\n");
out.write(" <tr>\r\n");
out.write(" <td> </td>\r\n");
out.write(" <td>\r\n");
out.write(" <label>\r\n");
out.write(" <input type=\"submit\" name=\"button\" id=\"button\" value=\"submit\"/>\r\n");
out.write(" <input name=\"button2\" type=\"reset\" id=\"button2\" value=\"reset\"/>\r\n");
out.write(" </label>\r\n");
out.write(" </td>\r\n");
out.write(" <td> </td>\r\n");
out.write(" </tr>\r\n");
out.write(" </table>\r\n");
out.write("</form>\r\n");
out.write("</body>\r\n");
out.write("</html>");
} catch (java.lang.Throwable t) {
if (!(t instanceof javax.servlet.jsp.SkipPageException)){
out = _jspx_out;
if (out != null && out.getBufferSize() != 0)
try {
if (response.isCommitted()) {
out.flush();
} else {
out.clearBuffer();
}
} catch (java.io.IOException e) {}
if (_jspx_page_context != null) _jspx_page_context.handlePageException(t);
else throw new ServletException(t);
}
} finally {
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
參考:Tomcat架構解析(劉光瑞)