目录
在动态网页开发中,经常需要动态生成HTML内容,例如,一篇新闻报道的浏览次数需要动态生成。这时,如果使用Servlet来实现HTML页面数据的统计,需要调用大量的输出语句,使静态内容和动态内容混合在一起,导致程序非常臃肿。为了克服Servlet的这些特点,Oracle(Sun)公司推出了JSP技术。
6.1 JSP概述
6.1.1 什么是JSP
JSP全名是Java Server Pages,它是建立在Servlet范围之上的动态网页开发技术。在JSP文件中,HTML代码与Java代码共同存在,其中,HTML代码用来实现网页中静态内容的显示,Java代码用来实现网页中动态内容的显示。为了与传统的HTML有所区别,JSP文件的扩展名为.jsp。
JSP技术所开发的Web应用是基于Java的,它可以用一种简捷而快速的方法从Java程序生成Web页面,其使用上具有如下几点特征。
- 跨平台性:由于JSP是基于Java语言的,它可以使用Java API,所以它也是跨平台的,可以应用于不同的系统中,如Windows、Linux等。当从一个平台移植到另一个平台时,JSP和JavaBean的代码并不需要重新编译,这是因为Java的字节码是与平台无关的,这也应验了Java语言“一次编译,到处运行”的特点。
- 业务和代码相分离:在使用JSP技术开发Web应用时,可以将界面的开发与应用程序的开发相分离开。开发人员使用HTML来设计界面,使用JSP标签和脚本来动态生成页面上的内容。在服务器端,JSP引擎(或容器,本书中是指Tomcat)负责解析JSP标签和脚本程序,生成所求的内容,并将执行结果以HTML页面的形式返回到浏览器。
- 组件重用:JSP中可以使用JavaBean编写业务组件,也就是使用一个JavaBean类封装业务处理代码或者作为一个数据存储模型,在JSP页面中共,甚至在整个项目中,都可以重复使用这个JavaBean。同时,JavaBean也可以应用到其他Java程序中。
- 预编译:预编译就是在用户第1次通过浏览器访问JSP页面时,服务器将对JSP页面代码进行编译,并且仅执行一次编译。编译好的代码将被保存,在用户下一次访问时,会直接执行编译好的代码。这样不仅节约了服务器的CPU的资源,还大大地提升了客户端的访问速度。
6.1.2 编写第一个JSP文件
在Eclipse中,创建一个名称为chapter06的Web项目,然后右键单击WebContent目录【new】【Other】,在弹出的窗口中找到JSP文件,如图6-1所示。
在图6-1中,选择JSP File后,单击【Next】按钮,在新窗口的File name文本框中填写JSP文件名称“HelloWorld”,如图6-2所示。
填写完图6-2中JSP文件名称后,单击【Next】按钮,进入选择模板窗口,此处采用默认设置,如图6-3所示。
单击图6-3中的【Finish】按钮后,第1个JSP文件就创建成功了。创建后的JSP文件代码如图6-4所示。
从图6-4中可以看出,新创建的JSP文件与传统的HTML文件几乎没有什么区别,唯一的区别是默认创建时,页面代码最上方多了一条page指令,并且该文件的后缀名是jsp,而不是html。关于page指令会在6.3节中详细讲解,此处了解即可。JSP文件必须发布到Web容器中的某个Web应用中才能查看出效果。在HelloWorld.jsp的<body>元素内添加上文字“My First JSP”并保存后,将chapter06项目发布到Tomcat中并启动项目,在浏览器地址栏中输入地址“http://localhost:8080/chapter06/HelloWorld.jsp”,此时浏览器的显示效果如图6-5所示。
从图6-5中可以看出,HelloWorld.jsp中添加的内容已被现实出来,这说明了HTML中元素可以被JSP容器所解析。实际上,JSP只是在原有的HTML文件中加入了一些具有Java特点的代码,这些代码具有其独有的特点,称为JSP的语法元素。
6.1.3 JSP运行原理
JSP的工作模式是请求/响应模式,客户端首先发出HTTP请求,JSP程序收到请求后进行处理并返回处理结果。在一个JSP文件第1次被请求时,JSP引擎(容器)把该JSP文件转换成为一个Servlet,而这个引擎本身也是一个Servlet。JSP的运行过程如图6-6所示。
JSP的运行过程具体如下。
(1)客户端发出请求,请求访问JSP文件。
(2)JSP容器先将JSP文件转换成一个Java源文件(Java Servlet源程序),在转换过程中,如果发现JSP文件中存在任何语法错误,则中断过程,并向服务器和哭护短返回出错信息。
(3)如果转换成功,则JSP容器将生成的Java源文件编译成相应的字节码文件*.class。该class文件就是一个Servlet,Servlet容器回想处理其他Servlet一样来处理它。
(4)由Servlet容器加载转换后的Servlet类(.class文件)创建一个该Servlet(JSP页面的转换结果)实例,并执行Servlet的JSPInit()方法。jspInit()方法在Servlet的整个生命周期中只会执行一次。
(5)执行jspService()方法来处理客户端的请求。对于每一个请求,JSP容器都会创建一个新的线程来处理它。如果多个客户端同时请求该JSP文件,则JSP容器也会创建多个线程,使得每一个客户端同时请求该JSP文件,则JSP容器也会创建多个线程,使得每一个客户端请求都对应一个线程。JSP运行过程中采用的这种多线程的执行方式可以极大地降低对系统资源的需求,提高系统的并发量并缩短响应时间。需要注意的是,由于第(4)步生成的Servlet是常驻内存的,所以响应速度非常快。
(6)如果JSP文件被修改了,则服务器将根据设置决定是否对该文件重新编译。如果需要重新编译,则使用重新编译后的结果取代内存中常驻的Servlet,并继续上述处理过程。
(7)虽然JSP效率很高,但在第1次调用的时候往往由于需要转换和编译,所以会产生一些轻微的延迟。此外,有系统资源不足等原因,JSP容器可能会以某种不确定的方式将Servlet从内存中移除,发生这种情况时首先会调用jspDestroy()方法,然后Servlet实例会被加入“垃圾收集”处理。
(8)当请求处理完成后,响应对象由JSP容器接收,并将HTML格式的响应信息发送回客户端。
了解了JSP的运行原理后,完全可以利用其中的一些步骤来做一些工作,如:可以在jspInit()中进行一些初始化工作(建立数据库的连接、建立网络连接、从配置文件中获取一些参数等),可以在jspDestroy()中释放相应的资源等。
为了使读者更容易理解JSP的运行原理,接下来简单分析一下JSP所生成的Servlet代码。以6.1.2小姐的HelloWorld.jsp为例,当用户第1次访问HelloWorld.jsp页面时,该页面会先被JSP容器转换成一个名称为HelloWorld_jsp的源文件,然后将源文件编译为一个名称为HelloWorld_jsp的.class文件。如果项目发布在Tomcat的webapps目录中,源文件和.class文件可以在“Tomcat安装目录/work/Catalina/localhost/应用名/”下找到;如果发布在Eclipse工作空间的.mamadata中,可在上述目录的localhost文件夹中创建一个名称为chapter06的文件夹,在chapter06文件夹中会多出两个文件,如图6-7所示。
在图6-7中,地址栏中的路径多出了org\apache\jsp,这是由于JSP文件转换成类文件时会带有包名,该包名为org.apache.jsp。从图中还可以看出,HelloWorld.jsp以被转换成源文件和.class文件。打开HelloWorld_jsp.java文件,可以查看从转换后的源代码,其主要代码如下图所示。
从上面的代码可以看出,HelloWorld.jsp文件转换后的源文件没有实现Servlet接口,但继承了org.apache.jasper.runtime.HttpJspBase类。在Tomcat源文件中查看HttpJspBase类的源代码,具体如下所示。
从HttpJspBase源代码中可以看出,HttpJspBase类是HttpServlet的一个子类,由此可见,HelloWorld_jsp类就是一个Servlet。
接下来,针对HttpJspBase源代码中的一部分内容进行详细讲解,具体如下。
(1)上述代码中,定义了init()方法,该方法使用final进行修饰,并且其内部调用了jspInit()和_jspInit()方法。因此,在JSP页面所生成的Servlet不能覆盖这两个方法。但是,如果要在JSP页面完成Servlet的init()方法的功能,只能覆盖jspInit()和_jspInit()这两个方法中的任何一个。同样,如果要在JSP页面中完成Servlet的destroy()方法,则只能覆盖jspDestroy()和_jspDestroy()两个方法中的任何一个。
(2)在service()方法中调用了_jspService()方法,也就是说在用户访问JSP文件时,会调用HttpJspBase类中的service()方法来响应用户的请求。根据Java的多态性的特征,在service()方法中,会调用HelloWorld_jsp类中实现_jspService()的方法,用来响应用户的请求。
6.2 JSP基本语法
6.2.1 JSP脚本元素
JSP脚本元素是指嵌套在<%和%>之中的一条或多条Java程序代码。通过JSP脚本元素都可以将Java代码嵌入到HTML页面中,所有可执行的Java代码,都可以通过JSP脚本来执行。
1.JSP Scriptlets
<% java 代码 (变量、表达式等) %>
主要用来输出内容,也可以定义属性,但是不可以定义方法。其中的变量为局部变量。
2.JSP 声明语句
<%!
定义的变量或方法等
%>
主要用来定义成员变量属性(类的属性)和方法(全局方法/类的方法),不可以用来输出。在一个JSP页面中可以有多个JSP声明语句,单个声明中的Java语句可以是不完整的,但是多个声明组合后的结果必须是完整的Java语句。
3.JSP 表达式
<%= expression %>
用于将程序数据(转换成字符串)输出到客户端。JSP表达式中的变量或表达式后面不能有分号(;)。
6.2.2 JSP 注释
<%-- 注释信息 --%>
6.3 JSP 指令
用来设置页面中的信息。
<%@ page 属性名1="属性值1" 属性名2="属性值2" ...%>
6.3.2 include 指令
<%@ include file="被包含的文件地址(相对路径)"%>
用于在JSP页面静态包含一个文件。
注意:
(1)被引入的文件必须遵循JSP语法,其中的内容可以包含静态HTML、JSP脚本元素和JSP指令等普通JSP页面所具有的一切内容。
(2)除了指令元素外,被引入的文件中的其他元素都被转换成相应的Java源代码,然后插入当前JSP页面所翻译成的Servlet源文件中,插入位置与include指令在当前JSP页面中的位置保持一致。
(3)file属性的设置值必须使用相对路径,如果以“/”(一般不用)开头,表示相对于当前Web应用程序的根目录(注意不是站点根目录),否则,表示相对于当前文件。需要注意的是,这里的file属性指定的相对路径是相对于文件(file),而不是相对于页面(page)。
6.4 JSP 隐式对象
6.4.1 隐式对象的概述
JSP为简化频繁使用的对象的创建而默认创建的9个对象,可以直接在JSP页面中使用。
6.4.2 out 对象
向客户端发送文本内容类型为JspWriter,它相当于一种带缓存功能的PrintWriter。
在JSP页面中,通过out隐式对象写入数据相当于将数据插入到JspWriter对象的缓冲区中,只有调用了ServletResponse.getWriter()方法,缓冲区中的数据才能真正写入到Servlet引擎所提供的缓冲区中。out对象通过print语句写入数据后,直到整个JSP页面结束,out对象中输入缓冲区的数据(即first line)才能真正写入到Servlet引擎提供的缓冲区中。而response.getWriter().println();语句则是直接把内容(即second line)写入Servlet引擎提供的缓冲区中,Servlet引擎按照缓冲区中的数据存放顺序输出内容。
使用page指令设置out对象的缓冲区大小
通过page指令中操作缓冲区的buffer属性可以直接将数据写入到Servlet引擎提供的缓冲区中。
此外,当写入到out对象中的内容充满了out对象的缓冲区时,out对象中输入缓冲区的数据也会真正写入到Servlet引擎提供的缓冲区中。
6.4.3 pageContext 对象
pageContext对象时javax.servlets.jsp.PageContext类的实例对象,它代表当前JSP页面的运行环境,并提供了一系列用于获取其他隐式对象的方法。
pageContext还提供了通过操作属性来存储数据的功能。
pageContext对象的作用范围有4个值。(由小到大如下)
- pageContext.PAGE_SCOPE:表示页面范围。
- pageContext.REQUEST_SCOPE:表示请求范围。
- pageContext.SESSION_SCOPE:表示会话范围。
- pageContext.APPLICATION_SCOPE:表示Web应用程序范围。
pageContext对象的使用。
首先使用pageContext获取了request对象,并设置page范围内属性;然后使用获取的request对象设置了request范围内属性,接下来使用pageContext对象获得page和request范围内的相应属性,最后使用JSP表达式输出数据。
通过pageContext对象可以获取到request对象,并且还可以获取不同范围内的属性。
6.4.4 exception 对象
用于封装JSP中抛出的异常信息。需要注意的是exception对象只有在错误处理页面才可以使用(即page指令中指定了属性<%@page idErrorPage="true"%>)。
6.5 JSP 动作元素
JSP动作元素用来控制JSP的行为,执行一些常用的JSP页面动作。
6.5.1 <jsp:include>动作元素
可以把其他资源的输出内容插入到当前JSP页面的输出内容中。
<jsp:include page="relativeURL" flush="true|false" />
page属性用于指定被引入资源的相对路径;flush属性(默认false)用于指定是否将当前页面的输出内容刷新到客户端。
当浏览器第1次请求一个使用<jsp:include>包含其他页面的页面时,Web容器首先会编译被包含的页面,然后将编译处理后的返回结果包含在页面中,之后编译包含页面,最后将两个页面组合的结果回应给浏览器。
将flush属性改为false,刷新浏览器,再次访问dynameicInclude.jsp,这时浏览器等待5秒后,将dynamicInclude.jsp和included.jsp页面的输出内容同时显示了出来。由此可见,Tomcat调用被引入的资源include.jsp时,并没有将当前JSP页面中以输出的内容刷新到客户端。
include指令和<jsp:include>标签都能够包含一个文件,但它们之间有很大的区别如下:
- <jsp:include>标签中要引入的资源和当前JSP页面是两个彼此独立的执行实体,即被动态引入的资源必须能够被Web容器独立执行。而include指令只能引入遵循JSP格式的文件。
- <jsp:include>标签中引入的资源是在运行时才包含的,而且只包含运行结果。而include指令引入的资源是在编译时期包含的,包含的是源代码。
- <jsp:include>标签运行原理RequestDispatcher.include()方法类似,即被包含的页面不能改变响应状态码或者设置响应头,而include指令没有这方面的限制。
6.5.2 <jsp:forward>动作元素
<jsp:forward page="relativeURL" />
将当前请求转发到其他Web资源(HTML页面、JSP页面和Servlet等),在执行请求转发之后的当前页面将不再执行,而是执行该元素指定的目标页面。page属性用于指定请求转发到的资源的相对路径,该路径是相对于当前JSP页面的URL。
由于请求转发是服务器端的操作,浏览器并不知道请求的页面,所以浏览器的地址栏不会发生变化。