您当前的位置: 首页 >  tomcat

恐龙弟旺仔

暂无认证

  • 1浏览

    0关注

    282博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

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

恐龙弟旺仔 发布时间:2018-12-06 11:18:33 ,浏览量:1

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中可以找到

    scratchdir
    web-app/tm/jsp/
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时,就会转发到这)

 
  
  
  
  
      
   
        default
        org.apache.catalina.servlets.DefaultServlet
        
            debug
            0
        
        
            listings
            false
        
        1
     

    注意:读者可以仔细阅读一下相关源码,可以发现,里面基本做了所有的异常处理,403、404...

 

        * JspServlet(处理.jsp)

  
  
  
  

  
        jsp
        org.apache.jasper.servlet.JspServlet
        
            fork
            false
        
        
            xpoweredBy
            false
        
        3
    

        * welcome-list(默认的欢迎页面)

  
  
  
  
  
  
  
  
  
  
  
  
  

    
        index.html
        index.htm
        index.jsp
    
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





    
    九九乘法表




    

请输入两个自然数给您打印乘法表

要求:startNumber <endNumber

startNumber:   endNumber      

    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 _jspx_dependants;

  private static final java.util.Set _jspx_imports_packages;

  private static final java.util.Set _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 getDependants() {
    return _jspx_dependants;
  }

  public java.util.Set getPackageImports() {
    return _jspx_imports_packages;
  }

  public java.util.Set 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("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("    \r\n");
      out.write("    九九乘法表\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("\r\n");
      out.write("    

请输入两个自然数给您打印乘法表

\r\n"); out.write("

要求:startNumber <endNumber

\r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" startNumber:\r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write("  \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" endNumber\r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write("  \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write("  \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write("  \r\n"); out.write(" \r\n"); out.write(" \r\n"); out.write("\r\n"); out.write("\r\n"); out.write(""); } 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架构解析(刘光瑞)

 

 

关注
打赏
1655041699
查看更多评论
立即登录/注册

微信扫码登录

0.0382s