Tomcat--Jasper概述、JSP编译方式(运行时编译,预编译)源码跟踪、JSP编译原理

Jasper概述

Jasper模块是Tomcat的JSP核心引擎,JSP本质上是一个Servlet。Tomcat使用Jasper对JSP语法进行解析,生成Servlet并生成Class字节码,用户在进行访问jsp时,会访问Servlet,最终将访问结果直接响应在浏览器端。另外,在运行的时候,Jasper还会检测JSP文件是否修改,如果修改,则会重新编译JSP文件。

JSP编译方式

运行时编译

Tomcat并不会在启动Web应用的时候自动编译JSP文件,而是在客户端第一次请求时,才编译需要访问的JSP文件。

准备一个web应用,编写JSP:

<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.text.DateFormat" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.Date" %>

<html>
  <head>
    <title>$Title$</title>
  </head>
  <body>

  <%
    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String format = dateFormat.format(new Date());
  %>
  Hello, Java Server Page...
  <br/>

  <%= format%>
  </body>
</html>

在这里插入图片描述

编译过程:

Tomcat在默认的web.xml中配置了一个org.apache.jasper.servlet.JspServlet,用于处理所有的.jsp或.jspx结尾的请求,该Servlet是实现运行时编译的入口:

    <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>
    <!-- The mappings for the JSP servlet -->
    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>

JspServlet处理流程图:

在这里插入图片描述

源码跟踪:

接受到对jsp的访问请求后,会最先到达JspServlet的service()方法中:
在这里插入图片描述
然后获取JSP的路径:
在这里插入图片描述
然后判断是否为预编译请求:
在这里插入图片描述
然后执行serviceJspFile,在serviceJspFile方法中获取了一个JspServletWrapper:
在这里插入图片描述
然后调用它的service()方法:
在这里插入图片描述

在service()方法中,调用了JspCompilationContext的compile()方法:进行编译
在这里插入图片描述
在JspCompilationContext的compile()方法中又调用了jspCompiler.compile();
在这里插入图片描述

最终到达Complier类中的compile(boolean compileClass, boolean jspcMode)方法:端点处做了2个事情,生成java文件、生成class文件
在这里插入图片描述
已经生成java文件,准备生成class文件
在这里插入图片描述
在这里插入图片描述

java文件和class文件生成后,回到JspServletWrapper类中,调用getServlet()方法:加载jsp对应的servlet
在这里插入图片描述
在这里插入图片描述
然后执行service()方法:
在这里插入图片描述

查看index_jsp源码:在_jspService()方法中写出响应:
在这里插入图片描述
在这里插入图片描述

编译结果

  1. 如果在tomcat/conf/web.xml中配置了参数scratchdir,则jsp的编译后的结果就会存储在该目录下:
<init-param>
	<param-name>scratchdir</param-name>
	<param-value>D:/tmp/jsp/</param-value>
</init-param>
  1. 如果没有配置该项,则会将编译后的结果,存储在Tomcat安装目录下的work/Catalina(Engine名称)/localhost(Host名称)/Context命名 假设项目名称为 jsp_demo_01,默认的目录为:work/Catalina/localhost/jsp_demo_01

  2. 如果使用的是IDEA开发工具继承Tomcat访问web工程中的jsp,编译后的结果存放在:

c:\Users\Administrator\.IntelliJIdea2019.1\system\tomcat\_project_tomcat\wor\Catalina\localost\jsp_demo_01_sar_exploded\org\apache\jsp

预编译

除了运行时编译,还可以直接在Web应用启动时,一次性将Web应用用的所有JSP页面一次性编译完成。在这种情况下,Web应用运行过程中,便可以不必在进行实时编译,而是直接调用JSP页面对应的Sevlet完成请求处理,从而提升系统性能。

Tomcat提供了一个Shell程序JspC,用于支持JSP预编译,而且在Tomcat的安装目录下提供了一个catalina-tasks.xml文件声明了Tomcat支持的Ant任务,因此,可以很容易使用Ant来执行JSP预编译。(如果想要使用这种方式,必须得确保在此之前已经下载并安装了Apache Ant)。

JSP编译原理

上面编译后的index_jsp源码如下:

/*
 * Generated by the Jasper component of Apache Tomcat
 * Version: Apache Tomcat/9.0.x-dev
 * Generated at: 2020-03-27 09:25:56 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;

import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;

public final class index_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 = new java.util.HashSet<>();
    _jspx_imports_classes.add("java.util.Date");
    _jspx_imports_classes.add("java.text.SimpleDateFormat");
    _jspx_imports_classes.add("java.text.DateFormat");
  }

  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 {

    if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
      final java.lang.String _jspx_method = request.getMethod();
      if ("OPTIONS".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        return;
      }
      if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
        response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
        response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSP 只å
è®¸ GET、POST 或 HEAD。Jasper 还å
è®¸ OPTIONS");
        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("\n");
      out.write("\n");
      out.write("\n");
      out.write("\n");
      out.write("\n");
      out.write("\n");
      out.write("<html>\n");
      out.write("  <head>\n");
      out.write("    <title>$Title$</title>\n");
      out.write("  </head>\n");
      out.write("  <body>\n");
      out.write("\n");
      out.write("  ");

    DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    String format = dateFormat.format(new Date());
  
      out.write("\n");
      out.write("  Hello, Java Server Page...\n");
      out.write("  <br/>\n");
      out.write("\n");
      out.write("  ");
      out.print( format);
      out.write("\n");
      out.write("  </body>\n");
      out.write("</html>\n");
    } 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);
    }
  }
}

由编译后的源码解读,分析出一下几点:

  1. 其类名为index_jsp,继承自org.apache.jasper.runtime.HttpJspBase,该类是HttpServlet的子类,所以jsp本质就是一个Servlet。
    在这里插入图片描述

  2. 通过属性_jspx_dependants保存了当前JSP页面依赖的资源,包含引入的外部JSP页面,导入的标签、标签所在的jar包等,便于后续处理过程中使用(如重新编译检测,因此它以Map的形式保存了每个资源的啥功上次修改时间)

  3. 通过属性_jspx_imports_packages存放导入的java包,默认导入javax_servlet、javax.servlet.http、javax.servlet.jsp

  4. 通过属性 _jspx_imports_classes存放导入的类,通过import指令导入的DateFormat、SimpleDateFormat、Date都会包含在集合中。_jspx_imports_packages和_spx_imports_classes主要用于配置EL引擎上下文

  5. 请求处理由方法_jspService完成,而在父类HttpJspBash中的service方法通过模板方法模式调用了子类_jspService方法
    在这里插入图片描述
    在这里插入图片描述

  6. _jspService方法中定义了几个重要的局部变量:pageContext、Session、application、config、out、page。由于整个页面的输出由 _jspService方法完成,因此这些变量会对整个JSP页面生效。

  7. 指定文档类型的指令(page)最终装换为response.setContentType方法调用

  8. 对于每一行的静态内容(HTML),调用out.write输出

  9. 对于<% ... %>中的java代码,将直接转换为Servlet类中的代码。如果在Java代码中嵌入了静态文件,同样调用out.write输出。

编译流程

在这里插入图片描述
Compiler编译过程主要包含代码生成和编译两部分:

代码生成:

  1. Compiler通过一个PageInfo对象保存JSP页面编译过程中的各种配置,这些配置可能来源与Web应用初始化参数,也可能源于JSP页面的配置(如page,inclue)
  2. 调用ParserController解析指令节点,验证其是否合法,同时将配置信息保存到PageInfo中,用于控制代码生成
  3. 调用ParserController解析整个页面,由于是逐行解析,所以对于每一行会创建一个具体的Node对象。如静态文本(TemplateText)、Java代码(Scriptlet)、定制标签(CustomTag)、Include指令(IncludeDirective)。
  4. 验证除指令外所有节点的合法性,如脚本、定制标签、EL表达式等
  5. 收集除指令外其它节点的页面配置信息
  6. 编译并加锁当前JSP依赖的标签
  7. 对于JSP页面的EL表达式,生成对应的映射函数
  8. 生成JSP页面对应Servlet源代码

编译:

代码生成后,Compiler还会生成SMAP信息。如果配置生成SMAP信息,Compiler则会在编译阶段将SMAP信息写到Class文件中。

在编译阶段,Compiler的两个实现AntCompiler和JDTCompiler分别调用相关框架的API进行源代码编译

  • 对于AntCompiler来说:构造一个Ant的javac的任务完成编译;
  • 对于JDTCompiler来说,调用org.eclipse.jdt.internal.compiler.Compiler完成编译
发布了892 篇原创文章 · 获赞 2314 · 访问量 31万+

猜你喜欢

转载自blog.csdn.net/cold___play/article/details/105143770