Tomcat卷三---Jasper引擎

x33g5p2x  于2022-02-28 转载在 其他  
字(9.2k)|赞(0)|评价(0)|浏览(453)

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

  1. <%@ page import="java.text.DateFormat" %>
  2. <%@ page import="java.text.SimpleDateFormat" %>
  3. <%@ page import="java.util.Date" %>
  4. <%@ page contentType="text/html;charset=UTF‐8" language="java" %>
  5. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
  6. <html>
  7. <head><title>$Title$</title></head>
  8. <body>
  9. <%
  10. DateFormat dateFormat = new SimpleDateFormat("yyyy‐MM‐dd HH:mm:ss");
  11. String format = dateFormat.format(new Date());
  12. %>
  13. Hello , Java Server Page 。。。。 <br/> <%= format %>
  14. </body>
  15. </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 , 编译后的结果, 存放在 :

  1. 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源码流程

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

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

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

JspServlet的service方法详解:

  1. public void service (HttpServletRequest request, HttpServletResponse response)
  2. throws ServletException, IOException {
  3. // jspFile may be configured as an init-param for this servlet instance
  4. String jspUri = jspFile;
  5. if (jspUri == null) {
  6. /*
  7. * Check to see if the requested JSP has been the target of a
  8. * RequestDispatcher.include()
  9. */
  10. jspUri = (String) request.getAttribute(
  11. RequestDispatcher.INCLUDE_SERVLET_PATH);
  12. if (jspUri != null) {
  13. /*
  14. * Requested JSP has been target of
  15. * RequestDispatcher.include(). Its path is assembled from the
  16. * relevant javax.servlet.include.* request attributes
  17. */
  18. String pathInfo = (String) request.getAttribute(
  19. RequestDispatcher.INCLUDE_PATH_INFO);
  20. if (pathInfo != null) {
  21. jspUri += pathInfo;
  22. }
  23. } else {
  24. /*
  25. * Requested JSP has not been the target of a
  26. * RequestDispatcher.include(). Reconstruct its path from the
  27. * request's getServletPath() and getPathInfo()
  28. */
  29. //如果jspUri为空request.getServletPath()得到的如果当前项目上下文环境路径加/index,jsp
  30. jspUri = request.getServletPath();
  31. String pathInfo = request.getPathInfo();
  32. if (pathInfo != null) {
  33. jspUri += pathInfo;
  34. }
  35. }
  36. }
  37. ....

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

service方法后半部分

  1. if (log.isDebugEnabled()) {
  2. log.debug("JspEngine --> " + jspUri);
  3. log.debug("\t ServletPath: " + request.getServletPath());
  4. log.debug("\t PathInfo: " + request.getPathInfo());
  5. log.debug("\t RealPath: " + context.getRealPath(jspUri));
  6. log.debug("\t RequestURI: " + request.getRequestURI());
  7. log.debug("\t QueryString: " + request.getQueryString());
  8. }
  9. try {
  10. //是否是预编译请求--默认返回false
  11. boolean precompile = preCompile(request);
  12. //重点: 处理JSP文件
  13. serviceJspFile(request, response, jspUri, precompile);
  14. } catch (RuntimeException e) {
  15. throw e;
  16. } catch (ServletException e) {
  17. throw e;
  18. } catch (IOException e) {
  19. throw e;
  20. } catch (Throwable e) {
  21. ExceptionUtils.handleThrowable(e);
  22. throw new ServletException(e);
  23. }
  24. }

serviceJspFile方法

  1. private void serviceJspFile(HttpServletRequest request,
  2. HttpServletResponse response, String jspUri,
  3. boolean precompile)
  4. throws ServletException, IOException {
  5. //尝试获取JspServletWrapper
  6. JspServletWrapper wrapper = rctxt.getWrapper(jspUri);
  7. if (wrapper == null) {
  8. synchronized(this) {
  9. wrapper = rctxt.getWrapper(jspUri);
  10. if (wrapper == null) {
  11. // Check if the requested JSP page exists, to avoid
  12. // creating unnecessary directories and files.
  13. if (null == context.getResource(jspUri)) {
  14. handleMissingResource(request, response, jspUri);
  15. return;
  16. }
  17. wrapper = new JspServletWrapper(config, options, jspUri,
  18. rctxt);
  19. rctxt.addWrapper(jspUri,wrapper);
  20. }
  21. }
  22. }
  23. try {
  24. //JspServletWrapper进行jsp文件处理
  25. wrapper.service(request, response, precompile);
  26. } catch (FileNotFoundException fnfe) {
  27. handleMissingResource(request, response, jspUri);
  28. }
  29. }

JspServletWrapper的service方法

  1. public void service(HttpServletRequest request,
  2. HttpServletResponse response,
  3. boolean precompile)
  4. throws ServletException, IOException, FileNotFoundException {
  5. Servlet servlet;
  6. try {
  7. if (ctxt.isRemoved()) {
  8. throw new FileNotFoundException(jspUri);
  9. }
  10. if ((available > 0L) && (available < Long.MAX_VALUE)) {
  11. if (available > System.currentTimeMillis()) {
  12. response.setDateHeader("Retry-After", available);
  13. response.sendError
  14. (HttpServletResponse.SC_SERVICE_UNAVAILABLE,
  15. Localizer.getMessage("jsp.error.unavailable"));
  16. return;
  17. }
  18. // Wait period has expired. Reset.
  19. available = 0;
  20. }
  21. /*
  22. * (1) Compile---第一步先对JSP文件进行解析然后编译成class文件
  23. */
  24. if (options.getDevelopment() || mustCompile) {
  25. synchronized (this) {
  26. if (options.getDevelopment() || mustCompile) {
  27. // The following sets reload to true, if necessary
  28. //进行jsp文件编译处理
  29. ctxt.compile();
  30. mustCompile = false;
  31. }
  32. }
  33. } else {
  34. if (compileException != null) {
  35. // Throw cached compilation exception
  36. throw compileException;
  37. }
  38. }
  39. ....

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

org.apache.jasper.JspCompilationContext的complie方法

  1. public void compile() throws JasperException, FileNotFoundException {
  2. //创建编译器
  3. createCompiler();
  4. if (jspCompiler.isOutDated()) {
  5. if (isRemoved()) {
  6. throw new FileNotFoundException(jspUri);
  7. }
  8. try {
  9. jspCompiler.removeGeneratedFiles();
  10. jspLoader = null;
  11. //进行编译操作
  12. jspCompiler.compile();
  13. jsw.setReload(true);
  14. jsw.setCompilationException(null);
  15. } catch (JasperException ex) {
  16. ....

jspCompiler.compile方法

  1. public void compile(boolean compileClass, boolean jspcMode)
  2. throws FileNotFoundException, JasperException, Exception {
  3. .....
  4. try {
  5. //将jsp转换为java文件
  6. String[] smap = generateJava();
  7. //java文件生成的位置
  8. File javaFile = new File(ctxt.getServletJavaFileName());
  9. Long jspLastModified = ctxt.getLastModified(ctxt.getJspFile());
  10. javaFile.setLastModified(jspLastModified.longValue());
  11. if (compileClass) {
  12. //将生成的java文件编译成为calss字节码文件
  13. generateClass(smap);
  14. .....
  15. }

回到JspServletWrapper的service方法

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

最后一步

  1. /*
  2. * (4) Service request
  3. */
  4. if (servlet instanceof SingleThreadModel) {
  5. // sync on the wrapper so that the freshness
  6. // of the page is determined right before servicing
  7. synchronized (this) {
  8. servlet.service(request, response);
  9. }
  10. } else {
  11. //调用生成的index_jsp_servlet的service方法
  12. //该方法最终通过输出流out,向浏览器写回html页面
  13. servlet.service(request, response);
  14. }

JSP编译原理

代码分析

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

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

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

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

相关文章