基本概念
- 对于基于 jsp 的 Web 应用,可以在 JSP 页面直接编写 Java 代码,添加第三方库,使用 EL 表达式.但是最终输出到客户端浏览器的都是标准的 html 页面,包括 js,css 等等.并不包含 java 相关的语法 .JSP 可以看作是一种运行在服务器端的脚本,最终以 HTML 页面方式响应给客户端
- 使用 tomcat 中的Jasper引擎将jsp文件转换为HTML页面文件:JSP 本质上是一个 Servlet Tomcat 使用 Jasper 对 JSP 语法进行解析,生成 servlet 并生成 Class 字节码文件用户在访问 jsp 文件时,会访问转换后的 Servlet, 最终的访问结果以 HTML 页面的方式直接响应在浏览器端在运行过程中 ,Jasper 引擎会检测 jsp 文件是否修改,如果修改则重新编译 jsp 文件
编译方式
运行时编译
- Tomcat 不会在启动 Web 应用时自动编译 JSP 文件,而是在客户端第一次请求时,才编译需要访问的 JSP 文件
编译过程
- Tomcat 在默认的 web.xml 中配置了 org. apache .jasper.servlet.JspServlet, 用于处理所有的 .jsp 和 .jspx 结尾的请求
- JspServlet 的实现就是运行编译时的入口
<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>e:/jsp/</param-value>
</init-param>
复制代码
- 如果没有配置该选项,则 jsp 编译后的结果,将会存放在 Tomcat 的安装目录的 work/Catalina/localhost/ 目录下
预编译
- 预编译: 直接在 web 项目启动时,一次性将 web 应用用的所有 jsp 页面一次性编译完成.这样在 web 项目运行过程中,可以不再需要实时编译,而是直接调用 jsp 页面对应的 servlet 完成请求处理,从而提升系统性能
- 要想进行预编译,必须首先确保下载并安装了 Apache Ant
- Tomcat 中提供了一个 shell 程序 JspC 用于支持 jsp 编译,而且在 Tomcat 安装目录下提供了一个 catalina-tasks.xml 文件声明了 Tomcat 支持的 Ant 任务,这样很容易使用 Ant 来执行 jsp 的预编译
编译原理
代码分析
- 生成的 Java 文件的类名为 index_jsp.java, 继承自 org.apache.jasper.runtime.HttpJspBase, 该类是 HttpServlet 的子类.所以 jsp 的本质就是一个 servlet
- 属性 _jspx_denpendants 保存了当前 jsp 页面依赖的资源,包含引入的外部 jsp 页面,导入的标签,标签所在的 jar 包.便于后续处理过程中使用. 比如以 Map 形式保存了每个资源的上次修改时间便于重新编译检测
- 属性 _jspx_imports_packages 存放导入的 java 包,默认导入 javax .servlet, javax.servlet.http, javax.servlet.jsp
- 属性 _jspx_imports_classes 存放导入的类 ,jsp 页面中通过 import 标签导入的类都会包含在该集合 . _jspx_import_packages 和 _jspx_import_classes 属性主要用于配置 EL 引擎上下文
- 请求处理由 _jspService 方法完成,在父类 HttpJspBase 中的 service 方法通过模板方法模式,调用了子类的 _jspService 方法
- _jspService 方法中定义了几个重要的局部变量 : pageContext, Session , application, config, out, page. 因为整个页面的输出都是由 _jspService 方法完成,因此这些变量和参数会对整个 jsp 页面生效. 这个就是在 jsp 页面中能够使用变量的原因
- jsp 页面中指定文档类型的 page 变标签的值最终作为 response.setContentType() 使用
- 对于生成的 html 文件的静态内容,调用 out.write() 输出
- 对于 <% ..%> 标签中的代码,直接转换为 Servlet 类中的代码,如果在代码中嵌入了静态文件,同样会调用 out.write() 输出
编译流程
- Jasper 的编译流程主要包括代码生成和编译两部分
- Compiler 通过一个 PageInfo 对象保存 jsp 页面编译过程中的各种配置. 这些配置可以是来自于 web 应用的初始化参数,也可以是来自于 jsp 页面的标签指令配置,比如 page, include 等
- 调用 ParseController 解析标签指令节点,验证标签指令是否合法,同时将配置信息保存到 PageInfo 中,用于控制代码生成
- 调用 ParseController 解析整个 jsp 页面,由于 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 完成编译