Tomcat卷三---Jasper引擎

2022-05-09 13:59:00 浏览数 (1)

Tomcat卷三---Jasper引擎

  • Jasper 简介
  • JSP 编译方式
    • 运行时编译
    • 编译过程
    • 编译结果
    • 预编译
    • JSP源码流程
    • JSP编译原理
      • 代码分析
      • 编译流程

Jasper 简介

对于基于JSP 的web应用来说,我们可以直接在JSP页面中编写 Java代码,添加第三方的 标签库,以及使用EL表达式。但是无论经过何种形式的处理,最终输出到客户端的都是 标准的HTML页面(包含js ,css…),并不包含任何的java相关的语法。 也就是说, 我 们可以把jsp看做是一种运行在服务端的脚本。 那么服务器是如何将 JSP页面转换为 HTML页面的呢?

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

JSP 编译方式

运行时编译

Tomcat 并不会在启动Web应用的时候自动编译JSP文件, 而是在客户端第一次请求时, 才编译需要访问的JSP文件。 创建一个 web项目, 并编写JSP代码 :

代码语言:javascript复制
<%@ page import="java.text.DateFormat" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.Date" %>
<%@ page contentType="text/html;charset=UTF‐8" language="java" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<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 实现即是运行时编译的入口。

JspServlet 处理流程图:

编译结果

1) 如果在 tomcat/conf/web.xml 中配置了参数scratchdir , 则jsp编译后的结果,就会 存储在该目录下 。

2) 如果没有配置该选项, 则会将编译后的结果,存储在Tomcat安装目录下的 work/Catalina(Engine名称)/localhost(Host名称)/Context名称 。 假设项目名称为 jsp_demo 01。

3) 如果使用的是 IDEA 开发工具集成Tomcat 访问web工程中的jsp , 编译后的结果, 存放在 :

代码语言:javascript复制
C:UsersAdministrator.IntelliJIdea2019.1systemtomcat_project_tomcatw orkCatalinalocalhostjsp_demo_01_war_explodedorgapachejsp

预编译

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

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

JSP源码流程

代码语言:javascript复制
//如果访问的是JSP页面请求,得到的就是JSPservelt
 servlet = wrapper.allocate();
代码语言:javascript复制
//生成过滤器链
ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
代码语言:javascript复制
//真正进行过滤操作
filterChain.doFilter(request.getRequest(), response.getResponse());
代码语言:javascript复制
doFilter方法中最后调用internalDoFilter方法,真正执行过滤操作,然后调用servlet的service方法
代码语言:javascript复制
//调用的是实际就是jspServelt方法
 servlet.service(request, response);

上面这些请求处理流程之前系列已经分析过了,如果不清楚可以参考前面两卷

JspServlet的service方法详解:

代码语言:javascript复制
public void service (HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException {

        // jspFile may be configured as an init-param for this servlet instance
        String jspUri = jspFile;

        if (jspUri == null) {
            /*
             * Check to see if the requested JSP has been the target of a
             * RequestDispatcher.include()
             */
            jspUri = (String) request.getAttribute(
                    RequestDispatcher.INCLUDE_SERVLET_PATH);
            if (jspUri != null) {
                /*
                 * Requested JSP has been target of
                 * RequestDispatcher.include(). Its path is assembled from the
                 * relevant javax.servlet.include.* request attributes
                 */
                String pathInfo = (String) request.getAttribute(
                        RequestDispatcher.INCLUDE_PATH_INFO);
                if (pathInfo != null) {
                    jspUri  = pathInfo;
                }
            } else {
                /*
                 * Requested JSP has not been the target of a
                 * RequestDispatcher.include(). Reconstruct its path from the
                 * request's getServletPath() and getPathInfo()
                 */
                 //如果jspUri为空request.getServletPath()得到的如果当前项目上下文环境路径加/index,jsp
                jspUri = request.getServletPath();
                String pathInfo = request.getPathInfo();
                if (pathInfo != null) {
                    jspUri  = pathInfo;
                }
            }
        }
....

web.xml中规定了默认的欢饮页映射文件名

service方法后半部分

代码语言:javascript复制
        if (log.isDebugEnabled()) {
            log.debug("JspEngine --> "   jspUri);
            log.debug("t     ServletPath: "   request.getServletPath());
            log.debug("t        PathInfo: "   request.getPathInfo());
            log.debug("t        RealPath: "   context.getRealPath(jspUri));
            log.debug("t      RequestURI: "   request.getRequestURI());
            log.debug("t     QueryString: "   request.getQueryString());
        }

        try {
           //是否是预编译请求--默认返回false
            boolean precompile = preCompile(request);
            //重点: 处理JSP文件
            serviceJspFile(request, response, jspUri, precompile);
        } catch (RuntimeException e) {
            throw e;
        } catch (ServletException e) {
            throw e;
        } catch (IOException e) {
            throw e;
        } catch (Throwable e) {
            ExceptionUtils.handleThrowable(e);
            throw new ServletException(e);
        }

    }

serviceJspFile方法

代码语言:javascript复制
  private void serviceJspFile(HttpServletRequest request,
                                HttpServletResponse response, String jspUri,
                                boolean precompile)
        throws ServletException, IOException {
        //尝试获取JspServletWrapper 
        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 {
           //JspServletWrapper进行jsp文件处理
            wrapper.service(request, response, precompile);
        } catch (FileNotFoundException fnfe) {
            handleMissingResource(request, response, jspUri);
        }

    }

JspServletWrapper的service方法

代码语言:javascript复制
  public void service(HttpServletRequest request,
                        HttpServletResponse response,
                        boolean precompile)
            throws ServletException, IOException, FileNotFoundException {

        Servlet servlet;

        try {

            if (ctxt.isRemoved()) {
                throw new FileNotFoundException(jspUri);
            }

            if ((available > 0L) && (available < Long.MAX_VALUE)) {
                if (available > System.currentTimeMillis()) {
                    response.setDateHeader("Retry-After", available);
                    response.sendError
                        (HttpServletResponse.SC_SERVICE_UNAVAILABLE,
                         Localizer.getMessage("jsp.error.unavailable"));
                    return;
                }

                // Wait period has expired. Reset.
                available = 0;
            }

            /*
             * (1) Compile---第一步先对JSP文件进行解析然后编译成class文件
             */
            if (options.getDevelopment() || mustCompile) {
                synchronized (this) {
                    if (options.getDevelopment() || mustCompile) {
                        // The following sets reload to true, if necessary
                        //进行jsp文件编译处理
                        ctxt.compile();
                        mustCompile = false;
                    }
                }
            } else {
                if (compileException != null) {
                    // Throw cached compilation exception
                    throw compileException;
                }
            }
            ....

下面都是第一步编译工作做的事情:

org.apache.jasper.JspCompilationContext的complie方法

代码语言:javascript复制
    public void compile() throws JasperException, FileNotFoundException {
    //创建编译器
        createCompiler();
        if (jspCompiler.isOutDated()) {
            if (isRemoved()) {
                throw new FileNotFoundException(jspUri);
            }
            try {
                jspCompiler.removeGeneratedFiles();
                jspLoader = null;
                //进行编译操作
                jspCompiler.compile();
                jsw.setReload(true);
                jsw.setCompilationException(null);
            } catch (JasperException ex) {
            ....

jspCompiler.compile方法

代码语言:javascript复制
 public void compile(boolean compileClass, boolean jspcMode)
            throws FileNotFoundException, JasperException, Exception {
         .....
        try {
            //将jsp转换为java文件
            String[] smap = generateJava();
            //java文件生成的位置
            File javaFile = new File(ctxt.getServletJavaFileName());
            Long jspLastModified = ctxt.getLastModified(ctxt.getJspFile());
            javaFile.setLastModified(jspLastModified.longValue());
            if (compileClass) {
                //将生成的java文件编译成为calss字节码文件
                generateClass(smap);
              .....
    }

回到JspServletWrapper的service方法

代码语言:javascript复制
            /*
             * (2) (Re)load servlet class file---这里获取到的就是被编译完成后的index.jsp对应的servlet
             */
            servlet = getServlet();

最后一步

代码语言:javascript复制
     /*
             * (4) Service request
             */
            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 {
            //调用生成的index_jsp_servlet的service方法
            //该方法最终通过输出流out,向浏览器写回html页面
                servlet.service(request, response);
            }

JSP编译原理

代码分析

编译后的.class 字节码文件及源码 :

代码语言:javascript复制
out.write("rn");
out.write("<!DOCTYPE html>rn");
out.write("<html lang="en">rn");
out.write("    <head>rn");
out.write("        <meta charset="UTF-8" />rn");
out.write("        <title>");
out.print(request.getServletContext().getServerInfo() );
out.write("</title>rn");
out.write("        <link href="favicon.ico" rel="icon" type="image/x-icon" />rn");
out.write("        <link href="favicon.ico" rel="shortcut icon" type="image/x-icon" />rn");
out.write("        <link href="tomcat.css" rel="stylesheet" type="text/css" />rn");
...

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

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 和 _jspx_imports_classes 属性主要用于配置 EL 引擎上下文 。

5) 请求处理由方法 _jspService 完成 , 而在父类 HttpJspBase 中的service 方法通过模 板方法模式 , 调用了子类的 _jspService 方法。

6) _jspService 方法中定义了几个重要的局部变量 : pageContext 、Session、 application、config、out、page。由于整个页面的输出有 _jspService 方法完成,因此 这些变量和参数会对整个JSP页面生效。 这也是我们为什么可以在JSP页面使用这些变量 的原因。

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

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

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

编译流程

Compiler 编译工作主要包含代码生成 和 编译两部分 :

代码生成

1) Compiler 通过一个 PageInfo 对象保存JSP 页面编译过程中的各种配置,这些配置可 能来源于 Web 应用初始化参数, 也可能来源于JSP页面的指令配置(如 page , include)。

2) 调用ParserController 解析指令节点, 验证其是否合法,同时将配置信息保存到 PageInfo 中, 用于控制代码生成。

3) 调用ParserController 解析整个页面, 由于 JSP 是逐行解析, 所以对于每一行会创 建一个具体的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 完成编译。

0 人点赞