1. 程式人生 > >Tomcat原始碼解析:Jsp檔案的編譯、實現

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 &lt;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">&nbsp;<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>&nbsp;<br/></td>
        </tr>
        <tr>
            <td>&nbsp;</td>
            <td>
                <label>
                    <input type="submit" name="button" id="button" value="submit"/>
                    <input name="button2" type="reset" id="button2" value="reset"/>
                </label>
            </td>
            <td>&nbsp;</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 &lt;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\">&nbsp;<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>&nbsp;<br/></td>\r\n");
      out.write("        </tr>\r\n");
      out.write("        <tr>\r\n");
      out.write("            <td>&nbsp;</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>&nbsp;</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架構解析(劉光瑞)