Tomcat源码解析:Jsp文件的编译、实现

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/qq_26323323/article/details/84849347

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架构解析(刘光瑞)

猜你喜欢

转载自blog.csdn.net/qq_26323323/article/details/84849347