Tomcat卷三---Jasper引擎

Posted 大忽悠爱忽悠

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Tomcat卷三---Jasper引擎相关的知识,希望对你有一定的参考价值。

Tomcat卷三---Jasper引擎


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代码 :

<%@ 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 , 编译后的结果, 存放在 :

C:\\Users\\Administrator\\.IntelliJIdea2019.1\\system\\tomcat\\_project_tomcat\\w ork\\Catalina\\localhost\\jsp_demo_01_war_exploded\\org\\apache\\jsp

预编译

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

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


JSP源码流程

//如果访问的是JSP页面请求,得到的就是JSPservelt
 servlet = wrapper.allocate();
//生成过滤器链
ApplicationFilterChain filterChain =
                ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
//真正进行过滤操作
filterChain.doFilter(request.getRequest(), response.getResponse());

doFilter方法中最后调用internalDoFilter方法,真正执行过滤操作,然后调用servlet的service方法
//调用的是实际就是jspServelt方法
 servlet.service(request, response);

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


JspServlet的service方法详解:

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方法后半部分

        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方法

  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方法

  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方法


    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方法

 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方法

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

最后一步

     /*
             * (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 字节码文件及源码 :

out.write("\\r\\n");
out.write("<!DOCTYPE html>\\r\\n");
out.write("<html lang=\\"en\\">\\r\\n");
out.write("    <head>\\r\\n");
out.write("        <meta charset=\\"UTF-8\\" />\\r\\n");
out.write("        <title>");
out.print(request.getServletContext().getServerInfo() );
out.write("</title>\\r\\n");
out.write("        <link href=\\"favicon.ico\\" rel=\\"icon\\" type=\\"image/x-icon\\" />\\r\\n");
out.write("        <link href=\\"favicon.ico\\" rel=\\"shortcut icon\\" type=\\"image/x-icon\\" />\\r\\n");
out.write("        <link href=\\"tomcat.css\\" rel=\\"stylesheet\\" type=\\"text/css\\" />\\r\\n");
...

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

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 完成编译。


以上是关于Tomcat卷三---Jasper引擎的主要内容,如果未能解决你的问题,请参考以下文章

JSPs

springboot 嵌入式 tomcat 和 tomcat-embed-jasper

Tomcat详解

各类地址及资源获取的方式

Tomcat日志系统详解

Tomcat日志系统详解