Java Web开发中,经常会用到jsp,这里需要知道,容器在处理jsp代码时,会将其转换为Java源代码,然后再编译成完整的Java Servlet类。下面将对这个过程进行介绍。
1、容器如何将jsp转换为Java Servlet
1.1 转换步骤
容器处理jsp的机制,可以理解为如下几个步骤:
- 查看指令(包括page、include和taglib等),得到转换时可能需要的信息。
- 创建一个HTTPServlet子类。对于Tomcat 5,所生成的Servlet会扩展org.apache.jasper.runtime.HttpJspBase。
- 如果一个page指令有import属性,它会在类文件的最上面(package语句下面)写import语句。对于Tomcat 5,package语句(我们在开发时不需要关心)是
package org.apache.jsp;
。 - 如果有声明(就是放在
<%! %>
中的变量声明或者方法声明),容器将这些声明写到类文件中,通常放在类声明的下面,并在服务方法前面。Tomcat 5声明了自己的一个静态变量和一个实例方法。 - 建立服务方法。服务方法具体的方法名是_jspService()。_jspService()由servlet父类被覆盖的service()方法调用,接收HttpServletRequest和HttpServletResponse参数,在该方法中,容器会声明并初始化所有的隐式对象。
- 将普通的HTML(也就是模板文本),scriptlet和表达式放到服务方法中,完成格式化,并写至PrintWriter响应输出。
1.2 例子
假如有jsp代码如下:
<%! int count = 0; %>
The page count is now:
<%= ++count %>
下面是一个Tomcat 5转换jsp生成的servlet类代码:
//这是一个Tomcat 5转化jsp生成的servlet类代码示例
/*
原jsp代码如下:
<%! int count = 0; %>
The page count is now:
<%= ++count %>
*/
package org.apache.jsp;
import javax.servlet.*;
import javax.servlet.http.*;
import javax.servlet.jsp.*;
//如果该jsp的page指令有import属性,这里就会有显式的import语句。该jsp的page指令没有import。
public final class BasicCounter_jsp
extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent {
//一开始是jsp代码中的声明语句
int count = 0;
//接下来是Tomcat 5自己声明的变量和方法
private static java.util.Vector _jspx_dependants;
public java.util.List getDependants(){
return _jspx_dependants;
}
//这里是服务方法的定义
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {
//在服务方法的一开始,容器声明了一堆局部变量,包括表示隐式对象的变量,jsp代码中常会用到这些隐式对象,比如out和request
JspFactory _jspxFactory = null;
PageContext pageContext = null;
HttpSession session = null;
ServletContext application = null;
ServletConfig config = null;
JspWriter out = null;
Object page = this;
JspWriter _jspx_out = null;
PageContext _jspx_page_context = null;
try {
_jspxFactory = JspFactory.getDefaultFactory();
response.setContentType("text/html");
//初始化各种隐式对象
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;
//运行并输出jsp中的HTML、scriptlet和表达式代码
out.write("\r\r\r");
out.write("\rThe page count is now: \r");
out.print(++count);
out.write("\r\r");
} catch (Throwable t){
if(!(t instanceof SkipPageException)){
out = _jspx_out;
if(out != null && out.getBufferSize() != 0)
out.clearBuffer();
if(_jspx_page_context != null)
_jspx_page_context.handlePageException(t);
}
}finally {
if(_jspxFactory != null)
_jspxFactory.releasePageContext(_jspx_page_context);
}
}
}
1.3 jsp转换为servlet之后源文件的存放位置
Tomcat启动之后,当jsp文件第一次被访问之后,jsp就完成了到servlet文件的转换。这样,在tomcat的目录中,我们就可以看到tomcat转换后的servlet文件,包括
.java
和.class
。
一般来说,该这些Tomcat编译后的JSP文件(_jsp.class
和_jsp.java
)可能出现在如下目录:
- 一般存放在你安装的Tomcat目录下的work目录下
C:\Program Files\Apache Software Foundation\Tomcat 8.0\apache-tomcat-8.5.32\work\Catalina\localhost
- 可能没有存放在Tomcat中,那么就是存放在你部署的编译器的workspace中,例如使用IntelliJ IDEA部署
C:\Users\Administrator\.IntelliJIdea2018.2\system\tomcat\_Hello-World-JSP\work\Catalina\localhost\
- 使用Eclipse部署的Tomcat存放的JSP编译后文件
\.metadata\.plugins\com.genuitec.eclipse.easie.tomcat.myeclipse\ tomcat\work\Catalina\localhost\
我在MacOS上,用IDEA 2017进行开发,Tomcat 8,在我的机器上,这些文件出现在如下目录:
/Users/chengxia/Library/Caches/IntelliJIdea2017.1/tomcat/Unnamed_HelloWorld/work/Catalina/localhost/ROOT/org/apache/jsp/
2、通过实例来理解Java Web开发中jsp转换为servlet的过程
为了充分理解jsp到servlet的转化,这里举一个实例。假如要在一个jsp页面中,实现对该jsp访问次数的统计,应该如何实现呢?通过对上面jsp转换为servlet过程的理解,这里我们提供两种思路:通过一个新建类的静态变量实现;通过jsp页面中的变量声明实现。
2.1 通过类的静态变量实现jsp访问次数统计
新建一个Counter类,其中,有一个静态的count成员变量,用于记录访问次数,有一个静态的方法getCount()用于获得count的值并每次自增1。在jsp页面中,只需要调用这个方法就行。代码如下:
com/web/comp/Counter.java:
package com.web.comp;
/**
* Created by chengxia on 2019/2/17.
*/
public class Counter {
/**
* 静态的count变量,属于整个类共享能够实现计数功能。
* */
private static int count;
/**
* getCount()方法,加同步的意思是为了确保并发访问时,能够正常统计访问次数。
* */
public static synchronized int getCount(){
count++;
return count;
}
}
CountViaClass.jsp:
<%--
Created by IntelliJ IDEA.
User: chengxia
Date: 2019/2/17
Time: 10:55 AM
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
<%--
这里必须要导入这个包,不然页面找不到Counter类。
该page import指令会在jsp转化为servlet的过程中,被处理成import语句。
--%>
<%@ page import="com.web.comp.Counter" %>
Counter via Class static member
The page count is:
<%
out.println(Counter.getCount());
%>
启动服务器之后的效果就是,当访问该jsp页面(http://localhost:8080/CountViaClass.jsp
)时,页面能显示当前的访问次数,如下。
2.2 通过jsp声明的方式实现访问次数统计
通过jsp转化为servlet的过程,可以看到jsp中的声明变量(<%! %>
中的变量)会被转化为servlet类的成员变量。从servlet的生命周期,我们知道,servlet在第一次被访问时被实例化,之后,所有对该servlet的访问都是在一个新的线程中执行该实例的服务方法来完成的。
这样,我们如果在jsp页面中通过声明标签定义了一个声明变量,然后,在页面中通过scriptlet实现对该变量的自加和访问,就能够实现访问次数统计功能。代码如下:
CountViaDeclareTag.jsp:
<%--
Created by IntelliJ IDEA.
User: chengxia
Date: 2019/2/17
Time: 10:55 AM
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
Counter via Class static member
<%! int count = 0; %>
The page count is:
<%--
这里是一个表达式标签,标签中的内容会被放入out.println()中,所以最后不能有分号。
--%>
<%= ++count %>
启动服务器之后的效果就是,当访问该jsp页面(http://localhost:8080/CountViaDeclareTag.jsp
)时,页面能显示当前的访问次数,如下。
3、jsp其它
3.1 注释
3.1.1 jsp中的注释
一般来说jsp中有几种注释方式:html注释、jsp注释和java注释。
(1) html注释
这里由于是html注释,如果其中包含scriptlet代码,最后仍然会被执行,这样注释中会看到最后scriptlet执行的结果。这些注释会传递给客户,客户在浏览器中通过查看源码可以看到这些注释。
3.1.2 jsp注释
<%--
这是jsp注释,可以单行也可以多行。
--%>
jsp注释是共jsp开发人员看的,最后不会传递给客户。即时客户在浏览器中查看html源码,也看不到这部分注释内容。这些注释甚至不会出现在jsp代码翻译成的servlet类文件的java代码中(可以在下面找到翻译后类文件的存放位置,然后,打开文件做下验证)。
3.1.3 java注释
由于jsp中可以嵌入java代码,这样,在嵌入的java代码块儿中,可以使用//
和/* */
等java格式的注释。当然,这部分注释也不会传递给客户。
3.2 jsp生成servlet的API
3.2.1 认识jsp相关的三个关键API
尽管在上面提到jsp转化为servlet时,会扩展org.apache.jasper.runtime.HttpJspBase
。但实际中,不需要了解这些,只需要知道如下三个方法关键方法即可:
- jspInt()
这个方法由servlet的init()方法调用,可以覆盖这个方法。 - jspDestroy()
这个方法由servlet的destroy()方法调用,也可以覆盖这个方法。 -
_jspService()
这个方法由servlet的service()方法调用。对于每一个请求,它都会在一个单独的线程中运行,容器将Request和Response对象传递给这个方法。不能覆盖这个方法。对于这个方法,开发人员什么都做不了。不过,在jsp中编写的代码会被放在里面,要由容器开发商来取得你的jsp代码,并生成使用这些jsp代码的_jspService()
方法。(这里有一个常识,下划线开头的方法都不能够被覆盖。)
这三个方法声明在HttpJspPage接口中,简单的接口示意图如下:
前面提到的jsp在转化成servlet时用到的HttpJspBase类,和这几个接口什么关系呢,如下:
org.apache.jasper.runtime Class HttpJspBase
java.lang.Object
javax.servlet.GenericServlet
javax.servlet.http.HttpServlet
org.apache.jasper.runtime.HttpJspBase
All Implemented Interfaces:
javax.servlet.jsp.HttpJspPage, javax.servlet.jsp.JspPage, java.io.Serializable, javax.servlet.Servlet, javax.servlet.ServletConfig
也就是说,org.apache.jasper.runtime.HttpJspBase
类实现了javax.servlet.jsp.HttpJspPage
接口。
3.2.2 通过重写jspInit方法读取jsp初始化参数
和Servlet一样,jsp也可以配置初始化参数。同样是在web.xml文件中,方法和常规的Servlet初始化参数配置基本一样,只是把servlet-class
子标签换成了jsp-file
。如下是一个例子:
web.xml部分:
TestJspInitServlet
/TestJspInit.jsp
name
PaopaoXia
email
[email protected]
TestJspInitServlet
/TestJspInit.jsp
然后,在jsp页面中通过用声明的方式,重写jspInit()方法,可以读取这些参数。
由于jspInit()方法是由Servlet的init()方法调用,所以运行jspInit()方法时,已经有了一个ServletConfig和ServletContext可以使用,所以,可以在jspInit()方法中,调用getServletConfig()和getServletContext()方法。下下面是一个例子,在其中,不但读取了Servlet初始化参数,也使用参数值设置了应用作用域的属性。
TestJspInit.jsp:
<%--
Created by IntelliJ IDEA.
User: chengxia
Date: 2019/2/17
Time: 10:55 AM
To change this template use File | Settings | File Templates.
--%>
<%@ page contentType="text/html;charset=UTF-8" language="java" %>
Test Jsp Init
<%!
String name = "";
String email = "";
public void jspInit(){
ServletConfig sConfig = getServletConfig();
String name = sConfig.getInitParameter("name");
String email = sConfig.getInitParameter("email");
ServletContext ctx = getServletContext();
ctx.setAttribute("name",name);
ctx.setAttribute("email",email);
this.name = name;
this.email = email;
}
%>
name:<%=name%>
email:<%=email%>
最后,启动tomcat,访问http://localhost:8080/TestJspInit2.jsp
,效果如下:
3.3 jsp中的属性
尽管可以像上面例子中那样,使用覆盖jspInit()方法的声明来在jsp页面中设置一个应用作用域属性。但是,大多数情况下,我们会使用四个隐式对象之一来得到和设置对应jsp中4个属性作用域的属性。
除了Servlet中常见的请求、会话和应用作用域外,jsp还增加了第四个作用域,页面作用域,可以从pageContext对象得到。
如下表格中介绍了这几种常见作用域的使用。
Servlet中 | Jsp中 | |
---|---|---|
应用 | getServletContext().setAttribute("foo", barObj) |
application.setAttribute("foo", barObj) |
请求 | request.setAttribute("foo", barObj) |
request.setAttribute("foo", barObj) |
会话 | request.getSession().setAttribte("foo", barObj) |
session.setAttribute("foo", barObj) |
页面 | 不适用 | pageContext.setAttribute("foo", barObj) |
同时,pageContext隐式对象有方法可以接受一个作用域标识参数,从而可以获得和设置任意作用域中的属性值。也有findAttribute()
方法,可以在各个作用域中依次查找指定属性名的属性。
3.4 jsp指令
jsp中有三种常用的指令:page、taglib和include。
3.4.1 page指令
<%@ page import="foo.*" session="false" %>
定义页面特定的属性,如字符编码、页面响应的内容类型,以及这个页面是否要有隐式的会话对象。page指令有如下13个属性:
属性 | 描述 |
---|---|
buffer | 指定out对象使用缓冲区的大小 |
autoFlush | 控制out对象的 缓存区 |
contentType | 指定当前JSP页面的MIME类型和字符编码 |
errorPage | 指定当JSP页面发生异常时需要转向的错误处理页面 |
isErrorPage | 指定当前页面是否可以作为另一个JSP页面的错误处理页面 |
extends | 指定servlet从哪一个类继承 |
import | 导入要使用的Java类 |
info | 定义JSP页面的描述信息 |
isThreadSafe | 指定对JSP页面的访问是否为线程安全 |
language | 定义JSP页面所用的脚本语言,默认是Java |
session | 指定JSP页面是否使用session |
isELIgnored | 指定是否执行EL表达式 |
isScriptingEnabled | 确定脚本元素能否被使用 |
3.4.2 taglib指令
定义可以使用的标记库。JSP API允许用户自定义标签,一个自定义标签库就是自定义标签的集合。
Taglib指令引入一个自定义标签集合的定义,包括库路径、自定义标签。
Taglib指令的语法:
<%@ taglib uri="uri" prefix="prefixOfTag" %>
uri属性确定标签库的位置,prefix属性指定标签库的前缀。
等价的XML语法:
3.4.3 include指令
<%@ include file="subfile.html" %>
定义在转换时增加到当前页面的文本和代码。从而允许用户建立可重用的块儿,如标准页面标题或导航栏,这些可以重用的块儿能增加到各个页面上,而不必在每个页面重复写。
参考资料
- IDEA中部署tomcat,运行JSP文件,编译后的JSP文件存放地点总结
- JSP 指令