第3章〓JSP技术基础

JSP(Java Server Pages)是一种动态页面技术,它的主要目的是将表示逻辑从Servlet中分离出来,实现表示逻辑。在MVC模式中,JSP页面实现视图功能。

本章首先介绍JSP语法和生命周期、脚本元素、隐含变量、JSP动作,然后介绍错误处理、作用域对象和JavaBean的使用,最后介绍MVC设计模式。

本章内容要点

■ JSP页面元素。

■ JSP生命周期。

■ JSP指令和动作。

■ JSP隐含变量。

■ 错误处理。

■ 作用域对象与JavaBean。

■ MVC设计模式。




3.1JSP页面元素

在JSP页面中可以包含多种JSP元素,如声明变量和方法、JSP表达式、指令和动作等,这些元素具有严格定义的语法。当JSP页面被访问时,Web容器将JSP页面转换成Servlet类执行后将结果发送给客户。与其他的Web页面一样,JSP页面也有一个唯一的URL,客户可以通过它访问页面。一般来说,在JSP页面中可以包含的元素如表31所示。


表31在JSP页面中可以包含的元素


JSP页面元素简 要 说 明标 签 语 法


指令指定转换时向容器发出的指令<%@  指令%>
动作向容器提供请求时的指令<jsp:动作名/>
EL表达式JSP页面使用的数据访问语言${EL表达式}
脚本元素

JSP声明声明变量与定义方法<%! Java 声明%>
JSP小脚本执行业务逻辑的Java代码<% Java 代码%>
JSP表达式用于在JSP页面中输出表达式的值<%= 表达式%>

注释用于文档注释<%-- 任何文本--%>


在一个JSP页面中,除JSP元素外,其他内容称为模板文本(template text),也就是HTML标记和文本。清单3.1是一个简单的JSP页面,其中包含了多种JSP元素。

清单3.1todayDate.jsp

<%@ page contentType="text/html;charset=UTF-8" %>

<%@ page import="java.time.LocalDate" %>

<%! LocalDate date=null; %>  

<html>

<head><title>当前日期</title>

</head>

<body>

<%

date=LocalDate.now();  

%> 

今天的日期是:<%=date.toString()%>

</body>

</html>

该页面中包含JSP指令、JSP声明、JSP小脚本、JSP表达式和模板文本。当JSP页面被客户访问时,页面首先在服务器端被转换成一个Java源程序文件,然后该程序在服务器端编译和执行,最后向客户发送执行结果,通常是文本数据。这些数据由HTML标签包围起来,然后发送到客户端。由于嵌入在JSP页面中的Java代码是在服务器端处理的,客户并不了解这些代码。

3.1.1JSP指令简介

JSP指令(directive)向容器提供关于JSP页面的总体信息。在JSP页面中,指令是以“<%@”开头,以“%>”结束的标签。指令有page指令、include指令和taglib指令3种类型。这3种指令的语法格式如下。

<%@ page attribute-list %>

<%@ include attribute-list %>

<%@ taglib attribute-list %>

在上面的指令标签中,attributelist表示一个或多个针对指令的“属性值”对,多个属性之间用空格分隔。

1.  page指令

page指令通知容器关于JSP页面的总体特性。例如,下面的page指令通知容器页面输出的内容类型和使用的字符集。

<%@ page contentType="text/html;charset=UTF-8" %>

2.  include指令

include指令实现把一个文件(HTML、JSP等)的内容包含到当前页面中。下面是include指令的一个例子。

<%@ include file="copyright.html" %>

3.  taglib指令

taglib指令用来指定在JSP页面中使用标准标签或自定义标签的前缀与标签库的URI。下面是taglib指令的例子。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

关于page指令的详细信息,请读者参考3.3.1节,在3.3.2节将详细讨论include指令,在第4章将学习taglib指令的使用。

3.1.2表达式语言

表达式语言(Expression Language,EL)是一种可以在JSP页面中使用的简洁的数据访问语言。它的语法格式如下。

${expression}

表达式语言以$开头,后面是一对大括号,括号里面的expression是EL表达式,也可以是作用域变量、EL隐含变量等。该结构可以出现在JSP页面的模板文本中,也可以出现在JSP标签的属性中。

清单3.1中对于日期的输出可以使用表达式语言改写如下。

今天的日期是:${LocalDate.now()}

由于LocalDate类的now()方法是静态工厂方法,所以可以在EL中直接调用,但需要使用page指令将类导入。第4章将详细讨论表达式语言。

3.1.3JSP动作

JSP动作(actions)是页面发给容器的命令,它指示容器在页面执行期间完成某种任务。动作的一般语法如下。

<prefix:actionName attribute-list />

动作是一种标签,在动作标签中,prefix为前缀名,actionName为动作名,attributelist表示针对该动作的一个或多个“属性值”对。

在JSP页面中可以使用3种动作,即JSP标准动作、标准标签库(JSTL)中的动作和用户自定义动作。例如,下面的代码指示容器把JSP页面copyright.jsp的输出包含在当前JSP页面的输出中。

<jsp:include page="copyright.jsp"/>

3.1.4JSP脚本元素

脚本元素(scripting elements)是在JSP页面中使用的Java代码,主要用于实现业务逻辑,通常有Java声明、Java小脚本和Java表达式3种脚本元素。

1.  JSP声明

JSP声明(declaration)用来在JSP页面中声明变量和定义方法。声明是以“<%!”开头,以“%>”结束的标签,其中可以包含任意数量的合法的Java声明语句。下面是JSP声明的一个例子。

<%! LocalDate date=null; %>

上面的代码声明了一个名为date的变量并将其初始化为null。声明的变量仅在页面第一次载入时由容器初始化一次,初始化后在后面的请求中一直保持该值。

注意: 由于声明包含的是声明语句,所以每个变量的声明语句必须以分号结束。

下面的代码在一个标签中声明了一个变量r和一个方法getArea()。

<%! 

double r=0;//声明一个变量r

double getArea(double r) {      //声明求圆面积的方法

return r*r*Math.PI;

}

%>

2.  JSP小脚本

JSP小脚本(scriptlets)是嵌入在JSP页面中的Java代码段。小脚本是以“<%”开头,以“%>”结束的标签。例如,在清单3.1中下面的代码就是JSP小脚本。

<%

date=LocalDate.now();  //创建一个LocalDate对象

%>

小脚本在每次访问页面时都被执行,因此date变量在每次请求时会返回当前日期。由于小脚本可以包含任何Java代码,所以它通常用来在JSP页面中嵌入计算逻辑。另外,用户还可以使用小脚本打印HTML模板文本。

与其他元素不同,小脚本的起始标签“<%”后面没有任何特殊字符,在小脚本中的代码必须是合法的Java语言代码。例如,下面的代码是错误的,因为它没有使用分号结束。

<% out.print(count) %>

注意: 不能在小脚本中声明方法,因为在Java语言中不能在方法中定义方法。

3.  JSP表达式

JSP表达式(expression)是以“<%=”开头,以“%>”结束的标签,它作为Java语言中表达式的占位符。下面是JSP表达式的例子。

今天的日期是:<%=date.toString()%>

在页面每次被访问时都要计算表达式,然后将其值嵌入HTML的输出中。与变量的声明不同,表达式不能以分号结束,因此下面的代码是非法的。

<%=date.toString();%>

使用表达式可以向输出流输出任何对象或任何基本数据类型(int、boolean、char等)的值,也可以打印任何算术表达式、布尔表达式或方法调用返回的值。

提示: 在JSP表达式的百分号和等号之间不能有空格。

3.1.5JSP注释

JSP注释是以“<%”开头,以“%>”结束的标签。注释不影响JSP页面的输出,但它对用户理解代码很有帮助。JSP注释的格式如下。

<%-- 这里是JSP注释内容 --%>

Web容器在输出JSP页面时去掉JSP注释内容,所以在调试JSP页面时可以将JSP页面中的一大块内容注释掉,包括嵌套的HTML和其他JSP标签,然而不能在JSP注释内嵌套另一个JSP注释。

3.2JSP生命周期

一个JSP页面在执行过程中要经历多个阶段,这些阶段称为生命周期阶段(lifecycle phases)。在讨论JSP页面的生命周期前需要了解JSP页面和它的实现类。

3.2.1JSP页面的实现类

JSP页面从结构上看与HTML页面类似,但它实际上是作为Servlet运行的。当JSP页面第一次被访问时,Web容器解析JSP文件并将其转换成相应的Java文件,该文件声明了一个Servlet类,该类称为页面实现类。接下来,Web容器编译该类并将其装入内存,然后与其他Servlet一样执行并将其输出结果发送到客户端。

这里以清单3.1的todayDate.jsp页面为例,看一下Web容器将JSP页面转换后的Java文件代码。在页面转换阶段Web容器自动将该文件转换成一个名为todayDate_jsp.java的类文件,该文件是JSP页面实现类。若Web项目部署到Tomcat服务器,该文件存放在安装路径的\work\Catalina\localhost\chapter03\org\apache\jsp目录中。限于篇幅,这里不给出页面实现类的源代码,感兴趣的读者可以自己查看。

页面实现类继承了HttpJspBase类,同时实现了JspPage接口,该接口又继承了Servlet接口。JspPage接口只声明了jspInit()和jspDestroy()两个方法,所有的JSP页面都应该实现这两个方法。在HttpJspPage接口中声明了一个_jspService()方法。这3个JSP方法的格式如下。

public void jspInit();


public void _jspService(HttpServletRequest request, 

HttpServletResponse response)

throws ServletException, IOException;


public void jspDestroy();

这3个方法分别等价于Servlet的init()、service()和destroy()方法,称为JSP页面的生命周期方法。

每个容器销售商都提供了一个特定的类作为页面实现类的基类。在Tomcat中,JSP页面转换的类继承了HttpJspBase类。

JSP页面中的所有元素都转换成页面实现类的对应代码,page指令的import属性转换成import语句,page指令的contentType属性转换成response.setContentType()调用,JSP声明的变量转换为成员变量,小脚本转换成正常的Java语句,模板文本和JSP表达式都使用out.write()方法打印输出,输出是用转换的_jspService()方法完成的。

3.2.2JSP执行过程

下面以todayDate.jsp页面为例说明JSP页面的生命周期阶段。当客户首次访问该页面时,Web容器执行该JSP页面要经过7个阶段,如图31所示。其中,前4个阶段将JSP页面转换成一个Servlet类并装载和创建该类的实例,后3个阶段是初始化阶段、提供服务阶段和销毁阶段。



图31JSP页面的生命周期阶段


1. 转换阶段

Web容器读取JSP页面对其解析,并将其转换成Java源代码。JSP文件中的元素都转换成页面实现类的成员。在这个阶段,容器将检查JSP页面中标签的语法,如果发现错误将不能转换。例如,下面的指令就是非法的,因为在“Page”中使用了大写字母P,这将在转换阶段被捕获。

<%@ Page import="java.util.*" %>

除检查语法外,容器还将执行其他有效性检查,其中一些涉及验证。

 指令中“属性值”对对于标准动作的合法性。

 同一个JavaBean名称在一个转换单元中没有被多次使用。

 如果使用了自定义标签库,标签库是否合法、标签的用法是否合法。

一旦验证完成,Web容器将JSP页面转换成页面实现类,它实际上是一个Servlet,3.2.1节中描述了页面实现类及其存放位置。

2. 编译阶段

在将JSP页面转换成Java文件后,Web容器调用Java编译器编译该文件。在编译阶段,将检查在声明中、小脚本中及表达式中所写的全部Java代码。例如,下面的声明标签尽管能够通过转换阶段,但由于声明语句没有以分号结束,所以不是合法的Java声明语句,因此在编译阶段会被查出。

<%! LocalDate date = null %>

大家可能注意到,当JSP页面被首次访问时,服务器响应要比以后的访问慢一些,这是因为在JSP页面向客户提供服务之前必须要转换成Servlet类的实例。对于每个请求,容器要检查JSP页面源文件的时间戳及相应的Servlet类文件,以确定页面是否为新的或是否已经转换成类文件。因此,如果修改了JSP页面,将JSP页面转换成Servlet的整个过程要重新执行一遍。

3.  类的加载和实例化

在将页面实现类编译成类文件后,Web容器调用类加载程序(class loader)将页面实现类加载到内存中,然后容器调用页面实现类的默认构造方法创建该类的一个实例。

4.  调用jspInit()

Web容器调用jspInit()方法初始化Servlet实例。该方法是在任何其他方法调用之前调用的,并在页面生命周期内只调用一次。通常在该方法中完成初始化或只需一次的设置工作,如获得资源及初始化JSP页面中使用<%! ... %>声明的实例变量。

5.  调用_jspService()

对该页面的每次请求,容器都调用一次_jspService(),并给它传递请求和响应对象。JSP页面中所有的HTML元素、JSP小脚本及JSP表达式在转换阶段都成为该方法的一部分。

6.  调用jspDestroy()

当容器决定停止该实例提供服务时,它将调用jspDestroy(),这是在Servlet实例上调用的最后一个方法,它主要用来清理jspInit()获得的资源。

一般不需要实现jspInit()和jspDestroy(),因为它们已经由基类实现,但可以根据需要使用JSP的声明标签<%! ... %>覆盖这两个方法。注意,不能覆盖_jspService(),因为该方法由Web容器自动产生。




3.3JSP指令

在3.1.1节中简要介绍了JSP指令,它用于向容器提供关于JSP页面的总体信息。在JSP页面中,指令是以“<%@”开头,以“%>”结束的标签。指令有page指令、include指令和taglib指令3种类型,下面详细介绍这3种指令的使用。

3.3.1page指令

page指令用于告诉容器关于JSP页面的总体特性,该指令适用于整个转换单元而不仅仅是它所声明的页面。例如,下面的page指令通知容器页面输出的内容类型和使用的字符集。

<%@ page contentType="text/html;charset=UTF-8" %>

表32列出了page指令的常用属性。


表32page指令的常用属性


属性名说明默认值


import导入在JSP页面中使用的Java类和接口,它们之间用逗号分隔java.lang.*; 

jakarta.servlet.*; 

jakarta.servlet.jsp.*; 

jakarta.servlet.http.*; 
contentType指定输出的内容类型和字符集text/html;charset=UTF8
pageEncoding指定JSP文件的字符编码UTF8
session用布尔值指定JSP页面是否参加HTTP会话true
errorPage用相对URL指定另一个JSP页面用来处理当前页面的错误null
isErrorPage用布尔值指定当前JSP页面是否为错误处理页面false
language指定容器支持的脚本语言java
extends任何合法地实现了jakarta.servlet.jsp.JspPage接口的Java类与实现有关
buffer指定输出缓冲区的大小与实现有关
autoFlush指定是否当缓冲区满时自动刷新true
info关于JSP页面的任何文本信息与实现有关
isThreadSafe指定页面是否同时为多个请求服务true
isELIgnored指定是否忽略对EL表达式求值false


大家应该了解page指令的所有属性及它们的取值,尤其是contentType、pageEncoding、import、session、errorPage和isErrorPage属性。

1.  contentType和pageEncoding属性

contentType属性用来指定JSP页面输出的MIME类型和字符集,MIME类型的默认值是text/html,字符集的默认值是UTF8。MIME类型和字符集之间用分号分隔,例如: 

<%@ page contentType="text/html;charset=UTF-8" %>

上述代码与Servlet中的下面一行等价。

response.setContentType("text/html;charset=UTF-8");

对于包含中文的JSP页面,字符编码指定为UTF8或GB18030,如果页面仅包含英文字符,字符编码可以指定为ISO88591,例如:

<%@ page contentType="text/html;charset=ISO-8859-1" %>

pageEncoding属性指定JSP页面的字符编码,它的默认值为UTF8。如果设置了该属性,JSP页面使用该属性设置的字符集编码; 如果没有设置该属性,则JSP页面使用contentType属性指定的字符集。如果页面中含有中文,应该将该属性值指定为UTF8或GB18030,例如: 

<%@ page pageEncoding="UTF-8" %>

2.  import属性

import属性的功能类似于Java程序中的import语句,它是将import属性值指定的类导入页面中。在转换阶段,容器将使用import属性声明的每个包都转换成页面实现类的一个import语句。在一个import属性中可以导入多个包,包名用逗号分开,例如: 

<%@ page import="java.util.*,java.text.*,com.boda.xy.*" %>

为了增强代码的可读性也可以使用多个page指令,如上面的page指令也可以写成: 

<%@ page import="java.util.*" %>

<%@ page import="java.text.*" %>

<%@ page import="com.boda.xy.*" %>

由于在Java程序中import语句的顺序是没有关系的,所以这里import属性的顺序也没有关系。另外,容器总是导入java.lang.*、jakarta.servlet.*、jakarta.servlet.http.*和jakarta.servlet.jsp.*包,所以不必明确地导入它们。

3.  session属性

session属性指示JSP页面是否参加HTTP会话,其默认值为true,在这种情况下容器将声明一个隐含变量session(将在3.4节学习更多的隐含变量)。如果不希望页面参加会话,可以明确地加入下面一行: 

<%@ page session = "false" %>

4.  errorPage和isErrorpage属性

在页面执行的过程中,嵌入在页面中的Java代码可能抛出异常。与一般的Java程序一样,在JSP页面中也可以使用trycatch块处理异常。JSP规范定义了一种更好的方法,它可以使错误处理代码与主页面代码分离,从而提高异常处理机制的可重用性。在该方法中,JSP页面使用page指令的errorPage属性将异常代理给另一个包含错误处理代码的JSP页面。在清单3.2创建的helloUser.jsp页面中,errorHandler.jsp被指定为错误处理页面。

清单3.2helloUser.jsp

<%@ page contentType="text/html; charset=UTF-8" %>

<%@page errorPage="errorHandler.jsp" %>

<html>

<body>

<%

if (request.getParameter("name")==null){

throw new RuntimeException("没有指定name请求参数。");

 }

%>

你好!<%=request.getParameter("name")%>

</body>

</html>

对该JSP页面的请求如果指定了name请求参数值,该页面将正常输出; 如果没有指定name请求参数值,将抛出一个异常,但它本身并没有捕获异常,而是通过errorPage属性指示容器将错误处理代理给errorHandler.jsp页面。

errorPage属性的值不必一定是JSP页面,也可以是静态的HTML页面,例如: 

<%@ page errorPage="errorHandler.html" %>

显然,在errorHandler.html文件中不能编写小脚本或表达式产生动态信息。

isErrorPage属性指定当前页面是否作为其他JSP页面的错误处理页面。isErrorPage属性的默认值为false。如在上例使用的errorHandler.jsp页面中该属性必须明确地设置为true,见清单3.3。

清单3.3errorHandler.jsp

<%@ page contentType="text/html;charset=UTF-8" %>

<%@page isErrorPage="true" %>

<html>

<body>

<table>

<tr><td><img src="images/error.png" width=150 height=100/></td>

<td>页面发生了下面的错误:<%=exception.getMessage()%><br>

请重试!</td></tr>

</table></body>

</html>

在这种情况下,容器在页面实现类中声明一个名为exception的隐含变量。

注意: 该页面仅从异常对象中检索信息并产生适当的错误消息。因为该页面没有实现任何业务逻辑,所以可以被不同的JSP页面重用。

一般来说,为所有的JSP页面指定一个错误页面是一个良好的编程习惯,这可以防止在客户端显示不希望显示的错误消息。

3.3.2include指令

代码的可重用性是软件开发的一个重要原则。使用可重用的组件可以提高应用程序的生产率和可维护性。JSP规范定义了一些允许重用Web组件的机制,其中包括在JSP页面中包含另一个Web组件的内容或输出。这可以通过静态包含或动态包含方式实现。

include指令用于把另一个文件(HTML、JSP等)的内容包含到当前页面中,这种包含称为静态包含。静态包含是在JSP页面转换阶段将另一个文件的内容包含到当前JSP页面中。使用JSP的include指令完成这一功能,它的语法如下。

<%@ include file="relativeURL" %>

file是include指令唯一的属性,它指被包含的文件。文件使用相对路径指定,相对路径或者以斜杠(/)开头,是相对于Web应用程序文档根目录的路径; 或者不以斜杠开头,是相对于当前JSP文件的路径。

下面是include指令的一个例子。

<%@ include file="copyright.html" %>

由于被包含文件的内容成为主页面代码的一部分,所以每个页面都可以访问在另一个页面中定义的变量,它们共享所有的隐含变量。清单3.4创建的hello.jsp页面中包含了response.jsp页面。

清单3.4hello.jsp

<%@ page contentType="text/html;charset=UTF-8" %>

<html>

<head><title>Hello</title></head>

<%! String usernname="Duke"; %>

<body>    

<img src="images/duke.gif"> 

我的名字叫Duke,你的名字叫什么?

<form action="" method="post">

<input type="text" name="username" size="25">

<input type="submit" value="提交">

<input type="reset" value="重置">

</form>

<%@include file="response.jsp" %>

</body>

</html>

清单3.5创建被包含页面response.jsp。

清单3.5response.jsp

<%@ page contentType="text/html;charset=UTF-8" %>

<% username=request.getParameter("username");%>

<h4 style="color:blue">你好,<%=username%>!</h4>



图32hello.jsp的运行结果


在hello.jsp页面中声明了一个变量username,并使用include指令包含了response.jsp页面。在response.jsp页面中使用了hello.jsp页面中声明的变量username。程序的运行结果如图32所示。

在使用include指令包含一个文件时需要遵循下列几个规则。

(1) 在转换阶段不进行任何处理,这意味着file属性值不能是请求时属性表达式,因此下面的使用是非法的。

<%! String pageURL="copyright.html"; %>

<%@ include file="<%= pageURL %>" %>

(2) 不能通过file属性值向被包含的页面传递任何参数,因为请求参数是请求的一个属性,它在转换阶段没有任何意义。下面例子中的file属性值是非法的。

<%@ include file="other.jsp?name=Hacker" %>

(3) 被包含的页面可能不能单独编译。清单3.5的文件就不能单独编译,因为它没有定义username变量。一般来说,最好避免这种依赖性,而使用隐含变量pageContext共享对象,通过使用pageContext的setAttribute()和getAttribute()实现。

3.3.3taglib指令

taglib指令用来指定在JSP页面中使用标准标签或自定义标签的前缀与标签库的URI,下面是taglib指令的例子。

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

对于指令的使用需要注意下面几个问题: 

 标签名、属性名及属性值都是大小写敏感的。

 属性值必须使用一对单引号或双引号引起来。

 在等号(=)与值之间不能有空格。




3.4JSP隐含变量

在JSP页面的转换阶段,容器在页面实现类的_jspService()方法中声明并初始化一些变量,可以在JSP页面的小脚本或表达式中直接使用这些变量。这些变量是由容器隐含声明的,所以一般被称为隐含变量或隐含对象(implicit objects)。表33给出了页面实现类中声明的9个隐含变量。


表33JSP隐含变量


隐 含 变 量类 或 接 口说明


requestjakarta.servlet.http.HttpServletRequest接口引用页面的当前请求对象
responsejakarta.servlet.http.HttpServletResponse接口用来向客户发送一个响应
outjakarta.servlet.jsp.JspWriter类引用页面输出流
pagejava.lang.Object类引用页面的Servlet实例
applicationjakarta.servlet.ServletContext接口引用Web应用程序上下文
sessionjakarta.servlet.http.HttpSession接口引用用户会话
pageContextjakarta.servlet.jsp.PageContext类引用页面上下文
configjakarta.servlet.ServletConfig接口引用Servlet的配置对象
exceptionjava.lang.Throwable类引用异常对象,用来处理错误


下面详细介绍几个最常用的隐含变量的使用。

注意: 这些隐含变量只能在JSP页面的JSP小脚本和JSP表达式中使用。

3.4.1request与response变量

request和response分别是HttpServletRequest和HttpServletResponse类型的隐含变量,当页面实现类向客户提供服务时,它们作为参数传递给_jspService()方法。在JSP页面中使用它们与在Servlet中使用完全一样,即用来分析请求和发送响应,例如: 

<%

String uri=request.getRequestURI();

response.setContentType("text/html;charset=UTF-8");

%>


请求方法为:<%=request.getMethod()%><br>

请求URI为:<%=uri %><br>

协议为:<%=request.getProtocol()%>

用户可以在JSP小脚本中使用隐含变量,也可以在JSP表达式中使用隐含变量。

3.4.2out变量

out是jakarta.servlet.jsp.JspWriter类型的隐含变量,JspWriter类扩展了java.io.Writer并继承了所有重载的write()方法,在此基础上还增加了自己的一组print()和println()来打印输出所有的基本数据类型、字符串及用户定义的对象。用户可以在小脚本中直接使用out变量,也可以在表达式中间接地使用它产生HTML代码。

<% out.print("Hello World!"); %>

<%= "Hello User!" %>

上面两行代码,在页面实现类中都使用out.print()语句输出。

3.4.3application变量

application是jakarta.servlet.ServletContext类型的隐含变量,它是JSP页面所在的Web应用程序的上下文的引用(在第2章中曾讨论了ServletContext接口)。在Servlet中可以使用如下代码访问ServletContext对象,将today存储到ServletContext对象中。

LocalDate today=LocalDate.now();

ServletContext context=getServletContext();

context.setAttribute("today",today);

在JSP页面中,使用下面application对象的getAttribute()方法检索存储在上下文中的数据。

<%= application.getAttribute("today") %>

3.4.4session变量

session是jakarta.servlet.http.HttpSession类型的隐含变量,它在JSP页面中表示HTTP会话对象。如果要使用会话对象,必须要求JSP页面参加会话,即要求将JSP页面的page指令的session属性值设置为true。

在默认情况下,session属性的值为true,所以即使没有指定page指令,该变量也会被声明并可以使用。下面的代码可以在页面中输出当前会话的ID。

会话ID=<%=session.getId()%>

然而,如果明确地将page指令的session属性设置为false,容器将不会声明该变量,对该变量的使用将产生错误,例如:

<%@ page session="false" %>

3.4.5exception变量

如果一个页面是错误处理页面,即页面中包含下面的page指令。

<%@ page isErrorPage="true" %>

则页面实现类中将声明一个exception隐含变量,它是java.lang.Throwable类型的隐含变量,被用来作为其他页面的错误处理器。为了使页面能够使用exception变量,必须在page指令中将isErrorPage的属性值设置为true,例如: 

<%@page isErrorPage="true" %>

<html><body>

页面发生了下面的错误:<br> 

<%=exception.toString()%>

</body></html>

在上述代码中,将page指令的isErrorPage属性设置为true,容器明确地定义了exception变量。该变量指向使用该页面作为错误处理器的页面抛出的未捕获的异常对象。

如果去掉第一行,容器将不会明确地定义exception变量,因为isErrorPage属性的默认值为false,此时使用exception变量将产生错误。

3.4.6config变量

config是jakarta.servlet.ServletConfig类型的隐含变量。在第2章曾介绍过,用户可以通过DD文件为Servlet传递一组初始化参数,然后在Servlet中使用ServletConfig对象检索这些参数。

类似地,用户也可以为JSP页面传递一组初始化参数,这些参数在JSP页面中可以使用config隐含变量来检索。如果要实现这一点,应该首先在DD文件web.xml中使用<servletname>声明一个Servlet,然后使用<jspfile>元素使其与JSP文件关联,这样Servlet的所有初始化参数就可以在JSP页面中通过config隐含变量使用。例如: 

<servlet>

<servlet-name>initTestServlet</servlet-name>

<jsp-file>/initTest.jsp</jsp-file>

<init-param>

<param-name>company</param-name>

<param-value>Beijing New Techonology CO.,LTD</param-value>

</init-param>

<init-param>

<param-name>email</param-name>

<param-value>smith@yahoo.com.cn</param-value>

</init-param>

</servlet>

<servlet-mapping>

<servlet-name>initTestServlet</servlet-name>

<url-pattern>/initTest.jsp</url-pattern>

</servlet-mapping>

以上代码声明了一个名为initTestServlet的Servlet并将它映射到/initTest.jsp文件,同时为该Servlet指定了company和email初始化参数,该参数可以在initTest.jsp文件中使用隐含变量config检索到。例如:

公司名称:<%=config.getInitParameter("company")%><br>

邮箱地址:<%=config.getInitParameter("email")%>

3.4.7pageContext变量

pageContext是jakarta.servlet.jsp.PageContext类型的隐含变量,它是一个页面上下文对象。PageContext类是一个抽象类,容器厂商提供了一个具体子类(如JspContext),它有下面3个作用。

(1) 存储隐含对象的引用。pageContext对象管理在JSP页面中使用的所有其他对象,包括用户定义的对象和隐含对象,并且提供了一个访问方法来检索它们。如果用户查看JSP页面生成的Servlet代码,会看到session、application、config与out这些隐含变量是调用pageContext对象的相应方法得到的。

(2) 提供了在不同作用域内返回或设置属性的方便的方法。本书3.7节将对相关内容进行详细说明。

(3) 提供了forward()和include()实现将请求转发到另一个资源和将一个资源的输出包含到当前页面中的功能,它们的格式如下。

 public void include(String relativeURL): 将另一个资源的输出包含在当前页面的输出中,与RequestDispatcher()接口的include()的功能相同。

 public void forward(String relativeURL): 将请求转发到参数指定的资源,与RequestDispatcher接口的forward()的功能相同。

例如,从Servlet中将请求转发到另一个资源,需要写下面两行代码。

RequestDispatcher rd=request.getRequestDispatcher("other.jsp");

rd.forward(request,response);

在JSP页面中,使用pageContext变量仅需要一行就可以完成上述功能。

<%

pageContext.forward("other.jsp");

%>

3.5JSP动作

JSP动作(action)是页面发给容器的命令,它指示容器在页面执行期间完成某种任务。动作的一般语法如下。

<prefix:actionName attribute-list/>

动作的表示类似于HTML的标签,在动作标签中需要指定前缀名(prefix)、动作名(actionName)和属性列表(attributelist),它是针对该动作的一个或多个属性值对。

在JSP页面中可以使用3种动作,即JSP标准动作、标准标签库(JSTL)中的动作和用户自定义动作。

下面是常用的JSP标准动作。

 <jsp:include.../>: 在当前页面中包含另一个页面的输出。

 <jsp:forward.../>: 将请求转发到指定的页面。

 <jsp:useBean.../>: 查找或创建一个JavaBean对象。

 <jsp:setProperty.../>: 设置JavaBean对象的属性值。

 <jsp:getProperty.../>: 返回JavaBean对象的属性值。

下面介绍<jsp:include>和<jsp:forward>两个动作,3.9节将介绍有关JavaBean使用的3个动作。

3.5.1<jsp:include>动作

在3.3.2节讨论了使用include指令实现静态包含,本节讨论使用JSP的<jsp: include>动作实现动态包含。动态包含是在请求时将另一个页面的输出包含到主页面的输出中。该动作的格式如下。

<jsp:include page="relativeURL" flush="true|false"/>

这里page属性是必需的,其值必须是一个相对URL,它指向任何静态或动态Web组件,包括JSP页面、Servlet等。可选的flush属性用于指定在将控制转向被包含页面之前是否刷新主页面。如果当前JSP页面被缓冲,那么在把输出流传递给被包含组件之前应该刷新缓冲区。flush属性的默认值为false。

例如,下面的动作指示容器把另一个JSP页面copyright.jsp的输出包含在当前JSP页面的输出中。

<jsp:include page="copyright.jsp" flush="true"/>

在功能上<jsp:include>动作的语义与RequestDispatcher接口的include()方法的语义相同,因此在Servlet中使用下面的代码实现包含。

RequestDispatcher rd=request.getRequestDispatcher("other.jsp");

rd.include(request,response);

在JSP页面还可以使用下面的结构实现动态包含,就是在脚本中使用pageContext.include()方法包含一个JSP页面的输出。

<%  

pageContext.include("other.jsp");

%>

<jsp:include>动作的page属性的值可以是请求时属性表达式,例如: 

<%! String pageURL="other.jsp"; %>

<jsp:include page="<%= pageURL %>"/>

1. 使用<jsp:param>传递参数

在<jsp:include>动作中可以使用<jsp:param/>向被包含的页面传递参数。下面的代码向somePage.jsp页面传递两个参数: 

<jsp:include page="somePage.jsp">

<jsp:param name="name1" value="value1"/>

<jsp:param name="name2" value="value2"/>

</jsp:include>

在<jsp:include>元素中可以嵌入任意多个<jsp:param>元素。value属性的值也可以像下面这样使用请求时属性表达式来指定。

<jsp:include page="somePage.jsp">

<jsp:param name="name1" value="<%= someExpr1 %>"/>

<jsp:param name="name2" value="<%= someExpr2 %>"/>

</jsp:include>

通过<jsp:param>动作传递的“名值”对保存在request对象中并且只能由被包含的组件使用,在被包含的页面中使用request隐含对象的getParameter()获得传递来的参数。这些参数的作用域是被包含的页面,在被包含的组件完成处理后,容器将从request对象中清除这些参数。

上面的例子使用的是<jsp:include>动作,这里的讨论也适用于<jsp:forward>动作。

2.  与动态包含的组件共享对象

被包含的页面是单独执行的,因此它们不能共享在主页面中定义的变量和方法; 它们处理的请求对象是相同的,因此可以共享属于请求作用域的对象。下面看清单3.6和清单3.7两个程序。

清单3.6hello2.jsp

<%@ page contentType="text/html;charset=UTF-8" %>

<html>

<head><title>Hello</title></head>

<body>

<img src="images/duke.gif"> 

我的名字叫Duke,你的名字叫什么?

<form action="" method="post">

<input type="text" name="username" size="25">

<input type="submit" value="提交">

<input type="reset" value="重置">

</form>

<% String userName=request.getParameter("username");

request.setAttribute("username",userName);

%>

<jsp:include page="response2.jsp"/>

</body>

</html>

清单3.6产生的输出结果与清单3.4产生的输出结果相同,但它使用了动态包含。主页面hello2.jsp通过调用request.setAttribute()把username对象添加到请求作用域中,然后被包含的页面response2.jsp通过调用request.getAttribute()检索该对象并使用表达式输出。

清单3.7response2.jsp

<%@ page contentType="text/html;charset=UTF-8" %>

<% String username=(String)request.getAttribute("username"); %>

<h4 style="color:blue">你好,<%=username%>!</h4>

这里,hello2.jsp文件中的隐含变量request与response2.jsp文件中的隐含变量request是请求作用域内的同一个对象。对<jsp:forward>动作可以使用相同的机制。

除request对象外,用户还可以使用隐含变量session和application在被包含的页面中共享对象,但它们的作用域不同。例如,如果使用application代替request,那么username对象就可以被多个客户使用。

3.5.2<jsp:forward>动作

使用<jsp:forward>动作把请求转发到其他组件,然后由转发到的组件把响应发送给客户,该动作的格式如下。

<jsp:forward page="relativeURL"/>

page属性的值为转发到的组件的相对URL,它可以使用请求时属性表达式。<jsp:forward>动作与<jsp:include>动作的不同之处在于,当转发到的页面处理完输出后,并不将控制转回主页面。使用<jsp:forward>动作,主页面也不能包含任何输出。

下面的<jsp:forward>动作将控制转发到other.jsp页面。

<jsp:forward page="other.jsp"/>

在功能上<jsp:forward>的语义与RequestDispatcher接口的forward()的语义相同,因此它的功能与在Servlet中实现请求转发的功能等价。

RequestDispatcher rd=request.getRequestDispatcher("other.jsp");

rd.forward(request,response);

在JSP页面中使用<jsp:forward>标准动作实现的实际上是控制逻辑的转移。在MVC设计模式中,控制逻辑应该由控制器(Servlet)实现而不应该由视图(JSP页面)实现,因此尽可能不要在JSP页面中使用<jsp:forward>动作转发请求。




3.6案例学习: 使用包含设计页面布局

Web应用程序页面应该具有统一的视觉效果,或者说所有的页面都有同样的整体布局。一种比较典型的布局通常包含标题部分、脚注部分、菜单、广告区和主体实际内容部分。在设计这些页面时,如果在所有的页面中复制相同的代码,不仅不符合模块化设计原则,将来修改布局也非常麻烦。使用JSP技术提供的include指令(<%@ include .../>)包含静态文件和使用include动作(<jsp:include .../>)包含动态资源就可以实现一致的页面布局。

清单3.8的home.jsp页面使用<div>标签和include指令实现页面布局。

清单3.8home.jsp

<%@ page contentType="text/html; charset=UTF-8"%>

<html>

<head>

<title>百斯特电子商城</title>

<link href="css/style.css" rel="stylesheet" type="text/css"/>

</head>

<body>

<div class="container">

<div class="header">

<%@include file="/WEB-INF/jsp/header.jsp"%>

</div>

<div class="topmenu">

<%@ include file="/WEB-INF/jsp/topmenu.jsp"%></div>

<div class="mainContent" class="clearfix">

<div class="leftmenu">

<%@ include file="/WEB-INF/jsp/leftmenu.jsp"%></div>

<div class="content">

<%@ include file="/WEB-INF/jsp/content.jsp"%></div>

</div>

<div class="footer">

<%@ include file="/WEB-INF/jsp/footer.jsp"%></div>

</div>

</body>

</html>

被包含的JSP文件存放在WEBINF/jsp目录中,WEBINF中的资源只能被服务器访问,这样可以防止这些JSP页面被客户直接访问。访问index.jsp页面,输出结果如图33所示。



图33index.jsp页面的运行结果


该页面使用了CSS样式进行布局,样式表文件style.css的代码见清单3.9。

清单3.9style.css

@CHARSET "UTF-8";

body,div,ul{

margin:0;

padding:0;

}

p{text-align:center;}

a:link{

color:blue;

text-decoration:none;

}

a:hover{

background-color:gray;

}

.container {

width:1004px;

margin:0 auto;   

}

.header { 

margin-bottom:5px;

}

.topmenu { 

margin-bottom:5px;

}

.mainContent {

margin:0 0 5px 0;

}

.leftmenu { 

float:left; width:120px;

padding:5px 0 5px 30px;

}

.leftmenu ul{

list-style:none;

}

.leftmenu p{

margin:0 0 10px 0;

}

.content { 

float:left; width:700px;

}

.footer { 

clear:both;    

height:60px;

}

清单3.10~清单3.14分别是标题页面header.jsp、顶部菜单页面topmenu.jsp、左侧菜单页面leftmenu.jsp、主体内容页面content.jsp和页脚页面footer.jsp的代码。

清单3.10header.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java">

<script language="JavaScript" type="text/javascript">

function register(){

open("/helloweb/register.jsp","register");

}

</script>

<p><img src="images/head.jpg" /></p>

清单3.11topmenu.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java">

<table border='0'>

<tr>

<td><a href="/helloweb/index.jsp">【首页】</a></td> 

<td>

<form action="login.do" method="post" name="login"> 

用户名<input type="text" name="username" size="13"/> 

密码 <input type="password" name="password" size="13"/>

<input type="submit" name="submit" value="登录"> 

<input type="button" name="register" value="注册" 

onclick="check();"> 

</form>

</td>

<td><a href="showOrder">我的订单</a>|</td>

<td><a href="showCart">查看购物车</a></td>

</tr>

</table>

清单3.12leftmenu.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java">

<p><b>商品分类</b></p>

<ul>

<li><a href="showProduct?category=101">手机数码</a></li>

<li><a href="showProduct?category=102">家用电器</a></li>

<li><a href="showProduct?category=103">汽车用品</a></li>

<li><a href="showProduct?category=104">服饰鞋帽</a></li>

<li><a href="showProduct?category=105">运动健康</a></li>

</ul>

清单3.13content.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java">

<table border="0">

<tr><td colspan="2">

<b><i>${sessionScope.message}</i></b></td>

</tr>

<tr>   

<td colspan="4">百斯特11.11!手机价格真正低,买华为手机送苹果13!</td>

</tr>

<tr>          

<td width=20%><img src="images/phone.jpg" width=100></td>

<td><p style="text-indent:2em">HUAWEI Mate 40 Pro麒麟9000 5G SoC芯片

超感知徕卡电影影像8GB+256GB秘银色5G全网通 特价:5288元</p>

<img src="images/gw.jpg">

</td>



<td width=20%><img src="images/comp.jpg" width=120></td>

<td><p style="text-indent:2em">联想(Lenovo)G460AL-ITH 14.0英寸

笔记本电脑(i8-370M 2G 500G 512独显 DVD刻录 摄像头 Win7)特价:3199元!</p>

<img src="images/gw.jpg">            

</td>

</tr>

</table>

清单3.14footer.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java">

<hr/>

<p>关于我们|联系我们|人才招聘|友情链接</p>    

<p>版权所有 &copy; 2024 百斯特电子商城公司,电话:8899123.</p>

在上面这些被包含的文件中没有使用<html>和<body>等标签。实际上,它们不是完整的页面,而是页面片段,因此文件名也可以不使用.jsp作为扩展名,而是可以使用任何的扩展名,如.htmlf、.jspf等。

由于被包含的文件是由服务器访问的,所以可以将被包含的文件存放到Web应用程序的WEBINF目录中,这样可以防止用户直接访问被包含的文件。

3.7错误处理

与任何Java程序一样,Web应用程序在执行过程中可能发生各种错误。例如,网络问题可能引发SQLException异常,在读取文件时可能因为文件损坏发生IOException异常。如果这些异常没有被适当处理,Web容器将产生一个Internal Server Error页面,给用户显示一个长长的栈跟踪。这在产品环境下通常是不可以接受的。

3.7.1声明式错误处理

可以在web.xml文件中为整个Web应用配置错误处理页面,使用这种方法还可以根据异常类型的不同或HTTP错误码的不同配置错误处理页面。

在web.xml文件中配置错误页面需要使用<errorpage>元素,它有以下3个子元素。

 <errorcode>: 指定一个HTTP错误代码,如404。

 <exceptiontype>: 指定一种Java异常类型(使用完全限定名)。

 <location>: 指定要被显示的资源位置。该元素值必须以“/”开头。

下面的代码为HTTP的状态码404配置了一个错误处理页面。

<error-page>

<error-code>404</error-code>

<location>/errors/notFoundError.html</location>

</error-page>

下面的代码声明了一个处理SQLException异常的错误页面。

<error-page>

<exception-type>java.sql.SQLException</exception-type>

<location>/errors/sqlError.html</location>

</error-page>

另外,还可以像下面这样声明一个更通用的处理页面。

<error-page>

<exception-type>java.lang.Throwable</exception-type>

<location>/errors/errorPage.html</location>

</error-page>

Throwable类是所有异常类的根类,因此没有明确指定错误处理页面的异常都将由该页面处理。

注意: 在<errorpage>元素中,<errorcode>和<exceptiontype>不能同时使用。<location>元素的值必须以斜杠(/)开头,它是相对于Web应用程序的上下文根目录。另外,如果在JSP页面中使用page指令的errorPage属性指定了错误处理页面,则errorPage属性指定的页面优先。

3.7.2使用Servlet和JSP页面处理错误

在前面的例子中使用HTML页面作为异常处理页面,HTML是静态页面,不能为用户提供有关异常的信息。可以使用Servlet或JSP作为异常处理页面,下面的代码使用Servlet处理403错误码,使用JSP页面处理SQLException异常。

<error-page>

 <error-code>403</error-code>

 <location>/errorHandler-servlet</location>

</error-page>


<error-page>

 <exception-type>java.sql.SQLException</exception-type>

 <location>/errors/sqlError.jsp</location>

</error-page>

为了在异常处理的Servlet或JSP页面中分析异常原因并产生详细的响应信息,Web容器将控制转发到错误页面前在请求对象(request)中定义了若干属性。

 jakarta.servlet.error.status_code: 类型为java.lang.Integer,该属性包含错误的状态码值。

 jakarta.servlet.error.exception_type: 类型为java.lang.Class,该属性包含未捕获的异常的Class对象。

 jakarta.servlet.error.message: 类型为java.lang.String,该属性包含在sendError()方法中指定的消息,或包含在未捕获的异常对象中的消息。

 jakarta.servlet.error.exception: 类型为java.lang.Throwable,该属性包含未捕获的异常对象。

 jakarta.servlet.error.request_uri: 类型为java.lang.String,该属性包含当前请求的URI。

 jakarta.servlet.error.servlet_name: 类型为java.lang.String,该属性包含引起错误的Servlet名。

下面的代码显示了MyErrorHandlerServlet的doGet()方法,它使用这些属性在产生的错误页面中包含有用的错误信息。

public void doGet(HttpServletRequest request, 

HttpServletResponse response)

throws ServletException,IOException{  

response.setContentType("text/html;charset=UTF-8");

PrintWriter out=response.getWriter();

out.println("<html>");

out.println("<head><title>Error Demo</title></head>");

out.println("<boy>");

String code=""+

 request.getAttribute("jakarta.servlet.error.status_code");

if("403".equals(code){

out.println("<h3>对不起,您无权访问该页面!</h3>");

out.println("<h3>请登录系统!</h3>");

}else{

out.println("<h3>对不起,我们无法处理您的请求!</h3>");

out.println("请将该错误报告给管理员 admin@xyz.com! " +

request.getAttribute("jakatar.servlet.error.request_uri"));

}

out.println("</body>");

out.println("</html>");

}

上述Servlet根据错误码产生一个自定义的HTML页面。

除了标准的异常,Web应用程序还可能定义自己的业务逻辑异常。例如,在银行应用中可能定义InsufficientFundsException表示资金不足异常和InvalidTransactionException表示非法交易异常,它们用来表示通常的错误条件。同样需要解析这些异常中的消息,并把这些消息展示给用户。




3.8作用域对象

在Web应用中经常需要将数据存储到某个作用域中,然后在JSP页面中使用它们。作用域(scope)有应用作用域、会话作用域、请求作用域和页面作用域4种,它们的类型分别是ServletContext、HttpSession、HttpServletRequest和PageContext,这4种作用域如表34所示。


表34JSP作用域


作 用 域 名对应的对象存在性和可访问性


应用作用域application在整个Web应用程序中有效
会话作用域session在一个用户会话范围内有效
请求作用域request在用户的请求和转发的请求内有效
页面作用域pageContext只在当前的页面(转换单元)内有效


在JSP页面中,用户定义的对象存储在这4种作用域的一种之中,这些作用域定义了对象的存在性和在JSP页面中的可访问性。在作用域中,应用作用域的应用范围最大,页面作用域的应用范围最小。

3.8.1应用作用域

存储在应用作用域的对象可以被Web应用程序的所有组件共享,并且在应用程序的生命周期内都可以访问。这些对象是通过ServletContext实例作为“属性值”对维护的。在JSP页面中,该实例可以通过隐含对象application访问。因此,如果要在应用程序级共享对象,可以使用ServletContext接口的setAttribute()和getAttribute()方法。例如,在Servlet中使用下面的代码将对象存储在应用作用域中。

User user=new User("张大海","12345");

ServletContext context=getServletContext();

context.setAttribute("user",user);

在JSP页面中可以使用脚本(不推荐)访问应用作用域中的数据,也可以使用EL表达式访问应用作用域中的数据,例如:

${applicationScope.user}

3.8.2会话作用域

存储在会话作用域的对象可以被属于一个用户会话的所有请求共享,并且只能在会话有效时才可以被访问。这些对象是通过HttpSession类的一个实例作为“属性值”对维护的。在JSP页面中,该实例可以通过隐含对象session访问。因此,如果要在会话级共享对象,可以使用HttpSession接口的setAttribute()和getAttribute()方法。

在购物车应用中,用户的购物车对象就应该存放在会话作用域中,它在整个用户会话中共享。

HttpSession session=request.getSession(true);

//从会话对象中检索购物车

ShoppingCart cart=(ShoppingCart)session.getAttribute("cart");

if (cart==null) {

cart=new ShoppingCart();

//将购物车存储到会话对象中

session.setAttribute("cart",cart);

}

存储在会话作用域中的数据在JSP页面中通过session隐含对象访问,例如:

<%

//从会话作用域中取出购物车对象cart

ShoppingCart cart=(ShoppingCart) session.getAttribute("cart");

//从购物车中取出每件商品并存储在ArrayList中

ArrayList<GoodsItem> items=new ArrayList<GoodsItem>(cart.getItems());

%>

3.8.3请求作用域

存储在请求作用域的对象可以被处理同一个请求的所有组件共享,并且仅在该请求被服务期间可以被访问。这些对象是由HttpServletRequest对象作为“属性值”对维护的。在JSP页面中,该实例是通过隐含对象request的形式被使用的。通常,在Servlet中使用请求对象的setAttribute()将一个对象存储到请求作用域中,然后将请求转发到JSP页面,在JSP页面中通过脚本或EL取出作用域中的对象。

例如,下面的代码在Servlet中创建一个User对象并存储在请求作用域中,然后将请求转发到validservlet去验证。

User user=new User();

user.setName(request.getParameter("name"));

user.setPassword(request.getParameter("password"));

request.setAttribute("user",user);

RequestDispatcher rd=request.getRequestDispatcher("/valid-servlet");

rd.forward(request,response);

在ValidateServlet中使用下面的代码验证用户的合法性。

User user=(User) request.getAttribute("user");

if (isValid(user)){//验证用户是否合法

request.removeAttribute("user");

session.setAttribute("user",user);

dispatchUrl="account.jsp";

}else{

dispatchUrl="loginError.jsp";

}

RequestDispatcher rd=request.getRequestDispatcher(dispatchUrl);

rd.forward(request,response);

这里用isValid()方法验证用户是否合法,若合法,将用户对象存储在会话作用域中并将请求转发给account.jsp页面,否则将请求转发到loginError.jsp。

3.8.4页面作用域

存储在页面作用域的对象只能在它们所定义的转换单元中被访问。它们不能存在于一个转换单元的单个请求处理之外。这些对象是由PageContext抽象类的一个具体子类的一个实例作为“属性值”对维护的。在JSP页面中,该实例可以通过隐含对象pageContext访问。

为了在页面作用域中共享对象,可以使用jakarta.servlet.jsp.PageContext定义的setAttribute()和getAttribute()方法。下面的代码设置一个页面作用域的属性。

<% Float one=new Float(42.5);%>

<% pageContext.setAttribute("number",one);%>

下面的代码获得一个页面作用域的属性。

<%= pageContext.getAttribute("number")%>

在PageContext类中还定义了几个常量和其他属性处理方法,使用它们可以方便地处理不同作用域的属性。




3.9JavaBean

JavaBean是Java平台的组件技术,在Java Web开发中常用JavaBean来存放数据、封装业务逻辑等,从而很好地实现业务逻辑和表示逻辑的分离,使系统具有更好的健壮性和灵活性。对程序员来说,JavaBean最大的好处是可以实现代码的重用,另外对程序的易维护性等也有很大的意义。

本节首先介绍JavaBean规范,然后重点介绍如何在JSP页面中使用JavaBean的3个动作,分别是<jsp:useBean>动作、<jsp:setProperty>动作和<jsp:getProperty>动作。

3.9.1JavaBean规范

JavaBean是用Java语言定义的类,这种类的设计需要遵循JavaBean规范的有关约定。任何满足下面3个要求的Java类都可以作为JavaBean使用。

(1) JavaBean应该是public类,且实现java.io.Serializable接口。该类应该提供无参数的public构造方法,通过定义不带参数的构造方法或使用默认的构造方法均可以满足这个要求。

(2) JavaBean类的成员变量一般称为属性(property)。每个属性的访问权限一般定义为private,而不是public。注意,属性名必须以小写字母开头。

(3) 每个属性通常定义两个public方法,一个是访问方法,另一个是修改方法,使用它们访问和修改JavaBean的属性值。

除了访问方法和修改方法,在JavaBean类中还可以定义其他方法实现某种业务逻辑,也可以只为某个属性定义访问方法,这样的属性就是只读属性。

清单3.15中的Customer类是一个JavaBean,它使用3个private属性封装了客户信息,并提供了访问和修改这些信息的方法。

清单3.15Customer.java

package com.boda.domain;


import java.io.Serializable;


public class Customer implements Serializable{

private String name;

private String email;

private String phone;


public Customer(){}


public Customer(String name,String email,String phone){

this.name=name;

this.email=email;

this.phone=phone;

}


public String getName(){ 

return this.name; 

}


public String getEmail(){ 

return this.email; 

}


public String getPhone(){ 

return this.phone; 

}


public void setName(String name){ 

this.name=name; 

}


public void setEmail(String email){ 

this.email=email; 

}


public void setPhone(String phone){ 

this.phone=phone; 

}

}

该类定义了3个属性,为其定义了无参数构造方法和带参数构造方法,还为该类的每个属性定义了getter方法和setter方法。

3.9.2使用Lombok库

在清单3.15定义的Customer类中不仅定义了属性,还定义了构造方法和每个属性的访问方法及修改方法,这就需要程序员编写很多行模板代码。使用Lombok库的注解标注类可以避免编写这些模板代码,需要在pom.xml文件中添加下面的依赖。

<dependency>

<groupId>org.projectlombok</groupId>

<artifactId>lombok</artifactId>

<version>1.18.24</version>

</dependency>

在JavaBean类的定义中就可以使用@Data等注解标注类,之后在运行时系统将自动添加访问方法和修改方法。另外,也可以通过注解添加无参数构造方法和带参数构造方法。

下面使用Lombok库的注解标注Customer类。

package com.boda.domain;


import lombok.AllArgsConstructor;

import lombok.Data;

import lombok.NoArgsConstructor;

import java.io.Serializable;


@Data

@NoArgsConstructor

@AllArgsConstructor

public class Customer implements Serializable{

private String name;

private String email;

private String phone;

}

在上述代码中@Data注解用于生成属性的访问方法和修改方法,@NoArgsConstructor注解用于生成无参数构造方法,@AllArgsConstructor注解用于生成带所有参数的构造方法。

注意: 在IntelliJ IDEA中使用Lombok库需要安装Lombok插件。

3.9.3<jsp:useBean>动作

<jsp:useBean>动作用来在指定的作用域中查找或创建一个bean实例,一般格式如下。

<jsp:useBean  id="beanName"  class="package.class"

scope="page|request|session|application"

其他元素

</jsp:useBean>

id属性用来唯一标识一个bean实例。在JSP页面的实现类中,id的值被作为Java语言的变量,因此可以在JSP页面的表达式和小脚本中使用该变量。

class属性指定创建bean实例的Java类。如果在指定的作用域中找不到一个现存的bean实例,将使用class属性指定的类创建一个bean实例。如果该类属于某个包,则必须指定类的全名,如com.boda.xy.Customer。

scope属性指定bean实例的作用域,该属性的取值可以是page、request、session或application,它们分别表示页面作用域、请求作用域、会话作用域和应用作用域。该属性是可选的,默认值为page作用域。如果将page指令的session属性设置为false,则bean不能在JSP页面中使用会话作用域。

下面的动作使用id、class和scope属性声明一个JavaBean。

<jsp:useBean id="customer" class="com.boda.xy.Customer" scope="session"/>

当JSP页面执行到该动作时,容器在会话(session)作用域中查找或创建bean实例,并用customer引用指向它。这个过程与下面的代码等价。

Customer customer=(Customer)session.getAttribute("customer");

if (customer==null){

customer=new Customer();

session.setAttribute("customer",customer);

}

3.9.4<jsp:setProperty>动作

<jsp:setProperty>动作用来给bean实例的属性赋值,它的格式如下。

<jsp:setProperty name="beanName" property="propertyName"  

value="{string|<%=expression%>}"/>

name属性用来标识一个bean实例,该实例必须是使用<jsp:useBean>动作声明的,并且name属性值必须与<jsp:useBean>动作中指定的id属性值相同。该属性是必需的。

property属性指定要设置值的bean实例的属性,容器将根据指定的bean的属性调用适当的setXxx(),因此该属性也是必需的。value属性指定属性值。

另外,还可以使用param属性指定请求参数名,如果请求中包含指定的参数,那么使用该参数值来设置bean的属性值。

提示: 在MVC设计模式中不建议在页面中使用<jsp:setProperty>动作设置JavaBean的属性值,而应该在Servlet或控制器中设置属性值,在页面中只显示属性值。

3.9.5<jsp:getProperty>动作

<jsp:getProperty>动作检索并向输出流中打印bean的属性值,它的语法如下,非常简单。

<jsp:getProperty name="beanName" property="propertyName"/>

该动作只有name和property两个属性,并且都是必需的。name属性指定bean实例名,property属性指定要输出的属性名。

下面的动作指示容器打印customer的email和phone属性值。

<jsp:getProperty name="customer" property="email"/>

<jsp:getProperty name="customer" property="phone"/>

下面通过清单3.16说明这几个动作的使用。如果要在JSP页面中使用JavaBean,可以通过JSP标准动作<jsp:useBean>创建类的一个实例,JavaBean类的实例一般称为一个bean。

inputcustomer.jsp页面接收用户输入的信息,然后将控制转到CustomerServlet,最后将请求转发到displaycustomer.jsp页面。

清单3.16inputcustomer.jsp

<%@ page contentType="text/html;charset=UTF-8" %>

<html>

<head> <title>输入客户信息</title></head>

<body>

<h4>输入客户信息</h4>

<form action="customer-servlet" method="post">

<table>

<tr><td>客户名:</td> <td><input type="text" name="name"></td></tr>

<tr><td>邮箱地址:</td><td><input type="text" name="email"></td></tr>

<tr><td>电话:</td><td><input type="text" name="phone"></td></tr>

<tr><td><input type="submit" value="确定"></td>

<td><input type="reset" value="重置"></td>

</tr>

</table>

</form>

</body>

</html>

清单3.17给出了如何在Servlet代码中创建JavaBean类的实例,以及如何使用作用域对象共享它们,可以直接在Servlet中使用JavaBean,并且可以在JSP页面和Servlet中共享bean实例。

清单3.17CustomerServlet.java

package com.boda.xy;


import jakarta.servlet.*;

import jakarta.servlet.http.*;

import jakarta.servlet.annotation.WebServlet;

import com.boda.domain.Customer;


@WebServlet(name="customerServlet",value={"/customer-servlet"})

public class CustomerServlet extends HttpServlet {

public void doPost(HttpServletRequest request,

HttpServletResponse response)

throws java.io.IOException,ServletException {

String name=request.getParameter("name");

String email=request.getParameter("email");

String phone=request.getParameter("phone");

Customer customer=new Customer(name,email,phone);


HttpSession session=request.getSession();

session.setAttribute("customer",customer);

response.sendRedirect("display-customer.jsp");

}

}

这个例子说明在Servlet中可以把JavaBean实例存储到作用域对象中。

清单3.18创建的页面在会话作用域内查找customer的一个实例,并用表格的形式打印出它的属性值。

清单3.18displaycustomer.jsp

<%@ page contentType="text/html;charset=UTF-8" language="java" %>

<jsp:useBean id="customer" 

class="com.boda.domain.Customer" scope="session"/>

<html>

<head><title>显示客户信息</title></head>

<body>

<h4>客户信息如下</h4>

<table border="1">

<tr>

 <td>客户名:</td>

 <td><jsp:getProperty name="customer" property="name"/></td>

</tr>

<tr>

 <td>邮箱地址:</td>

 <td><jsp:getProperty name="customer" property="email"/></td>

</tr>

<tr>

 <td>电话:</td>

 <td><jsp:getProperty name="customer" property="phone"/></td>

</tr>

</table>

</body></html>

该页面首先在会话作用域内查找名为customer的bean实例,如果找到,将输出bean实例的各属性值。

在JSP页面中使用JavaBean,早期的方法是使用3个JSP标准动作实现的,它们分别是<jsp:useBean>动作、<jsp:setProperty>动作和<jsp:getProperty>动作。在JSP 2.0中不推荐用这种方法使用JavaBean,而是使用EL表达式访问JavaBean对象,例如:

${sessionScope.customer.email}

${sessionScope.customer.phone}

表达式语言将在第4章中详细讨论。

3.10MVC设计模式

Sun公司在推出JSP技术以后提出了建立Web应用程序的两种体系结构方法,这两种方法分别称为模型1和模型2,二者的差别在于处理请求的方式不同。

3.10.1模型1介绍

在模型1体系结构中没有一个核心组件控制应用程序的工作流程,所有的业务处理都使用JavaBean实现。每个请求的目标都是JSP页面。JSP页面负责完成请求需要的所有任务,其中包括验证用户、使用JavaBean访问数据库及管理用户状态等。最后,响应结果通过JSP页面发送给用户。

该结构具有严重的缺点。首先,它需要将实现业务逻辑的大量Java代码嵌入JSP页面中,这对不熟悉服务器端编程的Web页面设计人员不友好; 其次,这种方法并不具有代码可重用性。例如,为一个JSP页面编写的用户验证代码无法在其他JSP页面中重用。

3.10.2模型2介绍

模型2体系结构如图34所示,这种体系结构又称为MVC(ModelViewController)设计模式。在这种结构中,将Web组件分为模型(Model)、视图(View)和控制器(Controller),每种组件完成各自的任务。在这种结构中,所有请求的目标都是Servlet或Filter,它充当应用程序的控制器。Servlet分析请求并将响应所需要的数据收集到JavaBean对象或POJO对象中,该对象作为应用程序的模型。数据可以从数据库中检索,也可以存储到数据库中。最后,Servlet控制器将请求转发到JSP页面。这些页面使用存储在JavaBean中的数据产生响应。JSP页面构成了应用程序的视图。



图34模型2体系结构


该模型的最大优点是将业务逻辑和数据访问从表示层分离出来。控制器提供了应用程序的单一入口点,提供了较清晰地实现安全性和状态管理的方法,并且这些组件可以根据需要实现重用。然后,根据客户的请求,控制器将请求转发给合适的表示组件,由该组件来响应客户。这使得Web页面开发人员可以只关注数据的表示,因为JSP页面不需要任何复杂的业务逻辑。

在Web应用系统开发中被广泛应用的Spring MVC框架就是基于模型2体系结构的。Spring MVC对系统中各部分要完成的功能和职责有一个明确的划分,采用Spring MVC开发Web应用程序可以节省开发时间和费用,同时开发出来的系统易于维护。现在越来越多的Web应用系统开始采用Spring MVC框架开发。

3.10.3实现MVC设计模式的一般步骤

使用MVC设计模式开发Web应用程序一般使用下面的步骤。

1. 定义JavaBean存储数据

在Web应用中通常使用JavaBean对象或实体类存放数据,在JSP页面作用域中取出数据,因此首先应该根据应用处理的实体设计合适的JavaBean。例如,在订单应用中可能需要设计Product、Customer、Orders、OrderItem等JavaBean类。

2. 使用Servlet处理请求

在MVC模式中,使用Servlet或Filter充当控制器,从请求中读取请求信息(如表单数据)、创建JavaBean对象、执行业务逻辑,最后将请求转发到视图组件(JSP页面)。Servlet通常并不直接向客户输出数据。在控制器创建JavaBean对象后需要填写该对象的值,可以通过请求参数值或访问数据库得到有关数据。

3.  将结果存储到作用域中

在创建了与请求相关的数据并将数据存储到JavaBean对象中以后,接下来应该将这些对象存储到JSP页面能够访问的地方。在Web中主要可以在3个位置存储JSP页面所需的数据,它们是HttpServletRequest对象、HttpSession对象和ServletContext对象。下面的代码创建Customer类对象并将其存储到会话作用域中。

Customer customer=new Customer(name,email,phone);

HttpSession session=request.getSession();

session.setAttribute("customer",customer);

4.  转发请求到JSP页面

在使用请求作用域共享数据时,应该使用RequestDispatcher对象的forward()方法将请求转发到JSP页面。在使用ServletContext对象或请求对象的getRequestDispatcher()方法获得RequestDispatcher对象以后,调用它的forward()方法将控制转发到指定的组件。

在使用会话作用域共享数据时,使用响应对象的sendRedirect()方法重定向可能更合适。

5. 从模型中提取数据

当请求到达JSP页面以后,使用表达式语言和JSTL标准标签库等从模型中取出数据,在JSP页面中显示。例如,下面的代码显示客户信息。

客户名:${customer.name}<br>

客户地址:${customer.address}

提示: 建议使用EL和JSTL输出数据,不建议使用<jsp:getProperty>输出模型数据,因为EL更简单、方便。第4章学习表达式语言和JSTL。

本章小结

本章讨论了在JSP页面中使用指令、声明、小脚本、表达式、动作及注释等语法元素。JSP页面在其生命周期中要经历7个阶段,前4个阶段是页面转换阶段、编译阶段、加载和实例化阶段,后3个阶段是初始化阶段、提供服务阶段和销毁阶段。

在JSP页面中可以使用的指令有3种,即page指令、include指令和taglib指令。在Java Web开发中可以有多种方式重用Web组件。在JSP页面中包含组件的内容或输出实现Web组件的重用有两种方式,即使用include指令的静态包含和使用<jsp:include>动作的动态包含。

JavaBean是遵循一定规范的Java类,它在JSP页面中主要用来表示数据。MVC设计模式是Web应用开发中最常使用的设计模式,它将系统中的组件分为模型、视图和控制器,实现了业务逻辑和表示逻辑的分离。

练习与实践