Jasper概述
Jasper模块是Tomcat的JSP核心引擎,JSP本质上是一个Servlet。Tomcat使用Jasper对JSP语法进行解析,生成Servlet并生成Class字节码,用户在进行访问jsp时,会访问Servlet,最终将访问结果直接响应在浏览器端。另外,在运行的时候,Jasper还会检测JSP文件是否修改,如果修改,则会重新编译JSP文件。
JSP编译方式
运行时编译
Tomcat并不会在启动Web应用的时候自动编译JSP文件,而是在客户端第一次请求时,才编译需要访问的JSP文件。
准备一个web应用,编写JSP:
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%@ page import="java.text.DateFormat" %>
<%@ page import="java.text.SimpleDateFormat" %>
<%@ page import="java.util.Date" %>
<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是实现运行时编译的入口:
<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>
<!-- The mappings for the JSP servlet -->
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
JspServlet处理流程图:
源码跟踪:
接受到对jsp的访问请求后,会最先到达JspServlet的service()方法中:
然后获取JSP的路径:
然后判断是否为预编译请求:
然后执行serviceJspFile,在serviceJspFile方法中获取了一个JspServletWrapper:
然后调用它的service()方法:
在service()方法中,调用了JspCompilationContext的compile()方法:进行编译
在JspCompilationContext的compile()方法中又调用了jspCompiler.compile();
最终到达Complier类中的compile(boolean compileClass, boolean jspcMode)
方法:端点处做了2个事情,生成java文件、生成class文件
已经生成java文件,准备生成class文件
java文件和class文件生成后,回到JspServletWrapper类中,调用getServlet()方法:加载jsp对应的servlet
然后执行service()方法:
查看index_jsp源码:在_jspService()方法中写出响应:
编译结果
- 如果在
tomcat/conf/web.xml
中配置了参数scratchdir
,则jsp的编译后的结果就会存储在该目录下:
<init-param>
<param-name>scratchdir</param-name>
<param-value>D:/tmp/jsp/</param-value>
</init-param>
-
如果没有配置该项,则会将编译后的结果,存储在Tomcat安装目录下的
work/Catalina(Engine名称)/localhost(Host名称)/Context命名
假设项目名称为 jsp_demo_01,默认的目录为:work/Catalina/localhost/jsp_demo_01
-
如果使用的是IDEA开发工具继承Tomcat访问web工程中的jsp,编译后的结果存放在:
c:\Users\Administrator\.IntelliJIdea2019.1\system\tomcat\_project_tomcat\wor\Catalina\localost\jsp_demo_01_sar_exploded\org\apache\jsp
预编译
除了运行时编译,还可以直接在Web应用启动时,一次性将Web应用用的所有JSP页面一次性编译完成。在这种情况下,Web应用运行过程中,便可以不必在进行实时编译,而是直接调用JSP页面对应的Sevlet完成请求处理,从而提升系统性能。
Tomcat提供了一个Shell程序JspC,用于支持JSP预编译,而且在Tomcat的安装目录下提供了一个catalina-tasks.xml文件声明了Tomcat支持的Ant任务,因此,可以很容易使用Ant来执行JSP预编译。(如果想要使用这种方式,必须得确保在此之前已经下载并安装了Apache Ant)。
JSP编译原理
上面编译后的index_jsp源码如下:
/*
* Generated by the Jasper component of Apache Tomcat
* Version: Apache Tomcat/9.0.x-dev
* Generated at: 2020-03-27 09:25:56 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;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
public final class index_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<java.lang.String,java.lang.Long> _jspx_dependants;
private static final java.util.Set<java.lang.String> _jspx_imports_packages;
private static final java.util.Set<java.lang.String> _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 = new java.util.HashSet<>();
_jspx_imports_classes.add("java.util.Date");
_jspx_imports_classes.add("java.text.SimpleDateFormat");
_jspx_imports_classes.add("java.text.DateFormat");
}
private volatile javax.el.ExpressionFactory _el_expressionfactory;
private volatile org.apache.tomcat.InstanceManager _jsp_instancemanager;
public java.util.Map<java.lang.String,java.lang.Long> getDependants() {
return _jspx_dependants;
}
public java.util.Set<java.lang.String> getPackageImports() {
return _jspx_imports_packages;
}
public java.util.Set<java.lang.String> 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 {
if (!javax.servlet.DispatcherType.ERROR.equals(request.getDispatcherType())) {
final java.lang.String _jspx_method = request.getMethod();
if ("OPTIONS".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
return;
}
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method)) {
response.setHeader("Allow","GET, HEAD, POST, OPTIONS");
response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, "JSP åªå
许 GETã€POST 或 HEAD。Jasper 还å
许 OPTIONS");
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("\n");
out.write("\n");
out.write("\n");
out.write("\n");
out.write("\n");
out.write("\n");
out.write("<html>\n");
out.write(" <head>\n");
out.write(" <title>$Title$</title>\n");
out.write(" </head>\n");
out.write(" <body>\n");
out.write("\n");
out.write(" ");
DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String format = dateFormat.format(new Date());
out.write("\n");
out.write(" Hello, Java Server Page...\n");
out.write(" <br/>\n");
out.write("\n");
out.write(" ");
out.print( format);
out.write("\n");
out.write(" </body>\n");
out.write("</html>\n");
} 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);
}
}
}
由编译后的源码解读,分析出一下几点:
-
其类名为
index_jsp
,继承自org.apache.jasper.runtime.HttpJspBase
,该类是HttpServlet
的子类,所以jsp本质就是一个Servlet。
-
通过属性
_jspx_dependants
保存了当前JSP页面依赖的资源,包含引入的外部JSP页面,导入的标签、标签所在的jar包等,便于后续处理过程中使用(如重新编译检测,因此它以Map的形式保存了每个资源的啥功上次修改时间) -
通过属性
_jspx_imports_packages
存放导入的java包,默认导入javax_servlet、javax.servlet.http、javax.servlet.jsp
-
通过属性
_jspx_imports_classes
存放导入的类,通过import指令导入的DateFormat、SimpleDateFormat、Date都会包含在集合中。_jspx_imports_packages和_spx_imports_classes
主要用于配置EL引擎上下文 -
请求处理由方法
_jspService
完成,而在父类HttpJspBash中的service方法通过模板方法模式调用了子类_jspService方法
-
_jspService
方法中定义了几个重要的局部变量:pageContext、Session、application、config、out、page
。由于整个页面的输出由 _jspService方法完成,因此这些变量会对整个JSP页面生效。 -
指定文档类型的指令(page)最终装换为
response.setContentType
方法调用 -
对于每一行的静态内容(HTML),调用out.write输出
-
对于
<% ... %>
中的java代码,将直接转换为Servlet类中的代码。如果在Java代码中嵌入了静态文件,同样调用out.write输出。
编译流程
Compiler编译过程主要包含代码生成和编译两部分:
代码生成:
- Compiler通过一个PageInfo对象保存JSP页面编译过程中的各种配置,这些配置可能来源与Web应用初始化参数,也可能源于JSP页面的配置(如page,inclue)
- 调用ParserController解析指令节点,验证其是否合法,同时将配置信息保存到PageInfo中,用于控制代码生成
- 调用ParserController解析整个页面,由于是逐行解析,所以对于每一行会创建一个具体的Node对象。如静态文本(TemplateText)、Java代码(Scriptlet)、定制标签(CustomTag)、Include指令(IncludeDirective)。
- 验证除指令外所有节点的合法性,如脚本、定制标签、EL表达式等
- 收集除指令外其它节点的页面配置信息
- 编译并加锁当前JSP依赖的标签
- 对于JSP页面的EL表达式,生成对应的映射函数
- 生成JSP页面对应Servlet源代码
编译:
代码生成后,Compiler还会生成SMAP信息。如果配置生成SMAP信息,Compiler则会在编译阶段将SMAP信息写到Class文件中。
在编译阶段,Compiler的两个实现AntCompiler和JDTCompiler
分别调用相关框架的API进行源代码编译
- 对于AntCompiler来说:构造一个Ant的javac的任务完成编译;
- 对于JDTCompiler来说,调用org.eclipse.jdt.internal.compiler.Compiler完成编译