Tomcat卷三---Jasper引擎
- Jasper 简介
- JSP 编译方式
-
- 运行时编译
- 编译过程
- 编译结果
- 预编译
- JSP源码流程
- JSP编译原理
-
- 代码分析
- 编译流程
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代码 :
<%@ 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>
编译过程
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 , 编译后的结果, 存放在 :
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源码流程
//如果访问的是JSP页面请求,得到的就是JSPservelt
servlet = wrapper.allocate();
//生成过滤器链
ApplicationFilterChain filterChain =
ApplicationFilterFactory.createFilterChain(request, wrapper, servlet);
//真正进行过滤操作
filterChain.doFilter(request.getRequest(), response.getResponse());
doFilter方法中最后调用internalDoFilter方法,真正执行过滤操作,然后调用servlet的service方法
//调用的是实际就是jspServelt方法
servlet.service(request, response);
上面这些请求处理流程之前系列已经分析过了,如果不清楚可以参考前面两卷
JspServlet的service方法详解:
public void service (HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// jspFile may be configured as an init-param for this servlet instance
String jspUri = jspFile;
if (jspUri == null) {
/*
* Check to see if the requested JSP has been the target of a
* RequestDispatcher.include()
*/
jspUri = (String) request.getAttribute(
RequestDispatcher.INCLUDE_SERVLET_PATH);
if (jspUri != null) {
/*
* Requested JSP has been target of
* RequestDispatcher.include(). Its path is assembled from the
* relevant javax.servlet.include.* request attributes
*/
String pathInfo = (String) request.getAttribute(
RequestDispatcher.INCLUDE_PATH_INFO);
if (pathInfo != null) {
jspUri += pathInfo;
}
} else {
/*
* Requested JSP has not been the target of a
* RequestDispatcher.include(). Reconstruct its path from the
* request's getServletPath() and getPathInfo()
*/
//如果jspUri为空request.getServletPath()得到的如果当前项目上下文环境路径加/index,jsp
jspUri = request.getServletPath();
String pathInfo = request.getPathInfo();
if (pathInfo != null) {
jspUri += pathInfo;
}
}
}
....
web.xml中规定了默认的欢饮页映射文件名
service方法后半部分
if (log.isDebugEnabled()) {
log.debug("JspEngine --> " + jspUri);
log.debug("\t ServletPath: " + request.getServletPath());
log.debug("\t PathInfo: " + request.getPathInfo());
log.debug("\t RealPath: " + context.getRealPath(jspUri));
log.debug("\t RequestURI: " + request.getRequestURI());
log.debug("\t QueryString: " + request.getQueryString());
}
try {
//是否是预编译请求--默认返回false
boolean precompile = preCompile(request);
//重点: 处理JSP文件
serviceJspFile(request, response, jspUri, precompile);
} catch (RuntimeException e) {
throw e;
} catch (ServletException e) {
throw e;
} catch (IOException e) {
throw e;
} catch (Throwable e) {
ExceptionUtils.handleThrowable(e);
throw new ServletException(e);
}
}
serviceJspFile方法
private void serviceJspFile(HttpServletRequest request,
HttpServletResponse response, String jspUri,
boolean precompile)
throws ServletException, IOException {
//尝试获取JspServletWrapper
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 {
//JspServletWrapper进行jsp文件处理
wrapper.service(request, response, precompile);
} catch (FileNotFoundException fnfe) {
handleMissingResource(request, response, jspUri);
}
}
JspServletWrapper的service方法
public void service(HttpServletRequest request,
HttpServletResponse response,
boolean precompile)
throws ServletException, IOException, FileNotFoundException {
Servlet servlet;
try {
if (ctxt.isRemoved()) {
throw new FileNotFoundException(jspUri);
}
if ((available > 0L) && (available < Long.MAX_VALUE)) {
if (available > System.currentTimeMillis()) {
response.setDateHeader("Retry-After", available);
response.sendError
(HttpServletResponse.SC_SERVICE_UNAVAILABLE,
Localizer.getMessage("jsp.error.unavailable"));
return;
}
// Wait period has expired. Reset.
available = 0;
}
/*
* (1) Compile---第一步先对JSP文件进行解析然后编译成class文件
*/
if (options.getDevelopment() || mustCompile) {
synchronized (this) {
if (options.getDevelopment() || mustCompile) {
// The following sets reload to true, if necessary
//进行jsp文件编译处理
ctxt.compile();
mustCompile = false;
}
}
} else {
if (compileException != null) {
// Throw cached compilation exception
throw compileException;
}
}
....
下面都是第一步编译工作做的事情:
org.apache.jasper.JspCompilationContext的complie方法
public void compile() throws JasperException, FileNotFoundException {
//创建编译器
createCompiler();
if (jspCompiler.isOutDated()) {
if (isRemoved()) {
throw new FileNotFoundException(jspUri);
}
try {
jspCompiler.removeGeneratedFiles();
jspLoader = null;
//进行编译操作
jspCompiler.compile();
jsw.setReload(true);
jsw.setCompilationException(null);
} catch (JasperException ex) {
....
jspCompiler.compile方法
public void compile(boolean compileClass, boolean jspcMode)
throws FileNotFoundException, JasperException, Exception {
.....
try {
//将jsp转换为java文件
String[] smap = generateJava();
//java文件生成的位置
File javaFile = new File(ctxt.getServletJavaFileName());
Long jspLastModified = ctxt.getLastModified(ctxt.getJspFile());
javaFile.setLastModified(jspLastModified.longValue());
if (compileClass) {
//将生成的java文件编译成为calss字节码文件
generateClass(smap);
.....
}
回到JspServletWrapper的service方法
/*
* (2) (Re)load servlet class file---这里获取到的就是被编译完成后的index.jsp对应的servlet
*/
servlet = getServlet();
最后一步
/*
* (4) Service request
*/
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 {
//调用生成的index_jsp_servlet的service方法
//该方法最终通过输出流out,向浏览器写回html页面
servlet.service(request, response);
}
JSP编译原理
代码分析
编译后的.class 字节码文件及源码 :
out.write("\r\n");
out.write("<!DOCTYPE html>\r\n");
out.write("<html lang=\"en\">\r\n");
out.write(" <head>\r\n");
out.write(" <meta charset=\"UTF-8\" />\r\n");
out.write(" <title>");
out.print(request.getServletContext().getServerInfo() );
out.write("</title>\r\n");
out.write(" <link href=\"favicon.ico\" rel=\"icon\" type=\"image/x-icon\" />\r\n");
out.write(" <link href=\"favicon.ico\" rel=\"shortcut icon\" type=\"image/x-icon\" />\r\n");
out.write(" <link href=\"tomcat.css\" rel=\"stylesheet\" type=\"text/css\" />\r\n");
...
由编译后的源码解读, 可以分析出以下几点 :
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 完成编译。