(七) Tomcat 源码系列之 Jsp 处理流程

接着昨天讲的, Tomcat 是如何响应的, 也就是如何请求转发和重定向的? 聊这个之前,先来看看 Tomcat 是如何处理 jsp 页面的?

查看 /conf/web.xml 配置文件

<web-app .... >
    <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>
    
    <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><servlet-mapping>
        <servlet-name>default</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>

    <servlet-mapping>
        <servlet-name>jsp</servlet-name>
        <url-pattern>*.jsp</url-pattern>
        <url-pattern>*.jspx</url-pattern>
    </servlet-mapping>
</web-app>

可以看到,Tomcat 全局配置文件中,默认配置了连个 Servlet ,DefaultServlet 和 JspServlet

  • DefaultServlet :处理的是无效的 URI 映射,如果找不到对应的 Wrapper, 该请求被 DefaultServlet 处理
  • JspServlet :毫无疑问,这就是处理 Jsp 请求

JspServlet

既然这是一个 Servlet,直接查看它的 service 方法

String jspUri = jspFile;

if (jspUri == null) {

    //检查所请求的 JSP 是否已成为 RequestDispatcher.include() 的目标
    jspUri = (String) request.getAttribute(
        RequestDispatcher.INCLUDE_SERVLET_PATH);
    if (jspUri != null) {
        .....
    }
    else {
        // 路径重构
        // 如果请求路径是 / , 根据欢迎页面一一在目录中匹配
        jspUri = request.getServletPath();
        String pathInfo = request.getPathInfo();
        if (pathInfo != null) {
            jspUri += pathInfo;
        }
    }
}

 ....

 try {
      // 判断是否为 预编译
      boolean precompile = preCompile(request);
      // 开始编译 jsp 文件
      serviceJspFile(request, response, jspUri, precompile);
  }
-----------------------
// 直接查看 serviceJspFile 方法
JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
if (wrapper == null) {
    synchronized (this) {
        wrapper = rctxt.getWrapper(jspUri);
        if (wrapper == null) {
            // 检查请求的 JSP 页面是否存在
            // 以避免创建不必要的目录和文件
            if (null == context.getResource(jspUri)) {
                handleMissingResource(request, response, jspUri);
                return;
            }
            // 直接创建 JSPServletWrapper 对象
            wrapper = new JspServletWrapper(config, options, jspUri, rctxt);
            rctxt.addWrapper(jspUri, wrapper);
        }
    }
}

  try {
    wrapper.service(request, response, precompile);
   ..
  }
 ---------------------
// 接着查看 JspServletWrapper 的 service 方法
try {
    ...
    // 第一步, 编译
    if (options.getDevelopment() || mustCompile) {
        synchronized (this) {
            if (options.getDevelopment() || mustCompile) {
                //  jsp 处理, 生成 java 源文件并编译成 class 字节码文件
                // 生成规则为 index.jsp -> index_jsp.java
                ctxt.compile();
                mustCompile = false;
            }
        }
    }
    ...

    // 第二步, 加载 class 文件, 在 JVM 中创建 Servlet 对象
    servlet = getServlet();

    // 如果是预编译, 直接返回
    if (precompile) {
        return;
    }

}
....

    try {
        // 第三步, Handle limitation of number of loaded Jsps (我也不明白)
        ....
    }

    // 第四步, 生成的 Servlet 对象处理请求
    if (servlet instanceof SingleThreadModel) {
        synchronized (this) {
            servlet.service(request, response);
        }
    }
    else {
        // Servlet 处理
        servlet.service(request, response);
    }

生成的 java 源文件和 class 字节码文件默认存放路径为 work/Catalina(Engine名称)/localhost(Host名称)/Context名称 , 可以在 web.xml 配置 scratchdir 指定

<init-param>
    <param-name>scratchdir</param-name>
    <param-value>指定路径</param-value>
</init-param>

Jsp 生成的 java 源文件

index.jsp 写的 java 代码只是输出了当前时间,创建了一个 Date 对象, 我们直接从查看 java 源文件

// 继承自 org.apache.jasper.runtime.HttpJspBase, 该类是 HttpServlet 的子类, 所以 jsp 本质就是一个 Servlet
public final class index_jsp extends org.apache.jasper.runtime.HttpJspBase
    implements ... {

  private static final javax.servlet.jsp.JspFactory _jspxFactory =
          javax.servlet.jsp.JspFactory.getDefaultFactory();

  // 保存了当前 jsp 页面依赖的资源, 包含引入的外部的 jsp 页面, 导入的标签库, 标签所在的 jar 包等,便于后续处理过程中使用
  private static java.util.Map<java.lang.String,java.lang.Long> _jspx_dependants;

  // 存放导入的 java 包, 默认导入 javax.servlet, javax.servlet.http, javax.servlet.jsp 
  private static final java.util.Set<java.lang.String> _jspx_imports_packages;

  //存放导入的类, 通过 import 导入的 Date 都会包含在该集合中
  private static final java.util.Set<java.lang.String> _jspx_imports_classes;

  // _jspx_imports_packages 和 _jspx_imports_classes 主要用于配置 EL 引擎上下文


  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");
  }

  ....

  // 由父类 HttpJspBase 的s ervice 方法调用过来
  public void _jspService(final javax.servlet.http.HttpServletRequest request, final javax.servlet.http.HttpServletResponse response)
      throws java.io.IOException, javax.servlet.ServletException {

    ....


    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("<html>\n");
      out.write("<head>\n");
      out.write("    <title>Hello</title>\n");
      out.write("</head>\n");
      out.write("<body>\n");

    Date date = new Date();

      out.write("\n");
      out.write("<h1>Hello Jsp! time : ");
      out.print( date );
      out.write("\n");
      out.write("</h1>\n");
      out.write("</body>\n");
      out.write("</html>\n");
    } ....
  }
}

可以看到 Jsp 直接使用 JspWriter 以流的方式将 Java 对象, HTML 传给浏览器, 这样就完成了请求的响应

预编译

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

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

代码生成

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

重定向

重定向是客户端的行为,可以请求的是外部的资源, 不允许携带数据。直接查看 HttpServletResponse 的 sendRedirect 方法, ResponseFacade 实现了 HttpServletResponse 接口, 一路调用,来到 Resopon 的 sendRedirect(String location, int status) 方法

// 重定向到指定位置
try {
    String locationUri;
    // 相对重定向需要 HTTP / 1.1
    if (getRequest().getCoyoteRequest().getSupportsRelativeRedirects() &&
        getContext().getUseRelativeRedirects()) {
        locationUri = location;
    }
    else {
        locationUri = toAbsolute(location);
    }
    // 设置响应状态码
    setStatus(status);
    // 在请求请求头中设置 URI
    // 在 Http11Processor 检查请求头时, 会再次交给 CoyoteAdapter 处理
    setHeader("Location", locationUri);

请求转发

请求转发是服务端的跳转, 它只能跳转到内部资源,并允许携带数据放入 Request 域, 直接查看 RequestFacade 的 getRequestDispatcher 方法,其实就是路径交给 ServletContext 判断,并对 URI 映射, 找到能处理该请求的 Wrapper , 然后封装成一个 RequestDispathcer 对象,调用其 forward 方法,查看该方法

// forward 直接调用 doForward
private void doForward(ServletRequest request, ServletResponse response)
    throws ServletException, IOException {

    // 重置所有已缓冲的输出,但保留请求头和 cookie
    if (response.isCommitted()) {
        throw new IllegalStateException
            (sm.getString("applicationDispatcher.forward.ise"));
    }
    try {
        response.resetBuffer();
    }
    catch (IllegalStateException e) {
        throw e;
    }

    // 设置为处理指定的请求和响应,封装一下
    State state = new State(request, response, false);
    ...

    wrapResponse(state);
    //  转发一个名为 HTTP 的调度程序
    if ((servletPath == null) && (pathInfo == null)) {

       ....
    }

    // 处理基于 HTTP 路径的转发
    else {
        ApplicationHttpRequest wrequest = (ApplicationHttpRequest) wrapRequest(state);
        HttpServletRequest hrequest = state.hrequest;
        if (hrequest.getAttribute(RequestDispatcher.FORWARD_REQUEST_URI) == null) {
           ...
        }

        wrequest.setContextPath(context.getEncodedPath());
        wrequest.setRequestURI(requestURI);
        wrequest.setServletPath(servletPath);
        wrequest.setPathInfo(pathInfo);
        if (queryString != null) {
            wrequest.setQueryString(queryString);
            wrequest.setQueryParams(queryString);
        }
        wrequest.setMapping(mapping);

        // 处理请求
        processRequest(request, response, state);
    }

    ...

}
-----------------------
// processRequest 里面调用了 invoke 方法, 查看它

    ....
    // 看到了这里, 是不是发现和 WrapperValve 的 invoke 方法很像 ?
	Servlet servlet = null;
	....
    try {
        if (!unavailable) {
            servlet = wrapper.allocate();
        }
    }
	....

    // 创建过滤器链
    ApplicationFilterChain filterChain =
    ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
	try {
	    if ((servlet != null) && (filterChain != null)) {
	         // 先调用完所有的 Filter , 然后执行 Servlet 的逻辑
    	    filterChain.doFilter(request, response);
   	 }...

热部署

Tomcat 在启动的时侯,创建了很多线程,比如 Acceptor ,Poller,AsyncTime,等等。 其中有一个线程,在所有容器组件启动完毕的时候, 并在启动 MapperListener 组件之前,调用了 ContainerBase 的 threadStart 方法, 查看它

protected void threadStart() {

    if (thread != null)
        return;
    if (backgroundProcessorDelay <= 0)
        return;

    threadDone = false;
    String threadName = "ContainerBackgroundProcessor[" + toString() + "]";
    // ContainerBackgroundProcessor 用于监听 Session 的过期时间, 和容器组件的变化
    thread = new Thread(new ContainerBackgroundProcessor(), threadName);
    thread.setDaemon(true);
    thread.start();

}
------------
// 查看 ContainerBackgroundProcessor 的 run 方法
public void run() {
    Throwable t = null;
    String unexpectedDeathMessage = sm.getString(
        "containerBase.backgroundProcess.unexpectedThreadDeath",
        Thread.currentThread().getName());
    try {
        //  一直循环 
        while (!threadDone) {
            try {
                // 休眠 10s
                Thread.sleep(backgroundProcessorDelay * 1000L);
            }
            catch (InterruptedException e) {
                // Ignore
            }
            if (!threadDone) {
                // 每 10 s处理一下子容器, 查看容器组件是否发生了变化
                processChildren(ContainerBase.this);
            }
        }
    }
----------------------------
// 查看 processChildren 方法
try {
    if (container instanceof Context) {
        Loader loader = ((Context) container).getLoader();
        // 对于 FailedContex t实例,加载器将为 null
        if (loader == null) {
            return;
        }

        // 确保在 WebApp 类加载器下执行 
        // Contexts 和 Wrappers 的后台处理
        originalClassLoader = ((Context) container).bind(false, null);
    }
    // 对容器组件的后台处理, 并发出事件, 交给监听器处理
    // 前面就已经为每个容器组件都绑定了一个 MapperListener 的监听器
    container.backgroundProcess();
    Container[] children = container.findChildren();
    for (Container child : children) {
        if (child.getBackgroundProcessorDelay() <= 0) {
            // 递归处理
            processChildren(child);
        }
    }
}
-------------
// MapperListener 为每个容器组件监听, 并作出相应处理
// 如果在 Tomcat 运行期间, 往 webapps 文件夹中扔一个 war 包
// 你觉得 Tomcat 能访问到此 Web 应用么, 答案是显然易见的
// 同样, 如果把 webapps 里面的 Web 项目删除, 也能被监听器监听到, 移除 Context

下面这个示例就能说明一切 :

在这里插入图片描述
此时 Tomcat 是启动状态, 且 webapps 目录下没有 demo 这个项目, 当我把 demo 拖进来的时候, 大约过了 5~6 秒 (容器后台线程 10 s 监听一次), 方法被断点挡住了

在这里插入图片描述

查看它的方法调用链

在这里插入图片描述

Jsp 热部署

在每次访问 Jsp 页面的时候,JspCompilationContext.compile() 方法会根据 class 文件(或 java 文件,通过一个参数可以选择)的最后修改时间,判断文件是否更新过,如果更新过,

  • 删除之前编译过的 class 文件和 java 文件
  • 把 JspLoader 类加载器置空,需要重新创建一个 ClassLoader,来达到热加载的目的。
  • 调用 Compiler.compile() 重新把 jsp 转换成 servlet,并编译 servlet 成 class 文件。
  • 把JspServletWrapper.reload 修改为 true, 后面getServlet() 根据这个参数判断是否重新加载该servlet

猜你喜欢

转载自blog.csdn.net/Gp_2512212842/article/details/107509388