Tomcat Jasper JSP引擎详解
Jasper (JSP引擎)简介
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文件。
使用标签库 <%@ taglib prefix=“c” uri=“http://java.sun.com/jsp/jstl/core” %> 需要加载标签库jar
创建一个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>
启动项目 访问web页面
运行时 编译过程
Tomcat 在默认的web.xmlorg.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>
<servlet-mapping>
<servlet-name>jsp</servlet-name>
<url-pattern>*.jsp</url-pattern>
<url-pattern>*.jspx</url-pattern>
</servlet-mapping>
JspServlet 处理流程图
编译结果 - 存放位置
如果在 tomcat/conf/web.xml 中配置了参数scratchdir , 则jsp编译后的结果,就会存储在该目录下。
<init-param>
<param-name>scratchdir</param-name>
<param-value>D:/tomcat-temp</param-value>
</init-param>
如果没有配置该选项, 则会将编译后的结果,存储在Tomcat安装目录下的work/Catalina(Engine名称)/localhost(Host名称)/Context名称
如果使用的是 IDEA 开发工具集成Tomcat 访问web工程中的jsp , 编译后的结果,存放在
Tomcat Jsp预编译
除了运行时编译,我们还可以直接在Web应用启动时, 一次性将Web应用中的所有的JSP页面一次性编译完成。在这种情况下,Web应用运行过程中,便可以不必再进行实时编译,而是直接调用JSP页面对应的Servlet 完成请求处理, 从而提升系统性能。
Tomcat 提供了一个Shell程序JspC,用于支持JSP预编译,而且在Tomcat的安装目录下提供了一个 catalina-tasks.xml 文件声明了Tomcat 支持的Ant任务, 因此,我们很容易使用 Ant 来执行JSP 预编译 。(要想使用这种方式,必须得确保在此之前已经下载并安装了Apache Ant)。
JSP编译原理
index_jsp.class 文件
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.apache.jsp;
import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import javax.el.ExpressionFactory;
import javax.servlet.DispatcherType;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.jsp.JspFactory;
import javax.servlet.jsp.JspWriter;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.SkipPageException;
import org.apache.jasper.runtime.HttpJspBase;
import org.apache.jasper.runtime.InstanceManagerFactory;
import org.apache.jasper.runtime.JspSourceDependent;
import org.apache.jasper.runtime.JspSourceImports;
import org.apache.tomcat.InstanceManager;
public final class index_jsp extends HttpJspBase implements JspSourceDependent, JspSourceImports {
private static final JspFactory _jspxFactory = JspFactory.getDefaultFactory();
private static Map<String, Long> _jspx_dependants = new HashMap(2);
private static final Set<String> _jspx_imports_packages;
private static final Set<String> _jspx_imports_classes;
private volatile ExpressionFactory _el_expressionfactory;
private volatile InstanceManager _jsp_instancemanager;
static {
_jspx_dependants.put("/WEB-INF/lib/standard.jar", 1656837029008L);
_jspx_dependants.put("jar:file:/D:/Program%20IDE/Tomcat/apache-tomcat-8.5.42/webapps/web-tomcat/WEB-INF/lib/standard.jar!/META-INF/c.tld", 1098682290000L);
_jspx_imports_packages = new 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 HashSet();
_jspx_imports_classes.add("java.util.Date");
_jspx_imports_classes.add("java.text.SimpleDateFormat");
_jspx_imports_classes.add("java.text.DateFormat");
}
public index_jsp() {
}
public Map<String, Long> getDependants() {
return _jspx_dependants;
}
public Set<String> getPackageImports() {
return _jspx_imports_packages;
}
public Set<String> getClassImports() {
return _jspx_imports_classes;
}
public ExpressionFactory _jsp_getExpressionFactory() {
if (this._el_expressionfactory == null) {
synchronized(this) {
if (this._el_expressionfactory == null) {
this._el_expressionfactory = _jspxFactory.getJspApplicationContext(this.getServletConfig().getServletContext()).getExpressionFactory();
}
}
}
return this._el_expressionfactory;
}
public InstanceManager _jsp_getInstanceManager() {
if (this._jsp_instancemanager == null) {
synchronized(this) {
if (this._jsp_instancemanager == null) {
this._jsp_instancemanager = InstanceManagerFactory.getInstanceManager(this.getServletConfig());
}
}
}
return this._jsp_instancemanager;
}
public void _jspInit() {
}
public void _jspDestroy() {
}
public void _jspService(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
String _jspx_method = request.getMethod();
if (!"GET".equals(_jspx_method) && !"POST".equals(_jspx_method) && !"HEAD".equals(_jspx_method) && !DispatcherType.ERROR.equals(request.getDispatcherType())) {
response.sendError(405, "JSPs only permit GET POST or HEAD");
} else {
JspWriter out = null;
JspWriter _jspx_out = null;
PageContext _jspx_page_context = null;
try {
response.setContentType("text/html; charset=UTF-8");
PageContext pageContext = _jspxFactory.getPageContext(this, request, response, (String)null, true, 8192, true);
_jspx_page_context = pageContext;
pageContext.getServletContext();
pageContext.getServletConfig();
pageContext.getSession();
out = pageContext.getOut();
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write("\r\n");
out.write("<html>\r\n");
out.write("<head>\r\n");
out.write("<title>$Title$</title>\r\n");
out.write("</head>\r\n");
out.write("<body>\r\n");
DateFormat dateFormat = new SimpleDateFormat("yyyy‐MM‐dd HH:mm:ss");
String format = dateFormat.format(new Date());
out.write("\r\n");
out.write("Hello , Java Server Page 。。。。\r\n");
out.write("<br/>\r\n");
out.print(format);
out.write("\r\n");
out.write("</body>\r\n");
out.write("</html>");
} catch (Throwable var15) {
if (!(var15 instanceof SkipPageException)) {
out = (JspWriter)_jspx_out;
if (_jspx_out != null && ((JspWriter)_jspx_out).getBufferSize() != 0) {
try {
if (response.isCommitted()) {
out.flush();
} else {
out.clearBuffer();
}
} catch (IOException var14) {
}
}
if (_jspx_page_context == null) {
throw new ServletException(var15);
}
_jspx_page_context.handlePageException(var15);
}
} 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 和 _jspx_imports_classes 属性主要用于配置 EL 引擎上下文
- 请求处理由方法 _jspService 完成 , 而在父类 HttpJspBase 中的service 方法通过模板方法模式 , 调用了子类的 _jspService 方法。
- _jspService 方法中定义了几个重要的局部变量 : pageContext 、Session、application、config、out、page。由于整个页面的输出有 _jspService 方法完成,因此这些变量和参数会对整个JSP页面生效。 这也是我们为什么可以在JSP页面使用这些变量的原因。
- 指定文档类型的指令 (page) 最终转换为 response.setContentType() 方法调用。
- 对于每一行的静态内容(HTML) , 调用 out.write 输出。
- 对于 <% … %> 中的java 代码 , 将直接转换为 Servlet 类中的代码。 如果在 Java代码中嵌入了静态文件, 则同样调用 out.write 输出。
JSP 编译过程
Compiler 编译工作主要包含代码生成 和 编译两部分 :
代码生成
- Compiler 通过一个 PageInfo 对象保存JSP 页面编译过程中的各种配置,这些配置可能来源于 Web 应用初始化参数, 也可能来源于JSP页面的指令配置(如 page ,include)。
- 调用ParserController 解析指令节点, 验证其是否合法,同时将配置信息保存到PageInfo 中, 用于控制代码生成。
- 调用ParserController 解析整个页面, 由于 JSP 是逐行解析, 所以对于每一行会创建一个具体的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 完成编译。