1 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
文件。
2 JSP 编译方式
2.1 运行时编译
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>
2.1.1 编译过程
Tomcat
在默认的
web.xml
中配置了一个
org.apache.jasper.servlet.JspServlet
,用于处理所有的.jsp
或
.jspx
结尾的请求,该
Servlet
实现即是运行时编译的入口。
<servlet>
<servlet-name>isp</servlet-name>
<servlet-class>org,apache,jasper.servlet.Jspservlet</servlet-class>
<init-param>
<param-name>fork</param-name>
<param-value>false</param-value><param-value>
</init-param>
<init-param>
<param-name>xpoweredBy</param-name>
<param-value>false</param-value><param-value>
</init-param>
<load-on-startup>3</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.ispx</url-pattern>
</servlet-mapping>
JspServlet
处理流程图:

2.2 编译结果
1
) 如果在
tomcat/conf/web.xml
中配置了参数
scratchdir
, 则
jsp
编译后的结果,就会存储在该目录下 。
<init-param>
<param-name>scratchdir</param-name>
<param-value>D:/tmp/jsp/</param-value>
</init-param>
2
) 如果没有配置该选项, 则会将编译后的结果,存储在
Tomcat
安装目录下work/Catalina(Engine名称
)/localhost(Host
名称
)/Context
名称 。 假设项目名称为jsp_demo
3
) 如果使用的是
IDEA
开发工具集成
Tomcat
访问
web
工程中的
jsp
, 编译后的结果,存放在 :
C:\Users\Administrator\.IntelliJIdea2019.1\system\tomcat\_project_tomcat\work\Catalina\localhost\jsp_demo_01_war_exploded\org\apache\jsp
2.2 预编译
除了运行时编译,我们还可以直接在
Web
应用启动时, 一次性将
Web
应用中的所有的
JSP页面一次性编译完成。在这种情况下,Web
应用运行过程中,便可以不必再进行实时编译,而是直接调用JSP
页面对应的
Servlet
完成请求处理, 从而提升系统性能。
Tomcat
提供了一个
Shell
程序
JspC
,用于支持
JSP
预编译,而且在
Tomcat
的安装目录下提供了一个 catalina-tasks.xml
文件声明了
Tomcat
支持的
Ant
任务, 因此,我们很容易使用 Ant
来执行
JSP
预编译 。(要想使用这种方式,必须得确保在此之前已经下载并安装了Apache Ant
)。
3 JSP编译原理
3.1 代码分析
编译后的.class 字节码文件及源码 :
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;
static {
_jspx_dependants = new
java.util.HashMap<java.lang.String,java.lang.Long>(2);
_jspx_dependants.put("jar:file:/D:/DevelopProgramFile/apache‐tomcat‐8.5.42‐windows‐x64/apache‐tomcat‐8.5.42/webapps/jsp_demo_01/WEB‐INF/lib/standard.jar!/META‐INF/c.tld", Long.valueOf(1098682290000L));
_jspx_dependants.put("/WEB‐INF/lib/standard.jar",
Long.valueOf(1490343635913L));
}
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 {
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, "JSPsonly 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("\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(" ");
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("\n");
out.write(" <br/>\n");
out.write("\n");
out.write(" ");
out.print( format );
out.write("\n");
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);
}
}
}
由编译后的源码解读, 可以分析出以下几点 :
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
输出。
3.2 编译流程
JSP
编译过程如下:

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