http://www.educity.cn/jiaocheng/j10259.html
1.2.1 JDk 简介
JDK是Sun公司在1995年推出的一套可以跨操作系统平台编译和运行Java程序的开发包。JDK包括JRE(Java的运行环境)、Java的编译环境、Java工具集和Java类库。根据JDK的使用领域,还可以分为Java SE、Java EE和Java ME三套开发包。
其中Java SE主要用于桌面程序、服务类程序的开发;
Java EE用于企业应用程序的开发(如Web、EJB等);
Java ME用于编写在移动设备、便携式设备上运行的程序。
这三套开发包都使用相同的Java编译环境和运行环境(也就是说,都可以在操作系统上使用相同的JDK进行开发),它们的区别是所带的Java类库不同。如Java EE包含了一些在企业应用中所需要的类库,如Servlet API、JSP API等。
在2006年,Sun已经逐步将JDK开源了。 目前JDK的最高版本为JDK1.7.这个版本可以从如下的地址下载:
http://download.java.net/jdk7/binaries/
从JDK1.7开始,JDK完全由开源社区进行维护和开发。因此,JDK1.7将是完全由开源社区发布的第一个JDK版本。但由于目前使用JDK1.7的开发人员和企业并不多,因此,本书的Java运行环境仍然使用比较成熟的JDK1.6.该JDK版本可以从如下的地址下载:http://java.sun.com/javase/downloads/index.jsp
1.2.2 安装和配置JDK
(1)在下载完JDK后,直接执行安装程序进行安装即可。
(2)在安装完后,需要将环境变量JAVA_HOME的值设为JDK的安装目录。
(3)假设JDK被安装在C:\java\jdk6目录中,则在"系统属性"对话框中选中"高级"选项卡,单击"环境变量"按钮,出现"环境变量"对话框,在"系统变量"列表框中添加一个JAVA_HOME环境变量,并将其值设为C:\java\jdk6,如图1.1所示。
图1.1 添加JAVA_HOME环境变量
(4)除了设置JAVA_HOME环境变量外,还需要在PATH环境变量中添加JDK的bin目录。该路径为<JDK安装目录>\bin.设置PATH环境变量的方法与设置JAVA_HOME环境变量的方法类似。
测试JDK
1.2.3 测试JDK
在安装完JDK后,在Windows控制台中输入如下的命令来显示JDK的当前版本:
java -version
在执行完上面的命令后,如果在Windows控制台中输出如下的信息,则说明JDK已经安装成功了。
java version "1.6.0_14-ea"
Java(TM) SE Runtime Environment (build 1.6.0_14-ea-b04)
Java HotSpot(TM) Client VM (build 14.0-b13, mixed mode, sharing)
下面来编写一个简单的Java程序来测试一下JDK的编译和运行环境。在C盘根目录新建一个TestJDK.java文件,代码如下:
public class TestJDK
{
public static void main(String[] args)
{
System.out.println("JDK安装成功,已通过测试!");
}
}
执行如下的命令编译上面的程序:
javac TestJDK.java
如果在C盘根目录生成了TestJDK.class文件,则执行下面的命令运行TestJDK.class:
javac -classpath . TestJDK
如果正常输出"JDK安装成功,已通过测试!",则表明JDK编译和运行环境没有问题。
要注意的是,在上面的javac命令中加入了"-classpath .",这是因为在当前操作系统中可能未将当前的目录加入CLASSPATH环境变量,在这时如果不加入"-classpath .",javac是不会自动在当前目录来寻找TestJDK.class文件的,因此,也就会抛出未找到TestJDK类的异常。当然,也可以直接在CLASSPATH环境变量中加入当前的路径,这样就不需要在javac后面加-classpath命令行参数了。
1.2.3 测试JDK
在安装完JDK后,在Windows控制台中输入如下的命令来显示JDK的当前版本:
java -version
在执行完上面的命令后,如果在Windows控制台中输出如下的信息,则说明JDK已经安装成功了。
java version "1.6.0_14-ea"
Java(TM) SE Runtime Environment (build 1.6.0_14-ea-b04)
Java HotSpot(TM) Client VM (build 14.0-b13, mixed mode, sharing)
下面来编写一个简单的Java程序来测试一下JDK的编译和运行环境。在C盘根目录新建一个TestJDK.java文件,代码如下:
public class TestJDK
{
public static void main(String[] args)
{
System.out.println("JDK安装成功,已通过测试!");
}
}
执行如下的命令编译上面的程序:
javac TestJDK.java
如果在C盘根目录生成了TestJDK.class文件,则执行下面的命令运行TestJDK.class:
javac -classpath . TestJDK
如果正常输出"JDK安装成功,已通过测试!",则表明JDK编译和运行环境没有问题。
要注意的是,在上面的javac命令中加入了"-classpath .",这是因为在当前操作系统中可能未将当前的目录加入CLASSPATH环境变量,在这时如果不加入"-classpath .",javac是不会自动在当前目录来寻找TestJDK.class文件的,因此,也就会抛出未找到TestJDK类的异常。当然,也可以直接在CLASSPATH环境变量中加入当前的路径,这样就不需要在javac后面加-classpath命令行参数了。
Tomcat简介
1.3 架设Tomcat
1.3.1 Tomcat简介
Tomcat是一个免费开源的Web服务器。由Apache组织负责开发和维护。目前Tomcat的最新版本是Tomcat6.0.18.该版本支持Servlet和JSP的最新规范。目前Servlet规范的最新版本是Servlet2.5、JSP规范的最新版本是JSP2.1。
与传统的桌面应用程序不同,在Tomcat中运行的应用程序是由若干。class、。jsp等文件及war文件组成的。这些文件不能单独运行,必须依靠Tomcat中内置的Servlet容器才能运行。在Tomcat中发布程序的方法很多,最简单方法的就是直接将war文件复制到<Tomcat安装目录>\webapps目录中就可以对应用程序进行发布。Tomcat的默认端口号是8080,在发布Web程序后,可以通过该端口访问相应的Web程序。
安装和测试Tomcat
1.3.2 安装和测试Tomcat
Tomcat的安装文件有3种形式:zip包、tar.gz包和Windows安装程序,读者可以下载任何一种安装包进行安装。
如果下载的是zip或tar.gz包,解压后,可运行<Tomcat安装目录>\bin目录中的startup.bat命令启动Tomcat.如果下载的是Windows安装程序,需要先安装,然后再启动Tomcat.
启动Tomcat后,在浏览器地址栏中输入如下的URL:
http://localhost:8080
如果在浏览器中显示如图1.2所示的页面,则说明Tomcat已安装成功。
图1.2 Tomcat的主页面
安装和配在本书中使用了Tomcat的最新版本6.0.18.读者可以从下面的网址下载Tomcat:
http://tomcat.apache.org/download-60.cgi
Eclipse简介
1.4 Eclipse的搭建
1.4.1 Eclipse简介
Eclipse是一种可扩展的开放源代码IDE.在2001年11月,IBM公司捐出了价值4000万美元的Eclipse源代码,同时组建了Eclipse基金会,并由该基金会中的成员负责这种工具的后续开发。虽然Eclipse基金会由IBM创建,但该基金会是一家非赢利机构,并独立于IBM公司。
Eclipse最强大,也是最有魅力的地方就是支持插件扩展。Eclipse的内核非常小,而Eclipse上的各种功能丰富且强大的应用都是由Eclipse插件来完成的。由于Eclipse可以使用插件进行扩展,因此,Eclipse不仅可以做为Java的开发工具,而且也可以作为其他语言(如C/C++、PHP、Ruby、Python等)的开发工具。除此之外,Eclipse还支持报表、Web、EJB、性能测试等功能。因此可以看出,Eclipse已经用其强大的插件资源构筑了一个强大的开放平台。
安装和配置Eclipse
1.4.2 安装和配置Eclipse
(1)下载完Eclipse后,直接解压安装包(一个zip文件),并运行<Eclipse安装目录>中的eclipse.exe命令即可启动Eclipse.
(2)为了在Eclipse中使用Tomcat,需要进行配置。选择Window | Preferences菜单项,出现Preferences对话框,在左侧的选项树中单击Service | Runtime Environments选项,单击右侧的Add按钮,并选择Apache Tomcat v6.0, 如图1.3所示。
图1.3 选择Tomcat服务器
(3)单击Next按钮,进入下一个页面来设置Tomcat的安装目录,如图1.4所示。
图1.4 设置Tomcat的安装目录
(4)在进行上面的设置后,会在Server Runtime Environments列表框中显示刚才选择的Tomcat服务器,如图1.5所示。
图1.5 Server Runtime Environments列表框
(5)除此之外,在Server视图中也会显示刚才选择的Tomcat服务器,如果在服务器可发布的工程中没有读者需要发布的工程,可以通过选择Tomcat服务器右键菜单的Add and Remove Projects菜单项添加相应的Eclipse工程。如果想启动、停止、调试Tomcat,只需单击Tomcat服务器右键菜单中相应的菜单项即可。图1.6是本书所涉及到的在Server视图中的四个Eclipse工程。
图1.6 Server视图
本书使用的是Eclipse 3.4.2的Java EE版本,可以从如下的网址下载这个Eclipse版本:
http://www.eclipse.org/downloads/
下载和安装MySQL
1.6 安装和运行本书的示例程序
本书涉及如下4个Eclipse工程:
demo:该工程包含了除了21章、22章和23章的综合实例外的所有示例程序。
entry:该工程是第21章的"用户登录和注册系统"的源代码。
album:该工程是第22章的"电子相册"的源代码。
blog:该工程是第23章的"Blog系统"的源代码。
对于Web应用程序,所有相关的jar包需要直接复制WEB-INF\lib目录中,而对于非Web程序(如控制台程序),需要在工程属性对话框中对这些包进行引用,如图1.8所示。
图1.8 工程属性对话框
在随书光盘中有一个lib目录,该目录中包含了本书示例使用的所有jar包。读者可以在demo工程(其他3个工程都是Web应用程序,不需要引用这些包)中直接引用这些jar包。在完成上面的工作后,可以使用1.4节的方法将这4个工程添加到Server视图的Tomcat服务器上。然后启动Tomcat,就可以在浏览器地址栏中输入相应的URL来运行本书的Web应用程序了。
除此之外,随书光盘还有一个tomcat目录,该目录中包含了同样的示例代码,读者可以将这些代码直接复制到<Tomcat安装目录>\webapps目录中,启动Tomcat后,即可运行本书提供的Web应用程序。
第2章 JDBC基础
JDBC全称是Java DataBase Connectivity Standard,它是一种基于Java的数据库访问接口。它本身并不能操作数据库,而必须依赖于数据库厂商提供的符合JDBC规范的JDBC驱动程序才可以操作数据库。 由于JDBC是Java操作数据库的核心,所以很多基于Java的数据持久框架(如Hibernate、IBATIS等)在底层都是用JDBC实现的。因此,在学习数据持久化框架之前,理解并掌握JDBC是非常必要的。本章结合了大量的实例,对JDBC的主要知识点进行了详细的讲解。通过对本章的学习,可以使读者比较全面地了解JDBC操作数据库的方方面面,并为学习后面的内容打下基础。
2.1 第一个JDBC程序
不管是什么开发语言或技术,操作数据库的基本步骤都是类似的。在本节首先介绍了操作数据库的基本步骤,然后将JDBC操作数据库的步骤和这些基本步骤进行对比,最后给出了一个例子来演示如何使用JDBC来操作数据库。
操作数据库的一般步骤
2.1.1 操作数据库的一般步骤
尽管不同的数据库产品的使用方法不同,但操作各种数据库的基本步骤总是十分类似。就拿网络数据库(如SQL Server、Oracle、等)来说,操作数据库一般分为如下4步:
(1)装载数据库驱动(可选)
这一步并不是必须的,在某些操作系统中,数据库驱动的相关信息在驱动程序安装时已经被保存到指定的位置(如在Windows中,SQL Server数据库驱动信息被保存到注册表中),因此,在编写程序时,一般并不需要开发人员来完成这一步。而这一步通常是由所使用的语言或开发工具(如C#、Delphi等)自动完成的。当然,对于JDBC驱动这一步还是必须要做的。
(2)建立数据库连接(就是获得Connection对象)
对于网络数据库,建立数据库连接一般要提供下面5种信息:
数据库服务器名(可以是机器名、域名、或IP地址)
端口号
数据库名
用户名
密码
上面五种信息根据数据库产品的不同略有差异,如数据库使用的是默认端口。在连接数据库时,端口号一般可以省略。
(3)获得用于进行数据操作的对象
这一步对于不同的数据库产品虽然有很大的差异,但这些差异大多都是表现形式上的,而它们的作用都类似。获得这些对象后,就可以进行数据库查询、对数据库的增、删、改以及执行存储过程等操作。
(4)关闭数据库
在操作完数据库后,显式地将数据库关闭是一个好的习惯(尽量不要通过退出程序的方式来关闭数据库)。
JDBC操作数据库的步骤
2.1.2 JDBC操作数据库的步骤
在上一节介绍了操作数据库的一般步骤。本节就以JDBC为例来一一对照这些步骤操作MySQL数据库。JDBC操作数据库的步骤如下:
(1)装载数据库驱动
这一步对于JDBC来说是必须的。用JDBC装载数据库驱动有2种方法。
① 使用Class.forName方法
forName方法是Class类的一个静态方法,返回Class对象。它有一个字符串类型的参数,需要传入一个JDBC驱动类名,如下面代码所示:
Class.forName("com.mysql.jdbc.Driver");
其中com.mysql.jdbc.Driver为MySQL的JDBC驱动类名。
② 静态创建JDBC驱动类实例
不仅可以使用forName方法动态装载JDBC驱动类,也可以直接使用new关键字静态创建JDBC驱动类对象,代码如下:
Driver myDriver = new com.mysql.jdbc.Driver();
DriverManager.registerDriver(myDriver);
其中registerDriver方法是DriverManager类的静态方法,用于注册创建的JDBC驱动类对象。
(2)建立数据库连接
在JDBC中,可以使用DriverManager类的getConnection方法获得数据库连接对象。在获得数据库连接对象之前,需要知道如下5种信息:
数据库服务器名:localhost
端口号:省略
数据库名:jdbcdemo
用户名:root
密码:1234
由于MySQL使用了默认的端口号(3306),因此,端口号信息可被省略。MySQL的连接字符串格式如下:
jdbc:mysql://servername/dbname?parameter
按着上面的信息依次填入这个连接字符串,填完后的连接字符串如下:
jdbc:mysql://localhost/jdbcdemo?user=root&password=1234&characterEncoding=UTF8
由于需要在数据库中处理中文,所以在连接字符串的最后需要加上characterEncoding=UTF8,以保证正确处理中文。获得数据连接对象的代码如下:
String connStr = "jdbc:mysql://localhost/mydb?" +
"user=root&password=1234&characterEncoding=UTF8";
Connection conn = DriverManager.getConnection(connStr);
我们也可以不在连接字符串中指定用户名和密码,而使用getConnection方法的另外一个重载形式传递用户名和密码,代码如下:
String connStr = "jdbc:mysql://localhost/mydb?characterEncoding=UTF8";
Connection conn = DriverManager.getConnection(connStr, "root", "1234");
除此之外,也可以使用Connection类的setCatalog方法改变当前数据库,代码如下:
conn.setCatalog("newdb");
(3)获得用于进行数据操作的对象
在JDBC中可以使用Statement对象和PreparedStatement对象来操作数据库。在本节只介绍Statement对象,PreparedStatement对象将在2.4.2节介绍。Statement对象可以通过Connection接口的createStatement方法创建,createStatement方法有三种重载形式,在本节中只介绍一种无参数的重载形式。创建Statement对象的代码如下:
Statement stmt = conn.createStatement();
通过Statement对象可以对数据库进行查询、增、删、改等操作。在本节只介绍Statement接口的两个方法:execute和executeQuery.Statement接口的其他方法将在后面的章节介绍。
execute方法一般用于执行DDL(CREATE、DROP等)语句,或是执行INSERT、UPDATE、DELETE等语句,如下面代码所示:
Statement stmt = conn.createStatement();
stmt.execute("DROP TABLE IF EXISTS t_books");
executeQuery一般用于执行SELECT语句。这个方法通过一个ResultSet对象返回查询结果,代码如下:
Statement stmt = conn.createStatement();
ResultSet result = stmt.executeQuery("SELECT * FROM t_books");
(4)关闭数据库
最后一步就是关闭数据库。也就是关闭Connection对象。但建议在关闭Connection对象之前,应先关闭Statement对象,代码如下:
stmt.close();
conn.close();
2.1.3 JDBC执行SQL语句
下面给出一个例子来演示一下如何使用JDBC来执行各种SQL语句,其中包括DDL语句(建立数据库和数据表)、INSERT语句和SELECT语句。
【实例2.1】 JDBC执行SQL语句
本程序首先创建一个mydb数据库(如果存在就不创建),然后创建一个用于保存图书信息的表t_books(如果存在,删除后再创建),最后向表中插入两条记录,并查询和显示其中的第2条记录。
package chapter2;
import java.sql.*;
public class ExecuteDemo
{
public static void main(String[] args) throws Exception
{
// 装载JDBC驱动
Class.forName("com.mysql.jdbc.Driver");
// 获得Connection对象
Connection conn = DriverManager.getConnection("jdbc:mysql://local host/?characterEncoding=UTF8","root","1234");
// 获得Statement对象
Statement stmt = conn.createStatement();
String createDB = "CREATE DATABASE IF NOT EXISTS mydb DEFAULT CHARACTER SET UTF8";
String dropTable = "DROP TABLE IF EXISTS mydb.t_books";
String createTable = "CREATE TABLE mydb.t_books (id int unsigned NOT NULL"+ " auto_increment, name varchar(50) NOT NULL,isbn varchar(20)"+"NOT NULL, author varchar(20) NOT NULL,price int unsigned,"+"PRIMARY KEY (id)) ENGINE=InnoDB DEFAULT CHARSET=UTF8";
String insertData1="INSERT INTO mydb.t_books(name,isbn,author,price) values(" + "'人月神话', '6787102165345', '布鲁克斯', 52)";
String insertData2="INSERT INTO mydb.t_books(name,isbn,author,price) values("+ "'Ajax基础教程', '5643489212407', '阿斯利森', 73)";
String selectData = "SELECT * FROM mydb.t_books where id=2";
// 建立数据库、表,并插入数据
stmt.execute(createDB);
stmt.execute(dropTable);
stmt.execute(createTable);
stmt.execute(insertData1);
stmt.execute(insertData2);
// 从数据库中查询数据
ResultSet result = stmt.executeQuery(selectData);
// 显示查询结果
while (result.next())
{
System.out.print(result.getString("id") + " ");
System.out.print(result.getString("name") + " ");
System.out.print(result.getString("isbn") + " ");
System.out.print(result.getString("author") + " ");
System.out.println(result.getInt("price"));
}
// 关闭Statement和Connection对象
stmt.close();
conn.close();
}
}
在上面的代码中由于数据库mydb可能不存在,所以在连接字符串中并没有指定数据库名。因此,在使用mydb中的表时必须指定数据库名,如代码中的mydb.t_books。
2.2.1 使用executeQuery查询数据
executeQuery方法用于执行产生单个结果集的SQL语句,如SELECT语句。executeQuery方法不能执行INSERT、UPDATE、DELETE以及DDL语句,如果执行这些语句,executeQuery将抛出SQLException异常。executeQuery方法的定义如下:
ResultSet executeQuery(String sql) throws SQLException;
从executeQuery方法的定义上可看出,executeQuery方法返回了一个ResultSet对象,可以通过这个对象来访问查询结果集。对结果集最常用的操作就是扫描结果集。可以使用ResultSet的next方法完成这个任务。next方法做了如下两件事:
1. 判断是否还有下一条记录。如果还有下一条记录,返回true,否则返回false.
2. 如果有下一条记录,将当前记录指针向后移一个位置。下面的代码演示了如何使用next方法来扫描查询结果集:
…
// 获得Connection对象
Connection conn = DriverManager.getConnection(url, "user", "password");
Statement stmt = conn.createStatement();
// 执行SQL语句(必须是SELECT语句),并返回ResultSet对象
ResultSet rs = stmt.executeQuery(sql);
// 通过next方法对记录集中每一条记录进行扫描
while(rs.next())
{
// 处理当前行记录
}
stmt.close();
conn.close();
…
在读取数据时可以根据列索引和列名来定义字段。读数据的方法的一般形似为getXxx.getXxx方法可以在读取数据时将数据转换为其他的数据类型。
通过列名读取数据的部分getXxx方法定义如下:
String getString(String columnName);
boolean getBoolean(String columnName);
byte getByte(String columnName);
short getShort(String columnName);
int getInt(String columnName);
long getLong(String columnName);
float getFloat(String columnName);
下面的代码演示了如何使用getXxx方法来获得字段的值:
…
Connection conn = DriverManager.getConnection(url, "user", "password");
Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sql);
while(rs.next())
{
System.out.print(rs.getString("id"));// 根据列id获得字段的值
System.out.print(rs.getInt(1));// 取当前记录的第1个字段的值
System.out.println(rs.getDate(2));// 取当前记录的第2个字段的值
}
stmt.close();
conn.close();
…
在使用getXxx方法时应注意如下3点:
1. 列索引从1开始,也就是说,第一列的索引为1.在通过列索引读取数据时要注意这一点。
2. 可以在读取数据时进行类型转换。如字段类型是String,在读取时可以使用getInt或其他的getXxx方法。但字段值的前面部分必须是合法的整型或其他类型的值。如一个String类型的字段值是123abc,调用getInt()后,将返回123.如果类型不合法,将抛出java.sql.SQLException异常。
3. 并不是每个getXxx方法都被当前的JDBC驱动程序支持。因此,在调用getXxx方法前需要确认当前的JDBC驱动程序是否支持要使用的getXxx方法,如果当前的JDBC驱动程序不支持某个getXxx方法,那么调用这个getXxx方法就会抛出一个异常(对于MySQL来说,将抛出com.mysql.jdbc.NotImplemented异常)。
在某些情况下并不需要得到全部的查询结果。这时可以使用Statement接口的setMaxRows方法限制返回的记录数。还可以使用getMaxRows()方法获得最大返回记录数。这两个方法的定义如下:
void setMaxRows(int max) throws SQLException;
int getMaxRows() throws SQLException;
其中max参数表示最大返回记录数。如果max的值大于等于实际查询到的记录数,则按实际查询到的记录数返回,如果max的值小于实际查询到的记录数,则返回的记录数为max.
ResultSet类还有一些其他的功能,如下面两个方法分别用来判断字段值是否为NULL和确定某一列是否存在:
1. wasNull方法
这个方法用于判断最后一次使用getXxx方法读出的字段值是否为NULL.假设name字段的值为NULL,则下面的代码将输出true:
ResultSet rs = stmt.executeQuery(sql);
rs.getString("name");
System.out.println(rs.wasNull());
2. findColumn方法
如果要知道某个列名在列集合中的位置,那么ResultSet接口的findColumn方法正好派上用场。findColumn方法的定义如下:
int findColumn(String columnName) throws SQLException;
2.2.2 使用execute查询数据
由于execute方法可以执行任何SQL语句,因此,execute方法并不直接返回ResultSet对象,而是通过一个boolean类型的值确定执行的是返回结果集的SQL语句(如SELECT),还是不返回结果集的SQL语句(如INSERT、UPDATE等)。execute方法的定义如下:
boolean execute(String sql) throws SQLException;
其中参数sql表示要执行的SQL语句。当sql为返回结果集的SQL语句时,execute方法返回true,否则返回false.当返回true时,可以通过getResultSet()方法返回ResultSet对象,如果返回false,可以通过getUpdateCount()方法返回被影响的记录数。这两个方法的定义如下:
ResultSet getResultSet() throws SQLException;
int getUpdateCount() throws SQLException;
用execute方法执行不返回结果集的SQL语句将在2.3.1节详细讲解,本节只介绍如何用execute执行返回结果集的SQL语句。
【实例2.2】 使用execute查询数据
本实例演示了如何使用execute方法查询数据,并显示查询结果。在这个例子中使用SQL语句查询t_books表中的所有数据,并使用execute方法来执行这条查询语句,最后通过Statement接口的getResultSet方法获得查询后返回的ResultSet对象。示例的实现代码如下:
package chapter2;
import java.sql.*;
public class ResultSetFromExecute
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8",
"root", "1234");
Statement stmt = conn.createStatement();
String selectData = "SELECT name FROM t_books";
// 执行查询语句
if (stmt.execute(selectData))
{
// 获得返回的ResultSet对象
ResultSet rs = stmt.getResultSet();
while (rs.next())
{
System.out.println(rs.getString("name"));
}
}
}
}
除了使用execute方法的返回值判断执行的是哪类SQL语句外,还可以通过getResultSet方法判断,如果执行的SQL语句不返回结果集,getResultSet方法返回null。
2.2.3 处理多个结果集
execute方法不仅可以执行单条查询语句,而且还可以执行多条查询语句,不同查询语句之间用分号(;)隔开。在给出例子之前,先使用如下SQL建立一个图书销售表t_booksale,并向其中插入三条记录。
建立t_booksale表
DROP TABLE IF EXISTS mydb.t_booksale;
CREATE TABLE mydb.t_booksale (
id int(10) unsigned NOT NULL auto_increment,
bookid int unsigned NOT NULL,
amount int unsigned NOT NULL,
saledate datetime NOT NULL,
PRIMARY KEY (id),
KEY bookid (bookid)
) ENGINE=InnoDB DEFAULT CHARSET=UTF8;
向t_booksale表插入三条记录
INSERT INTO mydb.t_booksale(bookid,amount,saledate) values(
1, 23, '2007-02-04');
INSERT INTO mydb.t_booksale(bookid,amount,saledate) values(
1, 120, '2007-05-16');
INSERT INTO mydb.t_booksale(bookid,amount,saledate) values(
2, 218, '2007-06-08');
要想处理所有的结果集,需要使用ResultSet接口的getMoreResults()方法来判断是否存在下一个结果集。getMoreResults()方法的定义如下:
boolean getMoreResults() throws SQLException;
getMoreResults()方法和ResultSet接口的next()方法不同。当execute方法返回多结果集时,当前位置就处于第一个结果集上,因此,应该使用do…while语句将getMoreResults方法作为while的条件,而不能使用while语句来扫描查询结果集。如下面的代码将不能获得第一个结果集:
…
while(stmt.getMoreResults())
{
rs = stmt.getResultSet(); // 只能获得第2个及后面的结果集
}
…
下面给出一个完整的实例来演示如何使用execute方法返回并处理多个结果集。
【实例2.3】 JDBC执行多条查询语句
在这个程序中,使用execute方法执行两条SELECT语句,这两条SELECT语句分别查询t_books和t_booksale表的所有记录。这两条SELECT语句之间使用分号(;)分隔。
public class MultiResultSet
{
public static void main(String[] args) throws Exception
{
// 装载mysql驱动
Class.forName("com.mysql.jdbc.Driver");
// 获得Connection对象
Connection conn = DriverManager
.getConnection("jdbc:mysql://localhost/mydb?characterEncoding=UTF8&allowMultiQueries=true", "root", "1234");
Statement stmt = conn.createStatement();
String selectData = "SELECT id,name, author FROM t_books;"
+ "SELECT bookid, amount, saledate FROM t_booksale";
// 执行两条SELECT语句
if (stmt.execute(selectData))
{
ResultSet rs = null;
do
{
// 依次获得执行两条SELECT语句返回的ResultSet对象
rs = stmt.getResultSet();
// 输出当前记录集中的记录
while (rs.next())
{
System.out.print(rs.getString(1) + " ");
System.out.print(rs.getString(2) + " ");
System.out.println(rs.getString(3));
}
}
while (stmt.getMoreResults());// 判断是否还有下一个记录集
}
}
}
在使用execute方法执行多条SQL语句时应注意如下两点:
1. 由于MySQL JDBC驱动在默认时不支持多结果集,因此,要想使用execute方法执行多条查询SQL语句,必须在连接字符串中加上allowMultiQueries=true,如本例中的实现代码所示。
2. 当产生多个结果集时,execute方法只根据多条SQL语句中的第一条的类型来返回true或false.如果第一条SQL语句是查询语句,则getResultSet方法会返回一个ResultSet对象,execute方法返回true.而如果第一条SQL语句是不返回结果集的语句(如INSERT、UPDATE等),execute方法返回false.因此,如果在使用execute方法执行SQL语句前已经确定多条SQL语句都是查询语句时,可以不使用execute方法的返回值来判断SQL语句的类型,但如果是混合形式(就是SELECT和INSERT、UPDATE等语句混合成的SQL语句)的,就不能使用execute方法的返回值来判断是否会返回了结果集。在2.3.2节将介绍如何处理混合形式的SQL语句。
2.3.1 用execute方法执行混合形式的SQL语句
execute方法不仅能执行查询语句,还可以执行不返回结果集的SQL语句,甚至可以同时执行这些SQL语句的混合形式,如下面的代码所示:
String insertData = "INSERT INTO jdbcdemo.t_books(name,isbn,author,price) values("
+ " '人月神话', '6787102165345', '布鲁克斯', 52)";
selectData = "SELECT * FROM jdbcdemo.t_books";
stmt.execute(insertData + ";" + insertData + ";" + selectData);
如果用execute方法执行上面代码所示的混合形式的SQL语句,就不能简单地使用execute方法的返回值或getMoreResults()方法来处理每条SQL语句的执行结果,而是要使用一个getUpdateCount()方法。如果当前执行的语句是返回结果集的SQL语句,getUpdateCount()方法返回-1,否则,返回实际的更新记录数。只有当getMoreResults()方法返回false,并且getUpdateCount()返回-1时,才表明所有的结果都被处理到了,如下面代码所示:
do
{
// 响应的处理代码
}
while (!(stmt.getMoreResults() == false && stmt.getUpdateCount() == -1));
下面给出一个实例来演示如何使用execute方法执行混合SQL语句(包括SELECT、INSERT、UPDATE、DELETE等SQL语句)。
【实例2.4】 用execute方法执行混合SQL语句
用户可以通过本程序输入一个或多个SQL语句,如果输入多个SQL语句,中间用分号(;)分隔。在输入完SQL语句后,按回车后,系统将执行这些SQL语句,并输出执行的结果。如果执行的是SELECT语句,就会输出查询结果,否则,会输出记录的更新数。
本系统是一个控制台程序,用户可以通过控制台输入SQL语句。当输入q时,系统退出。例子的实现代码如下:
public class DBConsole
{
// 为字符串补齐空格
private static String fillSpace(String s, int length)
{
int spaceCount = length - s.getBytes()。length;
for (int i = 0; i < spaceCount; i++)
s += " ";
return s;
}
// 处理用户输入的SQL语句或命令
private static boolean processCommand(String cmd, Statement stmt)
throws Exception
{
// 输入q,退出程序
if (cmd.equals("q"))
{
stmt.close();
return false;
}
stmt.execute(cmd);// 执行sql语句(可能是多条)
do
{
ResultSet rs = null;
rs = stmt.getResultSet();// 获得执行的第一条SQL语句的ResultSet对象
// 如果返回的不是空,则说明执行的第一条SQL语句是SELECT语句
if (rs != null)
{
// 得到当前记录集的列数
int columnCount = rs.getMetaData()。getColumnCount();
// 输出列名,每列宽度是20个字符
for (int i = 1; i <= columnCount; i++)
System.out.print(fillSpace(rs.getMetaData()
.getColumnName(i), 20));
System.out.println();
// 输出记录集中的记录
while (rs.next())
{
for (int i = 1; i <= columnCount; i++)
{
System.out.print(fillSpace(rs.getString(i), 20));
}
System.out.println();
}
}
//如返回的ResultSet对象是null,说明执行的第一条SQL语句是非SELECT语句
else
System.out.println("更新记录数:" + stmt.getUpdateCount());
}
// 判断是否处理完了所有的执行结果
while (!(stmt.getMoreResults() == false && stmt.getUpdateCount() == -1));
return true;
}
public static void main(String[] args) throws Exception
{
String serverName, dbName, userName, password;
// 定义服务器名、数据库名、用户名和密码
serverName = "localhost";
dbName = "mydb";
userName = "root";
password = "1234";
// 定义连接字符串
String url = "jdbc:mysql://" + serverName + "/" + dbName
+ "?characterEncoding=UTF8&allowMultiQueries=true";
// 装载mysql驱动
Class.forName("com.mysql.jdbc.Driver");
// 获得Connection对象
Connection conn = DriverManager.getConnection(url, userName, password);
Statement stmt = conn.createStatement();
String cmd = "";
do
{
java.io.InputStreamReader isr = new java.io.InputStreamReader(
System.in);
java.io.BufferedReader br = new java.io.BufferedReader(isr);
System.out.print("sql>");
cmd = br.readLine(); // 从控制台读入用户输入的命令
if(cmd.equals("")) continue;// 如果输入空串,重新循环
try
{
// 开始处理输入的SQL语句,如果输入的是q,则返回false,并退出系统
if (!processCommand(cmd, stmt))
break;
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
}
while (true);
conn.close();
}
}
使用如下的命令运行DBConsole:
java chapter2.DBConsole
在控制台中输入如下的SQL语句:
select name, author, price from t_books;delete from t_books where price=52
在输入上面的SQL语句后,按"回车键",在控制台中将输出如图2.1所示的运行结果。
图2.1 DBConsole运行界面
在使用execute方法执行多条SQL语句时,由于这些SQL语句可能是SELECT语句,也可能是UPDATE、INSERT等不返回结果集的SQL语句,因此,要想处理所有SQL语句执行的结果(处理SELECT语句返回的结果集,获得不返回结果集的SQL语句的更新记录数),必须要同时满足以下两个条件,才表示所有的执行结果都处理完毕了:
stmt.getMoreResults() == false
stmt.getUpdateCount() == -1
2.3.2 用executeUpdate方法更新数据
除了使用execute方法执行不返回结果集的SQL语句外,还可以使用executeUpdate方法来完成同样的工作。executeUpdate()方法的定义如下:
int executeUpdate(String sql) throws SQLException;
在2.2.1节曾讲到executeQuery方法不能执行象INSERT、UPDATE一样的不返回查询结果的语句。但这种说法并不严谨。这种说法对于一条SQL语句是没有任何问题的,但如果对于多条SQL语句同时执行的情况下,就不够准确。更严谨的说法应该是"executeQuery方法执行的第一条SQL语句必须是返回结果集的SQL语句,而后面跟着的其他SQL语句可以是任何正确的SQL语句,其中包括INSERT、DELETE、UPDATE、CREATE TABLE等".也就是说,下面的SQL语句是可以使用executeQuery方法成功执行的:
String sql = "SELECT name, author, price FROM t_books; DELETE FROM t_booksale WHERE id = 1";
stmt.executeQuery(sql);
如果executeQuery方法执行的是多条SQL语句,并且第一条是查询语句,仍然能正确返回ResultSet对象。
executeUpdate方法和executeQuery方法类似,也就是说,executeUpdate方法在执行多条SQL语句时,第一条SQL语句必须是不返回结果集的SQL语句,而第2条及以后的SQL语句可以是任何正确的SQL语句。因此,实例2-4中的execute方法可以用executeUpdate或executeQuery代替,但是输入SQL时就会有限制。如果用executeUpdate方法代替,所输入的第一条SQL语句必须是不返回结果集的SQL语句,而用executeQuery方法代替时,正好相反。
2.3.3 获得自增字段的值
有很多数据库都支持自增类型字段,但在插入数据时,同时获得自增字段的值是比较麻烦的。但如果使用JDBC,就非常容易做到这一点。
在Statement接口中提供了一个getGeneratedKeys方法,可以获得最近一次插入数据后自增字段的值。getGeneratedKeys()方法的定义如下:
ResultSet getGeneratedKeys() throws SQLException;
getGeneratedKeys方法返回一个ResultSet对象,第一个字段的值就是自增字段的值。如果同时插入多条记录,可使用next()方法对ResultSet对象进行扫描。
使用execute方法和executeUpdate方法都可以获得自增字段的值,如果执行多条INSERT语句,则只返回第一条INSERT语句生成的自增字段的值。
【实例2.5】 获得自增字段的值
下面的代码演示了如何用getGeneratedKeys()方法获得自增字段的值:
public class AutoGeneratedKeyValue
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8",
"root", "1234");
Statement stmt = conn.createStatement();
String insertData1 = "INSERT INTO t_booksale(bookid, amount, saledate) VALUES(1, 20, '2004-10-10')";
String insertData2 = "INSERT INTO t_booksale(bookid, amount, saledate) SELECT bookid,amount,saledate FROM t_booksale";
stmt.execute(insertData1);
ResultSet rs = stmt.getGeneratedKeys();// 获得单个递增字段值
if (rs.next())
{
System.out.println("自增自段的值: " + rs.getString(1));
System.out.println("-------------------------------");
}
stmt.executeUpdate(insertData2);
rs = stmt.getGeneratedKeys();// 获得多个递缯字段值
while (rs.next())
{
System.out.println("自增自段的值: " + rs.getString(1));
}
stmt.close();
conn.close();
}
}
除此之外,还可以通过execute和executeUpdate方法来控制是否可以获得自增字段的值,代码如下:
// 无法获得自增字段的值
stmt.execute(insertData1, Statement.NO_GENERATED_KEYS);
// 可以获得自增字段的值
stmt.execute(insertData1, Statement. RETURN_GENERATED_KEYS);
// 无法获得自增字段的值
stmt.executeUpdate(insertData1, Statement.NO_GENERATED_KEYS);
// 可以获得自增字段的值
stmt.executeUpdate(insertData1, Statement. RETURN_GENERATED_KEYS);
对于MySQL数据库来说,使用NO_GENERATED_KEYS和RETURN_GENERATED_KEYS都可以获得自增字段值,也就是说MySQL JDBC忽略了这个参数。而对于其它数据库的JDBC驱动,就未必是这个结果。如SQL Server2005 JDBC就必须使用Statement. RETURN_GENERATED_KEYS才可以获得自增字段的值。读者在使用这一特性获得自增字段值时应注意这一点。
2.4.1 调用存储过程
在JDBC中调用存储过程需要使用Connection接口的prepareCall方法。prepareCall方法的定义如下:
CallableStatement prepareCall(String sql) throws SQLException;
其中sql参数表示调用存储过程的SQL语句,如果存储过程含有参数,需要使用"?"作为占位符,并使用CallableStatement接口的setXxx方法为参数赋值。setXxx方法可以使用参数名或参数索引来确定参数的位置。
在使用prepareCall方法之前,先用如下的SQL语句建立一个存储过程,这个存储过程有一个输入参数:id,一个输出参数:total.存储过程的功能是统计t_booksale表中bookid字段的值等于id的图书销售总量。建立存储过程的SQL语句如下:
DROP PROCEDURE IF EXISTS mydb.p_myproc;
DELIMITER //
CREATE PROCEDURE mydb.p_myproc(IN id int, OUT total int)
begin
SELECT sum(amount) INTO total FROM mydb.t_booksale WHERE bookid = id;
end //
DELIMITER ;
对于存储过程的输出参数,需要使用registerOutParameter方法进行注册,registerOutParameter方法的定义如下:
void registerOutParameter(int parameterIndex,int sqlType)throws SQLException;
void registerOutParameter(String parameterName, int sqlType) throws SQLException;
从方法定义可以看出,registerOutParameter方法也可以使用参数索引或参数名来注册输出参数。其中sqlType参数表示输出参数的类型,它的值是java.sql.Types类定义的类型值中的一个。最后,可以使用getXxx方法获得输出参数返回的值。getXxx方法和setXxx方法类似,也可以通过参数索引或参数名来指定参数。
【实例2.6】 调用存储过程
下面是一个调用存储过程的例子,代码如下:
public class StoredProcedure
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8",
"root", "1234");
CallableStatement cstmt = conn.prepareCall("call p_myproc(?, ?)");
// 向输入参数传值
cstmt.setString(1, "2");
// 注册输出参数
cstmt.registerOutParameter(2, java.sql.Types.INTEGER);
// 调用存储过程
cstmt.executeUpdate();
// 获得输出参数返回的值
System.out.println(cstmt.getInt(2));
cstmt.close();
conn.close();
}
}
2.4.2 使用PreparedStatement对象执行动态SQL
动态SQL实际上就是带参数的SQL.通过PreparedStatement对象可以执行动态的SQL.由于动态SQL没有参数名,只有参数索引,因此,PreparedStatement接口的getXxx方法和setXxx方法只能通过参数索引来确定参数。PreparedStatement对象和Statement对象的使用方法类似,所不同的是Connection对象使用prepareStatement方法创建PreparedStatement对象。在创建PreparedStatement对象时,必须使用prepareStatement方法指定一个动态SQL.
【实例2.7】 执行动态SQL
下面是一个执行动态SQL语句的例子,代码如下:
public class DynamicSQL
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8",
"root", "1234");
String selectData = "SELECT name, author FROM t_books WHERE id = ?";
// 获得PreparedStatement对象
PreparedStatement pstmt = conn.prepareStatement(selectData);
pstmt.setInt(1, 1);// 赋参数值
ResultSet rs = pstmt.executeQuery();
// 输出返回的结果集
while (rs.next())
{
System.out.println("书名:" + rs.getString("name"));
System.out.println("作者:" + rs.getString("author"));
System.out.println("---------------------");
}
pstmt.close();// 关闭PreparedStatement对象
conn.close();
}
}
既然有了Statement对象,为什么JDBC又要引入PreparedStatement呢?其主要的原因有如下四点:
1. 提高代码可读性和可维护性
虽然使用PreparedStatement对象从表面上看要比使用Statement对象多好几行代码,但如果使用的SQL是通过字符串连接生成的,那么使用Statement对象的代码的可读性就会变得很差,如下面代码如示:
使用Statement对象的代码:
stmt.executeQuery("SELECT id, name, isbn, author,price FROM t_books WHERE id >" + id + " and name like '%" + subname + "%' and author = '" + author + "'");
使用PreparedStatement对象的代码:
pstmt = conn.prepareStatement ("SELECT id, name, isbn, author,price FROM t_books WHERE id >? and name like ? and author = ?");
pstmt.setString(1, id);
pstmt.setString(2, subname);
pstmt.setString(3, author);
pstmt.executeQuery();
从上面的代码可以看出,使用PreparedStatment对象的代码虽然多了几行,但显得更整洁。
2. 有助于提高性能
由于PreparedStatement对象在创建时就指定了动态的SQL,因此,这些SQL被DBMS编译后缓存了起来,等下次再执行相同的预编译语句时,就无需对其再编译,只需将参数值传入即可执行。由于动态SQL使用了"?"作为参数值占位符,因此,预编译语句的匹配几率要比Statement对象所使用的SQL语句大的多,所以,在多次调用同一条预编译语句时,PreparedStatement对象的性能要比Statement对象高得多。
3. 提高可复用性
动态SQL和存储过程十分类似,可以只写一次,然后只通过传递参数进行多次调用。这在执行需要不同条件的SQL时非常有用。
4. 提高安全性
在前面的章节讲过,execute、executeQuery和executeUpdate方法都可以执行多条SQL语句。那么这就存在一个安全隐患。如果使用Statement对象,并且SQL通过变量进行传递,就可能会受到SQL注入攻击,看下面的代码:
String author = "阿斯利森";
String selectData = "SELECT * FROM jdbcdemo.t_books where author = '" + author + "'";
stmt.executeQuery(selectData);
上面的代码并没有任何问题,但如果将author的值改为如下的字符串,就会在执行完SELECT语句后,将t_booksale删除。
"';drop table jdbcdemo.t_booksale;";
而如果使用PreparedStatement对象,就不会发生这样的事情。因此,在程序中应尽量使用PreparedStatement对象,并且在连接数据库时,除非必要,否则在连接字符串中不要加"allowMultiQueries=true"来打开执行多条SQL语句的功能。
2.4.3 存取BLOB字段值
许多数据库都支持二进制字段,对这类字段的处理相对繁琐一些,在读取时需要使用Statement,而在写入时,必须使用PreparedStatement对象的setBinaryStream方法。
【实例2.8】 向BLOB类型字段中写入数据
下面程序演示了如何向BLOB类型字段中写入数据,代码如下:
public class BlobView
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8","root", "1234");
// 打开D盘上的image.jpg文件
java.io.File picFile = new java.io.File("d:\\image.jpg");
// 获得该图像文件的大小
int fileLen = (int) picFile.length();
// 获得这个图像文件的InputStream对象
java.io.InputStream is = new java.io.FileInputStream(picFile);
// 获得PreparedStatement对象
PreparedStatement pstmt = conn
.prepareStatement("INSERT INTO t_image(name, image) VALUES(?, ?)");
pstmt.setString(1, "mypic");// 为第一个参数设置参数值
pstmt.setBinaryStream(2, is, fileLen);// 为第二个参数设置参数值
pstmt.executeUpdate();// 更新数据库
pstmt.close();
conn.close();
}
}
2.4.4 事务管理
数据库的事务就是将任意多个SQL语句看作一个整体,只有这些SQL语句都成功执行,DBMS才会保存这些SQL语句对数据库的修改(事务提交)。否则,数据库将恢复到执行SQL语句之前的状态(事务回滚)。大多数DBMS都支持两种事务模式:隐式模式和显式模式。当执行每一条SQL语句时,无需进行事务提交,就可以直接将修改结果保存到数据库中。这叫做隐式事务模式。显式模式必须使用相应的语句或命令开起事务、提交事务和回滚事务。
在使用JDBC时,默认情况下是隐式事务模式。但JDBC提供了setAutoCommit方法,可以将隐式模式改为显式模式。setAutoCommit方法的定义如下:
void setAutoCommit(boolean autoCommit) throws SQLException;
当autoCommit参数值为false时,JDBC工作在显式事务模式下,也就是说,只有使用commit方法进行提交,对数据库的修改才能生效。
【实例2.9】 事件的提交和回滚
下面的代码演示了如何在JDBC中使用事务:
public class Transaction
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8",
"root", "1234");
try
{
conn.setAutoCommit(false);// 开始事务
Statement stmt = conn.createStatement();
System.out.println("更新记录数:" + stmt
.executeUpdate("UPDATE t_books SET price = price - 3"));
stmt.close();
PreparedStatement pstmt = conn
.prepareStatement("INSERT INTO t_booksale(bookid, amount, saledate) VALUES(?, ?, ?)");
pstmt.setInt(1, 2);// 设置第一个参数值
pstmt.setInt(2, 206);// 设置第二个参数值
pstmt.setString(3, "2007-12-25");// 设置第三上参数值
System.out.println("更新记录数:" + pstmt.executeUpdate());
pstmt.close();
conn.commit();// 提交事务
}
catch (Exception e)
{
conn.rollback();// 回滚事务
}
conn.close();
}
}
在上面的代码中如果不使用commit方法提交事务,输出的更新记录数和使用common方法时一样,但t_books和t_booksale表中的数据并未改变。因此,在显式事务模式下,通过更新记录数并不能确定是否已经将数据保存到数据库中,这一点在使用中应注意。
在catch块中使用了rollback方法将事务回滚。可以将上面代码中的INSERT语句做一下更改,如将amount改为amount1,这样,在执行这条INSERT语句时就会抛出异常,然后程序将进入catch块中执行rollback方法进行事务回滚。在执行完程序后,从数据库中可以看到,t_books和t_booksale表中的数据均未改变。
2.5.1 数据库元数据
数据库元数据就是和数据库本身及其子项(表、视图等)相关的数据,使用Connection接口的getMetaData方法可以获得JDBC提供的所有的元数据。getMetaData方法的定义如下:
DatabaseMetaData getMetaData() throws SQLException;s
DatabaseMetaData接口为我们提供了很多用于访问数据库元数据的方法,如数据库版本、JDBC驱动名、JDBC驱动版本、表信息、视图信息、存储过程信息等。下面是一些常用的获得数据库元数据的方法:
String getDatabaseProductName();
String getDatabaseProductVersion() ;
String getDriverName();
String getDriverVersion();
ResultSet getCatalogs();
ResultSet getTables(String catalog, String schemaPattern, String tableNamePattern, String types[]);
ResultSet getProcedures(String catalog, String schemaPattern, String procedureNamePattern);
下面的代码演示了如何使用上面的方法来获得数据库元数据,这个程序将列出一些和数据库相关的信息,以及服务器中所有的数据库名、mydb数据库中的表名、视图名、存储过程名和函数名,
【实例2.10】 获得数据库元数据
实例的代码如下:
public class DBMetaData
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
// 获得Connection对象
Connection conn =
DriverManager.getConnection("jdbc:mysql://localhost/mydb?" +
characterEncoding=UTF8", "root", "1234");
// 获得DatabaseMetaData对象
DatabaseMetaData dbmd = conn.getMetaData();
// 开始输出和数据库有关的元数据
System.out.println("数据库产品名:" + dbmd.getDatabaseProductName());
System.out.println("数据库版本:" + dbmd.getDatabaseProductVersion());
System.out.println("JDBC驱动名:" + dbmd.getDriverName());
System.out.println("JDBC驱动版本:" + dbmd.getDriverVersion());
System.out.println("--------------数据库-------------");
// 获得数据库列表
ResultSet databases = dbmd.getCatalogs();
// 输出数据库名
while(databases.next())
{
System.out.println(databases.getString("TABLE_CAT"));
}
System.out.println("--------------表-------------");
// 获得mydb数据库中的表名
ResultSet tables = dbmd.getTables("mydb", null, null, new String[]{"table"});
while(tables.next())
{
System.out.println(tables.getString("TABLE_NAME"));
}
System.out.println("--------------视图-------------");
// 获得mydb数据库中的表名
ResultSet views = dbmd.getTables("mydb", null, null, new String[]{"view"});
while(views.next())
{
System.out.println(views.getString("TABLE_NAME"));
}
System.out.println("--------------存储过程、函数-------------");
// 获得mydb数据库中的存储过程
ResultSet procfun = dbmd.getProcedures("mydb", null, null);
while(procfun.next())
{
System.out.println(procfun.getString("PROCEDURE_NAME"));
}
conn.close();
}
}
2.5.2 结果集元数据
在前面讲过,execute、executeQuery和executeUpdate方法都可以返回ResultSet对象。通过ResultSet接口的next方法可以对数据进行扫描,但要获得ResultSet对象的元数据(列数、列名、字段类型等),就需要使用ResultSet接口的getMetaData方法,getMetaData方法的定义如下:
ResultSetMetaData getMetaData() throws SQLException;
可以通过ResultSetMetaData接口的getXxx和isXxx方法获得ResultSet对象的元数据,下面是部分getXxx和isXxx方法的定义代码:
int getColumnCount() ;
String getColumnName(int column);
String getColumnTypeName(int column);
String getColumnClassName(int column);
int getColumnDisplaySize(int column);
boolean isAutoIncrement(int column);
下面的例子演示了如何使用这些getXxx和isXxx方法来获得结果集元数据。
【实例2.11】 获得结果集元数据
实例的代码如下:
public class RSMetaData
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8",
"root", "1234");
Statement stmt = conn.createStatement();
stmt.setMaxRows(1); // 只返回一条数据
ResultSet rs = stmt.executeQuery("SELECT * FROM t_books");
// 获得返回结果集的元数据
ResultSetMetaData rsmd = rs.getMetaData();
// 输出结果集的元数据
for(int i = 1; i <= rsmd.getColumnCount(); i++)
{
System.out.println("列名:" + rsmd.getColumnName(i));
System.out.println("SQL类型:" + rsmd.getColumnTypeName(i));
System.out.println("对应的Java类型:" + rsmd.getColumnClassName(i));
System.out.println("列尺寸:" + rsmd.getColumnDisplaySize(i));
System.out.println("自增字段:" + rsmd.isAutoIncrement(i));
System.out.println("----------------------------");
}
conn.close();
}
}
2.5.3 参数元数据
在JDBC中可以通过PreparedStatement接口 和CallableStatement接口的getParamterMetaData方法获得参数元数据(参数个数、参数类型等)。getParamterMetaData方法的定义如下:
ParameterMetaData getParameterMetaData() throws SQLException;
下面的实例演示了如何使用ParameterMetaData接口的getXxx方法获得参数元数据。
【实例2.12】 获得参数元数据
实例的代码如下:
public class PMetaData
{
public static void main(String[] args) throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8",
"root", "1234");
// 获得CallableStatement对象
CallableStatement cstmt = conn.prepareCall("call p_myproc(?, ?)");
// 获得参数元数据
ParameterMetaData pmd = cstmt.getParameterMetaData();
// 输出参数元数据
for (int i = 1; i <= pmd.getParameterCount(); i++)
{
switch (pmd.getParameterMode(i))
{
case ParameterMetaData.parameterModeIn:
System.out.println("参数模式:IN");
break;
case ParameterMetaData.parameterModeOut:
System.out.println("参数模式:OUT");
break;
case ParameterMetaData.parameterModeInOut:
System.out.println("参数模式:INOUT");
break;
default:
System.out.println("参数模式:Unknown");
}
System.out.println("参数类型:" + pmd.getParameterTypeName(i));
System.out.println("参数类名:" + pmd.getParameterClassName(i));
System.out.println("------------------------------");
}
cstmt.close();
conn.close();
}
}
第3章 Java Web程序的Helloworld
本章给出了一个简单的例子来演示如何用Servlet和JSP技术开发一个简单的Web程序。这个程序的功能是通过JSP页面选择要查询的项(书名、作者、ISBN),并输入要查询的信息,然后通过Servlet返回查询结果,最后在另一个JSP页面中显示查询结果。通过学习本章的示例子,读者可以掌握使用JSP和Servlet技术开发Web程序的基本过程,并为后面的学习打下基础。
3.1 JSP与Servlet简介
JSP(JavaServet Pages)是Sun公司于上个世纪末(1999年)推出的一种动态网页技术。JSP技术和ASP技术非常类似,JSP在传统的静态网页文件(。htm,.html)中插入Java代码段和JSP标签(tag),从而形成了JSP文件(*.jsp)。
在JSP页面中可以使用由Java语言编写的标签和Java代码来封装产生动态网页的处理逻辑。这种标签的语法类似于XML,在运行JSP时,JSP页面中的标签被转换成Java语句的调用。JSP还可以通过标签和Java代码访问服务端的资源。JSP将网页逻辑与表现层分离,支持可重用的基于组件的设计与实现,使基于Web的应用程序的开发变得迅速和容易。
JSP是在服务器端执行的,它返回给客户端的都是一些客户端代码(如HTML、JavaScript等),因此,客户端只要有Web浏览器,就可以访问基于JSP和Servlet的Web程序。
由于JSP是基于Java的,因此,JSP也拥有和Java一样的跨平台能力,也就是说,JSP不仅可以在Windows中运行,而且还可以任何支持Java的操作系统平台上运行,如Linux、Unix等。
Servlet也是Sun公司推出的一种服务端技术,这种技术推出的时间要比JSP早一点(1998年),Servlet并不象JSP一样可以很容易地设计用户页面。实际上,Servlet技术一般被用来处理客户端请求,然后通过JSP将处理后的结果呈现给客户端浏览器。
从本质上讲JSP是基于Servlet实现的,也就是说,JSP页面在第一次访问时,被编译成了Servlet,当再次访问这个JSP页面时,就和Servlet没有任何区别了,因此,JSP在运行效率上要比ASP快得多。
综合上述,JSP有如下优势:
1. 一次编写,到处运行。这也是Java的优势之一。如果要将JSP程序移植到其他操作系统平台上,JSP代码并不需要做任何修改。
2. 操作系统平台的多样性。由于Java支持大量的操作系统平台,理所当然,JSP也同样跟着沾光。只要是Java程序能运行的平台,JSP就同样也可以在这种平台上运行。
3. 可伸缩性。JSP不仅可以通过一个小小的jar文件或单独的。jsp文件来运行,还可以在多台服务器组成的集群中运行,达到负载均衡。
4. 运行效率高。由于JSP页面在第一次访问时就会被编译成了Servlet,因此,在运行效率上,JSP和Servlet是一样的。
3.2 编写用于查询信息的Servlet
在本节建立的Servlet是这个例子的核心。这个Servlet负责接收客户端的请求消息,并通过请求消息从数据库中查询相应的信息,并将查询到的信息保存在request域中,以便负责显示查询结果的JSP页面读取并显示这些信息。下面通过IDE来建立一个Servlet程序。
选中demo工程,在右键菜单中单击New | Servlet菜单项,出现Create Servlet对话框,并在Java package文本框中输入chapter3,在Class name文本框中输入QueryBook,如图3.1所示。
图3.1 Create Servlet对话框的第一步
单击Next按钮进入下一步,在这一步不需要做任何修改,再次单击Next按钮进入Create Servlet对话框的第三步。选中service复选框,并取消Constructors from superclass复选择,如图3.2所示。
图3.2 【Create Servlet】对话框的第三步
在进行完上面的设置后,单击Finish按钮建立Servlet.
在生成的QueryBook.java文件中输入如下的代码:
package chapter3;
import java.io.*;
import java.sql.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.util.*;
public class QueryBook extends HttpServlet
{
// 用于处理GET、POST等HTTP请求的方法
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
try
{
// 获得queryField请求参数的值
String queryField = request.getParameter("queryField")。toString();
// 获得queryText请求参数的值
String queryText = request.getParameter("queryText")。toString();
// 为了解决乱码问题,必须进行编码转换
queryText = new String(queryText.getBytes("ISO-8859-1"), "UTF-8");
// 装载mysql的驱动
Class.forName("com.mysql.jdbc.Driver");
// 建立数据库连接,获得Connection对象
Connection conn = DriverManager.getConnection(
"jdbc:mysql://localhost/mydb?characterEncoding=UTF8",
"root", "1234");
// 使用带参数的SQL语句进行查询
PreparedStatement pStmt = conn
.prepareStatement("select * from t_books where "
+ queryField + " like ?");
// 设置查询参数值
pStmt.setString(1, "%" + queryText + "%");
// 执行查询语句,并返回ResultSet对象
ResultSet rs = pStmt.executeQuery();
// 定义一个用于保存查询结果的List<String[]>对象
List<String[]> result = new java.util.ArrayList<String[]>();
// 循环处理查询结果
while (rs.next())
{
String[] row = new String[4];
row[0] = rs.getString("name");
row[1] = rs.getString("author");
row[2] = rs.getString("isbn");
row[3] = rs.getString("price");
// 将查询结果放到result对象中
result.add(row);
}
pStmt.close();// 关闭PreparedStatement对象
conn.close();// 关于Connection对象
// 将查询结果保存在request域中,以便在显示查询结果的JSP页面中使用
request.setAttribute("result", result);
RequestDispatcher rd = request
.getRequestDispatcher("/chapter3/result.jsp");
// 转入result.jsp页面
rd.forward(request, response);
} catch (Exception e)
{
}
}
}
在编写上面的代码时,应注意以下几点:
1. Servlet类必须从HttpServlet类及其子类继承。
2. service方法是HttpServlet类的一个方法,用来处理各种HTTP请求。关于这个方法的细节,将在第4章介绍。
3. 在本程序中仍然使用在第2章建立的mydb数据库和t_books表。
4. 在QueryBook类中读取了两个请求参数:queryField和queryText,这两请求参数分别代表查询的类别(书名、作者和ISBN)和查询的内容。其中queryField请求参数的可取值有三个:name、author和isbn,这三个值分别和数据库中的t_books表的字段相对应。这两个请求参数值将通过用于输入查询信息的JSP页面提供。
5. 在读取queryText请求参数值后,又对其进行了编码转换,这是为了解决乱码问题。由于客户端可能提交中文信息,而提交的又是UTF-8编码,因此,需要将编码以UTF-8编码格式再转换成Java的内部编码格式。关于Java的乱码问题的系列结果方案,将在后面的内容详细讲解。
6. 本程序采用了带参数的SQL语句进行查询,这样做可以有效地避免SQL注入攻击,也可提高程序的运行效率。
7. 在程序的最后,将通过RequestDispatcher转入result.jsp页面,以显示查询结果。
在本程序中所涉及到的技术,例如,转发Web资源,request域等,将在后面的章节详细介绍,在这里读者只要知道它们的功能即可。
在建立QueryBook类的同时,IDE会自动在web.xml文件中添加如下的内容:
<servlet>
<!-- 定义Servlet名字 -->
<servlet-name>QueryBook</servlet-name>
<!-- 指定Servlet的类名 -->
<servlet-class>chapter3.QueryBook</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>QueryBook</servlet-name>
<!-- 指定访问Servlet的URL -->
<url-pattern>/QueryBook</url-pattern>
</servlet-mapping>
上面的配置代码对于Servlet是必须的,其主要内容就是通过一个Servlet名将Servlet类和访问Servlet的URL联系起来。也就是说,使用上面的配置,就可以通过URL来找到与之对应的Servlet类,并执行它。
3.3 编写用于输出查询结果的JSP页面
在这一节将建立一个用于显示查询结果的result.jsp页面。在IDE中建立JSP页面非常简单。在WebContent目录中建立一个chapter3目录,选中chapter3目录后,在右键菜单中单击New | JSP菜单项,打开New JavaServer Page对话框,在File name文本框中输入result.jsp,如图3.3所示。
图3.3 建立JSP页面
在完成上面的操作后,单击Finish按钮建立result.jsp文件。打开result.jsp文件,并输入如下的代码:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!-- 引用JSTL的core标签库 -->
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<html>
<head>
<title>查询结果</title>
</head>
<body>
<!-- 以表格形式显示查询结果 -->
<table border="1">
<!-- 显示表头 -->
<tr align="center">
<td>书名</td>
<td>作者</td>
<td>ISBN</td>
<td>价格</td>
</tr>
<!-- 使用JSTL读取查询结果 -->
<c:forEach var="row" items="${result}">
<tr>
<td>${row[0]}</td>
<td>${row[1]}</td>
<td>${row[2]}</td>
<td>${row[3]}</td>
</tr>
</c:forEach>
</table>
</body>
</html>
在上面的代码中使用了JSTL的core标签库。通过这个标签库中的<c:forEach>标签来从request域中读取查询结果,并动态生成HTML代码来显示查询结果。关于JSTL的内容将在后面的章节详细介绍。
下面在IE地址栏中输入如下的URL来测试QueryBook类和result.jsp:
http://localhost:8080/demo/QueryBook?queryField=name&queryText=ajax
在访问上面的URL后,在IE中将显示如图3.4的输出结果。
图3.4 测试QueryBook的显示结果
3.4 编写用于输入查询信息的JSP页面
在本节将实现用于输入查询信息的query.jsp页面。在chapter3目录中建立一个query.jsp文件,并输入如下所示的代码:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<title>输入查询信息</title>
</head>
<body>
<!-- 通过form提交查询信息 -->
<form action="/QueryBook" method="post">
<!-- 使用table来控制页面元素的位置 -->
<table style="font-size: 14px">
<tr>
<td width="100px" align="right">
选择查询项:
</td>
<td>
<!-- 显示查询项选择框 -->
<select name="queryField" style="width:100px">
<option value="name">书名</option>
<option value="author">作者</option>
<option value="isbn">ISBN</option>
</select>
</td>
</tr>
<tr>
<td align="right">
输入查询内容:
</td>
<td>
<!-- 显示查询内容文本框 -->
<input type="text" name="queryText"/>
</td>
</tr>
<tr>
<td></td>
<td>
<input type="submit" value="查询"/>
</td>
</tr>
</table>
</form>
</body>
</html>
在编写上面的代码时应注意如下几点:
1. 通过form元素的action属性指定了"/QueryBook"来访问QueryBook.由于QueryBook的访问路径是/demo/QueryBook,而query.jsp的访问路径是/demo/chapter3/QueryBook,类此,需要action属性值需要加上""以加到上一层路径。
2. 在<select>元素的<option>子元素中使用value属性指定queryField请求参数的值。也就是说,在QueryBook中获得的queryField请求参数值就是相应的<option>元素的value属性值。
在IE地址栏中输入如下的URL来测试query.jsp页面:
http://localhost:8080/demo/chapter3/query.jsp
在访问上面的URL后,将在IE中显示如图3.5所示的界面:
图3.5 query.jsp页面
在"选择查询项"下拉列表框中选择"作者",并在"输入查询内容"文本框中输入"布鲁克斯",如图3.6所示。
图3.6 输入查询信息
在输入完查询信息后,单击"查询"按钮后,将会显示如图3.7所示的查询结果。
图3.7 显示查询结果
3.5 小结
本章简要介绍了JSP和Servlet技术的特点和优势,并以一个简单的信息查询程序作为例子来逐步演示如何在IDE中开发JSP和Servlet程序。在本章的例子中涉及到了很多JSP和Servlet中常用的技术,如JSTL、转发Web资源、request域等。这些知识和技术都将在后面的章节详细介绍。而本章的目的就是使读者了解开发Java Web程序的基本步骤和流程。
第4章 Servlet开发基础
在本章将介绍Servlet的一些基础知识。由于Servlet必须运行在Web服务器中,因此,在本章介绍了如何在Tomcat中配置Servlet以及数据库连接池的配置。除此之外,在本章还着重介绍了三个Servlet API,它们是HttpServlet类、ServletConfig接口和ServletContext接口。其中HttpServlet类是Servlet的核心,所有的Servlet类都要从这个HttpServlet类继承。
4.1 在Tomcat中的配置Web程序
在本节将介绍Java Web程序的一些基本配置方法。主要涉及到如何配置web.xml文件,以及如何在Tomcat中配置数据库连接池。在第3章给出了一个例子来演示开发Java Web程序的过程,这个例子是通过IDE进行开发的,虽然这种方式可以大大提高程序开发的效率,但却将某些步骤隐藏了起来。这对于初学者来说并不利于充分理解Java Web程序开发的全过程,因此,在本节还给出了一个例子来介绍如何脱离IDE来开发Java Web程序。
4.1.1 编写web.xml文件
在Web服务器中运行Servlet的部分被称为Servlet容器。Servlet要想在Servlet容器中正常运行,必须要使用web.xml(在WEB-INF目录中)文件进行配置(虽然使用Java IDE在大多数情况下是不需要手工配置web.xml的,但理解和掌握web.xml的常用配置将会有助于更进一步学习Java Web技术)。web.xml是一个标准的XML格式文件。下面是一个标准的web.xml配置文件的内容:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<welcome-file-list>
<welcome-file>index.html</welcome-file>
<welcome-file>default.jsp</welcome-file>
</welcome-file-list>
<servlet>
<description></description>
<display-name>QueryBook</display-name>
<servlet-name>QueryBook</servlet-name>
<servlet-class>chapter3.QueryBook</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>QueryBook</servlet-name>
<url-pattern>/QueryBook</url-pattern>
</servlet-mapping>
</web-app>
在上面的web.xml文件中有四个主要的元素:
1. <web-app>:最顶层的元素。所有的web.xml文件都必须拥有这个元素。<web-app>主要描述了当前使用的Servlet的版本以及其他一些文档类型声明,如上面的web.xml文件中描述了Servlet的版本是2.5
2. <servlet>:用于定义和Servlet相关的信息,这个元素含有4个子元素:
(1)<servlet-class>:用来定义Servlet和哪一个具体的类对应,如本例中定义的是chapter3.QueryBook.
(2)<servlet-name>:用于定义Servlet的唯一标识(也就是Servlet名)。如本例中定义了QueryBook.<servlet>元素可以有多个,但是每个<servlet>的<servlet-name>元素的值不能重复,否则Tomcat在启动时会抛出异常。
(3)<description>:该元素提供了用于描述Servlet的文本信息。
(4)<display-name>:该元素提供了一些GUI工具可以显示的Servlet的文本信息。
3. <serlvet-mapping>:该元素一般和<servlet>元素成对出现。用于将Servlet映射成用户可访问的Web路径。其中<url-pattern>定义了可访问的Web路径,但要注意,这个Web路径必须以"/"开头,否则Tomcat在启动时会抛出异常。在第3章访问QueryBook的URL是http://localhost:8080/demo/QueryBook.而在<url-pattern>中定义的就是/QueryBook部分。当然,也可以将其定义成其他的形式,甚至可以将其模拟成其他语言的Web程序,如将<url-pattern>元素的值设为如下形式:
<url-pattern>/abc.php</url-pattern>
在IE地址栏中只要输入http://localhost:8080/demo/abc.php就可以访问QueryBook了。<url-pattern>中的<servlet-name>与<servlet>中的<servlet-name>完全一样,表示当前的<servlet-mapping>要映射的Servlet名。<servlet-mapping>和<servlet>是多对一的关系。也就是说,多个<servlet-mapping>可以对应一个<servlet>,这样就可以为一个Servlet定义多个可访问的Web路径。如下面的配置代码所示:
<servlet>
<servlet-name>QueryBook</servlet-name>
<servlet-class>chapter3.QueryBook</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>QueryBook</servlet-name>
<url-pattern>/QueryBook</url-pattern>
</servlet-mapping>
<servlet-mapping>
<servlet-name>QueryBook</servlet-name>
<url-pattern>/querybook.abc</url-pattern>
</servlet-mapping>
如果使用上面的配置,就可以同时通过如下两个URL来访问QueryBook:
http://localhost:8080/demo/QueryBook
http://localhost:8080/demo/querybook.abc
4. <webcome-file-list>:该元素其实就相当于IIS中的默认页。也就是说,如果在浏览器中只访问http://localhost:8080/demo,而不指定具体的Servlet或其他Web资源的路径,系统会自动访问<webcome-file-list>元素中<webcome-file>子元素所指定的文件或Web路径。要注意的是,<webcome-file>元素只能是相对于当前Web工程的相对路径,不能是绝对路径,如http://www.sina.com.cn是不合法的。<webcome-file>元素的值可以是任何形式的相对路径,但前面不能加"/",这一点和<url-pattern>元素恰恰相反。如<webcome-file>元素的值可以是"index.jsp",但不能是"/index.jsp",否则将无法访问。<webcome-file-list>元素可以有多个<webcome-file>子元素,如果第一个<webcome-file>元素所指的相对路径无法访问,系统就会访问第二个<webcome-file>元素所指的相对路径,以此类推。
4.1.2 手工编写Servlet
在本节将给出一个如何通过手工方式编写Servlet的例子。这个例子完全脱离IDE,只使用记事本和Java编译器来完成Servlet的编写、编译和发布工作。
【实例4-1】 手工编写Servlet
实现本示例的步骤如下:
1. 建立目录结构
在编写Servlet之前,需要建立Servlet所在的目录结构。读者可按如下3步来建立Servlet目录结构。
(1)在<Tomcat安装目录>\webapps目录中建立一个mydemo目录
(2)在<Tomcat安装目录>\webapps\mydemo目录中建立一个WEB-INF目录。
(3)在<Tomcat安装目录>\webapps\mydemo\WEB-INF目录中建立一个classes目录。
2. 编写Servlet类
在<Tomcat安装目录>\webapps\mydemo目录中建立一个MyDoGet.java文件,代码如下:
package chapter4;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
publi
public class MyDoGet extends HttpServlet
{
// 只处理HTTP GET请求
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
// 设置Content-Type字段值
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 向客户端输出信息
out.println("doGet方法被调用!");
}
}
上面的代码使用HttpServletResponse接口的getWriter方法获得了一个PrintWriter对象,用来向客户端输出文本信息。并通过HttpServletResponse接口的setContextType方法设置了HTTP响应头的Content-Type字段值。
3. 编译Servlet类
编译MyDoGet类需要一个servlet-api.jar文件,这个文件可以在<Tomcat安装目录>\lib目录中找到,为了方便,可以将这个文件复制到<Tomcat安装目录>\webapps\mydemo目录中。然后打开"Windows控制台",并进入<Tomcat安装目录>\webapps\mydemo目录,然后输入如下的命令来编译MyDoGet.java:
javac -classpath .;servlet-api.jar -d WEB-INF/classes MyDoGet.java
在成功执行上面的命令后,读者将会在<Tomcat安装目录>\webapps\mydemo\WEB-INF\classes\chapter4目录中看到一个MyDoGet.class文件。
4. 配置Servlet类
这是手工编写Servlet程序的最后一步。在<Tomcat安装目录>\webapps\mydemo\WEB-INF目录中建立一个web.xml文件,并输入如下的内容:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">
<!-- 开始配置MyDoGet -->
<servlet>
<servlet-name>MyDoGet</servlet-name>
<servlet-class>chapter4.MyDoGet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>MyDoGet</servlet-name>
<url-pattern>/ MyDoGet</url-pattern>
</servlet-mapping>
</web-app>
上面的配置代码中的开头部分(尤其是<web-app>标签的属性)很复杂,不过读者并不需要记这些东西,只需要找一个已经配置完的Java Web程序的例子,将web.xml文件中的相关内容复制过来即可。如在Tomcat中提供了一些Servlet的例子,读者可以在<Tomcat安装目录>\webapps\examples\WEB-INF目录找到一个已经配置完的web.xml文件。
在完成上面的几步后,可以通过<Tomcat安装目录>\bin\startup.bat命令来启动Tomcat,然后在IE地址栏中输入如下的URL来测试MyDoGet:
http://localhost:8080/mydemo/MyDoGet
在访问上面的URL后,将在IE中输出如图4.1所示的信息。
图4.1 MyDoGet的输出结果
5. 程序总结
在本例中将程序目录放在了<Tomcat安装目录>\webapps目录中,实际上,这是最简单的发布Java Web程序的方式。读者也可以将程序目录放在任何位置,如将mydemo目录放在D盘的根目录,然后打开<Tomcat安装目录>\conf\server.xml文件,找到<Host>元素,并使用<Context>子元素来发布程序,配置代码如下:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
… …
<Context path="/newdemo" docBase="d:\mydemo" debug="0" />
… …
</Host>
重新Tomcat后,可以通过http://localhost:8080/newdemo/MyDoGet来访问MyDoGet.在<Context>元素中,path属性表示Web程序的上下文路径,如果path属性值为空串,则表示Web站点的根目录。如下面的配置代码所示:
<Host name="localhost" appBase="webapps" unpackWARs="true" autoDeploy="true">
… …
<Context path="" docBase="d:\mydemo" debug="0" />
… …
</Host>
如果使用上面的配置代码,可以通过http://localhost:8080/ MyDoGet来访问MyDoGet.要注意的是,要想设置Web程序的上下文路径为Web站点的根目录,path属性值必须为空串,而不能为"/".
<Context>元素的docBase属性表示一个在磁盘上的实际存在的Web工程目录(可以是相对路径,也可以是绝对路径),或是一个*.war文件(也就是war包)。 如果docBase属性值是相对路径,那么这个路径将相对<Host>元素的appBase属性值所指的目录而言。在本例中,appBase属性值所指向的是<Tomcat安装目录>\webapps在<Host>元素中的unpackWARs属性值如果为true,所有放到webapps目录中的war包在发布时都会自动解压。而autoDeploy属性值如果为true,Tomcat在不重启的情况下,所复制到webapps目录中的Web工程目录或war包都会自动发布。
除了可以将<Context>作为<Host>的子元素外,还可以将<Context>元素提出来放到xml文件中。这些xml文件必须被放到<Tomcat安装目录>\conf\<引擎名>\<主机名>中。在Tomcat中,<引擎名>为Catalina,<主机名>就是<Host>元素中name属性的值,也就是localhost.因此,xml文件的存放目录为<Tomcat安装目录>\conf\Catalina\localhost.而且xml文件名就是上下文路径名,而<Context>目录的path属性将失效。如将hello.xml文件放到<Tomcat安装目录>\conf\Catalina\localhost目录中,内容如下:
<Context path="" docBase="d:\mydemo" debug="0" />
在IE地址栏中输入http://localhost:8080/hello/MyDoGet,就可以访问MyDoGet了。
在使用<Context>发布Web程序时应注意以下两点:
(1)<Context>元素必须是<Host>的子元素。
(2)<Context>元素在<Host>中可以存在多个,但每个<Context>元素中的path属性的值不能有重复,否则Tomcat在启动时将出现异常。
4.1.3 配置数据库连接池
由于基于HTTP协议的Web程序是无状态的,因此,在应用程序中使用JDBC时,每次处理客户端请求时都会重新建立数据库连接。如果客户端的请求非常频繁,服务端在处理数据库时将会消耗非常多的资源。因此,在Tomcat中提供了数据库连接池技术。数据库连接池负责分配、管理和释放数据库连接,它允许应用程序重复使用一个现有的数据库连接,而不是重新建立一个数据库连接。在使用完一个数据库连接后,将其归还数据库连接池,以备其他程序使用。
在Tomcat中配置数据库连接池有两种方法:
1. 配置全局数据库连接池
(1)打开<Tomcat安装目录>\conf\server.xml文件,并从中找到<GlobalNamingResources>元素,然后加入一个子元素<Resource>,这个子元素的配置代码如下:
<Resource name="jdbc/mydb" auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF8"
username="root"
password="1234"
maxActive="200"
maxIdle="50"
maxWait="3000"/>
上面的配置代码有几个和数据库连接池性能有关的属性需要说明一下:
maxActive:连接池可以存储的最大连接数,也就是应用程序可以同时获得的最大连接数。这个属性值一般根据Web程序的最大访问量设置。
maxIdle:最大空闲连接数。当应用程序使用完一个数据库连接后,如果连接池中存储的连接数小于maxIdle,这个数据库连接并不马上释放,而是仍然存储在连接池中,以备其他程序使用。这个属性值一般根据Web程序的平均访问量设置。
maxWait:暂时无法获得数据库连接的等待时间(单位:毫秒)。如果Web程序从数据库连接池中获得的数据库连接数已经等于maxActive,而且都没有归还给连接池,这时再有程序想获得数据库连接,就会等待maxWait所指定的时间。如果超过maxWait所指定的时间还无法获得数据库连接,就会抛出异常。
(2)在<Tomcat安装目录>\conf\Catalina\localhost中建立一个demo.xml文件(文件名要和path属性值一致),然后输入如下内容:
<Context path="/demo" docBase="demo" debug="0">
<ResourceLink name="jdbc/mydb" global="jdbc/mydb" type="javax.sql.DataSource"/>
</Context>
2. 配置局部数据库连接池
在<Tomcat安装目录>\conf\Catalina\localhost中建立一个demo.xml文件,然后输入如下内容:
<Context path="/demo" docBase="demo" debug="0">
<Resource name="jdbc/mydb" auth="Container"
type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF8"
username="root"
password="1234"
maxActive="200"
maxIdle="50"
maxWait="3000"/>
</Context>
在配置完数据库连接池后,可以在Servlet的service方法或其他处理HTTP请求的方法中使用如下代码来连接数据库:
javax.naming.Context ctx = new javax.naming.InitialContext();
// 获得DataSource对象
javax.sql.DataSource ds = (javax.sql.DataSource)
ctx.lookup("java:/comp/env/jdbc/mydb");
// 获得Connection对象
Connection conn = ds.getConnection();
在获得Connection对象后的操作就和不使用数据库连接池操作数据库是一样的了。
4.2 Generic Servlet与Http Servlet类
Generic Servlet类封装了Servlet的基本特征和功能,该类在javax.servlet包中。Http Servlet类是Generic Servlet的子类,也在javax.servlet包中,该类提供了处理HTTP协议的基本架构。如果Servlet想充分利用HTTP协议的功能,就应该从Http Servlet类继承。Generic Servlet类实现了Servlet和Servlet Config接口。在继承Http Servlet的Servlet类中不仅可以使用HttpServlet类中提供的方法,而且还可以使用在Servlet、Servlet Config接口和GenericServlet类中定义的一些方法。
4.2.1 service方法
service方法是Servlet接口的方法,该方法负责处理客户端的所有HTTP请求。service方法是在Servlet接口中定义的一个方法,该方法在GenericServlet中定没有实现,而是在HttpServlet类中实现的这个方法。service方法的定义如下:
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
由于service方法的两个参数类型分别是ServletRequest和ServletResponse,因此,这两个参数并没有处理HTTP消息的特殊功能。为了在service方法中处理HTTP消息,需要使用HttpServletRequest和HttpServletResponse接口中定义的方法。所以在service方法中需要分别将ServletRequest和ServletResponse类型的参数转换成HttpServletRequest和HttpServletResponse,代码如下:
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
response.getWriter()。println("test");
… …
}
为了简化这一过程,在HttpServlet中又提供了另一个service方法的重载形式,代码如下:
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException;
从上面的代码可以看出,这个重载形式的参数类型是HttpServletRequest和HttpServletResponse,这个重载形式被第一个service方法的重载形式调用,代码如下:
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException
{
HttpServletRequest request;
HttpServletResponse response;
try
{
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
}
catch (ClassCastException e)
{
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
如果在Servlet类中覆盖了service方法的第二个重载形式,那么在service方法中就无需再进行两个参数的类型转换了,代码如下:
public void service(HttpServletRequest req, HttpServletResponse res)
throws ServletException, IOException
{
res.getWriter()。println("test");
… …
}
实际上,虽然service的第二个重载形式可以给开发人员带来方便,但这个方法并不是Servlet接口中定义的方法。在Servlet接口中只定义了service的第一个重载形式。因此,Servlet引擎在调用时只会调用service方法的第一个重载形式。
4.2.2 doXxx方法
在Servlet类中除了可以使用service方法来处理HTTP请求外,也可以使用doXxx方法来处理某一个指定的HTTP方法的请求,如doGet方法可以处理HTTP GET请求,doPost方法可以处理HTTP POST请求。这些doXxx方法都是在HttpServlet类中定义的,在HttpServlet类中定义的doXxx方法如下:
doGet:用于处理HTTP GET请求。
doPost:用于处理HTTP POST请求。
doHead:用于处理HTTP HEAD请求。
doPut:用于处理HTTP PUT请求。
doDelete:用于处理HTTP DELETE请求。
doTrace:用于处理HTTP TRACE请求。
doOptions:用于处理HTTP OPTIONS请求。
doXxx的使用方法和service方法完全一样,所不同的是service方法可以处理所有的HTTP请求,而doXxx方法只能处理特定的HTTP请求。对于只需要处理某些HTTP方法的请求的Servlet类,可以使用相应的doXxx方法,代码如下:
// 处理HTTP POST请求
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException
{
response.getWriter()。println("test");
… …
}
在一般情况下,Servlet只需要处理HTTP GET和HTTP POST请求,因此,只需要覆盖doGet和doPost方法即可。
4.2.3 init和destroy方法
init和destroy方法分别在Servlet容器建立Servlet对象和销毁Servlet对象时调用。而且这两个方法只在Servlet的生命周期里调用一次。在Servlet接口中定义了这两个方法,在GenericServlet类中提供了这两个方法的默认实现。init方法有一个ServletConfig类型的参数,可以通过这个参数获得配置信息(也就是在web.xml文件中配置的内容),关于ServletConfig接口的内容将在4.4节详细介绍。destroy方法一般用于在Servlet对象被销毁时释放一些全局的资源,如数据库连接、网络资源等。
init方法和destroy方法的定义如下:
public void init(ServletConfig config) throws ServletException;
public void destroy();
有很多开发人员在编写Servlet类时往往直接覆盖了init方法来完成初始化工作。这么做一般没什么问题。但却将GenericServlet中init方法的默认实现覆盖了。先看看GenericServlet类中相关方法的实现代码:
public abstract class GenericServlet
implements Servlet, ServletConfig, java.io.Serializable
{
private transient ServletConfig config;
// 获得ServletConfig对象
public ServletConfig getServletConfig()
{
return config;
}
// 实现Servlet接口中的init方法
public void init(ServletConfig config) throws ServletException
{
this.config = config;
this.init();
}
// GenericServlet类中提供了空参数的init方法
public void init() throws ServletException
{
}
}
从上面的代码可以看出,带参数的init方法的第1行将config参数值赋给了类变量config.并且getServletConfig方法是通过config变量来返回ServletConfig对象的。如果开发人员覆盖了带参数的init方法,而又未调用super.init(config)语句,那么通过getServletConfig方法就无法再获得ServletConfig对象了
虽然开发人员也可以在Servlet类的init方法中将ServletConfig对象保存起来,但这实在是多此一举。当然,如果即不调用super.init(config)语句,也不保存ServletConfig对象,那么这个ServletConfig对象将会丢失,也就是说,在service、doXxx等方法中将无法获得并使用ServletConfig对象了。为了避免这个尴尬,在GenericServlet类中提供了一个不带参数的init方法,并且在带参数的init方法中调用该init方法,如上面的代码所示。
如果开发人员在Servlet类中覆盖这个不带参数的init方法,那么就仍然可以通过getServletConfig方法来获得ServletConfig对象。因此,笔者建议尽量在Servlet类中覆盖不带参数的init方法来初始化Servlet,如果要在init方法中使用ServletConfig对象,可以使用getServletConfig方法来获得ServletConfig对象。
下面的例子演示了init和destroy方法在Servlet的生命周期中的调用情况。
【实例4-2】 init和destroy方法调用演示
1. 实例说明
在本例中的Servlet类中覆盖了init和destroy方法。在第一次访问Servlet时,将调用init方法。然后通过重新发布Servlet的方式使Servlet引擎调用该Servlet类的destroy方法。然后再次访问这个Servlet,Servlet引擎再次调用init方法。最后通过停止Tomcat的方式使Servlet引擎再次调用destroy方法。也就是说,在本例中,Servlet引擎会调用两次init方法和两次destroy方法。
2. 编写Servlet类
本例中Servlet类的实现代码如下:
public class InitDestroyServlet extends HttpServlet
{
// 覆盖无参数的init方法
public void init() throws ServletException
{
System.out.println("init方法被调用!");
}
// 覆盖destroy方法
public void destroy()
{
System.out.println("destroy方法被调用");
}
// 处理客户端的HTTP请求
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
response.getWriter()。println("测试init和destroy方法");
}
}
3. 测试程序
通过测试来观察init和destroy方法的调用情况在IE地址栏中输入如下的URL:
http://localhost:8080/demo/InitDestroyServlet
这时在控制台中将输出"init方法被调用!"的信息。这时在Eclipse中稍微修改一下InitDestroyServlet类(可以加一个空格,并保存),这时Eclipse会自动重新发布Web应用,在控制台中就会输出"destroy方法被调用"信息。在等待Web应用重新发布成功后,再次访问上面的URL,并且在Eclipse中停止Tomcat,这时,Servlet引擎会再次调用Servlet类的destroy方法。以上测试过程输出的信息如图4.2所示。
图4.2 init和destroy方法的调用情况
4. 程序总结
从本例可以看出,Servlet类的destroy方法会在Web应用重新发布时,或Web服务端(如Tomcat)停止时调用。
4.2.6 getLastModified方法
HTTP响应消息头有一个Last-Modified字段,这个字段表示服务器内容最新修改时间。如果请求消息头中包含If-Modificed-Since字段,并且该字段的时间比Last-Modified字段的时间早。或是请求消息头中没有If-Modificed-Since字段。service方法就会调用doGet方法来重新获得服务端内容。但这有一个前提,就是getLastModified方法必须返回一个正数。但在默认情况下,getLastModified方法返回-1.因此,service方法调用用doGet方法的规则如下:
当getLastModified返回-1时,service方法总会调用doGet方法。
当getLastModified返回正数时,如果HTTP请求消息头中没有If-Modified-Since字段,或者If-Modified-Since字段中的时间比Last-Modified字段中的时间早,service方法会调用doGet方法。浏览器在下次访问该Servlet时,If-Modified-Since字段的值就是上一次访问该Servlet的Last-Modified字段的值。
当getLastModified方法返回正数时,如果If-Modified-Since字段中的时间比Last-Modified字段中的时间晚,或者这两个字段的时间相同,service将不会调用doGet方法,而是向浏览器反回一个304(Not Modified)状态码来通知浏览器继续使用以前缓冲过的内容。
下面的例子演示了如何通过getLastModified方法控制浏览器是否使用被缓存的内容。
【实例4-3】 用getLastModified方法控制浏览器使用缓冲内容
1. 实例说明
本程序通过使getLastModified方法返回不同的值来决定service方法是否执行doGet方法。如果service方法不执行doGet方法,虽然Servlet被成功调用,但是并没有执行doGet方法,因此,Servlet并没有返回新的服务端内容。
2. 编写Servlet类
在CacheServlet类中覆盖了getLastModifed方法,并返回了当前的时间(以毫秒为单位),代码如下:
public class CacheServlet extends HttpServlet
{
// 覆盖HttpServlet类的getLastModified方法
protected long getLastModified(HttpServletRequest req)
{
ong now = System.currentTimeMillis();
// 返回当前时间
return System.currentTimeMillis();
}
protected void doGet(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
response.getWriter()。println(System.currentTimeMillis());
System.out.println(request.getHeader("if-Modified-Since"));
}
}
3. 测试程序
通过测试程序来观察客户端输出的时间是否变化。在IE地址栏中输入如下的URL:
http://localhost:8080/demo/CacheServlet
读者会看到在IE中输出当前服务器的时间(毫秒值)。当使用F5不断刷新页面时,会看到这个值也在不断地变化。从而可以断定,CacheServlet的doGet方法被调用了。现在将服务器的时间往回调整一下(如调整到前一天),再次按F5刷新,这时页面的毫秒时间就不会再发生变化了。这是因为目前的服务器时间比HTTP请求消息头的If-Modifed-Since字段值指定的时间早,因此,service就不会调用doGet方法了,当然也就不会输出当前的服务器时间了。实际上,浏览器使用的是被缓存的内容。
读者可以从IE的缓存目录(C:\Documents and Settings\Administrator\Local Settings\Temporary Internet Files)找到CacheServlet,并将其删除,再次按F5刷新页面。就会看到页面的时间变化了,当再次按F5时,又不变化了。这是因为当把IE的相关缓存删除后,由于IE找不到缓存内容,因此,无法设置HTTP请求消息头的If-Modified-Since段,这也正符合上述第二个规则中调用doGet方法的条件。因此,第一次刷新页面时CacheServlet返回了时间信息,当再次刷新页面时,则CacheServlet又被缓存了,所以当再次发送HTTP请求时,If-Modifed-Since字段中的时间和Last-Modified字段中的时间相等,因此,service方法会返回304状态码,这时IE就会使用被缓存的CacheServlet.
4. 程序总结
在service方法中只有doGet方法考虑了If-Modified-Since和Last-Modified字段,其他的方法,如doPost,并不涉及到这两个字段,因此,除了doGet方法,其他的doXxx方法总会被调用。
对于需要实时获得返回结果的Servlet,笔者并不建议覆盖getLastModified方法。因为如果是这样,浏览器可能会在一定时间内使用浏览器缓存的内容。
4.3.1 getInitParameterNames方法
在web.xml文件中可以为Servlet设置多个初始化参数。可以通过getInitParameterNames方法来获得这些初始化参数的名称。该方法返回一个Enumeration对象,初始化参数的名称可以从Enumeration对象中获得。假设一个名为MyServletConfig的Servlet的配置代码如下:
<servlet>
<servlet-name>MyServletConfig</servlet-name>
<servlet-class>chapter4.MyServletConfig</servlet-class>
<!-- 配置Servlet初始化参数 -->
<init-param>
<param-name>product</param-name>
<param-value>洗衣机</param-value>
</init-param>
<init-param>
<param-name>price</param-name>
<param-value>300</param-value>
</init-param>
</servlet>
通过getInitParameterNames方法返回的Eenumeration对象中就包含了上面代码中的两个初始化参数名称:product和price。
4.4 Servlet Context接口
Servlet Context对象表示一个Web应用程序。Servlet Context接口定义了很多方法。通过这些方法,Servlet可以和Servlet容器进行通讯。Servlet引擎会为每一个Web应用程序创建一个Servlet Context对象,Servlet Context对象被包含在Servlet Config对象中,通过Servlet Config接口的getServlet Context方法可以获得Servlet Context对象。与Servlet API中的其他接口一样,Servlet引擎也为Servlet Context接口提供了默认的实现。
4.4.1 获取Web应用程序的初始化参数
在server.xml文件和web.xml文件中都可以设置Web应用程序的初始化参数。通过设置Web应用程序的初始化参数,可以在不需要修改程序的前提下,改变Web应用程序的某些设置。如一个Web应用程序可能不只运行在一家公司,如果将该程序部署在某一家公司,而且公司名称被设置成为Web应用程序的初始化参数。这时直接修改初始化参数就可以将公司名设置成这家公司的名称。
在ServletContext接口中定义了getInitParameter方法和getInitParameterNames方法来访问Web应用程序的初始化参数,其中getInitParameter方法可以通过初始化参数名获得参数值,而getInitParameterNames可以获得保存所有初始化参数名的Enumeration对象。
在web.xml文件中配置Web应用程序的初始化参数需要使用<context-param>元素,下面是一个在web.xml文件中配置初始化参数的例子:
<web-app … >
<context-param>
<param-name>companyName</param-name>
<param-value>Sun公司</param-value>
</context-param>
… …
</web-app>
如果想在server.xml文件中配置Web应用程序的初始化参数,需要在当前Web应用程序的<Context>元素中使用<Parameter>子元素来配置初始化参数,代码如下:
<Context docBase="demo" path="/demo" reloadable="true"
source="org.eclipse.jst.jee.server:demo">
<!-- 配置Web应用程序的初始化参数 -->
<Parameter name = "myParam" value = "newValue " override="true" />
</Context>
其中override属性值如果为true,表示web.xml文件中的初始化参数可以覆盖server.xml文件中的同名初始化参数,也就是说,当override属性为true时,如果web.xml文件和server.xml文件中有同名的初始化参数,以web.xml文件中的初始化参数为准。当override属性为false时,以server.xml文件中的同名初始化参数为准。override属性的默认值是true.
【实例4-4】 读取Web应用程序的初始化参数
1. 配置Web应用程序的初始化参数
由于本书使用的IDE是Eclipse IDE for Java EE,这个IDE使用了自己的server.xml文件,因此,如果在该IDE中测试本例的程序,不能在<Tomcat安装目录>\conf\server.xml文件中设置Web应用程序的初始化参数,而应该在IDE所使用的server.xml文件中设置这些参数。
如果在Eclipse IDE for Java EE配置了Tomcat作为Web服务器,那么会在Project Explorer页中添加一个Servers工程。其中该IDE所使用的server.xml文件就在其中的Tomcat v6.0 Server-config节点中,如图4.3所示。
图4.3 server.xml文件的位置
双击server.xml打开该文件后,找到如下的配置代码:
<Context docBase="demo" path="/demo" reloadable="true"
source="org.eclipse.jst.jee.server:demo"/>
将上面的配置代码修改成下面的形式:
<Context docBase="demo" path="/demo" reloadable="true"
source="org.eclipse.jst.jee.server:demo">
<Parameter name = "myParam" value = "newValue" override = "false" />
<Parameter name = "myParam1" value = "newValue1" override = "true" />
</Context>
下面来配置web.xml中的初始化参数,打开web.xml文件,在<web-app>元素中添加如下的配置代码:
<!-- 配置第一个Web应用程序的初始化参数 -->
<context-param>
<param-name>companyName</param-name>
<param-value>Sun公司</param-value>
</context-param>
<!-- 配置第二个Web应用程序的初始化参数 -->
<context-param>
<param-name>myParam</param-name>
<param-value>myParamValue</param-value>
</context-param>
在server.xml和web.xml文件中有一个同名的初始化参数myParam,由于在server.xml文件中的<Parameter>元素的override属性值为false,因此,使用getInitParameter方法读出来的是在server.xml文件中配置的参数值newValue.
2. 编写ContextParamServlet类
在ContextParamServlet类中通过getInitParameterNames方法得到保存所有初始化参数名的Enumeration对象,并逐个扫描初始化参数名,并通过getInitParameter方法获得相应的初始化参数值。ContextParamServlet类的代码如下:
public class ContextParamServlet extends HttpServlet
{
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("Web应用程序的初始化参数列表<p/>");
ServletContext context = getServletContext();
// 获得所有的初始化参数名称
Enumeration<String> params = context.getInitParameterNames();
while(params.hasMoreElements())
{
// 获得当前初始化参数名
String key = params.nextElement();
// 获得当前初始化参数值
String value = context.getInitParameter(key);
out.println(key + " = " + value + "<br/>");
}
}
}
3. 测试ContextParamServlet类
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/ContextParamServlet
浏览器显示的结果如图4.4所示。
图4.7 Web应用程序的初始化参数列表
4. 程序总结
从图4.4的显示结果可以看出,使用getInitParameterNames方法获得的初始化参数列表既包括在server.xml文件中配置的初始化参数,也包括在web.xml文件中配置的初始化参数。其中myParam1参数只在server.xml文件中配置,并未在web.xml中配置。而myParam参数同时在server.xml和web.xml文件中配置,但由于<Parameter>元素的override属性值为false,因此,myParam参数的值以server.xml文件中的配置为准。
4.4.2 application域
一个Web应用程序中的所有Servlet共享一个ServletContext对象,所以,ServletContext对象在JSP中也被称为application对象(application是JSP的9个内置对象之一)。在application对象内部有一个Map对象,用来保存Web应用程序中使用的key-value对,保存在application对象中的每一个key-value对也被称为application对象的属性。由于Web应用程序中的所有Servlet共享一个application对象,因此,application对象中的属性也可被称为application域范围内的属性,application域范围内的属性往往被当成Web应用程序的全局变量使用(如整个网站的访问计数器就可以作为application对象的属性被保存在application域中)。在ServletContext接口中定义了如下4个方法来操作application对象的属性:
1. getAttributeNames方法
该方法返回一个Enumeration对象,通过这个对象可以获得application对象中所有属性的key值。getAttributeNames方法的定义如下:
public Enumeration getAttributeNames();
2. getAttribute方法
该方法返回一个指定application域属性的值。getAttribute方法的定义如下:
public Object getAttribute(String name);
3. removeAttribute方法
该方法用于删除application对象中的属性。
4. setAttribute方法
向application对象添加一个属性,如果该属性存在,则替换这个属性。如果设置的属性值为null,则相当于调用removeAttribute方法来删除该属性。
Servlet引擎会为每一个Web应用程序创建一个application对象,一个Servlet程序可以被发布到不同的Web应用程序中,而在不同的Web应用程序中该Servlet所对应的application对象是不同的。
4.4.3 访问资源文件
ServletContext接口定义了三个方法来访问当前Web应用程序的资源文件,这3个方法如下:
1. getResourcePaths方法
该方法返回指定Web应用程序目录中的所有子目录和文件,这些返回的目录文件不包括嵌套目录和文件。这些返回的子目录和文件都封装在该方法返回的一个Set对象中。getResourcePaths方法的定义如下:
public Set getResourcePaths(String path);
其中path参数表示Web应用程序中的目录,必须以斜杠(/)开头,表示Web应用程序的根目录。如要得到WEB-INF目录中的所有目录和文件,可以使用如下的代码:
Set paths = getServletContext()。getResourcePaths("/WEB-INF");
2. getResource方法
该方法返回指定Web应用程序中某个资源的URL对象,getResource方法的定义如下:
public URL getResource(String path) throws MalformedURLException;
其中path表示资源的路径,必须以斜杠(/)开头,表示Web应用程序的根目录。如要得到封装web.xml文件路径的URL对象,可以使有下面的代码:
URL url = getServletContext()。getResource("/WEB-INF/web.xml");
3. getResourceAsStream方法
该方法返回某个资源的InputStream对象,getResourceAsStream方法实际上打开的是getResource方法返回的URL所指的资源文件。该方法的定义如下:
public InputStream getResourceAsStream(String path);
其中path参数的含义和getResource方法中的path参数相同。
除了使用ServletContext定义的方法来访问Web应用程序中的资源外,还可以使用如下的两种方法来访问资源文件:
1. 使用FileInputStream来访问资源文件
使用这种方法非常直接,但要通过ServletContext接口的getRealPath方法获得当前Web应用程序的本地目录。如打开web.xml文件的代码如下:
String resourceFileName =
getServletContext()。getRealPath("/WEB-INF/web.xml");
FileInputStream fis = new FileInputStream(resourceFileName);
要注意的是,FileInputStream可以打开以绝对路径指定的资源文件,也可以打开以相对路径指定的资源文件。如果使用相对路径,该相对路径是相对于当前路径而言的。Web服务器的当前路径可以使用如下的代码获得:
String path = System.getProperty("user.dir");
假设上面的代码返回的路径是D:\eclipse,如果在该目录下有一个abc子目录,并且在abc子目录中有一个xyz.properties目录,也就是说,xyz.properties文件的绝对路径是D:\eclipse\abc\xyz.properties,那么使用绝对路径和相对路径访问xyzproperties文件的代码如下:
// 使用绝对路径访问xyz.properties文件
FileInputStream fis1 = new
FileInputStream("D:\eclipse\abc\xyz.properties");
// 使用相对路径访问xyz.properties文件
FileInputStream fis1 = new FileInputStream("abc\xyz.properties");
2. 使用Class.getResourceAsStream方法访问资源文件
Class的getResourceAsStream方法和ServletContext接口的getResourceAsStream方法虽然方法名相同,但却有一定的差异。
Class的getResourceAsStream方法的定义如下:
public InputStream getResourceAsStream(String name);
其中name表示资源的路径,也是以斜杠(/)开头,但这个斜杠是指WEB-INF\classes目录,而ServletContext接口中定义的getResourceAsStream方法的path参数中的斜杠是指Web应用程序的根目录。这一点在使用时要注意。
假设在WEB-INF\classes\chapter4目录中有一个abc.properties文件,使用Class.getResourceAsStream方法访问该文件的代码如下:
InputStream is =
getClass()。getResourceAsStream("/chapter4/abc.properties");
一个良好的编程习惯是将动态的或可能变化的信息(如连接数据库的信息、连接网络的信息等)保存在资源文件中,以便更容易修改和维护这些资源。
在下面的示例中演示了如何使用上述的三种方法来访问Web应用程序中的资源文件,并从中读取信息。
4.4.4 Web应用程序之间的访问
当前的Web应用程序不仅可以通过ServletContext对象访问自己的资源,而且还可以访问其他Web应用程序中的资源。假设在server.xml中配置了如下两个Web应用程序:
<!-- 本例所在的应用程序 -->
<Context docBase="demo" path="/demo" reloadable="true"
crossContext="true" source="org.eclipse.jst.jee.server:demo">
<Parameter name="myParam" override="false" value="newValue" />
<Parameter name="myParam1" override="true" value="newValue1" />
</Context>
<!-- 要访问的Web应用程序 -->
<Context docBase="mydemo" path="/mydemo" reloadable="true"
source="org.eclipse.jst.jee.server:mydemo">
<Parameter name="mydemo.param" value="mydemo.value"
override="false" />
</Context>
上面的代码使用了两个<Context>元素分别配置了两个Web应用程序,而且在demo应用程序中的<Context>元素中使用了crossContext属性,如果该属性为true,则在当前Web应用程序中可以访问其他的Web应用程序,否则,将无法访问其他的Web应用程序,也就是无法获得其他Web应用程序的ServletContext对象。
在mydemo工程中配置了一个mydemo.param参数,同时在mydemo应用程序中的web.xml中也配置了一个初始化参数,代码如下:
<context-param>
<param-name>name</param-name>
<param-value>超人</param-value>
</context-param>
在demo应用程序中建立一个Servlet,并且在该Servlet中获得mydemo应用程序的ServletContext对象,并输出上述的两个初始化参数。该Servlet的代码如下:
public class OtherContextServlet extends HttpServlet
{
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 获得mydemo应用程序的ServletContext对象
ServletContext context = this.getServletContext()。getContext("/mydemo");
if(context != null)
{
// 输出mydemo应用程序中两个初始化参数值
out.println("mydemo.param="
+ context.getInitParameter("mydemo.param") + "<br/>"); out.println("name=" + context.getInitParameter("name"));
}
}
}
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/OtherContextServlet
浏览器显示的信息如图4.5所示。
图4.9 显示其他Web应用程序的初始化参数
4.4.5 ServletContext接口定义的其他的方法
在ServletContext接口中还定义了一些其他的方法,这些方法如下:
1. getMajorVersion方法
该方法得到当前Servlet引擎所支持的Servlet规范的主版本号。由于本书使用的是Servlet2.5,因此,getMajorVersion方法返回2.
2. getMinorVersion方法
该方法得到当前Servlet引擎所支持的Servlet规范的次版本号。由于本书使用的是Servlet2.5,因此,getMinorVersion方法返回5.
3. getMimeType方法
该方法返回Web应用程序中文件的MIME类型,如要返回web.xml文件的MIME类型,可以使用如下的代码:
System.out.println(getServletContext()。getMimeType("/WEB-INF/web.xml"));
上面的代码将输出application/xml.
4. getServerInfo方法
该方法返回Web服务器的名称和版本号,如Apache Tomcat/6.0.18.
5. getServletContextName方法
得到当前Web应用程序的上下文名称。也就是<Context>元素中path属性值的斜杠(/)后面的内容,既demo。
6. getContextPath方法
得到当前Web应用程序的上下文路径。也就是<Context>标签中path属性值,既"/demo"。
4.5 小结
本章主要讲解了Servlet的基础知识。Servlet类必须要实现Servlet接口,但为了尽量减少代码量,Servlet API又提供了GenericServlet和HttpServlet类来实现Servlet接口中的方法。这样开发人员在开发Servlet时就无需编写大量的代码了。而在Servlet类中处于核心地位的是service方法。该方法可以处理所有的HTTP请求。但为了可以单独处理不同的HTTP请求,Servlet API提供了一些doXxx方法,如doGet方法只能处理HTTP GET请求。这些doXxx方法的调用都将依赖于service方法。
在Servlet API中提供了一些接口,在这些接口中定义了很多可以访问Web应用程序的方法。如ServletConfig、ServletContext等,其中ServletConfig对象可以访问当前Servlet的配置信息,如Servlet名称 、Servlet的初始化参数等。而ServletContext对象则更为强大,它不仅可以获得当前Web应用程序的各种信息,如Web应用程序的初始化信息,Web资源的本地目录、访问application域等,还可以获得其他Web应用程序的ServletContext对象。
第5章 Servlet高级技术
本章将介绍Servlet的一些高级技术。在Servlet中,HttpServletResponse和HttpServletRequest接口是最常用的两个接口。在Servlet引擎中分别实现了这两个接口。在本章介绍了如何使用HttpServletResponse和HttpServletRequest对象来完成更高级的功能,如在HTTP响应消息头中传输中文、禁止浏览器的缓存、定时刷新网页、包含和转发Web资源等。除此之外,本章还介绍了Web应用程序中经常使用到的Cookie和Session的原理和应用,并给出了Cookie和Session的应用实例,如在Cookie中如何保存中文信息,通过URL重定向来跟踪Session等。读者通过对本章的学习,可以掌握Servlet的很多高级技术,并可以编写更复杂的Web应用程序。
5.1 HttpServletResponse的应用
Web服务器发送给客户端浏览器的信息有3部分:状态行、消息头和消息正文。为了更方便地操作这些信息,Servlet API提供了一个HttpServletResponse接口,在该接口中定义了很多方法来访问和控制这些信息,Servlet引擎必须实现这个接口,并在调用Servlet接口的service方法时传入HttpServletResponse对象。因此,开发人员可以在service方法以及doXxx方法中通过HttpServletResponse对象处理Web服务器发送给客户端的HTTP响应消息。
HttpServletResponse接口继承了ServletResponse接口,ServletResponse接口中定义了处理响应消息的基本方法,如最常用的getWriter和getOutputStream方法,可以通过这两个方法向客户端输出响应正文。HttpServletResponse接口在ServletResponse接口的基础上添加了一些和HTTP协议相关的方法,如与Cookie相关的方法、访问HTTP响应消息头的方法等。
5.1.1 产生响应状态行
HTTP响应消息的响应状态行分为3部分:HTTP版本、状态码和状态信息,如下所示:
HTTP/1.1 200 OK
其中HTTP版本可以是HTTP/1.1或HTTP/1.0,这由Web服务器所支持的HTTP版本决定。状态信息的内容和状态相关,如404状态码所对应的HTTP1.1规范中的状态信息是Not Found.由于HTTP版本一般是基本固定的,而状态信息是随着状态码的变化而变的。因此,在HTTP响应状态行中,只有状态码是经常需要变化的。
HTTP的状态响应码可分为如下5类:
100 ~ 199:表示服务端成功接收HTTP请求,但要求客户端继续提交下一次HTTP请求才能完成全部处理过程。
200 ~ 299:表示服务端已成功接收HTTP请求,并完成了全部处理过程。
300 ~ 399:表示客户端请求的资源已经移动了别的位置,并向客户端提供一个新的地址,一般这个新地址由HTTP响应消息头的Location字段指定。
400 ~ 499:表示客户端的请求有错误。
500 ~ 599:表示服务端出现错误。
HttpServletResponse接口定义一些可以修改HTTP状态码的方法,这些方法的描述如下:
1. setStatus方法
setStatus方法可以设置状态码,并生成响应状态行。由于响应状态行中的协议版本和状态信息是由Web服务器设置的,因此,只需设置响应状态码就可以了。setStatus方法的定义如下:
public void setStatus(int sc);
其中sc参数表示响应状态码,该参数值可以直接使用整数形式,也可以使用在HttpServletResponse接口中定义的常量(建议使用这种方式)。如状态码200的常量为HttpServletResponse.SC_OK.
2. sendRedirect方法
虽然setStatus方法可以随意设置响应状态吗,但HttpServletResponse接口还定义了一个sendRedirect方法,该方法可以更方便地将响应状态码设置成302.在300 ~ 399区间内的状态码需要客户端重定向URL(由HTTP响应消息头的Location字段指定的地址)。sendRedirect方法的定义如下:
public void sendRedirect(String location) throws IOException;
通过sendRedirect方法可以将当前的Servlet重定向到其他的Web资源上,这个URL可以是绝对路径(如http://www.csdn.net),也可以是相对路径(如/samples/test.html)。
3. sendError方法
sendError方法用于设置表示错误消息的状态码(也就是400 ~ 599之间的状态码)。而且还可以设置状态消息。sendError方法的定义如下:
public void sendError(int sc) throws IOException;
public void sendError(int sc, String msg) throws IOException;
其中sc参数表示响应状态码(一般是404,但也可以是其他的状态响应码,如500)、msg表示状态消息。
5.1.2 设置响应消息头
在HttpServletResponse接口中定义了若干设置HTTP响应消息头的方法,如addHeader方法可以添加响应消息头字段;addIntHeader方法可以添加整数值的响应消息头字段;setContextType方法可以设置Context-Type字段值。
HTTP响应消息头是由若干key-value对组成的,其中key表示字段名,value表示字段值,中间用冒号(:)分隔。如下面的内容就是一个标准的HTTP响应消息头:
Content-Length:1024
Content-Type:text/html
Content-Location:http://nokiaguy.blogjava.net
Accept-Ranges:bytes
Server:Microsoft-IIS/6.0
X-Powered-By:ASP.NET
Date:Tue, 30 Dec 2008 11:49:53 GMT
从上面的内容可以看出,每一行都由一个字段和字段值组成,字段和字段值之间用冒号分隔(如Content-Length: 1024)。当使用Servlet向客户端发送响应消息时,为了完成某个功能或动作,如通知浏览器使用何种字符集显示网页;指定响应正文的类型等,需要对某些响应消息头进行设置。这就需要使用下面设置响应消息头的方法:
1. addHeader与setHeader方法
addHeader和setHeader方法可用于设置HTTP响应消息头的所有字段。这两个方法的定义如下:
public void addHeader(String name, String value);
public void setHeader(String name, String value);
其中name表示响应消息头的字段名,value表示响应消息头的字段值。这两个方法都会向响应消息头增加一个字段。但它们的区别是如果name所指的字段名已经存在,setHeader方法会用value来覆盖旧的字段值,而addHeader会增加一个同名的字段(HTTP响应消息头允许存在多个同名的字段)。在设置时,name不区分大小写。如设置Content-Type时可使用下面两行代码中的任意一行:
response.setHeader("Content-Type", "image/png");
response.setHeader("content-type", "image/png");
2. addIntHeader与setIntHeader方法
HttpServletResponse接口定义了两个专门设置整型字段值的方法,这两个方法的定义如下:
public void addIntHeader(String name, int value);
public void setIntHeader(String name, int value);
这两个方法与setHeader和addHeader方法类似。它们在设置整型字段值时避免了将int类型转换为String类型的麻烦。
3. addDateHeader与setDateHeader方法
HttpServletResponse接口定义了两个专门设置日期字段值的方法,这两个方法的定义如下:
public void addDateHeader(String name, long date);
public void setDateHeader(String name, long date);
这两个方法与setHeader和addHeader方法类似。HTTP响应消息头中的日期一般为GMT时间格式。这两个方法在设置日期字段值时省去了将自1970年1月1日0点0分0秒开始计算的一个以毫秒为单位的长整数值转换为GMT时间字符串的麻烦。
4. setContentType方法
setContentType方法用于设置Servlet的响应正文的MIME类型,对于HTTP协议来说,就是设置Content-Type字段的值。如响应正文是png格式的图形数据,就需要使用该方法将响应正文的MIME类型设置成image/png,代码如下:
response.setContentType("image/png");
关于更详细的MIME类型消息,可以在<Tomcat安装目录>\conf\web.xml文件中找到。setContentType方法还可指定响应正文所使用的字符集类型,如"text/html; charset=UTF-8",在设置字符集类型时,charset应为小写,否则不起作用。如果在MIME类型中未指定字符集编码类型,并且使用getWriter方法(将在后面的部分介绍)返回的PrintWriter对象输出文本时,Tomcat将使用ISO8859-1字符集编码格式对输出的文本进行编码。因此,如果在Servlet中要向客户端输出中文时,应使用setContentType方法设置响应正文的字符集编码。
5. setCharacterEncoding方法
该方法设置了Content-Type字段的字符集部分,也就是设置"text/html; charset=UTF-8"中的"charset=UTF-8"部分。在使用setCharacterEncoding方法之前,如果Content-Type字段不存在,必须使用setContentType或setHeader方法添加Content-Type字段,否则setCharacterEncoding方法字符集类型不会出现在响应消息头上。setCharacterEncoding方法同时还设置了使用PrintWriter对象向客户端输出的字符的编码格式,这一点和setContextType方法类似。
6. setContentLength方法
setContentLength方法用于设置响应正文的大小(单位是字节)。对于HTTP协议来说,这个方法就是设置Content-Length字段的值。当使用下载工具下载文件时,会发现在每个下载文件的状态栏中都会显示文件大小,其实这个值就是从Content-Length字段中获得。如果下载某些文件时,无法正确显示文件大小,说明HTTP响应消息头中并未设置Content-Length字段的值。一般来说,在Servlet中并不需要使用setContentLength方法设置Content-Length的值,因为Servlet引擎会根据向客户端实际输出的响应正文的大小动态设置Content-Length字段的值。
7. containsHeader方法
containsHeader方法用于检查某个字段名是否在HTTP响应消息头中存在,如果存在,返回true,否则返回false.containsHeader方法的定义如下:
public boolean containsHeader(String name);
8. setLocale方法
该方法设置了响应消息的本地信息。setLocale方法的定义如下:
public void setLocale(java.util.Locale loc);
其中Locale对象包含了语言和国家地区信息。该方法有如下3个作用:
(1)设置Content-Language字段的值,该字段表示当前页面所使用的语言。如设置成zh-CN,表示当前页面是中文。
(2)设置Content-Type字段的字符集编码部分。但如果Content-Type字段不存在,则该方法不会自动添加Content-Type字段,也就是说,要想使用setLocale方法设置字符集编码,必须将使用addHeader方法或其他可以添加响应字段的方法添加一个Content-Type字段,才可以使用setLocale方法设置Content-Type字段的字符集编码部分,如下面的代码所示:
response.addHeader("content-type", "");
response.setLocale(new java.util.Locale("zh", "CN"));
response.getWriter()。println("设置Content-Type字段的字符集编码部分");
由于java.util.Locale类并未包含字符集编码信息,因此,如果要使用setLocale方法设置Content-Type字段值的字符集编码部分,还必须在web.xml文件中使用<local-encoding-mapping-list>元素进行映射,代码如下:
<locale-encoding-mapping-list>
<locale-encoding-mapping>
<locale>zh-CN</locale>
<encoding>UTF-8</encoding>
</locale-encoding-mapping>
</locale-encoding-mapping-list>
上面的配置代码将zh-CN映射成了UTF-8,因此,使用setLocale方法将Content-Language字段设置成zh-CN后,Content-Type字段值的字符集编码部分就会被设置成"charset=UTF-8".
(3)设置要输出到客户端的文本的编码格式。
在SevletResponse和HttpServletResponse接口中有很多方法可以设置Content-Type字段的字符集编码部分和服务端输出文本的编码格式,这些方法包括addHeader、setHeader、setContent-Type、setCharacterEncoding、setLocale.实际上,这些方法具有同等的地位,也就是说,后面调用的方法将覆盖前面调用的方法的设置。但这些方法必须在调用getWriter方法之前调用,否则设置不会生效。
5.1.3 用HTTP响应消息头传输中文信息
使用HTTP响应头传递信息是一件非常"酷"的事。但遗憾的是,在传递中文时,会出现乱码问题。其实要解决这个问题也非常简单,只需要对要传输的中文进行编码,然后在接收它们的客户端再对其进行解码即可。
【实例5-1】 用HTTP响应消息头传输中文信息
1. 实例说明
在本程序中通过HTTP响应消息头分别传输英文消息、中文消息和被编码后的中文消息(对中文消息的编码可以采用多种方式,在本例中采用了URL编码的方式,也就是使用java.net.URLEncoder.encode方法对中文消息进行编码),并在客户端使用Socket来访问该Servlet程序,并输出相应的中、英文消息。
2. 编写ChineseHeader类
ChineseHeader是一个Servlet类,负责向客户端发送HTTP响应消息,该类的实现代码如下:
public class ChineseHeader
{
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 设置英文响应消息头
response.addHeader("English", "information");
// 设置中文响应消息头
response.addHeader("Chinese", "中文头信息");
// 设置被编码的中文响应消息头
response.addHeader("NewChinese", java.net.URLEncoder.encode("中文头信息","utf-8"));
out.println("响应正文");
}
}
上面的代码向客户端输出了3个自定义的HTTP响应消息头字段:English、Chinese和NewChinese.其中English字段值是英文消息、Chinese字段值是未编码的中文消息,而NewChinese字段值是用UTF-8格式编码的中文消息。
3. 查看HTTP响应消息头
查看HTTP响应消息头的方法很多,如可以使用telnet或自己编写程序来获得HTTP响应消息头信息。但这些方式都需要编写程序,比较麻烦。因此,可以借助更简单的工具来完成这个工作。在本例中使用了一个叫"影音传送带"的下载工具。读者也可以使用其它的下载工具。
使用影音传送带下载如下的URL:
http://localhost:8080/demo/ChineseHeader
然后查看影音传送带的下载日志,如图5.1所示。
图5.1 查询HTTP响应消息头
图5.1中的黑框中的内容就是在服务端设置的3个自定义HTTP响应消息头。由此可以看出,English字段的值正常显示了,而Chinese和NewChinese字段的值并没有正常显示。其中Chinese字段的值是乱码,而NewChinese字段的值显示的是URL编码格式。其实这些编码就是"中文头消息"的UTF-8编码,要想获得正确的中文消息,必须要使用java.net.URLDecoder类对其解码。
从以上结果可以得出一个结论,使用setCharacterEncoding或其他方法设置字符集编码,并不会对HTTP响应消息头进行编码,而只会对响应正文进行编码。
4. 编写访问ChineseHeader的客户端程序(MyChineseHeader类)
MyChineseHeader类是一个控制台程序,在该类中通过java.net.Socket类来访问服务端程序,并对被编码的中文消息进行解码。MyChineseHeader类的实现代码如下:
package chapter5;
import java.net.*;
import java.io.*;
public class MyChineseHeader
{
public static void main(String[] args) throws Exception
{
Socket socket = new Socket("localhost", 8080);
OutputStream os = socket.getOutputStream();
OutputStreamWriter osw = new OutputStreamWriter(os);
// 向服务端发送HTTP请求消息(只有请求头)
osw.write("GET /demo/ChineseHeader HTTP/1.1\r\n");
osw.write("Host:localhost:8080\r\n");
osw.write("Connection:close\r\n\r\n");
osw.flush();
// 从服务端获得HTTP响应消息头
InputStream is = socket.getInputStream();
InputStreamReader isr = new InputStreamReader(is, "UTF-8");
BufferedReader br = new BufferedReader(isr);
String s = "";
String responseData = "";
// 按行读取HTTP响应消息头
while((s = br.readLine()) != null)
{
responseData += s + "\r\n";
}
responseData = java.net.URLDecoder.decode(responseData, "UTF-8");
System.out.println(responseData);// 输出解码后的HTTP响应头
is.close();
os.close();
}
}
在编写上面的代码时要注意的是在进行解码时,解码格式必须和服务端一致,也就是说decode方法和encode方法的第二个参数值必须是相同的编码格式,在本例中都是UTF-8。
5. 获得解码后的HTTP响应头信息
运行MyChineseHeader程序,在IDE的Console中将会输出如图5.2所示的HTTP响应消息头信息。从图5.2输出的信息可以看出,NewChinese字段的值已经正确显示了。
图5.2 输出正常的HTTP响应头消息
5.1.4 禁止浏览器缓存当前Web页面
所谓浏览器缓存,是指当第一次访问网页时,浏览器会将这些网页缓存到本地,当下一次再访问这些被缓存的网页时,浏览器就会直接从本地读取这些网页的内容,而无需再从网络上获取。
虽然浏览器提供的缓存功能可以有效地提高网页的装载速度,但对于某些需要实时更新的网页,这种缓存机制就会影响网页的正常显示。幸好在HTTP响应消息头中提供了3个字段可以关闭客户端浏览器的缓存功能。下面三条语句分别使用这三个字段来关闭浏览器的缓存:
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
虽然上面3个HTTP响应消息头字段都可以关闭浏览器缓存。但并不是所有的浏览器都支持这3个响应消息头字段,因此,最好同时使用上面这3个响应消息头字段来关闭浏览器的缓存。
【实例5-2】 禁止浏览器缓存当前Web页面
1. 实例说明
本程序演示了在未关闭浏览器缓存和关闭浏览器缓存两种情况下,通过form提交请求消息时的表现。
2. 编写Cache类
在Cache类中同时使用上述的三个响应消息头字段关闭了浏览器缓存,并向客户端输出一段HTML代码,以测试关闭缓存和未关闭缓存的效果。Cache类的实现代码如下:
public class Cache extends HttpServlet
{
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
String cache = request.getParameter("cache");
if (cache != null)
{
if (cache.equals("false"))
{
// 关闭浏览器缓存
response.setDateHeader("Expires", 0);
response.setHeader("Cache-Control", "no-cache");
response.setHeader("Pragma", "no-cache");
}
}
// 定义HTML代码
String html = "<form id = 'form', action='test' method='post'>"
+ "姓名:<input type='text' name = 'name'/>"
+ "<input type='submit' value='提交' />" + "</form>";
PrintWriter out = response.getWriter();
out.println(html);// 向客户端输出HTML代码
}
}
从上面的代码可以看出,当cache请求参数值为false时关闭浏览器的缓存。
3. 测试未关闭浏览器缓存的情况
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/Cache?cache=true
在"姓名"文本框中输入任意字符串,点击"提交"按钮,这时浏览器会显示一个异常(这个异常是由于所提交的test不存在而产生的,我们不用去管它),然后点击浏览器的返回按钮回到刚才输入数据的页面。我们可以看到,刚才输入的字符串仍然存在。这说明在返回时,浏览器并未从服务端重新获得这个页面,而是从本地的缓存里重新加载了当前的页面。
4. 测试关闭浏览器缓存的情况
在浏览器地址栏中输入如下的URL来关闭浏览器缓存:
http://localhost:8080/demo/Cache?cache=false
按着上一步的方式提交并返回,发现刚才输入的数据没有了。这说明在关闭浏览器缓存后,每次返回时,浏览器总会从服务端重新获得当前页面。因此,当前页面总是保持着初始值。
5. 程序总结
在关闭浏览器缓存时,为了尽可能保证在大多数浏览器中都有效,笔者建议同时使用上述三个HTTP响应消息头字段来关闭浏览器缓存。
5.1.5 网页定时刷新和定时跳转
有时一个网页需要按着一定的时间间隔刷新,或是在一定时间后跳到其他的网页上。这种功能在定时从服务端获得数据;或是在短时间显示一个公告页,在一段时间后,跳到主页的情况下特别有用。
虽然实现定时刷新和定时跳转有很多方法,但使用HTTP响应消息头中的Refresh字段无疑是最简单的方法,通过设置这个字段的值,可以使当前网页每隔一定的时间刷新一次,还可以使当前网页在一定时间后跳转到其他的网页。如果只想定时刷新,可以使用下面的代码来实现:
response.setHeader("Refresh", "3");// 每隔3秒页面刷新一次
下面的代码实现了3秒后跳转到其他网页的功能:
response.setHeader("Refresh", "3;URL=http://www.csdn.net");
时间和URL之间要用分号(;)隔开。其中URL指定了在一定时间间隔要跳转到的其他网页地址。
【实例5-3】 网页定时刷新和定时跳转
1. 实例说明
在本例中使用了url请求参数来指定要跳转到的网页地址,如果不指定url请求参数,则每隔3秒刷新一次网页,并显示当前的服务器时间。读者会看到网页上显示的服务器时间每隔3秒变化一次。
2. 编写Refresh类
Refresh类演示了如何使用Refresh字段实现网页定时刷新和定时跳转的功能。Refresh类的实现代码如下:
public class Refresh extends HttpServlet
{
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
String url = request.getParameter("url");
if(url == null)
{
response.setHeader("Refresh", "3");// 每隔3秒刷新一次网页
}
else
{
// 在3秒钟后定时跳转
response.setHeader("Refresh", "0;URL=" + url);
}
PrintWriter out = response.getWriter();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 输出当前服务器时间
out.println(dateFormat.format(new java.util.Date()));
}
}
3. 测试定时刷新
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/Refresh
然后读者就会看到,在浏览器中显示的时间每隔3秒就变化一次。
4. 测试定时跳转
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/Refresh?url=http://nokiaguy.blogjava.net
然后读者就会看到,3秒后,当前网页就会跳到www.csdn.net上。其中url参数可以是相对路径(如ChineseHeader),也可以是绝对路径(如http://www.csdn.net)。
5. 程序总结
如果将Refresh字段的时间间隔设为0,那么在当前网页装载完后会立即跳转到url所指的网页。除了使用Refresh来跳转网页外,还可以使用HttpServletResponse接口的sendRedirect来重定向网页,代码如下:
response.sendRedirect("http://www.csdn.net");
这两种跳转网页的方式可以达到同样的效果,但它们不同的是使用Refresh来跳转网页时,会先将当前网页装载完,才执行跳转动作,而使用sendRedirect方法来重定向网页,会直接转到目标网页上,而在sendRedirect方法之后的内容根本就不会输出到客户端。
5.1.6 实现动态文件下载
在Web服务器上实现文件下载功能很容易。只要将URL指向要下载的文件即可。但是这要有一个前提,就是要下载的文件必须位于在Web服务器中部署的Web目录中。但有时需要在下载文件之前做一些其他的事,如验证用户是否有权限下载该文件。在这种情况下,就必须通过动态下载的方式(也就是通过程序来读取待下载的文件,而不是直接由Web服务器负责下载)来实现。
下面的例子演示了如何通过Servlet实现动态下载文件的功能。
【实例5-4】 实现动态下载文件
1. 实例说明
在本例中将待下载的文件放到了非Web目录中(在web.xml中设置),使客户端无法直接访问待下载的文件。然后通过一个Servlet进行中转,如果待下载的文件存在,通过FileInputStream对象打开这个文件,并通过ServletOutputStream对象将待下载的文件按字节流的方式输出到客户端,如果待下载的文件扩展名是".jpg",则直接在浏览器中显示该图象。该程序还有一个功能,就是列出在web.xml文件中指定的目录中的所有文件(带链接)。只需要直接点击相应的文件就可下载或显示该文件的内容。
2. 编写Download类
该类负责列目录和下载文件,实现代码如下:
package chapter5;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import java.net.*;
public class Download extends HttpServlet
{
private void download(File file, HttpServletResponse response)
throws IOException
{
if (file.exists())
{
// 当扩展名是。jpg时,在浏览器中显示图象,而不是下载这个图象文件
if ((file.getName()。length() -
file.getName()。lastIndexOf(".jpg")) == 4)
{
response.setContentType("image/jpeg");
response.addHeader("Content-Disposition",
"filename=" + URLEncoder.encode(file.getName(), "UTF-8"));
}
// 设置要下载的文件的名称、Content-Type和长度
else
{
response.setContentType("application/octet-stream");
response.addHeader("Content-Disposition",
"attachment;filename=" +
URLEncoder.encode(file.getName(), "UTF-8"));
}
response.addHeader("Content-Length",
String.valueOf(file.length()));
InputStream is = new FileInputStream(file);
byte[] buffer = new byte[8192]; // 每次向客户端发送8K字节
int count = 0;
ServletOutputStream sos = response.getOutputStream();
// 向客户端输出下载文件的内容,每次输出8K字节
while ((count = is.read(buffer)) > 0)
sos.write(buffer, 0, count);
is.close();
sos.close();
}
}
// 输出path初始化参数指定的目录中的文件
private void listDir(File dir, HttpServletResponse response)
throws IOException
{
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 扫描目录的子目录和文件
for (File file : dir.listFiles())
{
// 如果是文件,输出文件名和其对应的URL
if (file.isFile())
{
out.print("<a href='Download?filename=" +
URLEncoder.encode(file.getName(), "UTF-8") + "'>");
out.println(file.getName() + "</a><br/>");
}
}
}
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// 读取path初始化参数的值
String path = this.getServletConfig()。getInitParameter("path");
String filename = request.getParameter("filename");
File dir = new File(path);
if (dir.exists())
{
if (filename != null)
{
filename = dir.getPath() + File.separator + filename;
File downloadFile = new File(filename);
download(downloadFile, response);// 下载文件
}
else
{
listDir(dir, response); // 列出文件目录
}
}
}
}
3. 配置Download类和path参数
path是Download类的初始化参数,需要在<servlet>元素中配置,代码如下:
<servlet>
<servlet-name>Download</servlet-name>
<servlet-class>chapter5.Download</servlet-class>
<!-- 配置path初始化参数 -->
<init-param>
<param-name>path</param-name>
<param-value>D:\download\</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>Download</servlet-name>
<url-pattern>/Download</url-pattern>
</servlet-mapping>
其中path表示要下载文件所在的目录,读者也可以指定其他存在的目录。
4. 测试程序
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/servlet/Download
在浏览器中会列出D:\download目录中的所有文件,如图5.3所示。
图5.3 列出待下载文件所在的目录
当单击"我的文章。doc"时,就会弹出如图5.4所示的下载对话框。
图5.4 下载对话框
5. 程序总结
在编写上面的代码时,应注意如下几点:
(1)在下载文件时必须设置Content-Type和Content-Disposition字段。其中Content-Type字段的值是application/octet-stream,表示下载的是二进制字节流。而Content-Disposition字段的值有两部分组成,其中attachment表示下载的是附件,也就是说,浏览器会弹出一个下载对话框。而后面的filename部分设置了下载对话框中显示的默认文件名。如果不设置Content-Disposition字段,要下载的文件将直接在浏览器中打开。
(2)如果要在浏览器中显示某些类型的文件,需要将Content-Type字段值设成相应的MIME类型,如本例中要显示jpg格式的图象,则该字段的值为image/jpeg.但要注意, Content-Disposition字段中不能有attachment,否则浏览器会下载这个jpg文件,而不会显示它。
(3)如果下载的文件名中包含中文,在设置Content-Disposition中的filename时,应使用java.net.URLEncoder.encode方法将文件名按UTF-8格式编码,否则,在下载对话框中无法正确显示中文名。
5.2 Http Servlet Request的应用
HttpServletRequest接口是Servlet API提供的另一个重要接口。Servlet引擎为每一个用户请求创建一个HttpServletRequest对象。该对象将作为service方法的第二个参数值传给service方法。HttpServletRequest接口是ServletRequest的子接口。在ServletRequest和HttpServletRequest接口中定义子大量的方法,通过这些方法,开发人员不仅可以获得HTTP响应消息头、HTTP响应消息正文、Cookie与Session对象等内容,还可以利用请求域进行数据的传递。
5.2.1 获得HTTP请求行信息
HTTP请求消息的请求行分为三部分:请求方法(GET、POST、HEAD等)、资源路径和HTTP协议版本,如下所示:
GET /demo/servlet/TestServlet?name=mike&salary=3021 HTTP/1.1
通过下面的URL可以产生如上所示的请求行消息:
http://localhost:8080/demo/servlet/TestServlet?name=mike&salary=3021
HttpServletRequest接口中定义了若干的方法来获取请求行中的各个部分的信息,如下所示:
1. getMethod方法
该方法返回HTTP请求消息的请求方法(如GET、POST、HEAD等),也是请求行的第一部分。
2. getRequestURI方法
该方法返回请求行中的资源名部分,也就是位于URL的端口号和请求参数之间的部分,例如,对于如下的URL:
http://localhost:8080/demo/servlet/TestServlet?name=mike&salary=3021
getRequestURI方法返回上面URL中的"/demo/servlet/TestServlet"部分。
3. getQueryString方法
该方法返回请求行中的参数部分,也就是资源路径中问号(?)后面的内容。而且返回的结果不会被解码,也就是说,将保持原样返回。例如:对于如下的URL:
http://localhost:8080/demo/servlet/TestServlet?name=mike&salary=3021
getQueryString方法返回上面URL中的"name=mike&salary=3021".如果在资源路径中没有请求参数部分,getQueryString方法返回null.
4. getProtocol方法
该方法返回请求行中的协议名和HTTP版本,即请求行的第3部分,一般是HTTP/1.0或HTTP/1.1.如果在Web应用程序中需要单独对不同的HTTP版本进行处理,可以使用该方法来判断当前请求的HTTP版本。
5. getContextPath方法
该方法返回请求URL中的Web应用程序的路径,也就是说,返回URL中端口号和Web资源路径之间的部分。这个路径以斜杠(/)开头,表示当前Web站点的根目录,路径的结尾不含斜杠(/)。如果请求URL属于Web站点的根目录,则该方法应返回空字符串("")。例如,对于如下的URL:
http://localhost:8080/demo/servlet/TestServlet?name=mike&salary=3021
对于上面的URL,"/servlet/TestServlet"是在web.xml中定义的Servlet映射URL(也可以称为Web资源路径),而getContextPath方法则返回端口号(8080)和Web资源路径(/servlet/TestServlet)之间的部分,也就是URL中的"/demo".
6. getPathInfo方法
该方法返回额外的路径部分。额外路径位于Web资源路径和参数之间,以"/"开头。如TestServlet在web.xml中的映射URL是"/TestServlet/*",那么就可以用"/TestServlet/a"、"/TestServlet/b"访问TestServlet,其中"/a"、"/b"就是getPathInfo方法返回的额外路径。如果URL中没有额外路径,getPathInfo方法返回null.
7. getPathTranslated方法
该方法返回URL中额外信息所对应的服务端的本地路径。如"/request/abc.jsp"中的"/abc.jsp"是额外路径信息,则getPathTranslated方法返回"/abc.jsp"所对应的服务端的本地路径。
8. getServletPath方法
该方法返回Servlet在web.xml中定义的<url-pattern>元素的值,也就是Servlet的访问路径。
9. getParameterNames方法
该方法返回一个Enumeration对象,在这个对象中封装了URL的所有的请求参数名。
10. getParameter方法
该方法返回某一个请求参数的值,如获得name请求参数值的代码如下:
String name = getParameter("name");
5.2.2 获得网络连接信息
由于客户端浏览器和服务端进行交互是建立在TCP连接基础上的,因此,有时在服务端就需要知道客户端的一些网络连接信息,因此,ServletRequest接口定义了若干可以获得网络连接信息的getter方法。通过这些方法,可以获得客户端和服务端的IP、端口以及访问协议等信息。
假设客户端的IP是192.168.18.10,服务器的IP是192.168.18.254,服务器主机名是webserver.并通过如下的URL来访问Servlet.
http://localhost:8080/demo/servlet/TestServlet?name=mike&age=52
使用上面的URL访问Sevlet将产生如下的HTTP请求消息:
GET /demo/servlet/TestServlet?name=mike&age=52 HTTP/1.1
Accept: */*
Accept-Language: zh-cn
Accept-Encoding: gzip, deflate
User-Agent: Mozilla/4.0
Host: localhost:8080
Connection: Keep-Alive
下面是ServletRequest接口中定义的用于获得网络连接信息的方法:
1. getRemoteAddr方法
该方法返回客户机用于发送请求的IP地址(192.168.18.10)。
2. getRemoteHost方法
该方法返回发出请求的客户机的主机名。如果Servlet引擎不能解析出客户机的主机名,则返回客户端的IP地址(192.168.18.10)。
3. getRemotePort方法
该方法返回客户机所使用的网络接口的端口号,这个值是由客户机的网络接口随机分配的,如1078,也有可能是其他的值。
4. getLocalAddr方法
该方法返回Web服务器上接收请求的网络接口使用的IP地址(192.168.18.254)。
5. getLocalName方法
该方法返回Web服务器上接收请求的网络接口使用的IP地址所对应的主机名(webserver)。
6. getLocalPort方法
该方法返回Web服务器上接收请求的网络接口的端口号(8080)。
7. getServerName方法
该方法返回HTTP请求消息的Host字段值的主机名部分(localhost)。
8. getServerPort方法
该方法返回HTTP请求消息的Host字段值的端口号部分(8080)。
9. getScheme方法
该方法返回请求的协议名,如http、https等,在本例中是http.
10. getRequestURL方法
该方法返回完整的请求URL(不包括参数部分)。这个方法返回的是StringBuffer类型,而不是String类型。在本例中返回"http://localhost:8080/demo/servlet/TestServlet".要注意该方法和getRequestURI方法的区别。关于getRequestURI方法的介绍详见5.2.1节的内容。
5.2.3 获得HTTP请求消息头
在HttpServletRequest接口中定义了若干读取HTTP请求消息中的头字段值的方法,其中getHeader方法是最常用的方法。通过该方法可以获得指定字段头的值。除了getHeader方法外,在HttpServletRequest接口中还定义了很多其他获得请求头消息的方法,如getIntHeader、getDateHeader、getContentLength等。通过这些方法获得的请求头消息,可以实现更加强大的功能,如可以根据浏览器的语言设置输出相应国家语言的网页内容,或者可以使用Referer字段防止盗链。这些获得HTTP请求头消息的方法如下:
1. getHeader方法
该方法返回指定的HTTP请求消息头字段的值。如获得Host字段值的代码如下:
String host = getHeader("Host");
2. getHeaders方法
该方法返回一个Enumeration对象,该对象封装了某个指定名称的头字段的所有同名字段的值。
3. getHeaderNames方法
该方法返回一个Enumeration对象,该对象封装了所有的HTTP请求消息头字段的名称。
4. getIntHeader方法
该方法返回一个指定的整型头字段的值。
5. getDateHeader方法
该方法返回一个指定的日期头字段的值。
6. getContentType方法
该方法返回请求消息中请求正文的MIME类型,也就是Content-Type头字段的值。
7. getContentLength方法
该方法返回请求消息中请求正文的长度(以字节为单位),也就是Content-Length字段的值,如果未指定长度,返回-1.
8. getCharacterEncoding方法
该方法返回请求消息正文的字符集编码,通常从Content-Type头字段中提取。
5.2.4 客户端身份验证
有时需要对某些网络资源(如Servlet、JSP等)进行访问权限验证,也就是说,有访问权限的用户才能访问该网络资源。进行访问权限验证的方法很多,但通过HTTP响应消息头的WWW-Authenticate字段进行访问权限的验证应该是众多权限验证方法中比较简单的一个。
通过HTTP响应消息头的WWW-Authenticate字段可以使浏览器出现一个验证对话框,访问者需要在这个对话框中输入用户名和密码,然后经过服务端验证,才可以正常访问网络资源的内容。但要注意,在发送WWW-Authenticate字段的同时,还要使用HttpServletResponse接口的setStatus方法将响应码设为401(HttpServletResponse.SC_UNAUTHORIZED),否则不会出现验证对话框。
通过WWW-Authenticate字段进行验证有两种方式:BASIC和DIGEST.其中BASIC方式比较简单,密码通过Base64编码格式进行传输,实际上就是通过明文进行传输。而DIGEST可以对传输的用户名、密码等敏感信息进行加密,也更加安全,但是实现起来也更复杂。在本节中将使用BASIC方式进行验证,关于DIGEST的详细信息,感兴趣的读者可以参考RFC2617(http://www.ietf.org/rfc/rfc2617.txt)。
进行验证的基本过程是首先判断HTTP请求消息头是否有Authorization字段,如果有这个字段,说明用户曾经登录过,可能登录成功,也可能登录失败,只要是输入了用户名和密码,并单击"确定"按钮后,再次在同一个浏览器窗口访问该Servlet,浏览器就会在HTTP请求消息头中加入Authorization字段,格式如下:
Authorization:Basic YWRtaW46MTIzNDEx
其中Basic是验证的类型,后面是被Basic64格式的用户名和密码信息。
如果HTTP请求消息头中没有Authorization字段,或者不是Basic验证,则在Servlet中要设置WWW-Authenticate响应消息头字段,格式如下:
WWW-Authenticate:BASIC realm="/demo"
其中realm表示当前资源所属的域,可以是任意字符串,一般情况下,同一个Web应用程序要将这个属性值设成同一个值,如可以设成上下文路径(通过getContentPath方法获得)。
在下面将给出一个实际的例子来演示如何使用BASIC方式进行身份验证。
【实例5-5】 客户端身份验证
1. 实例说明
在每一次访问本例中的程序(Servlet)时,将会弹出一个权限验证对话框,要求输入"用户名"和"密码".用户名和密码输入正确后,就会进入相应的页面。当再次访问当前页面时,就不会弹出权限验证对话框了,而是直接进入当前访问的页面。如果在浏览器的新窗口再次访问该Servlet时,仍然会弹出权限验证对话框,并重复上述的权限验证过程。
2. 编写AuthenticateServlet类
AuthenticateServlet类负责效验用户输入的用户名和密码,并且当第一次访问Servlet时设置WWW-Authenticate响应消息头字段,以通知浏览器显示权限验证对话框。AuthenticateServlet类的实现代码如下:
package chapter5;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class AuthenticateServlet extends HttpServlet
{
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
// 得到Authorization字段的值
String base64Auth = request.getHeader("Authorization");
if (base64Auth == null
|| !base64Auth.toUpperCase()。startsWith("BASIC"))
{
// 通知浏览器弹出验证对话框
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setHeader("WWW-Authenticate", "BASIC realm=\""
+ request.getContextPath() + "\"");
out.println("需要进行身份验证!");
return;
}
sun.misc.BASE64Decoder base64Decoder = new sun.misc.BASE64Decoder();
String auth = new String(base64Decoder.decodeBuffer(base64Auth
.substring(6)));
String[] array = auth.split(":");// 将用户名和密码分开
if(array.length == 2)
{
String user = array[0].trim();
String password = array[1].trim();
RequestDispatcher rd =
request.getRequestDispatcher("HeaderInfo");
rd.include(request, response);// 输出所有的HTTP请求头
// 进行身份验证
if(user.equals("admin") && password.equals("1234"))
out.println("身份验证成功,该Servlet已经进入!");
else
out.println("身份验证失败!");
}
}
}
从上面的代码可以看出,AuthenticateServlet类首先判断了请求消息头字段Authorization是否存在,如果该字段不存在,则设置了响应消息头字段WWW-Authenticate和状态码401,以通知浏览器显示权限验证对话框。如果Authorization字段存在,并且为Basic验证,则从Authorization字段值中取出用户名和密码进行验证,并输出验证结果信息。
3. 测试
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/AuthenticateServlet
浏览器会弹出一个权限验证对话框,并在"用户名"和"密码"文本框中分别输入admin和1234,如图5.5所示。
图5.5 权限验证对话框
单击"确定"按钮,浏览器将显示如图5.6所示的信息。
图5.6 权限验证成功后显示当前访问页面的内容
4. 程序总结
要注意的是,在单击图5.5所示的"确定"按钮后,浏览器会再次访问AuthenticateServlet,这时HTTP请求消息头已经包含了authorization字段,如图5.9的黑框中所示。如果单击"取消"按钮,浏览器不会再次访问AuthenticateServlet,同时,AuthenticateServlet会在浏览中输出"需要进行身份验证!"信息。
在等一次登录成功后,在同一个浏览器窗口再次访问AuthenticateServlet,在HTTP请求消息头中就会包含authorization字段,因此,也就不再需要进行身份验证了。
5.3.1 什么是Cookie
Cookie是一种在客户端保存HTTP状态信息的技术。可以将Cookie想象成商场的会员卡。如果顾客在商场里办了一张会员卡,就意味着该顾客以后的各种消费状态都会通过会员卡保存起来。如该顾客的累计消费金额以及有效期限。当该客户再次到商场消费时,如果顾客出示这张会员卡,商场就会根据这张会员卡进行一些处理,如计算折扣率、累计消费额等。
Cookie是在浏览器访问Web服务器上的某个资源时,由Web服务器在HTTP消息响应头中附带的一组传送给浏览器的数据。浏览器可以根据这些数据决定是否将它们保存在客户端。实际上,Web服务器通过HTTP响应消息头的Set-Cookie字段将多个Cookie发送到浏览器,每一个Cookie使用一个Set-Cookie字段来设置。当浏览器发现HTTP响应消息头中有Set-Cookie字段时,就会根据Set-Cookie字段的值来决定如何处理这些Cookie,如将Cookie保存在硬盘上(永久Cookie);或是只在当前浏览器窗口中有效,如果当前窗口关闭,Cookie就会被删除(临时Cookie)。
在浏览器中访问Web服务器的资源时,浏览器会检测本地和当前浏览器窗口中是否有满足当前访问路径的Cookie存在,如果有,则在HTTP请求消息头中通过Cookie字段将Cookie信息发送给Web服务器。也就是说,Cookie信息是通过HTTP响应消息头的Set-Cookie字段和HTTP请求消息头的Cookie字段在浏览器和Web服务器之间进行传递,可以利用Cookie的这一特性来跟踪服务端的对象,如Session对象就是利用Cookie来跟踪的(将在后面详细讲解)。
5.3.2 Cookie类
在Servlet API中,使用java.servlet.http.Cookie类来封装一个Cookie信息,在HttpServletResponse接口中定义了addCookie和getCookies方法可以用来处理Cookie信息。其中addCookie方法用来向浏览器传送Cookie信息,也就是添加Set-Cookie字段。getCookies方法返回一个Cookie数组,这个数组中保存了浏览器发送给Web服务器的所有Cookie信息。
在Cookie类中定义了生成和提取Cookie信息的方法,这些方法如下:
1. 构造方法
Cookie类只有一个构造方法,它的定义如下:
public Cookie(String name, String value)
其中name表示Cookie的名称(在name参数中不能包含任何空格字符、逗号(,)、分号(;),并且不能以"$"字符开头),value表示Cookie的值。
2. getName方法
该方法用于返回Cookie的名称。
3. setValue和getValue方法
这两个方法分别用于设置和返回Cookie的值。
4. setMaxAge和getMaxAge方法
这两个方法分别用于设置和返回Cookie在客户机的有效时间(以秒为单位)。如果有效时间为0,则表示当Cookie信息发送到客户端浏览器时立即被删除。如果设置为负数,则表示浏览器并不会把这个Cookie保存在硬盘上,这种Cookie被称为临时Cookie(保存在硬盘上的Cookie也被称为永久Cookie)。它们只存在于当前浏览器的进程中,当浏览器关闭后,Cookie自动失效。对于IE浏览器来说,不同的浏览器窗口不能共享临时Cookie,但按Ctrl+N键或使用JavaScript的windows.open语句打开的窗口由于和它们父窗口属于同一个浏览器进程,因此,它们可以共享临时Cookie.而在FireFox中,所有的进程和标签页都可以共享临时Cookie.
5. setPath和getPath方法
这两个方法分别用于设置和返回当前Cookie的有效Web路径。如果在创建某个Cookie时未设置它的path属性,那么该Cookie只对当前访问的Servlet所在的Web路径及其子路径有效。如果要想使Cookie对整个Web站点中的所有可访问的路径都有效,需要将path属性设置为"/"。
6. setDomain和getDomain方法
这两个方法分别用于设置和返回当前Cookie的有效域。
7. setComment和getComment方法
这两个方法分别用于设置和返回当前Cookie的注释部分。
8. setVersion与getVersion方法
这两个方法分别用于设置和返回当前Cookie的协议版本。
9. setSecure和getSecure方法
这两个方法分别用于设置和返回当前Cookie是否只能使用安全的协议传送。
5.3.3 读写Cookie信息与Cookie的中文问题
在一些Web应用程序中需要在客户端保存一些信息,如用户名等,以使在下次访问同一个Web程序时可以根据这些信息为用户提供方便,或作为其他的用途。这些信息是由一个key-value对组成。每一对这样的信息被称为一个Cookie.如有一些登录程序在下一次访问时,会自动将用户名显示在用户名文本框中,这就是通过Cookie实现的。
下面的例子演示了如何在Servlet中读、写Cookie信息。
【实例5-6】 读写Cookie信息(包括英文和中文信息)
1. 实例说明
本程序通过WriteCookie和ReadCookie类来分别设置和读取Cookie信息。其中WriteCookie类设置了不同类型的Cookie,包括临时Cookie和永久Cookie.ReadCookie类在不同的情况下读取了这些Cookie信息,读者可以从中了解到不同类型Cookie的区别。
2. 编写WriteCookie类
package chapter5;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class WriteCookie extends HttpServlet
{
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
// 临时Cookie
Cookie tempCookie = new Cookie("temp", "temporary cookie");
tempCookie.setMaxAge(-1); // 设为临时Cookie,-1是MaxAge的默认值
response.addCookie(tempCookie);
// 这个cookie不起作用
Cookie cookie = new Cookie("cookie", "deleted");
// Cookie的有效时间设为0,浏览器接收以Cookie后立即被删除
cookie.setMaxAge(0);
response.addCookie(cookie);
// 永久Cookie
// 由于永久Cookie的值是中文,所以需要对其进行编码
String chinese = java.net.URLEncoder.encode("将Cookie保存到硬盘上", "UTF-8");
Cookie persistentCookie = new Cookie("pCookie", chinese);
persistentCookie.setMaxAge(60 * 60 * 24); // 有效时间为1天
// 设置有效路径,设为"/"表示这个Cookie在整个站点都是有效的
persistentCookie.setPath("/");
response.addCookie(persistentCookie);
}
}
上面的代码分别建立了3种Cookie:临时Cookie、有效时间为0的Cookie和永久Cookie.其中临时Cookie只在当前的浏览器窗口有效。而将Cookie的有效时间设为0,则该Cookie不会起到任何作用,因为这种Cookie一传到浏览器就会被立即删除。浏览器会将永久Cookie保存在本地硬盘上,对于这种Cookie,即使是在新的浏览器窗口,只要在Cookie的有效期内,该Cookie就会永远有效。
从上面的描述可以看出,Cookie的不同类型是根据有效时间的取值范围确定的,如下所示:
lCookie有效时间 < 0:临时Cookie,只在当前浏览器窗口以及和该窗口处于同一个进程的窗口中有效。
lCookie有效时间 = 0:该Cookie会立即被浏览器删除。
lCookie有效时间 > 0:永久Cookie,在Cookie有效时间内有效。
3. 编写ReadCookie类
package chapter5;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class ReadCookie extends HttpServlet
{
// 通过一个Cookie名获得Cookie对象,未找到指定名的Cookie对象,返回null
private Cookie getCookieValue(Cookie[] cookies, String name)
{
if (cookies != null)
{
for (Cookie c : cookies)
{
if (c.getName()。equals(name))
return c;
}
}
return null;
}
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html; charset=UTF-8");
PrintWriter out = response.getWriter();
// 获得临时Cookie ,getCookies方法获得一个保存了请求消息头中所有Cookie的数组
Cookie tempCookie = getCookieValue(request.getCookies(), "temp");
if (tempCookie != null)
out.println("临时Cookie值:" + tempCookie.getValue() + "<br/>");
else
out.println("临时Cookie值:null</br>");
// 这个Cookie永远不可能获得,因为它的MaxAge为0
Cookie cookie = getCookieValue(request.getCookies(), "cookie");
if (cookie != null)
out.println("cookie:" + cookie.getValue() + "<br/>");
else
out.println("cookie:null</br>");
// 获得永久Cookie
Cookie persistentCookie = getCookieValue(request.getCookies(), "pCookie");
if (persistentCookie != null)
// 对永久Cookie中保存的中文信息进行解码
out.println("persistentCookie:" +
java.net.URLDecoder.decode(persistentCookie.getValue(), "UTF-8"));
else
out.println("persistentCookie:null!");
}
}
在上面的代码中分别读取了在WriteCookie类中设置的3个Cookie.由于HttpServletRequest接口中未提供通过Cookie名返回特定Cookie对象的方法,因此,在ReadCookie类中增加了一个getCookieValue方法,用于返回指定Cookie名的Cookie对象。
4. 在同一个浏览器窗口中测试
在浏览器地址栏中依次输入如下两个URL:
http://localhost:8080/demo/WriteCookie
http://localhost:8080/demo/ReadCookie
浏览器中显示的结果如图5.7所示。
图5.7 在同一个浏览器窗口读取Cookie值
实际上,在访问WriteCookie时,addCookie方法向HTTP响应消息头添加了如下的3个Set-Cookie字段:
Set-Cookie: temp="temporary cookie"
Set-Cookie: cookie=deleted; Expires=Thu, 01-Jan-1970 00:00:10 GMT
Set-Cookie:pCookie=%E5%B0%86Cookie%E4%BF%9D%E5%AD%98%E5%88%B0%E7%A1%AC%E7%9B%98%E4%B8%8A; Expires=Tue, 24-Jun-2008 14:43:23 GMT; Path=/
5. 在不同的浏览器窗口中测试
打开一个新的浏览器窗口,在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/ ReadCookie
浏览器中显示的结果如图5.8所示。
图5.8 在不同的浏览器窗口读取Cookie值
从上面的测试结果可以看出,在新的浏览器窗口中,并不存在这个临时Cookie,因此,临时Cookie的值为null.
6. 程序总结
在WriterCookie类中使用了setPath方法设置的Cookie有效路径。假设使用setPath设置的路径为"/path",如果通过localhost访问Web资源。要想该Cookie有效,访问Web资源的URL的前半部分必须是"http://localhost:8080/path".如下面的URL所指的Servlet可以访问这个Cookie:
http://localhost:8080/path/servlet/MyServlet
实际上,这个路径就相当于上下文路径,如果只想让某个Cookie在当前Web应用程序中有效,可以使用HttpServletRequest接口的getContextPath方法返回值来设置这个Cookie的有效路径。
在使用Cookie时,还有一点需要注意,由于Cookie只支持ISO-8859-1编码格式的字符,因此,不能直接向Cookie中写入中文,如果要让Cookie支持中文,需要将中文字符编码。如Base64编码、URL格式的UTF-8编码。在本例中采用的是URL格式的UTF-8编码。
5.4.1 什么是Session
虽然可以使用Cookie或URL请求参数在不同请求中传递信息,但如果传递的信息较多的话,将极大地降低网络传输效率和消耗网络带宽,同时也会增大在服务端程序的处理难度。就算这些都可以承受,使用Cookie或URL请求参数传递的信息量也是非常有限的。为此,在服务端的开发方案中提供了一种将用户会话中产生的大量信息保存在服务端的技术,这就是Session技术。
如果将Cookie比喻成商场里的会员卡,在会员卡里记录着顾客以前的消费累计金额和有效期限,那么就可以将Session比喻成银行卡,在银行卡中只保存了用户的帐号,而一些敏感信息以及很多的历史信息则保存在银行的服务器中,如密码、存款和取款记录、利率等。当储户在发生银行业务时,如取钱时,只需要提供银行卡(号),银行的系统就会通过银行卡中的帐号找到该用户的银行卡信息,在通过密码验证后,就可以进行正常的操作了,如提款、查询信息等。
从上面的描述可以知道,服务端Session保存了某个客户的一些信息(银行卡信息),但该客户必须提供一个Session标识(银行帐号)才可以锁定这个Session对象。在Web应用程序中,该Session标识实际上就是一个临时Cookie,对于Java Web程序来说,该临时Cookie的key是JSESSIONID,value是一个唯一标识Session的字符串,也被称为Session ID.正是由于Session ID在Web服务器和浏览器之间不断地传送,浏览器才能找到服务端以前为某个用户创建的Session对象。如果在新的浏览器窗口再次访问服务端程序,由于临时Cookie丢失,因此,在这个新的浏览器窗口也就无法再使用这个Session对象了。从表面上看好象是Session对象被删除,但实际上只是由于Session ID丢失从而导致Session对象的丢失(这就相当于银行卡丢失,从而导致和该银行卡相关的信息无法取到,但这些信息并未丢失,只要再次提供银行卡或正确的银行帐号,就可以找到这些信息)。
5.4.2 Http Session接口中的方法
一个Session就是一个HttpSession对象。实际上,HttpSession是一个接口,在Servlet引擎中实现了这个接口。在HttpSession接口中定义了若干的方法来操作HttpSession对象,这些方法如下:
1. getId方法
getId方法用于返回当前HttpSession对象的SessionID值。要注意的是,SessionID是由系统自动生成的,该值可以唯一标识Web服务器中的Session对象。因此,在HttpSession接口中并未定义setId方法来设置这个SessionID值。
2. getCreationTime方法
getCreationTime方法用于返回当前的HttpSession对象的创建时间,返回的时间是一个自1970年1月1日的0点0分0秒开始计算的毫秒数。
3. getLastAccessedTime方法
getLastAccessedTime方法用于返回当前HttpSession对象的上一次被访问的时间,返回的时间格式是一个自1970年1月1日的0点0分0秒开始计算的毫秒数。
4. setMaxInactiveInterval和getMaxInactiveInterval方法
这两个方法分别用来设置和返回当前HttpSession对象的可空闲的最长时间(单位:秒),这个时间也就是当前会话的有效时间。当某个HttpSession对象在超过这个最长时间后仍然没有被访问,该HttpSession对象就会失效,整个会话过程就会结束。如果有效时间被设置成负数,则表示会话永远不会过期。
5. isNew方法
isNew方法用来判断当前的HttpSession对象是否是新创建的,如果是新创建的,则返回true,否则返回false. 在以下两种情况下,isNew返回true.
(1)在请求消息中不包含SessionID,这时调用getSession方法返回的HttpSession对象一定是新创建的。
(2)在请求消息中包含SessionID,但这个SessionID在服务端没有找到与其匹配的HttpSession对象。发生这种情况的原因可能是HttpSession对象失效或客户端发送了错误的SessionID.
6. invalidate方法
invalidate方法用于强制当前的HttpSession对象失效,这样Web服务器可以立即释放该HttpSession对象。虽然会话在有效时间后会自动释放,但为了减少服务器的HttpSession对象的数量,节省服务端的资源开销,建议在不需要某个HttpSession对象时显式地调用invalidate方法,以尽快释放HttpSession对象。
7. getServletContext方法
getServletContext方法用于返回当前HttpSession对象所属的Web应用程序的ServletContext对象。这个方法和GenericServlet接口的getServletContext方法返回的是同一个ServletContext对象。
8. setAttribute方法
setAttribute方法用于将key-value对保存在Session域中,该方法和HttpRequestServlet接口中的setAttribute方法类似。如果value为null,则从Session域中删除该key-value对。
9. getAttribute方法
getAttribute方法用于返回Session域中指定key的value值。如果Session域中不存在指定的key,则返回null.
10. remoteAttribute方法
remoteAttribute方法用于根据key删除Session域中某一个key-value对。该方法和使用setAttribute方法时value为null的效果相同。在如下两种情况,系统会自动删除Session域中的对象:
(1)当调用invalidate方法使当前HttpSession对象失效后,系统将自动删除保存在当前Session域中的所有对象。
(2)使用setAttribute方法添加一个key-value对,如果key已经存在,并且value和已经存在的key所对应的value不同时,系统会先删除原来的key-value对,再添加新的key-value对。
11. getAttributeNames方法
getAttributeNames方法用于返回一个Enumeration对象,该对象包含了当前Session域中的所有key值。可以通过扫描该Enumeration对象来获得当前Session域中的所以key值,并通过getAttribute方法来获得它们的value值。
5.4.3 Http Request Session接口中的Session方法
在Http Request Session接口中也定义了若干和Session有关的方法,这些方法如下:
1. get Session方法
get Session方法用于根据当前的请求返回Http Session对象,该方法有两种重载形式,它们的定义如下:
public Http Session get Session();
public Http Session get Session(boolean create);
调用第一种重载形式时,如果在请求消息中包含SessionID,就根据这个SessionID返回一个HttpSession对象,如果在请求信息中不包含SessionID,就创建一个新的HttpSession对象,并返回它。在调用第二种重载方法时,如果create参数为true时,则等同于第一种重载形式。如果create为false时,当请求信息中不包含SessionID时,并不创建一个新的HttpSession对象,而是直接返回null.
2. is Requested SessionId Valid方法
当请求消息中包含的Session ID所对应的Http Session对象已经超过了有效时间,也就是说Http Session对象无效,is Requested SessionIdValid方法返回false.否则返回true(当请求消息中不包含SessionID时,is Requested SessionId Valid返回false)。
3. is Requested SessionId From Cookie方法
isRequested SessionIdFrom Cookie方法用于判断Session ID是否通过HTTP请求消息中的Cookie头字段传递过来的,如果该方法返回true,则表示SessionID是通过Coolie头字段发送到服务端的。
4. isRequested SessionId FromURL方法
is Requested SessionId From URL方法用于判断SessionID是否通过HTTP请求消息的URL请求参数传递过来的。在使用这个方法时要注意,还有一个isRequested SessionId FromUrl方法和这个方法的功能完全一样,只是最后的URL变成了Url.这个方法已经被加了@deprecated标记,也就是并不建议使用。因此,建议使用isRequested SessionId From URL方法来实现这个功能。
5.4.5 通过重写URL跟踪Session
如果用户把浏览器的Cookie功能关闭,或者浏览器不支持Cookie功能,那么SessionId就不能通过Cookie向服务端发送了。Servlet规范为了弥补这个不足,允许通过URL请求参数来发送SessionId.这样当浏览器的Cookie功能关闭时,在浏览器中仍然可以通过由URL请求参数发送的SessionId在服务端找到相应的HttpSession对象。
在下面的例子演示了如何通过URL的请求参数来发送SessionId,读者可以从该例子中学到如何通过重写URL的方式来跟踪Session对象。
【实例5-7】 通过重写URL跟踪Session
1. 实例说明
本例使用HttpResponseServlet接口的encodeURL方法重写了URL,发在浏览器未关闭Cookie和关闭Cookie的情况下测试本例中的Servlet.当关闭浏览器的Cookie功能时,encodeURL方法会在URL后面加一个jsessionid参数,该参数的值就是SessionId.
2. 编写RewriteURL类
public class RewriteURL extends HttpServlet
{
public void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
HttpSession session = request.getSession();
response.setContentType("text/html; charset=UTF-8");
RequestDispatcher rd = request.getRequestDispatcher("HeaderInfo");
// 包含HeaderInfo的内容
rd.include(request, response);
PrintWriter out = response.getWriter();
// 重写URL
out.println("<br/><br/><a href='" +
response.encodeURL("RewriteURL?param=value") + "'>RewriteURL</a>");
}
}
在上面的代码中包含了HeaderInfo,以显示请求消息头的内容。重写URL可以使用HttpResponseServlet接口中的encodeURL方法,在本例中,使用encodeURL方法将访问RewriteURL的URL进行了重写。
3. 测试未关闭Cookie功能的情况
假设本机的IP是192.168.18.212,在浏览器地址栏中输入如下的URL:
http://192.168.18.212:8080/demo/RewriteURL
在第一次访问上面的URL时,encodeURL方法在重写URL时会加入jsessionid参数,这是因为当第一次访问这个URL时,HTTP请求消息头的Cookie字段中并没有叫JSESSIONID的Cookie,所以encodeURL会在URL中加入jsessionid参数,如图5.9所示。
图5.9 第一次访问RewriteURL,在URL后添加了jsessionid参数
当在同一个浏览器窗口再次访问上面的URL时, jsessionid参数就不会在URL中出现了,这是由于浏览器已经在HTTP请求消息头的Cookie字段中加入了叫JSESSIONID的Cookie,就不需要在URL中传递SessionId了,如图5.10所示。
图5.10 再次访问RewriteURL时,重写URL时不再添加jsessionid参数
从这一点可以看出,encodeURL方法将根据HTTP请求消息头中是否有叫JSESSIONID的Cookie来决定是否在被重写的URL中加入jsessionid参数。
4. 测试关闭Cookie功能的情况
在IE中单击"工具"|"Internet选项"命令,打开"Internet选项"对话框,选中"隐私"页面 ,单击"高级"按钮,打开"高级隐私策略对话框"对话框,并按着如图5.11所示来设置。
图5.11 关闭Cookie功能
在浏览器地址栏中输入如下的URL:
http://192.168.18.212:8080/demo/RewriteURL
无论访问多少次上面的URL,在URL上都会加上一个jsessionid参数,也就是说,效果都会和5.9一样。
5. 程序总结
如果要使用URL参数来传递SessionId,不要选中图5.20所示的"总是允许会话cookie"复选框。如果选中,虽然Cookie被禁止,但却可以保存临时Cookie,由于JSESSIONID也是临时Cookie,所以在这种情况下,JSESSIONID仍然可以在客户端和服务端之间传递。
还要提一点的是jsessionid并不是请求参数,这个参数和RewriteURL使用了分号分隔(;)分隔,它是请求URL的一部分,可以使用HttpServletRequest的getRequestURI方法获得。如果URL含有请求参数,格式类似下面的形式:
http://192.168.18.212:8080/demo/RewriteURL;jsessionid=AB130896860CD37902CDEDEB63A372B5?param=value
如果使用localhost访问RewriteURL,就算关闭了Cookie功能,仍然可以使用临时Cookie,因此,用localhost来访问RewriteURL时,无论Cookie功能是否被关闭,都可以使用Cookie来传递SessionId,如下面的URL所示:
http://localhost:8080/demo/RewriteURL
5.5 小结
本章介绍了Servlet的高级功能。在Servlet API中有两个非常重要的接口:HttpServletResponse和HttpServletRequest,这两个接口分别作为service方法的两个参数的类型。在这两个方法中定义了操作HTTP响应消息和HTTP请求消息的各种方法。通过这些方法,可以实现更复杂的Web应用程序,如动态下载文件、客户端身份验证等。
除了上述的两个接口,Cookie和Session也是Web应用程序中经常使用的两种技术。其中Cookie是保存在客户端的信息。浏览器在每次访问Web资源时,都会检测在本机是否有对该Web资源有效的Cookie,如果存在这种Cookie,就会通过HTTP响应消息头的Cookie字段将这些Cookie信息发送到服务端。在服务端,也可以通过HTTP响应消息头的Set-Cookie字段向浏览器发送Cookie信息,并通知浏览器如何处理这些Cookie信息。
Session是服务端的对象(HttpSession对象)。为了将浏览器和服务端的HttpSession对象进行关联,客户端就需要一个SessionId值,这个SessionId值就是服务端唯一表示HttpSession对象的key值。客户端可以通过Cookie字段或URL的jsessionid参数进行传递。如果客户端发送的SessionId值在服务端存在和其对应的HttpSession对象,那么服务端就会取出这个HttpSession对象,并做进一步处理。如果不存在这个HttpSession对象,服务端要么创建一个新的HttpSession对象,要么什么都不做(getSession方法返回null),继续处理其他的服务端逻辑。
第6章 JSP基础
在很多动态网页中,绝大部分的内容是固定不变的,只有少量的动态内容由服务端生成。在这种情况下虽然可以使用Servlet来向客户端输出静态的和动态的内容,但这样就会使Servlet的控制逻辑和输出的网页代码混合在一起,非常不利于代码的编写和维护。而且大多数时候,需要使用网页设计工具对网页静态部分进行可视化的设计,而将网页的静态部分混在Servlet的代码中就无法使用网页设计工具进行页面设计了。
为了更好地实现网页的静态部分和动态部分的组成,Sun公司在1999年诞生了JSP(Java Server Pages)技术。JSP实际上就是一种动态的网页。在JSP页面中即可以有客户端代码,如HTML代码,JavaScript等,也可以有动态的代码,如Java代码、EL、JSTL等。系统在运行JSP程序时,会自动将JSP页面中的静态和动态的部分分离。而在设计JSP页面时就象在设计HTML页面一样,也可以使用可视化的网页设计器来完成页面的布局等工作。JSP的出现也大大简化了用Servlet生成页面的工作, 从而使Servlet只专注于生成不需要复杂界面的工作,或是向客户端提供不包含界面元素的数据。总之一句话,如果需要复杂界面的Web程序,就使用JSP,否则,应该使用Servlet来实现它。
6.1 认识JSP
在第3章已经给出了一个简单的JSP程序。但该程序是在IDE中编写的。虽然在IDE中开发JSP程序非常方便,但这并不利于充分地了解JSP的发布过程、运行原理和其他的一些细节。因此,在本节将脱离IDE,使用手工的方式编写一些简单的JSP程序,并揭示Tomcat如何来运行这些JSP程序,以及由JSP页面生成的Servlet类的结构。
6.1.1 初次接触JSP
在第3章已经介绍使用IDE开发JSP程序的过程。从其中的JSP页面可以看出,JSP页面是由静态和动态两部分组成。静态部分主要是HTML、CSS、JavaScript等客户端脚本。而动态部分主要是在服务端运行的程序,如使用<% … %>或<%=…%>包含的Java代码,以及使用${…}包含的EL表达式等。由于JSP在首次运行时被翻译成Servlet(将在6.1.3节详细介绍),因此,整个JSP页面翻译时都被转换成相应的Java代码,并插入到由JSP生成的Servlet中。
JSP页面中所有的静态部分使用out.write方法直接发送给客户端,而动态部分根据具体的内容进行相应的转换,如在<%…%>中的Java代码被直接插入到Servlet中,而<%=…%>中的Java代码使用out.println方法输出。
虽然JSP在运行时被翻译成Servlet,但在访问JSP时和访问静态的HTML页面类似,也就是说,可以直接在浏览器中访问。jsp页面,Web服务器会根据所访问的JSP页面去自动调用由该JSP页面生成的Servlet。
6.1.2 编写简单的JSP程序
手工编写一个JSP程序要比编写一个Servlet容易得多,只需要建立一个空的目录,然后在目录中建立JSP文件即可。
在<Tomcat安装目录>\webapps目录中建立一个myjsp目录,并在该目录中建立一个simple.jsp文件(文件要以UTF-8格式保存),simple.jsp的主要功能是使用Java代码显示服务器的当前时间,并输出name请求参数的值。simple.jsp的代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!-- 引用Java类 -->
<%@page import="java.text.SimpleDateFormat, java.util.Date"%><html>
<head>
<title>简单的JSP程序</title>
</head>
<body>
当前日期和时间:
<%
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 输出服务器的当前时间
out.println(dateFormat.format(new Date()));
%>
<p/>
<!-- 输出name请求参数值 -->
name请求参数值:<%= request.getParameter("name") %>
</body>
</html>
simple.jsp页面中的Java代码分别被写在了<%…%>和<%=…%>中,使用这两种格式编写的Java代码在JSP被翻译成Servlet时采用了不同的处理方法。
运行<Tomcat安装目录>\bin\ startup.bat命令,启动Tomcat,并在浏览器地址栏中输入如下的URL:
http://localhost:8080/myjsp/simple.jsp?name=bill
浏览器显示的信息如图6.1所示。
图6.1 显示服务器当前时间和name请求参数值
6.1.3 改变JSP的访问路径
虽然web.xml文件在JSP页面运行的过程中并不是必须的,但仍然可以在web.xml文件中配置JSP,以改变JSP页面的访问路径。
配置JSP的方法和配置Servlet的方法类似,只是将<servlet-class>元素替换成了<jsp-file>元素,以便指定JSP文件相对于Web应用程序的目录。如下面的配置将simple.jsp的访问路径配置成了"/jsp/simple.html":
<servlet>
<servlet-name>simple</servlet-name>
<jsp-file>/simple.jsp</jsp-file>
</servlet>
<servlet-mapping>
<servlet-name>simple</servlet-name>
<url-pattern>/jsp/simple.html</url-pattern>
</servlet-mapping>
在web.xml文件中输入上面的配置代码后,重启Tomcat,在浏览器地址栏中输入如下的URL:
http://localhost:8080/myjsp/jsp/simple.html
浏览器将会显示如图6.1所示的信息。由此可以,上面的URL和输入如下的URL的效果相同:
http://localhost:8080/myjsp/simple.jsp
在设置JSP访问路径时要注意,<jsp-file>元素的值必须以"/"开头,表示当前Web应用程序的根目录。
6.2.1 JSP表达式
实际上,JSP表达式也是Java代码,只是这些Java代码被放到了<%= … %>中。JSP编译器在翻译JSP表达式时,直接将<%= … %>中的内容作为Java变量或表达式使用println方法输出到客户端。也就是说,将<%= … %>中的内容翻译成println方法的参数值,而不是直接插入到由翻译JSP生成的Servlet类中。看下面的JSP表达式:
<%= (3+4) * 5 %>
JSP编译器会将上面的JSP表达式翻译成如下的Java代码:
out.println((3+4) * 5);
要注意的是,<%= … %>中的Java代码必须是println方法的参数的合法值,也就是说,如果将JSP表达式中的Java代码作为println方法的参数值,Java编译器会编译出错,那么该JSP表达式就是错误的。如不能在<%= … %>中的Java代码后加分号(;)。
6.2.2 在JSP中嵌入Java代码
除了通过JSP表达式在JSP页面中嵌入Java代码外,还可以通过<% … %>在JSP页面中直接嵌入Java代码。所有写在<% … %>之间的内容JSP编译器都会将其认为是Java代码,并直接将其插入由JSP页面生成的Servlet类的_jspService方法中,如下面的JSP代码:
<%
int n = (3 + 4) * 5;
%>
上面的代码在JSP页面中嵌入了一行非常简单的Java代码,这行Java代码会被直接放到_jspService方法(相当于Servlet中的service方法)的相应位置。因此,<% … %>之间的内容必须是合法的Java代码,例如,每条Java语句后面必须加分号(;),否则,访问该JSP页时会抛出异常。
在JSP页面中嵌入Java代码时有如下两点需要注意:
可以在JSP页面的任何位置使用<% … %>插入Java代码。<% … %>可以有任意多个。
每一个<% … %>中的代码可以不完整,但是该<% … %>中的内容和JSP页面中的一个或多个<% … %>中的内容组合起来必须完整。
下面的代码将一条Java语句分拆到多个<% … %>中,并在<% … %>之间包含静态的内容:
<!-- javacode.jsp -->
<%@ page language="java" pageEncoding="UTF-8"%>
<html>
<head>
<title>在JSP中嵌入多段不完整的Java代码,但整个JSP页面的Java代码必须完整</title>
</head>
<body>
<!-- 第一段Java代码 -->
<%
java.util.Random rand = new java.util.Random();
int gradeList[] = new int[5];
for(int i = 0; i < 5; i++)
gradeList[i] = rand.nextInt(100);
for(int i = 0; i < 5; i++)
{
out.println(gradeList[i]);
out.println(" ");
if(gradeList[i] >= 90)
{
%>
优秀
<!-- 第二段Java代码 -->
<%
}
else if(gradeList[i] >= 80 && gradeList[i] < 90)
{
%>
良好
<!-- 第三段Java代码 -->
<%
}
else if(gradeList[i] >= 60 && gradeList[i] < 80)
{
%>
及格
<!-- 第四段Java代码 -->
<%
}
else
{
%>
不及格
<!-- 第五段Java代码 -->
<%
}
out.println("<br>");
}
%>
</body>
</html>
在上面的代码中通过5对<%…%>,将嵌入的Java代码分成了5部分,这5部分中的Java代码的每一部分都不完整,但如果将它们合起来就是完成的Java代码。在这5对<%…%>之间是JSP页面中的静态部分,这部分在JSP页面被翻译成Servlet类时使用write方法直接输出到客户端。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/javacode.jsp
浏览器显示的信息如图6.2所示。
图6.2 多对<%…%>中的内容组合成完整的Java代码
6.2.3 JSP声明
虽然使用<%…%>可以将任何Java代码直接插入到_jspService方法中,但是如果想在_jspService方法外插入Java代码,<%…%>却无能为力。要想达到这个目的,就必须要使用JSP声明。JSP声明中的Java代码被封装在<%!…%>中。所有在<%!…%>中的Java代码都会被作为Servlet类的全局部分插入到_jspService方法外。如下面的JSP代码所示:
<!-- declare.jsp -->
<!-- JSP声明 -->
<%!
static
{
System.out.println("正在装载Servlet!");
}
private int globalValue = 0;
public void jspInit()
{
System.out.println("正在初始化JSP!");
}
public void jspDestroy()
{
System.out.println("正在销毁JSP!");
}
%>
<!-- 在_jspService方法中插入Java代码 -->
<%
int localValue = 0;
%>
globalValue:<%= ++globalValue %><br>
localValue:<%= ++localValue %>
在上面的JSP代码中使用JSP声明做了如下3件事:
在Servlet类中插入了一个静态块(static {…})。
定义了一个全局的int类型变量globalValue,并将该变量初始化为0.
覆盖了由JSP生成的Servlet类中的jspInit和jspDestroy方法。这两个方法分别在该Servlet对象初始化和销毁时被调用。
declare.jsp页面将被JSP引擎翻译成如下的Servlet类源代码:
… …
public final class declare_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent
{
static
{
System.out.println("正在装载Servlet!");
}
private int globalValue = 0;
public void jspInit()
{
System.out.println("正在初始化JSP!");
}
public void jspDestroy()
{
System.out.println("正在销毁JSP!");
}
… …
public void _jspService(HttpServletRequest request, HttpServletResponse
response) throws java.io.IOException, ServletException
{
… …
int localValue = 0;
out.write("\r\n");
out.write("globalValue:");
out.print(++globalValue);
out.write("<br>\r\n");
out.write("localValue:");
out.print(++localValue);
… …
}
}
从上面的Servlet源代码可以看出,所有<%!…%>中的内容都被作为declare_jsp类的全局部分插入到declare_jsp类中。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/declare.jsp
当首次访问上面的URL时,Tomcat控制台会输出如下的信息:
正在装载Servlet!
正在初始化JSP!
浏览器将显示如下的信息:
globalValue:1
localValue:1
在多次访问上面的URL后,由于globalValue是declare_jsp的全局变量,因此,在declare_jsp对象未被销毁之前,globalValue变量的值在每刷新一次页面时就会增1,而localValue是在_jspService方法定义的局部变量,因此,localValue变量始终是1.
修改declare.jsp页面中的内容(只要加几个空格或回车即可,目的就是该变declare.jsp页面的修改时间),然后再次访问上面的URL,Tomcat控制台会输出如下的信息:
正在销毁JSP!
正在装载Servlet!
正在初始化JSP!
从上面的输出信息可以看出,当修改JSP页面后,再次访问该JSP页面,JSP引擎会重新将该JSP页面翻译成Servlet,而Servlet引擎会先销毁以前由该JSP页面生成的Servlet的对象实例,然后再重新装载该Servlet。
6.2.4 JSP中的注释
在JSP代码中有3种注释:JSP注释、Java注释和HTML注释。
1. JSP注释
这种注释的格式如下:
<%-- JSP注释 --%>
JSP引擎在处理JSP代码时,会忽略JSP注释。也就是说,JSP注释既不会出现在由JSP生成的Servlet类中,也不会被作为静态内容输出到客户端。JSP注释的作用只是为了使JSP代码更容易理解。
2.Java注释
Java注释就是Java源代码的注释。该注释可以在<% … %>或<%=…%>中的Java代码中使用。JSP引擎在翻译JSP页面时会将Java注释直接插入到由JSP生成的Servlet类中。下面是Java注释的例子代码:
<%= (4+5) /* Java注释 */ %>
<%
/* Java注释1 */
// Java注释2
%>
3.HTML注释
HTML注释的格式如下:
<!-- HTML注释 -->
JSP引擎在处理这类注释时,将它们和其他的JSP静态内容一起使用write方法输出到客户端。也就是说,HTML注释将被当成JSP代码中的静态内容处理。
6.3.1 JSP指令简介
JSP指令的语法格式如下:
<%@ 指令 属性名 = "值" %>
在JSP2.0规范中提供了3个指令:page、include和taglib(这个指令将在后面的章节详细介绍)。每种指令都定义了若干属性。根据具体的需求,每个指令可以选择只使用一个属性,也可以选择多个属性的组合。如下面2条page指令分别设置了contentType和pageEncoding属性:
<%@ page contentType="text/html" %>
<%@ page pageEncoding="UTF-8" %>
上面的两条指令也可以写成一条page指令,如下面的代码所示:
<%@ page contentType="text/html" pageEncoding="UTF-8" %>
在使用JSP指令时应注意。JSP指令和属性名都是大小写敏感的。
6.3.2 page指令
page指令用于设置JSP页面的各种属性。大多数的JSP页面中都包含page指令。虽然page指令可以出现在JSP页面的任何位置,但最好将page指令放到JSP页面的起始位置。page指令的完整语法格式如下:
<%@ page
[ language="java" ] [ extends="package.class" ]
[ import="{package.class | package.*} , … " ]
[ session="true|false" ]
[ buffer="none| 8kb|sizekb" ] [ autoFlush="true|false" ]
[ isThreadSafe="true|false" ] [ info="text" ]
[ errorPage="relativeURL" ] [ isErrorPage="true| false" ]
[ contentType="{mimeType [ ; charset=characterSet ] | text/html ; charset=ISO-8859-1}" ]
[ pageEncoding="{characterSet | ISO-8859-1}" ]
[ isELIgnored="true | false" ]
%>
上面定义中的每对方括号"[]"分别表示page指令的一个属性。属性值中用坚杠(|)分隔的不同部分为该属性可以设置的值,如session属性可以设置为true或false.属性值中黑体部分为该属性的默认值。在这些属性中,import属性是唯一允许出现多次的属性。下面是对page指令的所有属性的详细描述。
1. language属性
language属性用来设置JSP页面所使用的开发语言(也就是在<% … %>和<%= … %>中所使用的语言)。由于目前JSP只支持Java,因此,language属性的值只能为java,而且这个值也是language属性的默认值。因此,可以不指定该属性。
2. extends属性
extends属性设置了由JSP生成的Servlet类的父类。一般不需要设置这个属性。如果在某些特殊情况下非要设置这个属性,应该注意设置后可能会对JSP造成的影响。
3. import属性
import属性指定在JSP页面被翻译成Servlet源代码后要导入的包或类。也就是翻译成Java中的import语句。在page指令中可以有多个import属性,如下面的page指令所示:
<%@ page import="java.util.*" import = "java.text.SimpleDateFormat" %>
上面的page指令将被翻译成如下的Java代码:
import java.util.*;
import java.text.SimpleDateFormat;
在page指令中不仅可以有多个import属性,还可以在一个import属性中使用逗号(,)分割不同的包或类。如上面的page指令也可以写成如下的形式:
<%@ page import="java.util.*, java.text.SimpleDateFormat"s %>
4. session属性
session属性用于指定在JSP页面中是否可以使用内置session对象。session属性的默认值为true.也就是说,JSP在默认的情况下会自动创建HttpSession对象。
5. buffer属性
buffer属性用于设置JSP中out对象的缓冲区大小,默认值是8kb.如果将buffer设为none,out对象则不使用缓冲区。还可以通过这个属性来自定义out对象的缓冲区的大小。但单位必须是kb,也就是说,buffer属性的值的最后两个字母必须是kb,而且必须是非负整数。如10kb,120kb等。将buffer属性的值设为0kb的效果和设为none是一样的。
6. autoFlush属性
autoFlush属性用于设置当out对象的缓冲区已满时如何处理缓冲区中的内容,如果autoFlush属性的值为true,则当out对象的缓冲区已满时直接将缓冲区中的内容刷新到客户端。如果autoFlush属性的值为false,则对于已满的缓冲区,系统会抛出缓冲区溢出的异常。该属性其默认值为true.如果buffer属性的值为none或0kb,autoFlush属性的值不能为false.因为将buffer属性的值设为none或0kb,表明未使用缓冲区,也就相当于缓冲区永远是满的。这时将autoFlush属性值设为false,JSP引擎会在编译JSP页面时会产生一个内部编译错误。
7. isThreadSafe属性
isThreadSafe属性用于设置JSP页面是否是线程安全的,该属性的默认值是true.当isThreadSafe属性值为true时,说明当前的JSP页面在设计时已经考虑到了线程安全(如全局共享的资源已经同步),并不需要Servlet引擎再来考虑这些问题了。当isThreadSafe属性为false时,由JSP页面翻译成的Servlet类会实现SingleThreadModel接口,也就是说,线程完全将由Servlet引擎来负责。
8. info属性
info属性用于定义一个描述当前JSP页面的字符串信息。在翻译JSP页面时,info属性值被翻译成getServletInfo方法的返回值。看下面的JSP代码:
<%@ page info ="输出info属性的值" contentType="text/html" pageEncoding="UTF-8"%>
上面的JSP代码中的info属性将被翻译成getServletInfo方法的返回值,代码如下:
… …
public final class info_jsp extends org.apache.jasper.runtime.HttpJspBase
implements org.apache.jasper.runtime.JspSourceDependent
{
// info属性值实际上是getServletInfo方法的返回值
public String getServletInfo()
{
return "输出info属性的值";
}
… …
public void _jspService(HttpServletRequest request, HttpServletResponse response) throws java.io.IOException, ServletException
{
… …
}
… …
}
在JSP页面中可以通过如下的代码来输出info属性的值:
info属性的值:<%= getServletInfo() %>
9. errorPage属性
errorPage属性用于指定处理当前JSP页面抛出的异常的页面。如果JSP页面抛出了未被捕获的异常,就会自动跳转到errorPage属性所指的页面。errorPage属性的值必须是相对路径。如果以"/"开头,表示相对于当前Web应用程序的根目录,否则,表示相对于当前JSP页面所在的目录。要注意的是,errorPage属性的值可以是JSP页面,也可以是静态的页面(如html、图象文件等)。
10.isErrorPage属性
isErrorPage属性指定当前JSP页面是否可用于处理其他JSP页面未捕获的异常。该属性的默认值为false.errorPage属性所指的异常处理JSP页面必须将isErrorPage属性设为true.否则,无法在异常处理页中使用exception对象。关于JSP页面异常的处理将在6.3.3节详细讲解。
11. contentType属性
contentType属性用于设置响应消息头的Content-Type字段,该字段设置了响应正文的MIME类型和JSP页面中文本内容的字符集编码。contentType属性的默认MIME类型是text/html,默认字符集是ISO-8859-1.对于简体中文来说,可以将contentType属性设为如下两个值中的任何一个:
<%@ page errorPage="error.jsp" contentType="text/html; charset=GBK"%>
<%@ page errorPage="error.jsp" contentType="text/html; charset=UTF-8"%>
12. pageEncoding属性
pageEncoding属性用于指定JSP页面中文本内容的字符集编码格式。如果指定了pageEncoding属性,contentType属性中的charset就不再表示JSP页面的字符集编码了。如果contentType属性中未指定字符集编码格式(也就是没有charset),pageEncoding属性同时还具有设置Content-Type字段中的字符集编码的作用(相当于设置了contentType属性的charset)。
13. isELIgnored属性
isELIgnored属性用于设置JSP页面是否支持EL(表达式语言,Expression Language)。如果Web应用程序是遵循Servlet2.3或更低版本,isELIgnored属性的默认值为true(表示JSP页面在默认情况下不支持EL),如果遵循Servlet2.4或更高版本,isELIgnored属性的默认值为false(表示JSP页面在默认情况下支持EL)。
6.3.3 JSP页面中的异常处理
JSP页面可以通过page指令的errorPage和isErrorPage属性进行异常处理。errorPage属性要用在抛出异常的JSP页面,该属性指定了处理异常的页面(一般是JSP页面)。generator_error.jsp页面是一个抛出异常的JSP页面,代码如下:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" errorPage="deal_error.jsp"%>
<%
out.println("发生错误之前");
//抛出java.lang.ClassNotFoundException异常
Class.forName("NoExist");
out.println("发生错误之后");
%>
在上面的代码中使用forName方法动态装载了一个不存在的NoExist类,因此会抛出ClassNotFoundException异常,如果不使用errorPage属性,异常信息将直接在访问generate_error.jsp页面时在浏览器中显示。但如果使用了errorPage属性,就可以在另外一个处理异常的JSP页面由开发人员决定如何处理抛出的异常。
处理异常的页面为deal_error.jsp,代码如下:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
isErrorPage="true"%>
<%
out.println("<font color='#FF0000'>异常信息</font><hr>");
exception.printStackTrace(new java.io.PrintWriter(out));
%>
在deal_error.jsp页面中使用了page指令的isErrorPage属性。如果将该属性设为true,则JSP引擎在翻译deal_error.jsp页面时会建立exception对象,因此,在deal_error.jsp页面中可以直接使用exception对象。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/generate_error.jsp
浏览器显示的信息如图6.3所示。
图6.3 显示抛出的异常信息
除了通过errorPage属性指定处理异常的JSP页面外,还可以在web.xml文件中配置处理异常的JSP页面。由于_jspService方法只能抛出java.io.IOException和javax.servlet.ServletException异常,而且在抛出java.lang.RuntimeException异常时不需要在方法定义中显式地声明,因此,在web.xml文件中只能配置处理如下三种异常类及其子类的JSP页面:
java.io.IOException
javax.servlet.ServletException
java.lang.RuntimeException
除了上述3种异常,其他的异常将在_jspService内部进行处理。
下面将在generate_error.jsp页面中编写如下的代码:
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%
if("servlet".equals(request.getParameter("error")))
{
throw new ServletException("Servlet异常");
}
else if("io".equals(request.getParameter("error")))
{
throw new java.io.IOException("IO异常");
}
else
{
int i = 1 / 0;
}
%>
从上面的代码可以看出,在page指令中并未指定errorPage属性,这是因为如果指定errorPage属性,系统将会优先考虑errorPage属性的设置,也就是说,系统会使用errorPage属性所指的异常处理页面,而不会考虑在web.xml文件中配置的异常处理页面。因此,要想使用web.xml文件来配置异常处理页面,就不能在抛出异常的页面中的page指令中指定errorPage属性。
新建一个处理异常的deal_error1.jsp页面,代码如下:
<%@page import="java.io.PrintStream"%>
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" isErrorPage="true"%>
<%
out.println("<font color='#FF0000'>异常信息(deal_error1.jsp)</font><hr>");
out.println(exception.getMessage());
exception.printStackTrace(new java.io.PrintWriter(out));
%>
为了处理上述三种异常,需要在web.xml文件中添加如下的配置代码:
<error-page>
<exception-type>javax.servlet.ServletException</exception-type>
<location>/chapter6/deal_error1.jsp</location>
</error-page>
<error-page>
<exception-type>java.io.IOException</exception-type>
<location>/chapter6/deal_error1.jsp</location>
</error-page>
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/chapter6/deal_error1.jsp</location>
</error-page>
从上面的配置代码可以看出,使用了3个<error_page>元素分别用来配置上述3类异常的处理页面。在<error_page>元素中有两个子元素:<exception-type>和<location>,其中<exception-type>元素用来指定异常类名,<location>元素用来指定异常处理页的路径,必须以"/"开头,表示相对当前Web应用程序的根目录。
读者可以在浏览器地址栏中输入如下三个URL来测试上述3种异常的处理情况:
http://localhost:8080/demo/chapter6/generate_error.jsp?error=servlet
http://localhost:8080/demo/chapter6/generate_error.jsp?error=io
http://localhost:8080/demo/chapter6/generate_error.jsp
处理Servlet异常时的输出结果如图6.4所示。
图6.4 处理Servlet异常
<error-page>元素除了可以使用<exception-type>元素指定异常类外,还可以使用<error-code>元素指定HTTP响应状态码,如下面的配置代码所示:
<error-page>
<error-code>404</error-code>
<location>/images/error.jpg</location>
</error-page>
上面的配置代码使用<error-code>元素设置了HTTP响应状态码404,这就意味着访问所有在服务端不存在的Web资源,从页产生404状态码的请求,都会交由<location>元素所指定的异常处理页面来处理。在该例中指定了一个图像文件(error.jpg),读者也可以在<location>元素中指定其他的Web资源,如HTML页面、JSP页面等。
在使用web.xml文件配置异常处理页面时要注意,<error_code>和<exception-type>元素只能同时在一个<error-page>元素中出现一个。
6.3.4 include指令
include指令用于将其他文件的内容合并到当前的JSP程序中。这种合并是静态的,也就是说,将其他文件的内容合并到由当前JSP页面生成的Servlet类中。include指令的语法格式如下:
<%@ include file="relativeURL" %>
include指令只有一个file属性。这个属性的值是一个相对路径,如果以"/"开头,则相对于Web应用程序的根目录,否则,相对于当前JSP页面所在的目录。在使用include指令时应注意以下几点:
1. 被合并的文件可以是任何扩展名。但该文件的内容必须符合JSP页面的规范。因为JSP引擎会按着处理JSP页面的方式处理被引入的文件。
2. include指令是静态引入文件的,也就是说,被引入文件内容将成为由JSP所生成的Servlet类的一部分。
3. 由于JSP引擎将合并被引入的文件与当前JSP页面中的指令,因此,除了page指令的import和pageEncoding属性外,其他的属性不能在当前的JSP页面和被引入的JSP页面中有不同的值。否则JSP引擎在翻译JSP页面时会抛出JasperException异常。
4. 合并文件的过程是在JSP引擎翻译成Servlet的过程中进行的,因此,如果当前JSP页面和被引入的页面需要采用不同的字符集编码,必须在各自的页面单独设置。也就是说,当前页面设置的字符集编码并不代表被引入页面的字符集编码。
5. Tomcat会自动检测被引入页面是否被修改。如果被引入页面被修改,在访问当前页面时,JSP引擎会重新翻译当前页面。
6.4.1 out对象
out对象用来向客户端输出信息。如下面的代码所示:
<!-- jspout.jsp -->
<%@ page language="java" pageEncoding="UTF-8" %>
<%
out.println("使用out对象输出<br>");
java.io.PrintWriter myOut = response.getWriter();
myOut.println("使用PrintWriter对象输出<br>");
%>
在上面的代码中,首先使用了out对象向客户端输出信息,然后调用了response对象的getWriter方法获得一个PrintWriter对象,并通过该对象的println方法向客户端输出信息。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/jspout.jsp
浏览器显示的信息如图6.5所示。
图6.5 使用out和PrintWriter对象输出的信息
从图6.5所示的信息可以看出,使用PrintWriter对象输出的信息显示在了使用out对象输出的信息的前面。这是因为out对象实际上通过pageContext对象的getOut方法获得的JspWriter对象,通过JspWriter对象输出的信息首先会被写入out对象的缓冲区,在满足如下两个条件中的一个时,系统会将out对象缓冲区中的内容写入Servlet引擎提供的缓冲区:
整个JSP页面结束时。
当前out对象缓冲区已满时。
将out对象缓冲区中的内容写到Servlet引擎提供的缓冲区后,再通过PrintWriter对象将这些内容输出到客户端。也就是说,不管是JSP,还是Servlet,最终都是依靠PrintWriter对象向客户端输出信息的。
从上面的程序可以看出,虽然一开始就使用了out对象输出信息,但这些信息都被写入out对象的缓冲区,而使用PrintWriter对象输出的内容则直接被写入了Servlet引擎提供的缓冲区,当整个页面结束时,系统会将out对象缓冲区中的内容写入Servlet引擎提供的缓冲区。因此,从写入Servlet引擎提供的缓冲区的顺序看,使用PrintWriter对象输出的信息要比使用out对象输出的内容更早地被写入Servlet引擎提供的缓冲区,这也就是为什么输出信息的顺序会和out及PrintWriter对象在JSP页面中的调用顺序正好相反的原因。
如果想让输出顺序和JSP页面中的调用顺序保持一致,可以通过禁止out对象缓冲区的方法来解决,如下面的代码所示:
<!-- jspout.jsp -->
<%@ page language="java" pageEncoding="UTF-8" buffer="none" %>
<%
out.println("使用out对象输出<br>");
java.io.PrintWriter myOut = response.getWriter();
myOut.println("使用PrintWriter对象输出<br>");
%>
上面的代码在page指令中加了一个buffer属性,并将该属性的值设为"none",也就是禁止out对象的缓冲区。这时再次访问jspout.jsp页面,就会看到信息的输出顺序改变了。
由于buffer属性的默认值是8k,因此,当使用out对象输出的信息总量超过8k时,就算JSP页面未结束,也会将信息(out对象缓冲区中的8k的内容)写入Servlet引擎提供的缓冲区,并清空out对象的缓冲区。下面的JSP页面将buffer属性值设为1k(该值是buffer属性可设置的最小值),来模拟out缓冲区溢出的过程。
<!-- jspbuffer.jsp -->
<%@ page language="java" pageEncoding="UTF-8" buffer="1kb" %>
<%
for(int i = 0; i < 1024; i++)
out.println("x");
java.io.PrintWriter myOut = response.getWriter();
myOut.println("使用PrintWriter对象输出信息");
%>
下面的代码循环产生了1024个"x"字符,并通过out对象输出的客户端,在最后使用PrintWriter对象输出了一条信息。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/jspbuffer.jsp
浏览器显示的信息如图6.6所示。
图6.6 out对象缓冲区溢出
从图6.6所示的输出信息可以看出,使用PrintWriter对象输出的信息被夹在了1024个x字符中间。在该信息前面的x字符是out对象缓冲区未满时写入的。虽然用程序产生了1024个x,但由于JSP的静态部分(如页面开头的注释部分)也占用了一定的out对象缓冲区空间,因此,out对象缓冲区空间容纳的x字符数要小于1024,因此,会出现1024个x未被完全写入out对象的缓冲区,该缓冲区就溢出了的现象。
当out对象缓冲区被第一次写满时,就会将该缓冲区的内容一次性地写入Servlet引擎的缓冲区,然后清空out对象缓冲区,并会再次写入剩余的x.因此,在使用PrintWriter对象输出信息之前,已经有1024个字节的信息被写入到了Servlet引擎的缓冲区。所以会出现图6.6所示的输出结果。
由于JSP向客户端输出信息时使用了JspWriter对象(out对象),并且在out对象缓冲区被写入Servlet引擎的缓冲区后,Servlet引擎会使用PrintWriter输出缓冲区中的内容,因此,如果JSP页面中包含有静态内容,则无法使用ServletOutputStream对象来输出信息,否则会造成冲突,如下面的代码所示:
<!-- jspstream.jsp -->
<%@ page language="java" pageEncoding="UTF-8" %>
<%
ServletOutputStream sos = response.getOutputStream();
sos.println(new String("使用ServletOutputStream输出信息".getBytes("UTF-8"), "ISO-8859-1"));
%>
在上面的代码中使用了response.getOutputStream方法获得了一个ServletOutputStream对象,并通过该对象的println方法向客户端输出信息。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/jspstream.jsp
浏览器将显示如图6.7所示的异常信息。
图6.7 使用ServletOutputStream对象输出信息时抛出的异常
产生图6.7所示的异常的原因是由于jspstream.jsp页面包含了静态部分(注释、\r\n等),而这些注释部分最终要通过PrintWriter输出到客户端,但在jspstream.jsp页面中又使用了ServletOutputStream对象,在前面讲过,不能同时使用ServletOutputStream和PrintWriter对象向客户端输出信息。因此,才会抛出上面的异常。
如果将jspstream.jsp页面中所有的静态部分都删除,那么JSP引擎不会向out对象缓冲区写入任何内容,也不会使用PrintWriter对象向客户端输出信息。因此,这时Servlet引擎实际上只使用了ServletOutputStream对象,所以可以正常向客户端输出信息。
除了直接在JSP页面中使用ServletOutputStream对象可能会抛出异常外,使用forward和include方法转发和包含页面时也可能会抛出和图6.7相同的异常信息,如下面的代码所示:
<!-- jspforward.jsp -->
<%@ page language="java" pageEncoding="UTF-8" %>
<%
RequestDispatcher rd = request.getRequestDispatcher("/test.html");
rd.forward(request, response);
// 使用include方法和使用forward都会带来同样的问题
// rd.include(request, response);
%>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/jspforward.jsp
浏览器将会显示如图6.7所示的异常信息。
抛出异常的原因是由于Servlet引擎通过默认的Servlet来处理html、jpg等Web资源。默认Servlet会首先检查是否调用了getWriter方法获得PrintWriter对象,如果系统还未获得PrintWriter对象,则默认的Servlet会使用ServletOutputStream对象来处理这些Web资源。而在jspforward.jsp页面中并未显式地调用getWriter方法来获得PrintWriter对象,而且out对象缓冲区也未满,因此,也不可能通过将out对象缓冲区的内容写入Servlet引擎缓冲区的方式来调用getWriter方法获得PrintWriter对象。所以这时使用forward方法来转发test.html页面,实际上是使用ServletOutputStream对象来处理的。
当jspforward.jsp页面结束时,会因为将out对象缓冲区的内容写入Servlet引擎的缓冲区而调用getWriter方法。因此,实际上jspforward.jsp页面相当于先调用了getOutputStream方法,再调用了getWriter方法,因此,就会造成冲突,从而抛出异常。
读者可以通过如下3种方法来解决这个问题,从而避免抛出异常:
清空jspforward.jsp页面中的所有静态部分,包括\r\n.这样系统就不会向out对象的缓冲区写入任何内容了。
如果在<%…%>前面有静态内容的话(在一般情况下<%…%>前都会有一些静态内容),可以使用page指令的buffer属性将out对象缓冲区关闭,也就是将buffer属性设为"none".这样只要在<%…%>前面有静态内容,就可以直接写到Servlet引擎的缓冲区中,也就相当于调用了getWriter方法。
在<%…%>中的开始部分加上response.getWriter方法的调用。这样再调用forward或include方法,默认Servlet就会检测到已经调用了getWriter方法,因此,就会使用PrintWriter来处理完成forward或include方法的工作。
6.4.4 page对象
page对象表示由JSP页面生成的Servlet类的对象实例本身。page对象实际上是Object类型的对象。但可以将page对象转换成相应的Servlet类型的对象。在下面的代码中输出了page对象的类型信息,并通过反射技术输出了由JSP生成的Servlet类中的所有public方法名。
<!-- page.jsp -->
<%@ page language="java" pageEncoding="UTF-8" %>
<%
out.println(page.getClass());
out.println("<hr>");
java.lang.reflect.Method[] methods = page.getClass()。getMethods();
// 通过反射技术列出由JSP生成的Servlet中的所有public方法
for(java.lang.reflect.Method method: methods)
{
out.println("{" + method.getName() + "}");
}
%>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/page.jsp
浏览器显示的信息如图6.8所示。
图6.8 输出page对象中的所有public方法名
从图6.8所示的输出信息可以看出,在page对象中有一些我们很熟悉的方法,如_jspService、init方法等。
6.4.5 session对象
session对象用实际上是HttpSession对象实例。用来操作服务端的Session对象。JSP中的Session对象和Servlet中的Session对象基本一样,但有一点不同。就是在默认情况下,每一个JSP页面都会建立一个HttpSession对象。而在Servlet中只有通过调用HttpServletRequest接口的getSession方法时才会建立一个HttpSession对象(当SessionId没有对应的HttpSession对象时创建新的HttpSession对象)。要想关闭JSP中自动建立HttpSession对象的功能,需要将page指令的session属性值设为false.关于session对象的详细用法,请读者参阅5.4节所讲的内容。
6.4.6 application对象
application对象实际上就是ServletContext对象。该对象除了可以获得一些系统的信息外,也可以将对象保存在自己的域中。到现在为止,已经讲过了三个对象(request、session和application)可以在自己的域中保存对象信息。在6.4.9节还会讲到一个pageContext对象,也拥有自己的域。
在上述四个对象中,application域的应用范围最大,保存在application域的信息可以被当前Web应用程序中的所在Servlet和JSP页面访问。而保存在session域中的信息只能被属于同一个会话的Servlet和JSP页面访问。而request域只能被属性同一个请求的Servlet和JSP页面访问,如当前页面和在该页面中通过forward或include方法转发或包含的页面之间就属性同一个request.应该范围最小的是pageContext域,该域只能在当前JSP页面中访问。关于application对象的详细用法,请读者参阅4.5节所讲的内容。
6.2.9 pageContext对象
pageContext对象是javax.servlet.jsp.PageContext类的对象实例,该类是javax.servlet.jsp.JspContext的子类。pageContext对象封装了当前JSP页面的各种信息,通过pageContext对象的getter方法可以获得JSP页面的其他8个内置对象,这些getter方法如下:
getException:该方法返回exception对象。
getOut:该方法返回out对象。
getPage:该方法返回page对象。
getRequest:该方法返回request对象。
getResponse:该方法返回response对象。
getServletConfig:该方法返回config对象。
getServletContext:该方法返回application对象。
getSession:该方法返回session对象。
如果在JSP页面中要使用某个普通的类,在该类中要使用JSP的内置对象,为了方便起见,可以将pageContext对象作为参数传入该类的对象实例,这样在该类中就可以使用JSP页面中所有9个内置对象了。
在前面讲过,request和application对象都可以通过forward和include方法转发和包含Web资源。实际上,在pageContext对象中也提供了forward和include方法来简化转发和包含Web资源的编码工作。
在pageContext对象中有一个forward方法和两个include方法,这3个方法的定义如下:
abstract public void forward(String relativeUrlPath)
throws ServletException, IOException;
abstract public void include(String relativeUrlPath)
throws ServletException, IOException;
abstract public void include(String relativeUrlPath, boolean flush)
throws ServletException, IOException;
其中flush参数为true,表示在调用include方法之前,将out对象的缓冲区中的内容刷新到Servlet引擎提供的缓冲区中。pageContext对象中的forward和include方法与前面讲的相应方法类似,只是forward方法在处理out对象缓冲区上有一些区别,看如下的代码:
<!-- pageContext.jsp -->
<%@ page language="java" pageEncoding="UTF-8" %>
<%
pageContext.forward("/test.html");
%>
如果将pageContext.jsp页面中<%…%>后面的静态部分都删除,则可以正常访问该页面,但如果在<%…%>后面还有静态部分,则在访问pageContext.jsp页面时会抛出如图6.8所示的异常。这是由于pageContext对象的forward对象在转发Web资源之前,会先清空out对象的缓冲区,因此,在<%…%>之前写入out对象缓冲区的内容将作废,这时如果<%…%>后面没有静态部分,则系统就不会调用getWriter方法获得PrintWriter对象,因此,也就不会抛出异常了。
如果在<%…%>后面也加上JSP页面的静态部分,则仍然会抛出图6.8所示的异常。这是因为虽然在调用forward方法之前清空了out对象的缓冲区,但在调用forward方法之后,仍然会继续将静态内容写入out对象的缓冲区。当JSP页面结束时,还会调用getWriter方法来获得PrintWriter对象。因此,就会抛出异常。
pageContext对象拥有自己的域,也就是说,可以通过setAttribute、getAttribute、removeAttribute方法设置、获得和删除域信息外。还有如下两个方法可以访问pageContext、request、session和application四个域:
findAttribute:该方法可以依次从pageContext、request、session和application四个域中获得指定的属性值。如果前一个域中没有要找的属性,则继续在下一个域中寻找。如果在这四个域中都没有要找的属性,则该方法返回null.
getAttributeNamesInScope:该方法返回某个域中的所有属性名,这些属性名将被放在一个Enumeration对象中返回。该方法有一个参数,可以通过PageContext的常量设置。如要获得request域中的所有属性的名称,可以使用PageContext.REQUEST作为该方法的参数值。
6.5.1 <jsp:include>标签
<jsp:include>标签用于把另外一个Web资源引入当前JSP页面的输出内容之中。该标签的语法格式如下:
<jsp:include page="relativeURL | <%=expression%> | EL" flush="true|false"/>
其中page属性用于指定被引入的Web资源的相对路径,该属性可以用普通字符串指定相对路径,也可以使用JSP表达式或EL(表达式语言)来指定相对路径(EL将在下一章详细讲解)。flush属性表示在引入Web资源时,是否先将out对象缓冲区中的内容刷新到Servlet引擎提供的缓冲区。如果flush属性为true,表示在引入Web资源时,先刷新out对象的缓冲区。flush属性的默认值是false.
在使用<jsp:include>标签时应注意如下几点:
引入资源的方式:<jsp:include>标签和6.3.4节讲的include指令在引入资源时有很大的差别。它们之间最大的差别就是include指令是静态引入的,也就是在JSP引擎翻译JSP页面时就将当前JSP页面和被引入的页面合并了,最终生成的Servlet就已经是这两个JSP页面的合体的。而<jsp:include>标签是动态引入Web资源。也就是说,在JSP每次运行时都会引入page属性指定的Web资源。
引入资源的路径:如果单从引用文件的目录结构来看,<jsp:include>标签的page属性和include指令的file属性指定的路径是一样的。如将test.jsp文件放在"WEB-INF"目录中,通过<jsp:include>标签的page属性可设为page="/WEB-INF/test.jsp",include指令的file属性也可设为file="/WEB-INF/test.jsp",但page和file不同的是page属性可以设置在web.xml文件中配置的路径,而file属性的值只能是在目录结构中存在的文件。如将"/WEB-INF/test.jsp"映射成"/jsp/test.jsp",这个新的路径并不存在,只是个虚拟的映射路径。在page属性中该值是有效的,而将file属性设成该值,JSP引擎会提示该路径不存在。
引入资源的内容:<jsp:include>标签引入的资源可以是任何内容,而include指令引入的资源必须符合JSP语法规范,即使引入的资源文件的扩展名不是。jsp,该文件的内容也必须符合JSP的语法规范。这是由于include指令在引入任何资源文件时,都会将该文件作为JSP页面进行翻译。如果有一个test.html文件,该文件的内容是<% abcd %>,很明显,该文件的内容不符合JSP语法规范(abcd并未定义,也不是表达式,在翻译成Java代码时会编译出错)。如果这个文件被<jsp:include>标签引用,会直接输出<% abcd %>,但被include指令引用,则会抛出异常。当然,如果将test.html改名为test.jsp,不管是<jsp:include>标签,还是include指令,都会抛出异常。这是由于<jsp:include>标签是根据引入文件的扩展名来决定如何处理该文件的,如果扩展名是。jsp,也会按着JSP页面来处理,所以会抛出异常。
<jsp:include>标签和RequestDispatcher.include方法类似,在被引入的页面中修改响应状态码和响应消息头的语句将被忽略。
<jsp:include>标签无论在任何情况下,都会使用PrintWriter对象来输出信息。这一点和include方法有很大的差别。对于include方法来说,系统会根据include方法前面的代码是否使用了PrintWriter或ServletOutputStream对象来决定使用哪一个对象来输出信息。而<jsp:include>标签通过某些机制使得ServletOutputStream永远不可用,因此,该标签只能使用PrintWriter对象来输出信息。从这一点可以看出,只要在JSP页面中不使用ServletOutputStream对象来输出信息,<jsp:include>标签是绝对不会由于同时调用了PrintWriter和ServletOutputStream对象来抛出异常的。所以也可以有一个推论,就是在使用<jsp:include>标签的JSP页面中使用ServletOutputStream对象,不管任何情况,都会抛出异常。关于<jsp:include>标签为什么会有这样的特性,将在本节的后面部分详细讲解。
效率:include指令的效率是最高的,但include指令不如<jsp:include>标签灵活。如include指令的file属性不能使用EL和JSP表达式。
<jsp:include>标签在引入资源文件时可以传递请求参数,但由于include指令是静态引用资源文件的,因此,include指令在引用资源文件时不能传递请求。
<jsp:include>标签的page属性必须是相对路径,如果以"/"开头,表示相对于当前Web应用程序的根目录(不是站点根目录),否则,相对于当前页面。
【实例6-1】 <jsp:include>标签演示
1. 编写dynamicincluding.jsp页面
该页面使用<jsp:include>指令引入一个included.jsp页面,dynamicincluding.jsp页面的代码如下:
<%@ page language="java" pageEncoding="UTF-8" %>
使用out对象输出信息<br>
<jsp:include page="included.jsp" flush="false"/>
<br>
<%
response.getWriter()。println("使用PrintWriter输出信息<br>");
%>
在上面的代码中,<jsp:include>指令前面有一行静态的内容,这部分内容将通过out对象输出到客户端,在<jsp:include>指令后面通过PrintWriter对象输出了一条信息。如果<jsp:include>标签的flush指令为false,则在引入included.jsp页面时不刷新out对象的缓冲区,因此,使用PrintWriter对象输出的信息将会在最前面显示。
2. 编写included.jsp页面
该页面只是一个普通的JSP页面,代码如下:
<%@ page language="java" import = "java.util.*" pageEncoding="UTF-8"%>
included.jsp中的内容<br>
3. 测试<jsp:include>标签引入资源的效果
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/dynamicincluding.jsp
浏览器显示的信息如图6.9所示。
图6.9 使用<jsp:include>标签引入资源文件
从图6.9所示的信息可以看出,使用PrintWriter对象输出的信息显示在了页面的开始部分。如果在dynamicincluding.jsp页面的任何位置调用了response.getOutputStream方法,则一定会抛出异常。读者可以自己做这个实验。
4. 在引入资源文件时刷新out对象的缓冲区
将dynamicincluding.jsp页面中<jsp:include>标签的flush属性设为true,代码如下:
<%@ page language="java" pageEncoding="UTF-8" %>
使用out对象输出信息<br>
<jsp:include page="included.jsp" flush="true"/>
<br>
<%
response.getWriter()。println("使用PrintWriter输出信息<br>");
%>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter6/dynamicincluding.jsp
浏览器显示的信息如图6.10所示。
图6.10 引入资源文件时刷新out对象的缓冲区
从图6.10所示的输出信息可以看出,由于在引入included.jsp页面时已经将out对象的缓冲区刷新,所以在此之前被写入out缓冲区的内容将会首先输出的客户端,因此,<jsp:include>标签前面的静态内容会显示在最前面。
5. 引用web.xml文件中配置的资源文件
如果将dynamicincluding.jsp和included.jsp页面在web.xml中重新配置一下它们的访问路径,使用<jsp:include>标签仍然可以使用这些新的路径来引用included.jsp页面。配置代码如下:
<!-- 配置dynamicincluding.jsp -->
<servlet>
<servlet-name>dynamicincluding</servlet-name>
<jsp-file>/chapter6/dynamicincluding.jsp</jsp-file>
</servlet>
<servlet-mapping>
<servlet-name>dynamicincluding</servlet-name>
<url-pattern>/abcd/including.jsp</url-pattern>
</servlet-mapping>
<!-- 配置included.jsp -->
<servlet>
<servlet-name>included</servlet-name>
<jsp-file>/chapter6/included.jsp</jsp-file>
</servlet>
<servlet-mapping>
<servlet-name>included</servlet-name>
<url-pattern>/myjsp/included.jsp</url-pattern>
</servlet-mapping>
如果按着上面的配置代码引用included.jsp,则dynamicincluding.jsp页面的代码如下:
<%@ page language="java" pageEncoding="UTF-8" %>
使用out对象输出信息<br>
<jsp:include page="/myjsp/included.jsp" flush="true"/>
<br>
<%
response.getWriter()。println("使用PrintWriter输出信息<br>");
%>
在浏览器地址栏输入如下的URL:
http://localhost:8080/demo/abcd/including.jsp
浏览器输出的信息和图6.10所示的输出内容完全相同。
6. 为什么<jsp:include>标签一定会使用PrintWriter对象输出信息
如果读者查询由dynamicincluding.jsp页面生成的Servlet源代码,就会发现<jsp:include>标签被翻译成了下面的Java代码:
org.apache.jasper.runtime.JspRuntimeLibrary.include(request, response,
"/test.html", out, true);
其中include方法的最后一个参数就是flush属性的值。继续查看JspRuntimeLibrary.include方法的源代码(该源代码可以在Tomcat的源代码中找到)。include方法的相关代码如下:
public static void include(ServletRequest request,
ServletResponse response,
String relativePath,
JspWriter out,
boolean flush)
throws IOException, ServletException {
… …
RequestDispatcher rd = request.getRequestDispatcher(resourcePath);
rd.include(request,
new ServletResponseWrapperInclude(response, out));
}
从上面的代码可以看出,实际上,<jsp:include>标签最终调用的是RequestDispatcher接口的include方法。从这一点还看不出<jsp:include>标签使用的一定是PrintWriter对象。然而,"玄机"就在ServletResponseWrapperInclude类中,这个类实现了HttpServletResponse接口,因此,该类可以转换成HttpServletResponse对象。
在Tomcat源代码中找到ServletResponseWrapperInclude.java,该类的相关代码如下:
package org.apache.jasper.runtime;
… …
public class ServletResponseWrapperInclude extends HttpServletResponseWrapper
{
private PrintWriter printWriter;
private JspWriter jspWriter;
public ServletResponseWrapperInclude(ServletResponse response,
JspWriter jspWriter)
{
super((HttpServletResponse)response);
this.printWriter = new PrintWriter(jspWriter);
this.jspWriter = jspWriter;
}
public PrintWriter getWriter() throws IOException
{
return printWriter;
}
// 抛出异常,使getOutputStream方法永远不可用
public ServletOutputStream getOutputStream() throws IOException
{
throw new IllegalStateException();
}
… …
}
从上面的代码中可以看出,在getOutputStream方法中抛出一个异常,这说明getOutputStream方法是永远不可用的。但光在getOutputStream方法中抛出异常并不足以说明<jsp:include>标签一定使用了PrintWriter对象输出信息(还有一种可能,就是最后会抛出一个异常)。
决定<jsp:include>标签使用哪个对象输出信息的最后一道"关卡"就是处理默认请求的DefaultSevlet类,该类也可以在Tomcat源代码中找到(DefaultServlet.java)。该类是通过try…catch语句来选择使用哪个对象输出信息的。下面是DefaultServlet类选择PrintWriter或ServletOutputStream对象的主要逻辑:
ServletOutputStream ostream = null;
PrintWriter writer = null;
try
{
ostream = response.getOutputStream();
}
catch(Exception(IllegalStateException e)
{
writer = response.getWriter();
}
从上面的代码可以看出,首先在try{…}块中尝试获得ServletOutputStream对象,在这时response对象实际上是ServletResponseWrapperInclude对象实例,而ServletResponseWrapperInclude类中的getOutputStream方法只有一条抛出异常的语句,而且抛出的异常正好是IllegalStateException,刚好被catch{…}捕获,因此,使用ServletResponseWrapperInclude对象实例作为include方法的第二个参数时,一定使用的是PrintWriter对象输出的信息(因为getOutputStream方法总是抛出IllegalStateException异常)。所以笔者建议在JSP中引用资源文件时,应尽量使用<jsp:include>标签。
下面的JSP代码将抛出一下异常:
<%@ page language="java" pageEncoding="UTF-8" %>
abcdefg
<%
request.getRequestDispatcher("/test.html")。include(request, response);
%>
由于上面代码中的include方法使用了ServletOutputStream对象,因此,在访问上面的JSP页面时将抛出一个异常。根据上面的描述,可以采用ServletResponseWrapperInclude对象来包装response,如使用下面的代码将不会抛出异常:
<%@ page language="java" pageEncoding="UTF-8" %>
abcdefg
<%
// 下面的语句一定使用PrintWriter对象来输出信息
request.getRequestDispatcher("/test.html")。 include(request, new
org.apache.jasper.runtime.ServletResponseWrapperInclude(response, out));
%>
要注意的是,在使用ServletResponseWrapperInclude类时,需要在demo工程中引用jasper.jar文件,该文件可以在<Tomcat安装目录>\lib目录中找到。
6.5.2 <jsp:forward>标签
<jsp:forward>标签用于转发Web资源。<jsp:forward>标签的语法格式如下:
<jsp:forward page="relativeURL | <%=expression%> | EL " />
<jsp:forward>标签和pageContext.forward方法的功能完全一样,看如下的代码:
<!-- forward.jsp -->
<%@ page language="java" pageEncoding="UTF-8"%>
<jsp:forward page="/test.html"/>
上面的代码通过<jsp:forward>标签转入"/test.html",查询由forward.jsp页面翻译成的Servlet源文件,其中和<jsp:forward>标签相关的代码如下:
out.write('\r');
out.write('\n');
if (true) {
_jspx_page_context.forward("/test.html");
return;
}
out.write('\r');
out.write('\n');
从上面的代码可以看出,<jsp:forward>标签实际上被翻译成了调用pageContext对象的forward方法。因此,<jsp:forward>标签和pageContext.forward方法是完全一样的。但它们有一点不同,虽然<jsp:forward>标签和pageContext.forward方法等效,但是由<jsp:forward>标签翻译成的Java代码在调用完forward方法后,直接通过return语句退出了_jspService方法,也就是说,使用<jsp:forward>标签转发Web资源,不管在<jsp:forward>标签后面有没有静态的内容,都不会被写入out对象的缓冲区,自然也就不会使用PrintWriter对象将信息输出的客户端了。从这一点可以看出,在<jsp:forward>标签后面的内容是不会造成由于同时使用PrintWriter和ServletOutputStream对象而抛出异常的结果的。
从上面的描述可能看出,在JSP页面中使用<jsp:forward>标签转发Web资源将大大降低抛出异常的可能性。但<jsp:forward>标签至少在如下3种情况下仍然会抛出异常:
page指令的buffer属性值为none.
在调用<jsp:forward>标签之前,out对象缓冲区中的内容的大小由于已经超过了缓冲区的大小,从而被刷新了。
显示调用out.flush方法刷新out对象缓冲区。
上面的3种情况之所以会抛出异常,是由<jsp:forward>标签的一个特性决定的,由于调用<jsp:forward>标签时,out对象缓冲区会被清空,而在调用clear方法清空缓冲区时,不能在此之前调用flush来刷新缓冲区,否则会抛出IOException异常。因此,在调用<jsp:forward>标签之前,不能通过任何方式刷新out对象的缓冲区。
对于上述情况的第一种,如果将buffer属性设为none,那么只要有一个字节的数据被写入out对象的缓冲区,该缓冲区都会被刷新。而对于第二种和第三种情况则毫无疑问会刷新缓冲区。但第三种情况则仍然会在浏览器中显示out对象缓冲区中的内容,而抛出的异常将在Tomcat控制台中显示(这种情况将会抛出java.io.IOException:异常)。前两种情况则既会在浏览器中显示异常,也会在Tomcat控制台中显示异常,而且抛出的异常是java.lang.IllegalStateException。
6.5.3 <jsp:param>标签
当使用<jsp:include>和<jsp:forward>标签引入或转发的Web资源需要请求参数时,可以通过<jsp:param>标签进行传递。<jsp:param>标签的语法格式如下:
<jsp:param name="parameterName" value="parameterValue | <%= expression %> | EL"/>
下面的代码通过<jsp:param>标签向<jsp:include>标签引用的included.jsp标签传递一个name请求参数:
<%@ page language="java" pageEncoding="UTF-8"%>
<jsp:include page="included.jsp">
<jsp:param name="name" value="bill" />
</jsp:include>
可以在included.jsp页面中使用${param.name}来获得name请求参数的值。
需要注意的是,如果使用<jsp:param>标签传递中文请求参数时,在默认情况下,将会输出"??".如下面的代码所示:
<%@ page language="java" pageEncoding="UTF-8"%>
<jsp:include page="included.jsp">
<jsp:param name="name" value="比尔" />
</jsp:include>
发生这种情况的原因也很简单,就是<jsp:param>标签在被翻译成的Java代码中的参数名和参数值时按着URL的编码格式进行编码了,所使用的字符集编码是通过request.getCharacterEncoding方法获得的,在默认情况下,通过该方法获得的字符集编码是ISO-8859-1,该字符集编码不支持中文字符,因此会输出"?".
如果要解决这个问题,可以使用setCharacterEncoding方法将字符集编码设成UTF-8.如下面的代码所示:
<%@ page language="java" pageEncoding="UTF-8"%>
<%
request.setCharacterEncoding("UTF-8");
%>
<jsp:include page="included.jsp">
<jsp:param name="name" value="比尔" />
</jsp:include>
<jsp:param>标签在<jsp:forward>标签中的使用方法和<jsp:include>标签完全一样。
6.5.4 <jsp:useBean>标签
<jsp:useBean>标签用于在指定的范围(pageContext、request、session和application)中查找一个指定名称的Java对象,如果在指定的范围存在该对象,则<jsp:userBean>标签直接返回该对象的引用,否则创建一个新的对象,并将这个新对象存储在指定的范围。
<jsp:useBean>标签的id属性用来指定对象名,class属性用来指定要查找或创建的对象所对应的类名。scope属性用来指定搜索范围。该属性可以接受如下四个值:
page:表示<s:useBean>标签将从PageContext对象中搜索指定的对象,或将新创建的对象存储在PageContext对象中。page是scope属性的默认值。
request:表示<s:useBean>标签将从ServletRequest对象中搜索指定的对象,或将新创建的对象存储在ServletRequest对象中。
session:表示<s:useBean>标签将从HttpSession对象中搜索指定的对象,或将新创建的对象存储在HttpSession对象中。
application:表示<s:useBean>标签将从ServletContext对象中搜索指定的对象,或将新创建的对象存储在ServletContext对象中。
下面的代码使用Java代码将一个java.util.Date对象保存在request对象中,并通过<s:useBean>标签来读取该对象,最后输出该对象。
<!-- usebean.jsp -->
<%@ page language="java" pageEncoding="UTF-8"%>
<%
java.util.Calendar calendar = java.util.Calendar.getInstance();
calendar.set(2001, 2,1);
request.setAttribute("myDate", calendar.getTime());
%>
<jsp:useBean id="myDate" scope="request" class="java.util.Date"/>
<%
out.println(myDate);
%>
访问usebean.jsp页面,在浏览器中将输出如下的信息:
Thu Mar 01 13:19:14 CST 2001
在上面的输出信息中,时间是当天的时间,而日期是使用Calendar.set方法设置的日期。从而可以断定,<jsp:useBean>返回的对象实例是保存在request对象中的对象。如果读者将保存在request对象中的myDate对象改成其他的名,<jsp:useBean>标签就会由于未找到相应的对象,而创建一个新的java.util.Date对象,从而输出当天的日期和时间。
6.5.5 <jsp.setProperty>标签
<jsp:setProperty>标签用于设置JavaBean对象的属性。实际上,该标签是通过调用JavaBean的setter方法设置属性值的。<jsp:setProperty>标签的语法格式如下:
<jsp:setProperty name="beanInstanceName" prop_expr />
prop_expr ::=
property="*" |
property="propertyName"|
property="propertyName" param="parameterName"|
property="propertyName" value="propertyValue"
propertyValue ::= string | <%= expression %> | EL
下面是<jsp:setProperty>标签中各个属性的含义:
name(必选):该属性用于指定JavaBean对象实例名,该属性值应与<jsp:useBean>标签的id属性值相同。
property(必选):该属性用于指定JavaBean对象实例的属性名。如果该属性值为 "*",则为JavaBean对象的所有属性赋值
value(可选):该属性用于指定JavaBean对象实例的属性值。value属性可以是普通字符串,也可以是JSP表达式或EL.<jsp:setProperty>标签为将value属性指定的值类型换成JavaBean对象属性的值类型。如果类型无法转换,将抛出异常。如果不指定该属性。则<jsp:setProperty>标签会寻找和property属性值匹配的请求参数,如果找到,会以该请求参数值作为相应的JavaBean对象属性值。如果指定value属性,则property属性值不能为"*".
param(可选):该属性指定将哪一个请求参数赋给指定的属性。如果请求消息中没有param属性所指的请求参数,则<jsp:setProperty>标签什么都不会做,仍然会保留JavaBean对象原来的属性值。value和param属性不能同时使用,它们在同一个<jsp:setProperty>标签中只能出现一个。如果指定param属性,则property属性值不能为"*".
下面是一个JavaBean的代码:
package chapter6;
public class MyBean
{
private String name;
private int age;
// 省略了属性的getter和setter方法
… …
}
下面的代码演示了各种使用<jsp:setProperty>标签的方式:
1. 使用value属性设置JavaBean对象的指定属性值
<jsp:useBean id="myBean" class="chapter6.MyBean"/>
<jsp:setProperty property="name" name="myBean" value = "比尔"/>
<jsp:getProperty property="name" name="myBean"/>
在浏览器地址栏中输入如下的URL来测试上面的代码:
http://localhost:8080/demo/chapter6/setproperty.jsp
2. 使用请求参数设置JavaBean对象的指定属性值
<jsp:useBean id="myBean" class="chapter6.MyBean"/>
<jsp:setProperty property="name" name="myBean" />
<jsp:getProperty property="name" name="myBean"/>
在浏览器地址栏中输入如下的URL来测试上面的代码:
http://localhost:8080/demo/chapter6/setproperty.jsp?name=bill
在访问上面的URL后,name请求参数的值将被赋给MyBean对象的name属性。
3. 使用请求参数设置JavaBean对象中的所有属性值
<jsp:useBean id="myBean" class="chapter6.MyBean"/>
<jsp:setProperty property="*" name="myBean"/>
<jsp:getProperty property="name" name="myBean"/>
<jsp:getProperty property="age" name="myBean"/>
在浏览器地址栏中输入如下的URL来测试上面的代码:
http://localhost:8080/demo/chapter6/setproperty.jsp?name=bill&age=22
在访问上面的URL后,name和age请求参数的值分别将被赋给MyBean对象的name和age属性。
4. 使用param指定为JavaBean对象指定属性赋值的请求参数
<jsp:useBean id="myBean" class="chapter6.MyBean"/>
<jsp:setProperty property="name" name="myBean" param="myname"/>
<jsp:getProperty property="name" name="myBean"/>
在浏览器地址栏中输入如下的URL来测试上面的代码:
http://localhost:8080/demo/chapter6/setproperty.jsp?myname=Mike
在访问上面的URL后,myname请求参数的值将被赋给MyBean对象的name属性。
6.5.6 <jsp:getProperty>标签
在上一节已经使用了<jsp:getProperty>标签,该标签用于输出JavaBean对象中的指定属性值。<jsp:getProperty>标签的语法格式如下:
<jsp:getProperty name="beanInstanceName" property="propertyName" />
其中name属性值应与<jsp:useBean>标签的id属性值相同。property属性表示JavaBean对象的属性名。关于<jsp:getProperty>标签的使用方法可以参阅上一节的例子。
第7章 表达式语言(EL)
EL是Expression Language(表达式语言)的英文缩写。EL最初是在JSTL(JSP Standard Tag Library)1.0中定义的。有了EL,使得网页设计人员无需精通更复杂的编程语言(如Java)就可以访问和操作应用程序数据。为了使EL更加成功,Sun公司从JSTL1.1开始将EL从JSTL中剥离出来,使其成为JSP2.0规范的单独的一部分,并为EL增加了很多新的功能。
7.1 EL概述
EL表达式是一种被设计用来满足表现层需求的语言,基本语法格式为"${表达式}".当JSP引擎在翻译JSP页面的过程中遇到"${表达式}"这样的字符串时,JSP引擎就会将"${…}"中的内容提取出来作为EL表达式来处理。"${表达式}"中的表达式必须符合EL的语法,该语法具有如下特点:
1. 在EL表达式中可以直接引用Java变量,并且可以通过嵌套属性的方式访问Java对象中的属性,如下面的代码如下:
<jsp:useBean id="date" class="java.util.Date"/>
<!-- 访问date变量 -->
${date}
<jsp:useBean id="myBean" class="chapter6.MyBean"/>
<jsp:setProperty property="age" name = "myBean" value="20" />
<!-- 通过嵌套属性方式访问myBean对象的age属性 -->
${myBean.age}
2. 在EL表达式中可以执行基本的关系运算、逻辑运算、算术运算、条件运算,并且可以使用empty操作符。下面的EL表达式输出的结果为15.0:
${(4+5) * 20 / 12}
3. 在EL表达式中可以使用自定义函数来完成一些更复杂的工作。EL表达式的自定义函数由Java语言编写。实际上,一个自定义函数就是一个Java类的静态方法。如下面的EL表达式调用了一个自定义函数:
${fun:invoke("abcd")}
其中fun是invoke所在类的别名,invoke是自定义函数名, abcd是传递给自定义函数的参数。
4. 在EL表达式中提供了一系列的内置对象,如pageContext、requestScope等,通过这些内置对象,EL表达式可以访问JSP页面中的各种信息。如通过requestScope对象可以请求域中的属性信息。如果不使用EL表达式,要获得这些信息必须在JSP页面中编写复杂的Java代码。
5. EL表达式的语法非常宽松,尽量提供默认值和类型转换,以使得尽可能少地输出错误信息。
由于"${"是EL表达式的开始标记,因此,JSP引擎不会直接输出这个字符串。要想将"${"直接输出到客户端,需要对"$"字符使用反斜杠"\"对"$"字符进行转义。如要输出"An expression is ${(4 + 5) * 20}",可以使用如下的代码:
An expression is \${(4+5)*20}
如果"${"作为"${…}"内部的表达式,如可以使用如下的代码来输出"${":
${"${"}(4+5)*20}
7.2.1 在JSP页面中使用EL
EL表达式最简单的使用方法就是将其直接放到JSP页面中。JSP引擎在遇到"${…}"时,会将里面的内容作为EL表达式来处理。并且将EL表达式的执行结果作为JSP页面的静态部分在表达式所在的位置输出。如在JSP页面中有如下的内容:
1 + 3 = ${1 + 3}
JSP引擎在翻译上面的代码时,会将如下的内容输出到客户端:
1 + 3 = 4
如果要客户端输出HTML或XML格式的内容,由于这些文档的内容包含了一些特殊字符,因此,最好不要使用EL表达式来输出这些具有特殊格式的内容,而要使用JSTL标签<c:out…/>标签(<c:out…/>标签是一个JSTL标签,将在第9章详细讲解)输出,这是由于<c:out…/>标签在默认情况也可以对HTML或XML格式的内容中的特殊字符进行转换,以使这些特殊字符可以正常在浏览器中显示。
7.2.2 在标签属性中使用EL表达式
EL表达式可以使用在任何接收动态内容的标签属性中。在这些属性中既可以只包含一个的EL表达式,也可以包含多个EL表达式和静态文本。
标签属性中只包含一个EL表达式的语法如下:
<prefix:tag value = "${表达式}" />
下面是标签属性包含一个单独EL表达式的示例代码:
<jsp:setProperty property="age" name = "myBean" value="${requestScope.abc}" />
<c:out value="${myBean.name}" />
标签属性中包含多个EL表达式和静态文本的语法如下:
<prefix:tag value="The first is ${value1}, the second is ${value2}" />
JSP引擎在翻译标签属性时,会将其中的EL表达式的执行结果作为属性的静态内容插入到表达式所在的位置。如果EL表达式执行的结果不是字符串类型,系统将会对其进行类型转换,如下面的代码所示:
<c:out value="I'm a ${value1}. I like ${value2}" />
value属性中的两个EL表达式在被执行完后,会将它们的执行结果分别插入到表达式所在的位置,然后再进行输出。
7.2.3 使用isELIgnored属性禁止EL表达式
JSP在2.0以前不支持EL表达式,因此,在这些老版本的JSP页面中,如果包含了"${…}"格式的信息,将会被当作普通的字符串来处理。如果这些老版本的JSP页面被移植到支持新版JSP标准(2.0及以上版本)的JSP引擎上,系统就会将"${…}"格式的信息当成EL表达式来处理。这就可能会使同一个JSP页面中不同版本的JSP引擎中运行结果不一致。
为了使JSP引擎向下兼容,在page指令中提供了一个isELIgnored属性,通过将该属性设为true,可以将高版本的JSP引擎的EL表达式功能关闭。也就是说,当isELIgnored属性为true时,支持JSP2.0及以上版本的JSP引擎会将"${…}"当成是普通字符串处理。
看下面的JSP代码:
<!-- elignored.jsp -->
<%@ page isELIgnored="true" pageEncoding="UTF-8"%>
<jsp:useBean id="date" class="java.util.Date"/>
当前的日期是:${date}
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter7/elignored.jsp
浏览器显示的效果如图7.1所示。
图7.1 使用isELignored属性禁止EL
7.2.4 在web.xml中禁止EL表达式
虽然可以通过page指令的isELIgnored属性禁止在JSP页面中使用EL表达式,但是对每个JSP页面都设置isELIgnored属性就变得非常麻烦,因此,也可以在web.xml文件中禁止在所有或部分JSP页面中使用EL表达式语言。如果要在当前应用程序所有的JSP页面中禁止使用EL表达式,可以使用如下的配置代码:
<web-app …>
… …
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</jsp-config>
</web-app>
如果只想禁止在部分的JSP页面中使用EL表达式,可以使用如下的配置代码:
<web-app …>
… …
<jsp-config>
<jsp-property-group>
<url-pattern>/chapter7/*</url-pattern>
<el-ignored>true</el-ignored>
</jsp-property-group>
</jsp-config>
</web-app>
上面的配置代码禁止在chapter7目录及其子目录中所有的JSP页面中使用EL表达式。
JSP页面的设计者也可以通过isELIgnored属性来覆盖web.xml中的配置。虽然在web.xml文件中禁止在JSP页面中使用EL表达式,但可以通过将isELIgnored属性值设为false的方式单独打开某个JSP页面的EL表达式功能。也就是说,如果既在web.xml文件配置了JSP页面是否支持EL表达式,也在JSP页面中使用page指令的isELIgnored属性设置了JSP页面是否支持EL表达式,那么以JSP页面中的isELIgnored属性的设置为准。
7.2.5 在web.xml中禁止Java代码
在JSP页面中使用EL表达式可以完成一些基本的功能,并且会使JSP页面变得更加整洁。但在某些时候,开发人员总爱在JSP页面中编写一些Java代码。虽然Java代码功能强大,但在JSP页面中加入大量的Java代码会使用页面更加混乱。因此,良好的编程习惯是在JSP页面中只使用EL表达式或标签。为了更有效地规范这个习惯,在web.xml中提供了一个<scripting-invalid>元素可以关闭JSP页面对Java代码的支持,如果将<scripting-invalid>元素值设为true,则在JSP页面中加入<%…%>或<%=…%>后,JSP页面就会抛出异常。
禁止在JSP页面中使用Java代码的完整配置代码如下:
<web-app …>
… …
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern>
<scripting-invalid>true</scripting-invalid>
</jsp-property-group>
</jsp-config>
</web-app>
7.3.1 内置对象与域对象
在处理EL表达式的标识符时,会先判断标识符是否为EL的内置对象,如果为EL的内置对象,则按内置对象来处理,如果不是EL内置对象,则会将表达式中的表示符当成域对象来处理。相当于pageContext.findAttribute方法返回域属性中的相应对象。如果标识符在域中未找到相应的对象,则什么都不会输出,也就是说返回结果为null.
表7.1列出了所有的EL内置对象及其作用。
表7.1 EL内置对象及其作用
表7.1所示的11个EL内置对象中只有pageContext对象和JSP中的pageContext完全对应,其他10个内置对象都是Map对象。通过这10个对象只能访问相应的key-value对,并不能操作这些内置对象所对应的JSP内置对象的方法、属性。
如果EL表达式中的标识符和内置对象重名,系统会将该标识符当作EL内置对象处理。如下面的代码所示:
<!-- elobject.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
request.setAttribute("requestScope", "myRequest");
request.setAttribute("pageContext1", "pageContext1");
session.setAttribute("pageContext", "pageContext");
%>
<!-- 下面两个EL表达式中的表示符被当成EL内置对象处理 -->
${requestScope}<br>
${pageContext}<br>
<!-- 下面的EL表达式中的表示域被当成域属性处理 -->
${pageContext1}
上面的JSP页面的运行结果如图7.2所示。
图7.2 按内置对象处理EL表达式中的标识符
7.3.2 获得域属性集合的内置对象
pageScope、requestScope、sessionScope和applicationScope四个EL内置对象分别对应page、request、session和application四个域的属性集合。这四个EL内置对象可以使用如下两种方法访问域属性集合中的对象:
1. 获得特定域属性集合中的对象:这种方法需要指定要获得哪个域的属性。如下面的代码将获得request域中的name属性:
${requestScope.name}
上面的代码相当于如下的Java代码:
<%
out.println(request.getAttribute("name"));
%>
2. 按顺序搜索每个域中的属性:这种方法不需要指定域,只需要指定域中的属性。系统会依次从page、request、session和application四个域中搜索该属性。直到发现该属性为止。也就是说,如果在request域中找到该属性,则不会再继续搜索下一个域。如下面的代码输出了name属性的值:
${name}
上面的代码相当于如下的Java代码:
<%
out.println(pageContext.findAttribute("name"));
%>
7.3.3 pageContext内置对象
EL表达式中的pageContext对象相当于JSP内置对象中的pageContext.在EL表达式中可以通过pageContext对象访问其他的JSP内置对象。这也正是EL表达式语言要引入pageContext对象的原因。下面的代码演示了如何用pageContext对象来访问out、page以及ServletConfig:
<!-- pagecontext.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
out对象缓冲区大小:${pageContext.out.bufferSize}<br>
由当前JSP页面生成的Servlet类名:<br>${pageContext.page.class}<br>
配置默认Servlet的名称:${pageContext.servletConfig.servletName}
上面的JSP页面的运行结果如图7.3所示。
图7.3 使用pageContext对象获得JSP的其他内置对象
7.3.4 获得请求参数集合的内置对象
EL表达式中的param和paramValues对象都可以获得请求参数集合,它们的区别是param对象返回的Map对象的value是String类型,而paramValues对象返回的Map对象的value是String[]类型。因此,paramValues对象可以用于获得可能有重名的请求参数集合。而param对象用于获得没有重名的请求参数集合。如要获得请求参数name的值,可以使用如下的代码:
${param.name}
${paramValues.name[0]}
如果使用paramValues对象返回Map对象时,由于value是一个String数组,即使没有重名的请求参数,value的类型仍然为只有一个元素的String数组,因此,必须使用name[0]来输出name请求参数的值。
如果不为param和paramValues对象指定请求参数,则输出所有的请求参数,代码如下:
<!-- param.jsp -->
${param}<hr>
${paramValues}
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter7/param.jsp?name=bill&age=22
浏览器显示的效果如图7.4所示。
图7.4 使用param和paramValues对象输出所有的请求参数
7.3.5 获得HTTP请求头消息集合的内置对象
EL表达式中的header和headerValues对象都可以获得HTTP请求消息头字段集合,它们的区别是header对象返回的Map对象的value是String类型,而headerValues对象返回的Map对象的value是String[]类型。因此,headerValues对象可以用于获得可能有重名的请求消息头字段集合。而header对象用于获得没有重名的请求消息头字段集合。如要获得HTTP请求消息头的cookie字段,可以使用如下的代码:
${header.cookie}
${headerValues.cookie[0]}
如果使用headerValues对象返回Map对象时,由于value是一个String数组,即使没有重名的请求消息头字段,value的类型仍然为只有一个元素的String数组,因此,必须使用cookie[0]来输出cookie字段的值。
不为header或headerValues指定请求消息头字段,则输出所有的请求消息头字段的值,代码如下:
${header}<hr>
${headerValues}
在运行上面的JSP代码时,输出的结果如图7.5所示。
图7.5 使用header和headerValues对象输出所有的HTTP请求消息头字段
从图7.5所示的输出信息可以看出,在输出由headerValues对象返回的请求消息头字段集合时,只输出了字段值的String[]数组地址。而由header对象返回的请求消息头字段集合时,同时输出的字段名和字段值。
7.3.6 cookie内置对象
EL表达式中的cookie对象表示所有Cookie信息的集合。实际上,cookie对象返回的Map对象的value是Cookie类型。使用cookie对象的好处是可以直接通过Cookie名来获得Cookie值。而如果通过HTTPServletRequest.getCookies方法获得指定的Cookie,必须得扫描该方法返回的Cookie对象数组才能获得指定的Cookie对象。如果多个Cookie共用一个名称,Cookie对象数组中第一个与其对应的Cookie对象。
<!-- cookie.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%
Cookie cookie = new Cookie("product", "bike");
response.addCookie(cookie);
%>
${cookie.product}<hr>
${cookie.product.name} = ${cookie.product.value}
由于cookie对象是从HTTP请求消息头的cookie字段中提取Cookie信息的。而在第一次执行上面的JSP代码后,由于HTTP请求消息头中并没有cookie字段,因此,第一次执行上面的JSP代码并不会输出Cookie名和Cookie值,当再次执行上面的JSP代码后,就会输出如图7.6所示的信息。
图7.6 使用cookie对象输出Cookie名和Cookie值
7.3.7 initParam内置对象
EL表达式中的initParam对象可以获得Web应用程序中的初始化参数值。相当于调用ServletContext.getInitParameter方法返回的初始化参数值。Web应用程序的初始化参数可以在server.xml或web.xml文件中配置。
在server.xml文件中指定初始化参数,可以使用如下的配置代码:
<Context docBase="demo" path="/demo" reloadable="true"
source="org.eclipse.jst.jee.server:demo">
<Parameter name = "myParam" value = "newValue " override="true" />
</Context>
在web.xml文件中指定初始化参数,可以使用如下的配置代码:
<web-app … >
<context-param>
<param-name>companyName</param-name>
<param-value>Sun公司</param-value>
</context-param>
… …
</web-app>
可以使用如下的EL表达式来输出myParam和companyName参数的值:
<!-- initparam.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
${initParam.myParam}<br>
${initParam.companyName}
7.4.3 EL中的常量
EL中的常量又称字面量(Literal)。常是是不可改变的数据。在EL中有以下几种类型的常量:
1. 布尔(Boolean)类型常量
布尔常量只有两个值:true和false.该常量可用在条件判断中,也可以在EL表达式中直接输出,如${true}将输出true.
2. 整数(Integer)类型常量
整型常量和Java的十进制的整型常量(被声明为final的变量)的取值范围相同。也就是说,整型常量的取值范围在Long.MIN_VALUE和Long.MAX_VALUE之间。
3. 浮点(Floating point)类型常量
浮点类型常量的Java的双精度浮点类型常量的取值范围相同,取值范围在Double.MIN_VALUE和Double.MAX_VALUE之间。
4. 字符串(String)类型常量
字符串常量是由单引号或双引号括起来的一连串字符。由于字符串常量需要使用单引号或双引号括起来,所以如果字符串中包含单引号或双引号,就需要使用反斜杠(\)进行转义,如果字符串中包含有反斜杠,也需要使用反斜杠来进行转义,例如,"\\"表示字符串中的反斜杠。
如果字符串是被双引号括起来的,则单引号不需要转换,但单引号要成对出现,如${"a'b'c"},如果单引号个数为奇数,则会抛出如图7.7所示。
如果对单引号使用反斜杠,则会抛出如图7.8所示的异常。
图7.7 奇数个单引号抛出异常
图7.8 对单引号使用转义符抛出的异常
综上所述,如果在由双引号括起来的字符串中,单引号必须成对出现,而且不能对单引号使用转义符。但可以对双引号使用转义符,例如${"a\"b"}可以输出"a"b".
对于由单引号括起来的字符串正好和双引号括起来的字符串相反,也就是说,双引号必须成对出现,而且不能对双引号使用转义符。但可以对单引号使转义符,例如${'a\'b'}可以输出"a'b".如果违反这个规则,将抛出如图7.7或图7.8所示的异常。
5. Null常量
Null常量用于判断某个对象是否为空,该常量只有一个值,用null表示。例如,${param==null}输出的值为false(由于param是EL的内置对象,因此,param对象不可能为空)。
7.4.4 EL中的变量
在EL表达式中可以直接使用变量来引用EL内置对象或域对象。例如,${name},EL引擎会先判断"name"是否为EL内置对象的标识符,如果不是,则调用PageContext.findAttribute方法依次在page、request、session和application四个域中查找名为"name"的域对象,如果找到该对象,则输出它的值,否则输出空串(实际上是返回了null,但EL会使用空串代替null进行输出)。
从上面的描述可以看出,EL变量并不是预先对某个对象的引用,而只是对EL表达式的引用。在EL引擎翻译该变量表达式时,会根据该变量标识符是否为EL内置对象的标识来决定是按着EL内置对象处理,还是域对象来处理。
7.4.5 EL中的枚举类型
枚举类型是Java SE5新增加的特性。使用enum关键字来定义枚举类型,如下面的代码所示:
enum MyEnum{ABC, XYZ}
如果在Java代码中使用枚举类型,可将枚举类型中的值当成常量来处理,也可以使用字符串来为枚举类型变量赋值,便必须使用EnumvalueOf方法将字符串转换成枚举类型。下面的代码演示了Java代码操作枚举类型变量的过程:
<%!
enum Seasons{SPRING, SUMMER, AUTUMN, WINTER}
%>
<%
Seasons season = Seasons.SPRING;
out.println(season);// 输出SPRING
// 使用字符串为枚举类型变量赋值
season=Enum.valueOf(Seasons.class, "AUTUMN");
out.println(season);// 输出AUTUMN
%>
如果直接枚举类型变量,则会将变量值当成字符串输出。
在EL表达式中也可以直接输出枚举类型变量,也可以对枚举类型变量进行逻辑判断。但要将枚举类型中的值当成字符串来处理,也就是要将枚举类型的值用单引号或双引号括起来。下面的代码演示了如何在EL表达式中来使用枚举类型变量:
<!-- enum.jsp -->
<%@ page language="java" pageEncoding="UTF-8"%>
<%!
enum Seasons{SPRING, SUMMER, AUTUMN, WINTER}
%>
<%
Seasons season = Seasons.SPRING;
request.setAttribute("season", season);
%>
<!-- 输出SPRING -->
\${season}:${season}<br>
<!-- 输出true -->
\${season == "SPRING" }:${season == "SPRING" }<br>
<!-- 输出false -->
\${season == "SPRING" }:${season == 'AUTUMN' }<br>
<!-- 如果请求参数为SPRING,输出true,否则输出false -->
\${season == "SPRING" }:${season == param.season}
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter7/enum.jsp?season=AUTUMN
浏览器输出的信息如图7.9所示。
图7.9 在EL表达式中使用枚举类型变量
在EL表达式中用字符串来代替枚举类型值进行逻辑判断时,必须要考虑开字符串的大小写。也就是说,season请求参数的值必须是Seasons枚举类型中的四个值,而且大小写要一致,否则enum.jsp页面中最后一个EL表达式将抛出异常。
注意:由于目前很多开发JSP的IDE(如MyEclipse等)还不支持在EL表达式中对枚举类型的变量进行逻辑判断,例如,${session="SPRING"},因此,在这些IDE中编写JSP页面时,如果在EL表达式使用枚举类型的变量进行逻辑判断,IDE可能会提示语法错误,不过这并不影响JSP的运行。读者在使用IDE开发JSP页面时应注意这一点。
第8章 Java Web国际化
随着Internet的普及,很多Web应用程序可能要被很多国家或地区的用户访问,为了适应不同国家或地区的用户的习惯,Web应用程序必须支持国际化功能。实现国际化功能最直接的方法就是为每一个国家或地区的用户单独设计页面,但这样做工作量会很大,也不易维护和升级。为了解决这个问题,现在普遍的做法是将需要国际化的资源信息保存在资源文件中,并根据本地信息来读取相应资源文件中的国际化信息。
8.1 Web程序国际化的原理
国际化程序需要通过Locale对象确定具体的本地信息。在Web程序中,可以通过HttpServletRequest类的getLocale方法获得客户端浏览器支持的首选本地信息(Locale对象)。创建Locale对象需要指定语言和国家,在Web程序中这些信息一般是由HTTP请求消息头的Accept-Language字段指定这些信息。
查看浏览器发给服务端的Accept-Language字段值的方法有很多。在这里笔者推荐使用HTTP监视软件(如HTTP Analyzer)来截获HTTP请求消息头。读者可以在浏览器中访问任何一个本地或Internet上的网址,如http://nokiaguy.blogjava.net,HTTP Analyzer截获的HTTP请求消息头如图8.1所示。
图8.1 HTTP请求消息头
从图8.1所示的HTTP请求消息头可以看出,Accept-Language字段的内容如下:
Accept-Language:zh-cn,en-us;q=0.5
浏览器支持的所有本地信息都包含在Accept-Language字段中,如果有多个本地信息,中间用逗号(,)分隔。HttpServletRequest类的getLocale方法会根据这些信息返回相应的Locale对象。实际上,Accept-Language字段的信息和浏览器的设置有关,在IE浏览器中通过单击"工具"|"Internet选项"菜单项打开"Internet选项"对话框,单击"语言"按钮打开"语言首选项"对话框。Accept-Language字段的值就是在"语言首选项"对话框中设置的值。在笔者的机器上的"语言首选项"对话框如图8.2所示。
图8.2 "语言首选项"对话框
如果使用图8.2所示的设置,在访问服务端资源时,IE发送的HTTP请求消息头中的Accept-Language字段值就会和图8.1所示的Accept-Language字段值相同。
HttpServletRequest类除了getLocale方法外,还有一个getLocales方法用来获得客户端支持的所有本地信息。下面的程序列出了客户端浏览器的首选本地信息和支持的所有本地信息:
package chapter8.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.Enumeration;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class ListClientLocale extends HttpServlet
{
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
Locale locale = request.getLocale();
out.println("首选的语言和国家<p/>");
out.println("语言:" + locale.getLanguage() + "<br>");
out.println("国家:" + locale.getCountry() + "<hr>");
out.println("客户端浏览器支持的所有本地信息列表,按优先级的高级排序<p/>");
Enumeration<Locale> allLocale = request.getLocales();
while(allLocale.hasMoreElements())
{
Locale loc = allLocale.nextElement();
out.println(loc.getLanguage() + "-" + loc.getCountry() + "<br>");
}
}
}
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter8/ListClientLocale
浏览器显示的输出结果如图8.3所示。
如果将8.2所示的两个本地信息调换,再次访问上面的URL,将会得到如图8.4所示的输出结果。
图8.3 显示客户端的首选本地信息和支持的所有本地信息
图8.4 显示首选本地信息和支持的所有信息
从图8.4所示的输出结果可以看出,首选的本地信息变成了英文(美国),而浏览器支持的所有本地信息的顺序也变化了。
【实例8-1】 编写国际化的Web程序
本实例演示了如何在Web程序中根据客户端支持的本地信息显示不同语言的信息。在本例中通过改变IE的默认语言来模拟中文和英语的用户。
1. 建立中文资源文件
在WEB-INF\classes\resources目录中建立一个I18nResource_zh_CN.properties文件,该文件的内容如下:
i18n.welcome=欢迎访问国际化web程序
i18n.datetime = 今天是{0, date, long}, 现在的时间是 {0, time, long}.
i18n.message = 我买了{0, number}本英语书, 共花费 {1, number, currency}.现在是学习英语的时间。请不要打扰我!
2. 建立英文资源文件
在WEB-INF\classes\resources目录中建立一个I18nResource_en_US.properties文件,该文件的内容如下:
i18n.welcome=welcome to the internationalization web program!
i18n.datetime = Today is {0, date,long}, time is {0, time,long}.
i18n.message = I bought {0, number} English books, and spent {1, number,currency}. Now is the time to learn English. Please Don't bother me!
3. 编写I18nServlet类
I18nServlet是一个Servlet类,负责根据客户端浏览器的默认语言将相应语言的国际化信息输出的客户端。I18nServlet类的实现代码如下:
package chapter8.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Locale;
import java.util.ResourceBundle;
import java.text.MessageFormat;
import java.util.Date;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class I18nServlet extends HttpServlet
{
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
Locale locale = request.getLocale();
// 装载相应语言的资源文件
ResourceBundle rb =
ResourceBundle.getBundle("resources.I18nResource", locale);
// 读取资源文件的内容
String webcome = rb.getString("i18n.welcome");
String datetime = rb.getString("i18n.datetime");
String message = rb.getString("i18n.message");
// 输出webcome
out.println(webcome + "<p/><hr>");
MessageFormat mf = new MessageFormat(datetime, locale);
// 输出datetime,并指定相应的占位符的参数值
out.println(mf.format(new Object[]{new Date()})+ "<br>");
mf.applyPattern(message);
// 输出message,并指定相应的占位符的参数值
out.println(mf.format(new Object[]{5, 332}));
}
}
4. 测试
在IE地址栏中输入如下的URL:
http://localhost:8080/demo/chapter8/I18nServlet
如果IE的默认本地信息是"中文(中国)",则在浏览器中显示的信息如图8.5所示。
图8.5 中文(中国)本地环境下的显示效果
从图8.5所示的显示效果可以看出,I18nResource向客户端输出了中文信息,而且日期、时间和货币信息都符合中文习惯。由此可以断定,I18nResource类读取的是I18nResource_zh_CN.properties文件中的国际化信息。
将IE的默认本地环境改为"英语(美国)".刷新图8.5所示的页面,输出的信息如图8.6所示。
图8.6 英语(英文)本地环境下的显示效果
从图8.6所示的显示效果可以看出,当IE的默认本地信息变成"英语(美国)"时,I18nResource类就会读取I18nResource_en_US.properties文件,而且日期、时间和货币的信息都变成了英语的习惯。
5. 程序总结
在Web程序中进行国际化,不仅要在获得ResourceBundle对象时指定Locale对象,而且要在创建MessageFormat对象时也指定Locale对象。否则就会出现文本信息根据指定的本地信息显示,但日期、时间和货币等信息却按着服务端的默认本地信息来显示。读者在国际化Web程序时要注意这一点。
8.2 GMT、UTC和本地时间
时间从宇宙诞生之日起就存在,时间也和我们现在的生活息息相关。在软件系统中更是经常会处理和时间相关的问题。如果读者使用的是Windows操作系统,会在任务栏的右边显示当前的时间。这个时间一般就是用户所有国家或地区的本地时间。
由于地球自转的原因,产生了24个时区。也就是说,将地球按经度划分,每15度是一个时区。每相临的两个时区相差1个小时。这就会带来一个非常大的问题。由于世界不同的国家或地区分布在不同的时区中,因此,各地的本地时间是不同的。如地区A的本地时间是22:00,而地区B的本地时间是12:00.这就相当于地区A已经是晚上了,而地区B刚到中午。在对于同一个时区的活动基本不会有影响。但不同时区由于时间不统一,有可能会造成时间混乱。
假设有一个面向全球用户的Web系统。服务端在美国洛杉矶,而在韩国也有该Web系统的用户。对于韩国的用户来说,系统当前显示的时间是韩国的本地时间。正好该系统需要用户录入日期和时间,并将用户录入的日期和时间保存在服务端的数据库中。在韩国的用户肯定录入的是韩国本地的时间。而这时法国的用户要浏览韩国用户录入的数据。但法国的用户看到的是韩国本地的时间,而由于时区不同,韩国的本地时间在法国又是另外一个时间。因此,法国用户必须将该时间转换成法国本地的时间。这虽然从理论上完全可行,但是对于大多数用户来说,自己进行时间转换相当不现实。
既然不能让用户自己来进行时间转换,那这个工作就只好由Web系统来完成了。最理想的效果是无论哪一个国家或地区的用户浏览网页时,系统都会显示本地的时间。要想达到这个目的,最简单的方法就是有一个参照物。就象这个世界上存在着各种货币,由于这些货币的汇率不同,就需要找到一种货币,并这种货币世界标杆,其他的所有货币的汇率都相对于这种世界货币。这种世界货币就是美元。
就象从若干种货币中选中一种货币作为世界货币一样。由于地球分为24个时区,因此,就需要从这24个时区中选中一个时区作为标准时区,其他时区都根据这个标准时区的本地时间加上或减去整数个小时,就可以得到当前时区的本地时间了。
在1884年美国华盛顿召开的国际大会上通过的协议选中了英国伦敦伦敦泰晤士河南岸的一个叫格林威治小镇所在的时区作为标准时区。这个时区的本地时间被称为格林威治时间,简称GMT(Greenwich Mean Time)。其他时区的本地时间都在GMT时间的基础上加或减去若干个整数小时。如北京的本地时间是GMT+8,也就是说,如果GMT时间是2008年10月20日11:20:12,那么北京时间就会在这个时间的基础上加8个小时,也就是2008年10月20日19:20:12.由于某些地区实行夏令时,因此,在实行夏令时的地区的本地时间还需要加1小时。在英国伦敦每年4月到10月实行夏令时,因此,在4月到10月,北京时间和伦敦时间差了7个小时,而不是8个小时。而1至3月、11月和12月北京时间和伦敦时间差了8小时。因此,在前面的伦敦时间2008年10月20日11:20:12对应的北京时间应该是2008年10月20日18:20:12.
如果读者使用的是Windows操作系统,可以在"日期和时间 属性"对话框的"时区"页中将当前时间设为伦敦时区,如图8.7所示。
图8.7 设置当前时区
如果读者的机器在设置时区之前是的时间是北京所在的时区(GMT+8),并且月份在4月至10月之间,在设置完时区后,会发现任务栏右边的时间减了7小时。如果是其他的月份,则会减8小时。
由于地球自转轴和轨道角度的影响,GMT时间往往和世界标准时间有一些偏差,因此,需要使用世界协调时间进行修正,这种时间就叫UTC(Coordinated Universal Time)。虽然GMT和UTC存在着一定的偏差,但这种偏差很小。因此,在通常情况下,可以将GMT和UTC视为同一个时间。
从上面的描述可知,伦敦的格林威治镇的本地时间(GMT)被作为其他时区本地时间的参照时间,其他时区的本地时间都相对于GMT加或减若干个整数小时。如北京所在的时区是东八区(GMT+8),因此,北京时间要比伦敦时间快了8个小时(夏令时快了7小时)。UTC虽然和GMT有一定的偏差,但通常将两个时间看作同一个时间。在实际的系统中,如果保存在服务端的时间需要为不同时区的用户服务,最好直接保存GMT,当不同时区的用户要查看保存在服务端的时间,系统再根据用户所在的时区将时间自动转换成用户本地的时间。
8.3 将本地时间转换成GMT
JDK中提供了一个java.util.Date类,该类是Java中用来处理时间的类。在Date类中有很多和日期/时间相关的方法,如getDate、getDay等,但这些方法都是Date类的遗留产物,这些方法在以后的JDK版本中可以被去掉,因此,并不建议在程序中使用这些方法。不过Date类中有少数的方法不在这些方法之列,这些方法仍然可以在程序中放心地使用。其中getTime和setTime方法是经常被用到的。getTime方法返回从1970年1月1日0时0分0秒GMT到当前时间的毫秒数。setTime方法用于设置日期/时间对应的毫秒数。要注意的是,Date对象中封装的毫秒数是GMT时间,并不是本地的时间。看如下的代码:
java.util.Date date = new java.util.Date();
System.out.println(date.getTime());
System.out.println(date);
上面代码中的第2行代码输出了GMT时间的毫秒数。第3行输出了本地时间。将操作系统设为不同的时区,并执行上面的代码。读者会发现,在不同的时间,输出的毫秒数基本保持不变(毫秒数会有一定的变化,但毫秒数之差只是读者切换时区和再次运行程序所花的时间,可能只有几秒钟),而输出的本地时间却相差数小时。假设两次设置的时区分别是GMT+8和GMT-8,两次输出的本地日期将会相差16个小时,可能还不是同一天。
从上面的实验结果可知,getTime方法返回的始终是以GMT计算的毫秒数,无论操作系统的时区怎样改变,GMT时间始终不会变。而在输出本地日期/时间时,Date类在内部通过时差和夏令时对这个GMT毫秒数进行了处理,因此,输出的本地日期/时间就会和操作系统任务栏右侧显示的时间一致。
如果想获得当前时区和GMT的时间差以及当前年份是否使用了夏令时,可以使用java.util.TimeZone类的getRawOffset和getDSTSavings,代码如下:
java.util.TimeZone tz = java.util.TimeZone.getDefault();
System.out.println("与GMT的时差(毫秒):" + tz.getRawOffset());
System.out.println("是否为夏令时:" + tz.inDaylightTime());
如果当前的时区是GMT+08.00(北京时间),则上面的代码输出的结果如下:
与GMT的时差(毫秒):28800000
是否为夏令时:false
如果当前的时区是GMT-04.00(圣地亚哥),则上面的代码输出的结果如下:
与GMT的时差(毫秒):-14400000
是否为夏令时:true
上面的测试时间是2008年10月22日15:05:00(读者需要将本机的年和月改成2008和10)。
如果时区使用的是夏令时,可以使用TimeZone类的getDSTSavings方法获得夏令时快的毫秒数(3600000毫秒,1小时)
为了对日期/时间进行更多的处理,从JDK1.1开始增加了一个java.util.Calendar类。通过该类的getTimeZone方法也可以获得当前操作系统的默认时区。
如果想获得格林威治标准时间(GMT),可以使用如下的代码:
package chapter8;
import java.util.Calendar;
import java.util.Locale;
import java.text.DateFormat;
public class GMT
{
public static void main(String[] args)
{
Calendar calendar = Calendar.getInstance();
// 获得本地时间相对于GMT的时间差(不考虑夏令时)
int rawOffset = calendar.getTimeZone()。getRawOffset();
int dstSavings = 0;
// 返回当前时区是否使用夏令时
boolean dst = calendar.getTimeZone()。inDaylightTime(calendar.getTime());
if(dst)
{
// 如果当前时区采用了夏令时,则本地时间要快一小时(3600000毫秒)
dstSavings = calendar.getTimeZone()。getDSTSavings();
}
System.out.println("相对GMT的时间差(毫秒):" + rawOffset);
System.out.println("是否为夏令时:" + dst);
dstSavings = (rawOffset > 0) ? dstSavings : (-dstSavings);
DateFormat df = DateFormat.getDateTimeInstance(DateFormat.LONG,
DateFormat.LONG, Locale.getDefault());
// 使用当前的GMT毫秒 、本地时间相对于GMT的时间差和夏令时快的毫秒重新设置当前毫秒数
calendar.setTimeInMillis(calendar.getTimeInMillis() -
(rawOffset - dstSavings));
// 输出当前的GMT
System.out.println("GMT:" + calendar.getTime());
}
}
由于Calendar对象中封装的毫秒是GMT,而使用getTime方法返回的日期是使用时差和夏令时处理过的日期。假设当前时区是GMT+8(北京时间),这时调用getTime方法,系统会将GMT毫秒加上时差(3600 * 1000 * 8毫秒)。如果本地实行夏令时,还会减去3600 * 1000毫秒。如果要让系统输出GMT的时间,就需要先在GMT毫秒上减去这些增量,如下面的代码所示:
calendar.setTimeInMillis(calendar.getTimeInMillis() - (rawOffset - dstSavings));
在运行GMT程序时,无论操作系统的时区如何改变,都会输出一个相对固定的时间。下面是GMT程序的运行结果:
相对GMT的偏移量:28800000
是否为夏令时:false
GMT:Wed Oct 22 13:22:50 CST 2008
如果操作系统的当前时区是GMT+8,上面的输出结果的GMT就是本地时间减8小时。如果操作系统是在其他的时区,则GMT为本地时间减去或加上若干个小时。读者在运行本程序时应注意这一点。
8.4 将GMT转换成本地时间
对于国际化的程序,往往有如下的需求:
在某一时区A的用户向系统提交的数据被保存在了服务端的数据库中。同时,系统也保存了时区A的用户提交请求时的GMT.而在另一个时区B的用户要浏览时区A的用户录入的信息。但时间要显示成时区B的本地时间。
对于上面的需求,最简单的方法是将服务端保存的GMT直接转换成时区B的本地时间。对于Web系统。这个工作可以交给JavaScript来完成。也就是说,服务端将服务端保存的日期/时间的相应值(年、月、日、时、分、秒)传给客户端,然后JavaScript再根据本地的时区将其以本地时间输出(JavaScript会考虑到夏令时)。
JavaScript的Date类可以很容易地完成这个工作。通过Date类的setUTCXxx方法可以设置年、月、日、时、分、秒信息。但要注意所设置的时间是UTC(GMT)时间,不是本地时间。因此,服务端传过来的必须是UTC(GMT)时间。设置年、月、日、时、分、秒的setUTCXxx方法如下:
setUTCFullYear:设置年(4位)。
setUTCMonth:设置月,从0开始,也就是说,0表示1月、1表示2月,以此类推。
setUTCDate:设置日。
setUTCHours:设置小时。
setUTCMinutes:设置分。
setUTCSeconds:设置秒。
除此之外,JavaScript还可以判断操作系统的当前时区在指定时间是否处于夏令时。Date类有一个getTimezoneOffset方法,该方法返回本地时间和GMT的时间差(单位:分钟)。如果当前时区的指定时间不是夏令时,那么将Date对象设为一年中的任何一天,调用getTimezoneOffset方法返回的值都是相同的。如果采用了夏令时,在夏令时的时间和非夏令时的时间使用getTimezoneOffset方法返回的值差了60分钟(1小时),因此,只需要为Date对象指定相应的时间,并判断两个不同时间的Date对象的getTimezoneOffset方法的返回值是否相等就可以得知当前时区的指定时间是否处于夏令时。
由于每个国家或地区的夏令时的开始和结束时间不同,但1月份都会处在非夏令时的时间段,而7月份都会处在夏令时的时间段。因此,可以选中1月1号和7月1号来判断这两天的时差是否相同。
下面的JavaScript代码判断了当前时区指定的时间是否采用了夏令时:
var date = new Date();
date.setUTCFullYear(2002);
date.setUTCMonth(6);// 设置7月
date.setUTCDate(1);// 设置1日
var offset1 = date.getTimezoneOffset();// 返回2002年夏令时和GMT的时间差
date.setUTCMonth(0);// 设置1月
var offset2 = d.getTimezoneOffset();// 返回2002年非夏令时和GMT的时间差
// 如果offset1和offset2不相等,表示2002年采用了夏令时
if(offset1 != offset2)
{
alert("夏令时");
}
为了使用JavaScript向服务端发送请求方便,在本例中使用了prototype组件。该组件只是一个JavaScript文件(prototype.js)。该文件可以从如下的网址下载:
http://www.prototypejs.org/download
在笔者写作本书时,prototype.js的最新版本是1.6,本书使用的也是该版本。
在<Web根目录>建立一个javascript目录,将prototype.js文件放到该目录即可。
【实例8-2】 在Web程序中将GMT转换成本地时间
1. 实例说明
本实例通过Servlet向客户端发送信息。在发送的信息中包含了一个GMT时间的年、月、日、时、分、秒信息。客户端的JSP页面通过prototype组件以同步的方式请求该Servlet,并获得该Servlet返回的信息。然后通过Date对象将Servlet发送的客户端的GMT时间信息转换成本地时间,并显示在页面中。
2. 编写GMTServlet
GMTServlet是一个Servlet类,负责向客户端发送GMT时间信息。该类的实现代码如下:
package chapter8.servlet;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Calendar;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class GMTServlet extends HttpServlet
{
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
Calendar calendar = Calendar.getInstance();
// GMT:2008年10月21日21时16分34秒
calendar.set(2008, 9, 21, 21, 16, 34);
// 设置要发送到客户端的年、月、日、时、分、秒
out.println("{'year':" + calendar.get(Calendar.YEAR) + ",");
out.println("'month':" + calendar.get(Calendar.MONTH) + ",");
out.println("'date':" + calendar.get(Calendar.DATE) + ",");
out.println("'hour':" + calendar.get(Calendar.HOUR_OF_DAY) + ",");
out.println("'minute':" + calendar.get(Calendar.MINUTE) + ",");
out.println("'second':" + calendar.get(Calendar.SECOND) + "}");
}
}
在上面的代码中使用"2008年10月21日21时16分34秒"作为一个GMT.由于客户端使用prototype组件发送、接收请求,并将响应信息转换成JavaScript对象。因此,GMTServlet返回的信息必须符合prototype组件要求的格式(prototype组件采用了json格式标准)。如果只将返回信息映射成简单对象,如obj.name形式。只需要使用如下的格式即可:
{"property1":value1, "property2": value2, … ,}
prototype组件会将符合上面格式的内容转换成JavaScript对象。冒号(:)前面的部分将被作为对象的属性名,后面的部分将作为属性值。如可以通过如下的代码访问该对象的属性:
alert(obj.property1);
根据上面的格式,GMTServlet程序返回的信息应是如下的格式:
{'year': 2008, 'month': 9, 'date': 21, 'hour': 21, 'minute': 16, 'second': 34}
prototype组件在将上面的内容转换成JavaScript对象后,可通过如下的代码访问相关的属性:
alert(obj.year);
alert(obj.month);
3. 编写gmt.jsp
gmt.jsp页面负责通过prototype组件请求GMTServlet,并将GMTServlet返回的GMT时间转换成本地时间,最后将本地时间显示在页面中。gmt.jsp页面的代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<html>
<head>
<!-- 引用prototype组件 -->
<script type="text/javascript" src="/javascript/prototype.js">
</script>
</head>
<body>
<!-- 用于显示本地时间 -->
<div id="div"></div>
<script type="text/javascript">
function requestGMTServlet()
{
// 要请求的url
var url = "GMTServlet";
// 以同步的方式请求GMTServlet
var myAjax = new Ajax.Request(url, {
method :'post',
parameters :'',
onComplete :processGMT,
asynchronous :false
});
}
// 如果GMTServlet成功返回GMT信息,则回调processGMT方法
function processGMT(request)
{
// 将GMTServlet返回的GMT信息转换成JavaScript对象
var obj = request.responseText.evalJSON();
var date = new Date();
// 使用obj对象的属性值设置date对象
date.setUTCFullYear(obj.year);
date.setUTCMonth(obj.month);
date.setUTCDate(obj.date);
date.setUTCHours(obj.hour);
date.setUTCMinutes(obj.minute);
date.setUTCSeconds(obj.second);
// 创建用于判断当前时区的指定时间是否处于夏令时的Date对象
var date1 = new Date();
// 设置date1对象为7月1日
date1.setUTCFullYear(obj.year);
date1.setUTCMonth(6);
date1.setUTCDate(1);
// 返回夏令时和GMT的时差
var offset1 = date1.getTimezoneOffset();
// 设置date1对象为1月
date1.setUTCMonth(0);
// 返回非夏令时和GMT的时差
var offset2 = date1.getTimezoneOffset();
// 获得div标签对象
var div = document.getElementById("div");
// 如果offset1和offset2不相等,则表示当前时区的指定时间处于夏令时
if (offset1 != offset2)
{
div.innerHTML += "夏令时<br>";
}
div.innerHTML += "本地时间:" + date.toLocaleString();
}
requestGMTServlet();
</script>
</body>
</html>
4. 测试非夏令时的情况
读者可以将操作系统的时区设为非夏令时的时区,如GMT+8(北京时间)。在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter8/gmt.jsp
浏览器中显示的信息如图8.8所示。
图8.8 显示非夏令时的时区的本地时间
由于GMT+8时区的本地时间比GMT快了8小时,因此,GMTServlet类中的GMT对应的GMT+8的时间就是"2008年10月22日5:16:34".
5. 测试夏令时的情况
在这一步读者需要将操作系统的时区修改成使用夏令时的时区。如"伦敦"所处的GMT时区。并且将当前月份设为4至10月之间。并刷新图8.8所示的页面。这时浏览器中将显示如图8.9所示的信息。
图8.9 显示夏令时的时区的本地时间
从图8.9所示的效果可以看出,由于当前时区处于夏令时,因此,在页面中显示出了"夏令时"信息。而且由于夏令时要快一个小时,因此,当前伦敦的时间应该比GMT标准时间快了1个小时,所有是22点。
第9章 JSP标准标签库(JSTL)
有很多情况下,JSP页面中也需要很多复杂的操作,如逻辑判断,对集合元素的迭代、分析和使用XML、操作数据库等。虽然这些技术可以通过Java语言很容易地完成,但在JSP页面中嵌入Java语言被认为是一种非常不好的习惯。而JSP标准标签的功能又十分有限。由于从JSP1.1规范开始,允许用户为JSP开发自定义标签。这种标签使用Java语言开发。在使用上和JSP标签类似。由于这些自定义标签开发得越来越多,从而导致了同样的功能可能会有非常多的类似的标签,在这种情况下,开发人员会觉得无所适从,不知选择哪种标签会更好。为了解决这个问题,Apache的开发小组将网页开发人员经常遇到的问题进行了汇总,并开发了一套解决这些问题的自定义标签,这套标签后来被Sun公司定义为标准标签库(JavaServer Pages Standard Tag Library),简称JSTL。
在笔者写作本书时,JSTL的最新规范是1.2,为了在JSP中使用JSTL,读者需要使用支持JSTL的Web容器,如Tomcat6.x。
9.1 JSTL的5个组成部分
JSTL1.2规范定义了5个组成部分。这5个组成部分包括4个标签库和1个EL自定义函数库。为了方便用户,以及使JSP页面更规范。JSP规范描述了引用JSTL的URI地址和建议使用的前缀名。这5个组成部分的功能描述、URI地址和建议使用的前缀名如表9.1所示。
表9.1 JSTL的5个组成部分
9.2 建立JSTL的开发环境
JSTL由三个jar包组成。这三个jar包是jstl.jar、standard.jar和xalan.jar.其中xalan.jar包用于为XML标签库增加处理XPath的能力,如果读者不使用XML标签库,则不需要xalan.jar包。
读者可以从所下的网址下载jstl.jar包和standard.jar包:
http://jakarta.apache.org/taglibs/
从上面的网址可以下载支持JSTL1.1规范的jstl.jar包和standard.jar包。如果读者机器上有MyEclipse6.x,也可以从MyEclipse6.x的安装目录复制相应的jar包。在MyEclipse6.x的安装目录中有一个jstl-1.2.jar文件。该文件将jstl.jar和standard.jar合在了一起,因此,只需要一个jstl-1.2.jar包就够了。读者可以选择jstl.jar、standard.jar或MyEclipse6.x中的jstl-1.2.jar.
如果读者要使用XML标签库,需要从如下的网址下载xalan.jar包:
http://xml.apache.org/xalan-j/
在下载完xalan-j后,将其中的xalan.jar包以及jstl.jar、standard.jar复制到WEB-INF\lib目录即可。
为了证明JSTL已经安装成功,下面来编写一个简单的JSP页面来测试JSTL包。
在JSP页面中使用JSTL需要使用JSP的taglib指令指定标签库的URI和前缀。然后在JSP页面中就可以根据前缀使用JSTL中的标签了,代码如下:
<!-- myjstl.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!-- 使用核心标签库中的out标签输出信息 -->
<c:out value="这是第一个JSTL程序"/>
在上面的代码中使用taglib指令引用了核心标签库,并指定前缀为"c".其中<c:out>是核心库中的标签,用于向客户端输出信息。
启动Tomcat后,在浏览器地址栏中输入如下的URL,浏览器就会输出"这是第一个JSTL程序".
http://localhost:8080/demo/chapter9/myjstl.jsp
如果读者想查看标签库的URI,可以打开standard.jar或jstl-1.2.jar包,进入META-INF目录,就会看到很多。tld文件,如图9.1所示。
图9.1 与JSTL的5个组成部分对应的.tld文件
从图9.1所示的内容可以看出,与表9.1列出的JSTL的5个组成部分相对应的5个。tld文件如下:
c.tld:核心标签库
fmt.tld:国际化标签库
fn.tld:EL自定义函数库
sql.tld:数据库标签库
x.tld:XML标签库
读者可以打开其中的一个。tld文件,如打开x.tld文件,可以找到如下的<uri>元素:
<uri>http://java.sun.com/jsp/jstl/xml</uri>
其中<uri>元素的值就是tablib指令的uri属性值。如果读者忘记了某个标签库的uri,可以采用这种方法查看该标签库的uri。
9.3.1 <c:out>标签
<c:out>标签用于向客户端输出文本内容。如果该标签value属性值是java.io.Reader对象实例,则<c:out>标签会使用Reader类的read方法读取Reader对象中的数据,每次读取4K字节,直到将Reader对象中的数据读完。如果value属性值是其他的对象,<c:out>标签会调用该对象的toString方法来获得要输出的文本内容。不管value属性值是Reader对象还是其他的对象,<c:out>标签都会将要输出的文本内容写到out对象中(JSP的隐含对象),实际上,这个out对象是通过pageContext对象获得的。从上面的描述可以看出,如果输出的文本内容较大时,使用Reader对象将会大大提高系统的性能。
在默认情况下(escapeXML属性值为true),<c:out>标签在输出文本内容时会将特殊字符<,>,&,',"进行HTML编码转换。表9.2是这些特殊字符和相应的字符实体编码对照表。
表9.2 特殊字符转换对照表
如果将escapeXML属性值设为false,则<c:out>标签并不会对这些特殊字符进行转换,而是按着原样输出文本内容。
只有在value属性值为null时,才会输出default属性的值或<c:out>标签体的内容。如果未指定默认值,则输出空串。当value属性值不为null,即使default属性或<c:out>标签体即使有值,<c:out>标签也不会输出这些值。
value属性值为null,并不是指将value属性值直接设为null,如下面的代码所示:
<c:out value="null" default="abcd"/>
上面的代码输出的并不是abcd,而是null.如果要将value属性值设为null,需要使用JSP表达式或EL,代码如下:
<c:out value = "<%= null %>" default = "JSP表达式" />
<c:out value = "${null}" default = "EL" />
上面的两个<c:out>标签都会输出default属性的值。
要注意的是,default属性和<c:out>的标签体不能同时指定默认值(也就是说,default属性和标签体不能同时存在),否则<c:out>标签将抛出异常。
out.jsp页面是一个演示<c:out>标签的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%
request.setAttribute("test", "在value属性值中使用EL");
String jspPath=application.getRealPath(request.getServletPath());
// 读取out.jsp文件的内容
java.io.Reader isr = new java.io.InputStreamReader(
new java.io.FileInputStream(jspPath), "UTF-8");
// 将Reader对象保存在request域中
request.setAttribute("reader", isr);
%>
<c:out value="输出文本内容:abcd" /><hr>
<c:out value="${null}" default = "输出默认值(EL)"/><hr>
<c:out value="<%= null %>" default = "输出默认值(JSP表达式)"/><hr>
<c:out value="${test}"/> <hr>
<!-- escapeXML属性值为true,进行编码转换 -->
<c:out value="${null}">
<input type = "text" name = "txtName"/>
</c:out>
<hr>
<!-- escapeXML属性值为false,未进行编码转换 -->
<c:out value="${null}" escapeXml="false">
<input type = "text" name = "txtName"/>
</c:out>
<hr>
<!-- 输出out.jsp文件的内容,进行编码转换 -->
<c:out value="${reader}" escapeXml="true"/>
使用<c:out>标签输出Reader对象时,如果要读取的文件内容使用了非西欧字符集编码(如UTF-8、GBK等),应使用InputStreamReader类的构造方法指定读取文件内容时所采用的字符集编码(应和要读取的文件内容的字符集编码保持一致)。在本例中由于out.jsp文件使用了UTF-8编码格式,因此,在读取out.jsp页面的内容时应指定UTF-8编码格式。如果读者使用如下的代码输出out.jsp文件的内容,将会输出乱码:
<%
String jspPath=application.getRealPath(request.getServletPath());
java.io.Reader fr = new java.io.FileReader(jspPath);
// 输出Reader对象使用的字符集编码,如果不是UTF-8,<c:out>标签将输出乱码
System.out.println(fr.getEncoding());
request.setAttribute("reader", fr);
%>
9.3.2 <c:set>标签
<c:set>标签用于设置或删除各种Web域的属性。在向Web域中的java.util.Map对象中添加key-value对时,如果Map对象中存在相应的key-value对,则修改value.如果<c:set>标签未指定value,则删除Map对象中的key-value对。除此之外,<c:set>标签还可以设置Web域中JavaBean对象的属性值,或将JavaBean对象的属性值设为null.
在使用<c:set>标签时应注意如下几点:
<c:set>标签不能同时指定value属性和标签体,否则将抛出异常。
如果value属性值或标签体中的内容不能转换成property属性指定的对象属性类型时,<c:set>标签将会抛出异常。
在不指定value属性或标签体的情况下,<c:set>标签将删除var属性指定的Web域中的属性,或删除property属性指定的Map对象的key-value对。如果target属性指定的是JavaBean对象,<c:set>标签会将property属性指定的JavaBean属性的值设为null.如果JavaBean对象的属性类型无法设为null(如int),<c:set>标签并不会抛出异常,而是输出空串。
如果同时指定了var属性和target属性,那么var属性的优先级更高。也就是说,<c:set>标签会优先设置var属性指定的Web域属性的值,而target属性指定的对象将被忽略。
虽然<c:set>标签的所有属性都是可选的,但var属性和target属性必须至少有一个,否则<c:set>标签将抛出JspTagException异常。
如果将var属性和target属性都设为null,那么<c:set>标签也会抛出JspTagException异常。
set.jsp页面是一个演示<c:set>标签的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
java.util.Map<String, String> map = new
java.util.HashMap<String,String>();
chapter9.Message message = new chapter9.Message();
request.setAttribute("map", map );
request.setAttribute("message", message );
%>
使用value属性值设置request域属性的值<br>
<c:set var="webrequestdomain" value = "webrequestdomain_value" scope="request"/>
<c:out value="${requestScope.webrequestdomain}"/><hr>
使用标签体设置session域属性的值<br>
<c:set var="websessiondomain" value = "websessiondomain_value" scope="session"/>
<c:out value="${sessionScope.websessiondomain}"/><hr>
使用value属性值向java.util.Map对象中添加key-value对<br>
<c:set target="${map}" property="map_key" value = "map_value" />
<c:out value="${map.map_key}"/><hr>
使用标签体设置JavaBean对象的属性值<br>
<c:set target="${message}" property="name" value = "property_value" />
<c:out value="${message.name}"/><hr>
request域中的webrequestdomain属性已经被删除
<c:set var="webrequestdomain" scope="request"/>
<!-- 输出空串 -->
<c:out value="${requestScope.webrequestdomain}"/><hr>
Message对象的name属性值已经被设为null
<c:set target="${message}" property="name" />
<!-- 输出空串 -->
<c:out value="${message.name}"/><hr>
其中Message类的实现代码如下:
package chapter9;
public class Message
{
private String name;
public String getName()
{
return name;
}
public void setName(String name)
{
this.name = name;
}
}
9.3.3 <c:remove>标签
<c:remove>标签用于删除Web域中的指定属性。如果在<c:remove>标签中指定了scope属性,则<c:remove>标签使用如下的代码删除Web域中的属性:
pageContext.removeAttribute(var, scope);
如果在<c:remove>标签中未指定scope属性,则<c:remove>标签使用如下的代码删除Web域中的属性:
pageContext.removeAttribute(var);
remove.jsp页面是一个演示<c:remove>标签的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
使用set标签设置request域中的name和age属性的值<br>
<c:set var="name" value="bill" scope="request" />
<c:set var ="age" value = "23" scope="request"/>
<c:out value="name:${name}" /><br>
<c:out value="age:${age}" />
<hr>
使用remove标签删除request域中的name和age属性的值<br>
<c:remove var="name" scope="request"/>
<c:remove var ="age" />
<c:out value="name:${name}" /><br>
<c:out value="age:${age}" />
9.3.4 <c:catch>标签
<c:catch>标签用于捕获标签体抛出的java.lang.Throwable异常。该标签只有一个var属性,用来表示标签体抛出的异常对象。var属性是String类型,不支持动态属性值。如果指定var属性,<c:catch>标签会以var属性指定的名称将异常对象保存在page域中。如果未指定var属性,则<c:catch>标签只捕获异常,不保存异常对象。<c:catch>标签可以捕获任何标签或Java代码抛出的异常。
catch.jsp页面是一个演示<c:catch>标签的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
java.util.Calendar cal = java.util.Calendar.getInstance();
request.setAttribute("cal", cal);
%>
<c:catch var="myException">
<!-- 错误设置了Calendar对象的time属性值,将抛出异常 -->
<c:set target="${cal}" property="time" value="abcd" />
</c:catch>
<!-- 输出异常对象的相应信息 -->
myException:
<c:out value="${myException}" />
<br>
myException.getMessage():
<c:out value="${myException.message}" />
<br>
myException.getCause():
<c:out value="${myException.stackTrace}" />
在上面的程序中,由于使用<c:set>标签将Calendar对象的time属性值设置成了String类型的值,因此会抛出一个异常。在<c:catch>标签结束后,使用了<c:out>标签和EL输出了异常对象的相应属性值。
9.3.5 <c:if>标签
<c:if>标签用于进行条件判断,该标签相当于Java语言中的if(…){…}语句。if.jsp页面是一个演示<c:if>标签的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
将test属性的执行结果保存在page域中<br>
<c:if test="${2 > 1}" var = "var_page"/>
${var_page}<hr>
将test属性的执行结果保存在session域中<br>
<c:if test="${1 > 2}" var = "var_session" scope="session"/>
${var_session}<hr>
如果test属性的执行结果为true,输出标签体的内容<br>
<jsp:useBean id="message" class="chapter9.Message"/>
<jsp:setProperty property="name" name="message" value="bike"/>
<c:if test="${message.name == param.name}">
name属性值为bike
</c:if>
在浏览器地址栏中输入如下的URL来测试if.jsp页面:
http://localhost:8080/demo/chapter9/if.jsp?name=bike
9.3.6 <c:choose>、<c:when>和<c:otherwise>标签
<c:choose>标签用于在多个条件中进行选择。<c:when>和<c:otherwise>标签必须作为<c:choose>的子标签使用。这三个标签相当于Java语言中的switch(…){case …; default …;}语句。
<c:choose>和<c:otherwise>标签没有属性,<c:when>标签只有一个test属性。该属性是boolean类型,支持动态属性值。如果test属性的执行结果为true,就会处理这个<c:when>标签体的内容。如果所有的<c:when>标签的test属性的执行结果都为false,则处理<c:otherwise>标签体中的内容。在使用<c:choose>、<c:when>和<c:otherwise>标签时应注意如下几点:
<c:when>和<c:otherwise>标签必须是<c:choose>标签的子标签,不能单独使用。
<c:choose>标签中包含一个或多个<c:when>标签,包含0个或一个<c:otherwise>标签。
<c:when>标签必须放在<c:otherwise>标签的前面。
<c:when>标签和<c:otherwise>标签中可以包含任意的JSP代码。
<c:choose>标签中除了包含<c:when>和<c:otherwise>标签外,还可以包含空格、"\r\n"、制表符和JSP注释,除此之外,不能在<c:choose>标签中(<c:when>和<c:otherwise>标签的外部)包含其他任何字符,否则<c:choose>标签将抛出异常。
对于最后一点,读者可以看如下的代码:
<c:choose>
<%--除了包含空格、"\r\n"、制表符和JSP注释外,在choose标签中不能包含任何其他字符 --%>
test
<c:when test = "true">abcd</c:when>
</c:choose>
在执行上面的代码时将抛出异常,这是因为在<c:choose>标签内包含了test.
choose.jsp页面是一个演示<c:choose>、<c:when>和<c:otherwise>标签的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
这双旅游鞋的价格:
<c:choose>
<c:when test="${param.price <= 100}">
非常便宜
</c:when>
<c:when test = "${param.price > 100 && param.price <= 600}">
适中
</c:when>
<c:when test = "${param.price > 600 && param.price <= 1200}">
比较高
</c:when>
<c:otherwise>
太高了
</c:otherwise>
</c:choose>
在浏览器地址栏中输入如下的URL来测试choose.jsp页面:
http://localhost:8080/demo/chapter9/choose.jsp?price=56
http://localhost:8080/demo/chapter9/choose.jsp?price=2000
9.3.7 <c:forEach>标签
<c:forEach>标签用于根据集合对象或指定的次数循环迭代标签体中的内容。在使用<c:forEach>标签时应注意如下几点:
如果指定begin属性,该属性的值必须大于或等于0,否则会抛出javax.servlet.jsp.JspTagException异常。
如果指定end属性,该属性的值不能小于begin属性的值,否则不会进行迭代操作。
如果指定step属性,该属性的值必须大于或等于1,否则会抛出javax.servlet.jsp.JspTagException异常。
如果items属性的值为null,或items属性指定的集合对象不存在,items属性值将被作为一个空集合对待。<c:forEach>标签不会进行迭代操作。
如果begin属性值大于或等于集合对象的元素个数,则不会进行迭代操作。
如果begin属性值在有效的范围内,但end属性值大于或等于集合对象的元素个数,则迭代到集合对象的最后一个元素为止。
<c:forEach>标签的items属性支持如下的数据类型:
任意类型的数组
java.util.Collection
java.util.Iterator
java.util. Enumeration
java.util.Map
String
如果items属性值是String类型,该字符串必须用逗号(,)分隔。<c:forEach>标签会将以逗号分隔的字符串当作String数组来处理(每一个被逗号分隔的子串相当于String数组中的一个元素)。
1. 迭代数组
forEach_array.jsp页面是一个使用<c:forEach>标签迭代数组的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
String[] strArray = new String[]{"超人", "飞机", "神化", "地球"};
int[] intArray = new int[]{100, 200, 320, 400, 1200};
chapter9.Message[] messages = new chapter9.Message[3];
messages[0] = new chapter9.Message();
messages[0].setName("bill");
messages[1] = new chapter9.Message();
messages[1].setName("Mike");
messages[2] = new chapter9.Message();
messages[2].setName("赵明");
// 将数组保存在request域中
request.setAttribute("strArray", strArray);
request.setAttribute("intArray", intArray);
request.setAttribute("messages", messages);
%>
迭代输出String数组中的元素<br>
<c:forEach var="str" items="${strArray}" >
${str}
</c:forEach>
<hr>
迭代输出int数组中的元素,并指定begin、end和varStatus属性<br>
<c:forEach var="int" items="${intArray}" begin = "1" end = "3" varStatus="status" >
intArray[${status.index}]=${int}
</c:forEach>
<hr>
迭代Message对象数组,并通过step属性指定迭代步长<br>
<c:forEach var="message" items="${messages}" step="2" varStatus="status">
messages[${status.index}].name =${message.name}<br>
</c:forEach>
在浏览器地址栏中输入如下的URL来测试forEach_array.jsp页面:
http://localhost:8080/demo/chapter9/forEach_array.jsp
2. 迭代Collection和Iterator类型的集合对象
forEach_collection.jsp页面是一个使用<c:forEach>标签迭代Collection和Iterator类型的集合对象的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
java.util.List<Object[]> objList = new java.util.ArrayList<Object[]>();
java.util.Random random = new java.util.Random();
for (int i = 0; i < 7; i++)
{
Object[] objArray = new Object[]
{ "随机数" + String.valueOf(i + 1), random.nextInt(10000) };
objList.add(objArray);
}
// 将Collection和Iterator类型的集合对象保存在request域中
request.setAttribute("objList", objList);
request.setAttribute("objIterator", objList.iterator());
%>
<table width="100%">
<tr>
<td align="right" style="padding-right: 20px">
<table border="1">
<tr>
<th>名称</th>
<th>随机数</th>
</tr>
<!-- 迭代Collection类型的集合对象 -->
<c:forEach var="obj" items="${objList}">
<tr>
<td>${obj[0]}</td>
<td>${obj[1]}</td>
</tr>
</c:forEach>
</table>
</td>
<td style="padding-left: 20px">
<table border="1">
<tr>
<th>随机数</th>
<th>名称</th>
</tr>
<!-- 迭代Iterator类型的集合对象 -->
<c:forEach var="obj" items="${objIterator}" >
<tr>
<td>${obj[1]}</td>
<td>${obj[0]}</td>
</tr>
</c:forEach>
</table>
</td>
</tr>
</table>
在浏览器地址栏中输入如下的URL来测试forEach_collection.jsp页面:
http://localhost:8080/demo/chapter9/forEach_collection.jsp
3. 使用双重循环迭代Enumeration类型的集合对象和数组
forEach_enumeration.jsp页面是一个使用<c:forEach>标签迭代Enumeration类型的集合对象和数组的例子。内层循环迭代数组,外层循环迭代Enumeration类型的集合,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
java.util.Vector v = new java.util.Vector();
String[] strArray = new String[]
{ "超人", "飞机", "神化", "地球" };
int[] intArray = new int[6];
java.util.Random random = new java.util.Random();
for (int i = 0; i < intArray.length; i++)
{
intArray[i] = random.nextInt(123456);
}
v.add(strArray);
v.add(intArray);
// 将Enumeration对象保存在request域中
request.setAttribute("elements", v.elements());
%>
使用双重循环输出Enumeration对象中的值<hr>
<!-- 迭代Enumeration集合对象 -->
<c:forEach var="element" items="${elements}">
<!-- 迭代Enumeration集合对象中的元素,每一个元素是一个数组 -->
<c:forEach var="value" items="${element}">
${value}
</c:forEach>
<br>
</c:forEach>
Vector对象的elements方法返回了一个Enumeration对象。该对象描述了Vector对象中的所有元素。
在浏览器地址栏中输入如下的URL来测试forEach_enumeration.jsp页面:
http://localhost:8080/demo/chapter9/forEach_enumeration.jsp
4. 迭代Map类型的集合对象
foreach_map.jsp页面是一个使用<c:forEach>标签迭代Map类型的集合对象的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
java.util.Map<String, Integer> map = new java.util.HashMap<String, Integer>();
map.put("自行车", 23);
map.put("皮球", 20);
map.put("电脑", 120);
// 将Map对象保存在request域中
request.setAttribute("map", map);
%>
<br>
<center>迭代输出Map对象中的key-value对<p/>
<table border="1">
<tr>
<th>商品</th>
<th>数量</th>
</tr>
<!-- 迭代Map类型的集合对象 -->
<c:forEach var="entry" items="${map}">
<tr>
<!-- 输出集合元素的key -->
<td>${entry.key}</td>
<!-- 输出集合元素的value -->
<td>${entry.value}</td>
</tr>
</c:forEach>
</table>
</center>
在浏览器地址栏中输入如下的URL来测试forEach_map.jsp页面:
http://localhost:8080/demo/chapter9/forEach_map.jsp
5. 迭代用逗号(,)分隔的字符串
<c:forEach>标签除了可以迭代各种类型的集合对象和数组外,还可以迭代用逗号分隔的字符串。但要注意,分隔符必须是逗号。forEach_string.jsp页面是一个使用<c:forEach>标签迭代以逗号分隔的字符串的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<center>
<table border="1">
<tr>
<!-- 迭代用逗号分隔的字符串 -->
<c:forEach var="s" items="超人, 飞机, 神化, 地球">
<td>${s}</td>
</c:forEach>
</tr>
</table>
</center>
在浏览器地址栏中输入如下的URL来测试forEach_string.jsp页面:
http://localhost:8080/demo/chapter9/forEach_string.jsp
6. 获得当前迭代元素的状态信息
通过<c:forEach>标签的status属性可以指定保存当前迭代元素状态信息的对象名。该对象被保存在page属性。这个对象实际上就是javax.servlet.jsp.jstl.core.LoopTagStatus对象实例。在LoopTagStatus接口有如下几个方法可以获得当前迭代元素的状态信息:
public Object getCurrent():该方法返回当前迭代的元素对象。
public int getCount():该方法返回当前已循环迭代的次数。
public int getIndex():该方法返回当前迭代的元素索引号。
public boolean isFirst():如果当前迭代元素是迭代集合对象的第一个元素,该方法返回true,否则返回false.
public boolean isLast():如果当前迭代元素是迭代集合对象的最后一个元素,该方法返回true,否则返回false.
public Integer getBegin():该方法返回<c:forEach>标签的begin属性值。如果未设置begin属性,则返回null.
public Integer getEnd():该方法返回<c:forEach>标签的end属性值。如果未设置end属性,则返回null.
public Integer getStep():该方法返回<c:forEach>标签的step属性值。如果未设置step属性,则返回null.
forEach_status.jsp页面是一个使用<c:forEach>标签输出当前迭代元素状态信息的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
String[] strArray = new String[]{ "超人", "飞机", "神化", "地球" };
// 将数组保存在request域中
request.setAttribute("strArray", strArray);
%>
<center>
未设置begin、end和step属性<p/>
<table border="1">
<tr>
<th>当前迭代元素</th>
<th>current</th>
<th>count</th>
<th>index</th>
<th>first</th>
<th>last</th>
<th>begin</th>
<th>end</th>
<th>step</th>
</tr>
<c:forEach var="s" items="${strArray}" varStatus="status">
<tr>
<td>${s}</td>
<td>${status.current}</td>
<td>${status.count}</td>
<td>${status.index}</td>
<td>${status.first}</td>
<td>${status.last}</td>
<td>${status.begin}</td>
<td>${status.end}</td>
<td>${status.step}</td>
</tr>
</c:forEach>
</table>
<hr>
设置了begin、end和step属性<p/>
<table border="1">
<tr>
<th>当前迭代元素</th>
<th>current</th>
<th>count</th>
<th>index</th>
<th>first</th>
<th>last</th>
<th>begin</th>
<th>end</th>
<th>step</th>
</tr>
<c:forEach var="s" items="${strArray}" varStatus="status" begin = "1" end = "3" step="2">
<tr>
<td>${s}</td>
<td>${status.current}</td>
<td>${status.count}</td>
<td>${status.index}</td>
<td>${status.first}</td>
<td>${status.last}</td>
<td>${status.begin}</td>
<td>${status.end}</td>
<td>${status.step}</td>
</tr>
</c:forEach>
</table>
</center>
在浏览器地址栏中输入如下的URL来测试forEach_status.jsp页面:
http://localhost:8080/demo/chapter9/forEach_status.jsp
7. 设置表格偶数行的背景色
使用迭代元素状态信息对象的属性可以实现很多有趣的功能。如使用count属性可以实现改变表格的偶数行背景色的功能,代码如下:
<!-- forEach.jsp -->
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%
java.util.Map<String, Integer[]> map =
new java.util.HashMap<String, Integer[]>();
map.put("自行车", new Integer[]{ 321, 23 });
map.put("皮球", new Integer[]{ 45, 20 });
map.put("电脑", new Integer[]{ 4320, 120 });
map.put("啤酒", new Integer[]{ 3, 320 });
map.put("照相机", new Integer[]{ 1200, 12 });
// 将Map对象保存在request域中
request.setAttribute("map", map);
%>
<br>
<center>设置表格偶数行的背景色<p/>
<table border="1" width="80%">
<tr>
<th>商品</th>
<th>单价</th>
<th>数量</th>
</tr>
<!-- 迭代Map类型的集合对象 -->
<c:forEach var="entry" items="${map}" varStatus="status">
<!-- 使用EL判断count属性值是否为偶数,以便设置偶数行的背景色 -->
<tr ${status.count % 2 == 0? 'style="
<!-- 输出集合元素的key -->
<td>${entry.key}</td>
<!-- 输出集合元素的value中的第一个元素 -->
<td>${entry.value[0]}</td>
<!-- 输出集合元素的value中的第二个元素 -->
<td>${entry.value[1]}</td>
</tr>
</c:forEach>
</table>
</center>
在上面的代码中,使用了EL表达式判断了count属性值是否为偶数(不包括表头)。如果count属性值为偶数,则使用CSS设置了偶数行的背景色。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/forEach.jsp
浏览器的显示效果如图9.2所示。
图9.2 设置表格偶数行的背景色
9.3.8 c:forTokens 标签
<c:forTokens>标签用于迭代指定分隔符分隔的字符串。分隔符号必须是单个字符,但<c:forTokens>支持包含多个分隔符的字符串。假设",;?"为三个分隔符,那么<c:forTokens>标签对"a,b;c?d"字符串迭代后,可以获得4个迭代元素(a,b,c,d)。
虽然<c:forEach>标签也可以对用分隔符分隔的字符串进行迭代,但<c:forEach>标签只支持逗号分隔符,而<c:forTokens>标签支持更多的分隔符,而且还支持多个分隔符。
<c:forTokens>标签实际上是通过java.util.StringTokenizer类来迭代有分隔符的字符串的,如下面的代码所示:
java.util.StringTokenizer st = new java.util.StringTokenizer("a,b;c", ",;", true);
while(st.hasMoreElements())
{
System.out.println(st.nextElement());
}
上面的代码使用了两个分隔符:","和";",来迭代"a,b;c".因此会分别输出a,b,c三个子字符串。下面的代码使用了<c:forTokens>标签实现了和上面的代码相同的功能:
<c:forTokens var="s" items="a,b;c" delims=",;">
${s}<br>
</c:forTokens>
在使用<c:forTokens>标签时应注意如下几点:
如果指定begin属性,该属性值必须大于或等于0,否则会抛出javax.servlet.jsp.JspTagException异常。
如果指定end属性,该属性值不能小于begin属性的值,否则不会进行迭代操作。
如果begin属性值在有效的范围内,但end属性值大于或等于字符串中子字符串的个数,则迭代到最后一个子字符串为止。
如果指定step属性,该属性的值必须大于或等于1,否则会抛出javax.servlet.jsp.JspTagException异常。
如果items属性值为null,或通过items属性的动态属性值指定的字符串不存在,items属性值将被作为一个空集合对待。<c:forTokens>标签不会进行迭代操作。
如果delims属性值为null或空串,items属性的值会被当作没有分隔符的字符串。也就是说,<c:forTokens>标签只会迭代一次,迭代的子字符串就是items属性值本身。
如果某个迭代子字符串是空串,<s:forTokens>标签会自动忽略这个空串。
forTokens.jsp页面是一个使用<c:forTokens>标签迭代字符串的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
使用"^"作为分隔符<br>
要迭代的字符串:"one^two^three^four"<br>
迭代结果:
<c:forTokens var="s" items="one^two^three^four" delims="^" >
${s}
</c:forTokens>
<hr>
使用"^"、"*"和"%"作为分隔符<br>
要迭代的字符串:"汽车:火箭*轮船:飞机%自行车"<br>
迭代结果:
<c:forTokens var="s" items="汽车^火箭*轮船^飞机%自行车" delims="^*%">
${s}
</c:forTokens>
<hr>
使用"^"、"*"和"%"作为分隔符,某些子字符串是空串,会被自动忽略<br>
要迭代的字符串:"汽车^火箭*轮船^%自行车%%^*"<br>
迭代结果:
<c:forTokens var="s" items="汽车^火箭*轮船^%自行车%%^*"
delims="^*%" varStatus="status">
${s}(${status.index})
</c:forTokens>
<hr>
在浏览器地址栏中输入如下的URL来测试forTokens.jsp页面:
http://localhost:8080/demo/chapter9/forTokens.jsp
9.3.9 <c:param>标签
在JSTL核心标签库中有如下3个标签和URL有关:
<c:import>标签
<c:url>标签
<c:redirect>标签
在为这些标签指定URL时经常要为这些URL指定一些参数,而<c:param>标签的功能就是为上述3个标签的URL指定参数。如果指定的参数包含中文,<c:param>标签会自动为其编码。编码规则是按着JSP页面的contentType属性设置的字符集进行编码。如下面的JSP页面将按着GBK编码格式将"超人"转换为"%b3%ac%c8%cb".
<%@ page language="java" contentType="text/html; charset=GBK" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<c:redirect url="http://nokiaguy.blogjava.net">
<!-- 按着GBK格式进行编码 -->
<c:param name="abc" value="超人"/>
</c:redirect>
如果将contentType属性设为"text/html; charset=UTF-8",则"超人"会被转换为"%e8%b6%85%e4%ba%ba"。
9.3.10 <c:url>标签
<c:url>标签主要用于对URL的重写。重写URL其实就是为URL增加Session ID和请求参数。在使用<c:url>标签时应注意如下几点:
value属性可以是绝对路径,也可以是相对路径。
如果指定context属性,var属性和context属性的值必须以"/"开头,否则<c:url>标签会抛出异常。
如果为URL指定的请求参数中包含中文,应在<c:url>标签中使用<c:param>子标签指定请求参数。而不要直接将请求参数放到URL后面。因为<c:param>标签会自动对中文请求参数进行编码,而<c:url>标签并不会对中文请求参数进行编码。
如果指定scope属性,必须指定var属性,否则<c:url>标签会抛出异常。
如果未指定var属性,<c:url>标签会将重写后的URL直接输出到客户端。如果指定了var属性,则<c:url>标签会将重写后的URL保存在指定的Web域中,并不会将重写后的URL输出到客户端。要想输出或引用重写后的URL,可以使用EL或其他方法从Web域中读取被重写的URL.
如果value属性指定的URL中包含的请求参数名和<c:param>标签指定的请求参数重名,<c:url>标签会使用两个重名的请求参数来重写URL,而不会使用其中一个请求参数值来覆盖另外一个请求参数值。
url.jsp页面是一个使用<c:url>标签重写URL的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
重写绝对路径,并生成链接<br>
<a href='<c:url value="http://nokiaguy.blogjava.net"/>'>
http://nokiaguy.blogjava.net
</a>
<hr>
使用context属性重写相对路径<br>
<c:url value="/chapter9/url.jsp" context="/demo" />
<hr>
将重写后的URL保存在session域中,并通过param标签指定中文请求参数<br>
<c:url var="newURL" value="http://localhost:8080/demp/chapter9/url.jsp" >
<c:param name="name">超人</c:param>
</c:url>
使用EL输出session域中被重写的URL<br>
${newURL}
<hr>
指定重名的请求参数<br>
<c:url value="http://localhost:8080/demp/chapter9/url.jsp?name=bill" >
<c:param name="name">超人</c:param>
</c:url>
<hr>
假设本地的IP地址是192.168.17.127(读者需要使用自己机器的IP地址),在浏览器地址栏中输入如下的URL:
http://192.168.17.127:8080/demo/chapter9/url.jsp
浏览器的显示效果如图9.3所示。
图9.3 使用<c:url>标签重写URL
从图9.3所示的输出结果可以看出,在重写相对路径的URL时,<c:url>标签会将Session ID作为请求参数自动添加在URL的后面。当刷新图9.3所示的页面后,这个Session ID将消失。这说明浏览器使用了Cookie来传递Session ID.在新的浏览器窗口多次访问上面的URL,每次都会在被重写的相对路径后面出现Session ID,而且每次都不相同。这说明当关闭浏览器的Cookie功能时,Session ID将通过URL来传递。
9.3.11 <c:redirect>标签
<c:redirect>标签用于执行URL重定向操作,相当于调用response.sendRedirect方法。该标签的url属性指定要重定向的URL时,可以使用相对路径和绝对路径。redirect.jsp页面是一个使用<c:redirect>标签重定向到其他URL的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<c:url var="url" value="/choose.jsp" context="/demo/chapter9">
<c:param name="price" value="210" />
</c:url>
<c:redirect url="${url}" />
在浏览器地址栏输入如下的URL来测试redirect.jsp页面:
http://localhost:8080/demo/chapter9/redirect.jsp
9.3.12 <c:import>标签
<c:import>标签用于在JSP页面中导入一个由URL指定的Web资源的内容。该标签和<jsp:include>标签(详见6.5.1节的内容)的功能类似,但要比<jsp:include>标签的功能更强大。在使用<c:import>标签时应注意如下几点:
url属性指定的要导入资源的URL可以是相对路径(如abc.jsp),也可以是绝对路径(如http://nokiaguy.blogjava.net)。当被导入的资源文件是相对路径时,相对路径的首字符可以是"/",也可以不是"/".如果是"/",表示相对于当前JSP页面所在的Web应用程序的根路径;如果相对路径的首字符不是"/",表示相对于当前JSP页面的路径。
在使用<c:import>标签导入Web资源时相当于在浏览器中直接访问该Web资源。也就是说,<c:import>标签导入的资源内容是Web资源在服务端执行后输出到客户端的内容。例如,如果导入的是JSP页面,导入后的结果是JSP页面输出到客户端的内容。如果要想将导入的资源按原样进行导入,需要将资源文件的扩展名修改成非服务端程序的扩展名,如。txt.
如果使用语法1,在未指定var属性的情况下,<c:import>标签将导入的资源内容以字符串形式直接输出;如果指定了var属性,<c:import>标签将导入的资源内容保存在scope属性指定的Web域中,var属性指定了将导入的资源内容保存在Web域中的属性名。
如果导入的资源中包含有非西欧字符(如GBK、UTF-8),必须使用charEncoding属性指定相应的字符集编码,否则在导入资源内容后会出现乱码。
如果使用语法2,<c:import>标签导入的资源内容保存在page域中的java.io.Reader对象中,varReader属性指定了该Reader对象在page域中的属性名。
由于在<c:import>标签结束时就会自动将java.io.Reader对象关闭,并从page域中删除。因此,在使用语法2时,必须在<c:import>的标签体内使用Reader对象。而且在<c:import>的标签体内不能有<c:param>标签,否则<c:import>标签会抛出异常。如果要给URL传参数,必须直接在url属性中设置好这些参数,这时可以使用<c:url>标签来生成带参数的URL.
如果url属性值是null、空串或指定的URL无效,<c:import>标签会抛出异常。
如果指定了charEncoding属性,但charEncoding属性值是null或空串,<c:import>标签会忽略charEncoding属性。
下面的例子演示了如何使用<c:import>标签来导入Web资源。在实现这个例子之前,先将在9.2节建立的myjstl.jsp文件复制一份,并改名为myjstl.txt。
import.jsp页面是一个使用<c:import>标签导入Web资源的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
使用语法1导入JSP页面,并直接输出导入的内容<p/>
<c:import url="myjstl.jsp" />
<hr>
使用语法1导入txt文件,并将导入的内容保存在request域中<p/>
<c:import var="myjstl" url="myjstl.txt" charEncoding="UTF-8" scope="request" />
<c:out value="${myjstl}" />
<hr>
使用语法2导入JSP页面,并将导入的内容保存在page域中<p/>
<c:url var="url" value="choose.jsp" >
<c:param name="price" value="${param.price}"/>
</c:url>
<c:import varReader="reader" url="${url}">
<c:out value="${reader}" />
</c:import>
<hr>
使用语法2导入txt文件,并将导入的内容保存在page域中<p />
<c:import varReader="reader" url="myjstl.txt" charEncoding="UTF-8">
<c:out value="${reader}" />
</c:import>
<hr>
使用语法2导入跨域Web资源(使用绝对路径)<p/>
<c:import varReader="reader" url="http://www.csdn.net" charEncoding="UTF-8">
<c:out value="${reader}" />
</c:import>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/import.jsp?price=230
浏览器显示的结果如图9.4所示。
图9.4 使用<c:import>标签导入资源内容
9.4.1 <fmt:setLocale>标签
<fmt:setLocale>标签用于设置本地化信息,并将封装本地化信息的Locale对象保存在指定的Web域中,Locale对象在Web域中的属性名为javax.servlet.jsp.jstl.fmt.locale.scope.其中scope表示指定的Web域。如果Web域是request,则保存在request域中的Locale对象的属性名是javax.servlet.jsp.jstl.fmt.locale.request.使用<fmt:setLocale>标签设置本地化信息后,其他的国际化标签都会使用该本地化信息,而忽略从请求消息中传递过来的本地信息。
在使用<fmt:setLocale>标签时应注意如下几点:
如果value属性值是null或空串,其他的国际化标签将使用服务端默认的本地信息(注意,不是从客户端发送过来的本地信息)。
如果不使用<fmt:setLocale>标签,其他的国际化标签将使用从客户端发送过来的本地信息。
setLocale.jsp页面是一个使用<fmt:setLocale>标签设置当前的本地化信息的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<!--
使用字符串设置当前的本地化信息,并将Locale对象保存在request域中
属性名为javax.servlet.jsp.jstl.fmt.locale.request
如果将Locale对象保存在其他域中,属性名为
javax.servlet.jsp.jstl.fmt.locale加上scope属性值作为后缀。如保存在session域中
的属性名为javax.servlet.jsp.jstl.fmt.locale.session
-->
<fmt:setLocale value="${param.locale}" scope="request" />
<!-- 创建ResourceBundle对象实例 -->
<fmt:setBundle basename="resources.I18nResource" var="i18n"/>
<!-- 输出相应的国际化信息 -->
<fmt:message bundle="${i18n}" key="i18n.welcome"/><hr>
<jsp:useBean id ="now" class="java.util.Date"/>
<fmt:message bundle="${i18n}" key="i18n.datetime">
<fmt:param value="${now}"/>
<fmt:param value="${now}"/>
</fmt:message>
<hr>
<fmt:message bundle="${i18n}" key="i18n.message">
<fmt:param value="${20}"/>
<fmt:param value="${466}"/>
</fmt:message>
<hr>
<!-- 从request域中获得并输出Locale对象-->
${requestScope["javax.servlet.jsp.jstl.fmt.locale.request"]}
在上面的程序中涉及到了很多国际化标签,这些国际化标签的具体细节将在后面的部分详细介绍。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/setLocale.jsp?locale=en-US
浏览器输出的信息如图9.5所示。
图9.5 使用英语(美国)本地化信息
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/setLocale.jsp?locale=zh-CN
浏览器输出的信息如图9.6所示。
图9.6 使用中文(中国)本地化信息
9.4.2 <fmt:bundle>标签
<fmt:bundle>标签用于创建ResourceBundle对象实例,该对象实例只在<fmt:bundle>标签体中有效。使用该标签的prefix属性指定<fmt:message>标签的key属性值的前缀时,key属性值不能再包含前缀部分。
bundle.jsp页面是一个使用<fmt:bundle>标签创建ResourceBundle对象实例,并输出资源信息的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
未使用prefix属性指定前缀<br>
<jsp:useBean id="now" class="java.util.Date" />
<fmt:bundle basename="resources.I18nResource">
<fmt:message key="i18n.welcome" />
<br>
<fmt:message key="i18n.datetime">
<fmt:param value="${now}" />
<fmt:param value="${now}" />
</fmt:message>
</fmt:bundle>
<hr>
使用prefix属性指定前缀
<br>
<fmt:bundle basename="resources.I18nResource" prefix="i18n.">
<fmt:message key="message">
<fmt:param value="${15}" />
<fmt:param value="${388}" />
</fmt:message>
</fmt:bundle>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/bundle.jsp
浏览器显示的结果如图9.7所示。
图9.7 使用<fmt:bundle>标签创建ResourceBundle对象实例
9.4.3 <fmt:setBundle>标签
<fmt:setBundle>标签和<fmt:bundle>标签的基本功能相同,也用于创建ResourceBundle对象实例,但<fmt:setBundle>标签可以将创建的LocalizationContext对象实例(该对象实例中封装了由<fmt:setBundle>标签创建的ResourceBundle对象)以指定的属性名保存在某个Web域中。在使用<fmt:setBundle>标签时应注意如下几点:
如果basename属性值为null、空字符串,或找不到basename属性指定的资源,<fmt:setBundle>标签创建的ResourceBundle对象实例为null.
<fmt:setBundle>标签保存在某个域中的对象并不是ResourceBundle对象实例,而是javax.servlet.jsp.jstl.fmt.LocalizationContext对象实例。LocalizationContext类可以在JSTL库的源代码中找到。LocalizationContext对象封装了ResourceBundle对象和Locale对象。通过LocalizationContext类的getResourceBundle方法和getLocale方法可以分别返回ResourceBundle对象和Locale对象。从这一点可以看出,如果<fmt:setBundle>标签创建的ResourceBundle对象实例为null,保存在Web域中的对象也不是null,而是LocalizationContext对象实例,只是通过LocalizationContext类的getResourceBundle方法会返回null.
如果指定var属性,<fmt:setBundle>标签会把LocalizationContext对象实例以var属性值作为属性名保存在Web域中。
如果未指定var属性,<fmt:setBundle>标签会把LocalizationContext对象实例以javax.servlet.jsp.jstl.fmt.LocalizationContext.scope作为属性名保存在Web域中,其中scope表示scope属性的值,如scope属性值是request,则javax.servlet.jsp.jstl.fmt.LocalizationContext.request就是保存在Web域中的LocalizationContext对象的属性名。
关于<fmt:setBundle>标签的使用方法请读者详见9.4.1节中的例子。
9.4.4 <fmt:message>标签
<fmt:message>标签用于从资源文件中读取资源信息,并进行格式化输出。使用<fmt:message>标签应注意如下几点:
如果指定的资源信息不存在;或者未指定bundle属性;或者从bundle属性指定的LocalizationContext对象的getResourceBundle方法返回的ResourceBundle对象为null,<fmt:message>标签会输出"???<key>???"形式的错误信息。
如果key属性为null或空字符串,<fmt:message>标签会输出"??????"形式的错误信息。
如果指定var属性,<fmt:message>标签会将格式化的资源信息保存在Web域中以var属性值为属性名的对象中。如果未指定var属性,<fmt:message>标签会直接输出格式化的资源信息。
如果使用语法2和语法3格式化资源信息,并且在标签体中包含<fmt:param>子标签,则<fmt:param>子标签的出现顺序和资源信息中的相应占位符顺序要一致,也就是说,第一个<fmt:param>子标签对应于资源信息中第一个占位符,第二个<fmt:param>子标签对应于资源信息中的第二个占位符,以此类推。
如果资源信息中的占位符没有对应的<fmt:param>子标签,则<fmt:message>标签会直接输出资源信息中的占位符。
如果<fmt:param>子标签指定的值无法转换成资源信息中相应的占位符的数据类型,<fmt:message>标签会抛出异常。
message.jsp页面是一个使用<fmt:message>标签格式化资源信息的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<fmt:setBundle var="bundle" basename="resources.I18nResource"/>
在标签体中指定资源信息的key和占位符参数值<br>
<fmt:message bundle="${bundle}" >
i18n.message
<fmt:param value="${12}" />
<fmt:param value="${216}" />
</fmt:message>
<hr>
将格式化的资源信息保存在page域中,并通过EL输出格式化的资源信息<br>
<fmt:message var="message" bundle="${bundle}" key="i18n.datetime">
<jsp:useBean id="now" class="java.util.Date"/>
<fmt:param value="${now}" />
<fmt:param value="${now}" />
</fmt:message>
${message}
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/message.jsp
浏览器的输出结果如图9.8所示。
图9.8 使用<fmt:message>标签格式化资源信息
9.4.6 <fmt:requestEncoding>标签
<fmt:requestEncoding>标签用于设置请求消息的字符集编码。实际上,在该标签内部通过调用request.setCharacterEncoding来设置请求消息的字符集编码。
在使用<fmt:requestEncoding>标签时应注意如下几点:
<fmt:requestEncoding>标签和request.setCharacterEncoding方法一样,必须在获得任何请求参数之前调用。
如果未指定value属性,或value属性值为null,并且从HTTP请求消息头中Content-Type字段无法确定字符集编码,<fmt:requestEncoding>标签会在session域中查找属性名为javax.servlet.jsp.jstl.fmt.request.charset的属性值,如果session域中存在该属性,<fmt:requestEncoding>标签就会使用该属性值设置字符集编码。如果session域中不存在该属性,<fmt:requestEncoding>标签会采用ISO-8859-1字符集编码。
requestEncoding.jsp页面是一个使用<fmt:requestEncoding>标签设置HTTP请求消息的字符集编码的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<fmt:requestEncoding value="UTF-8"/>
${param.product}
<form method="post">
商品:<input type="text" name="product"/><br>
<input type="submit" value="提交" />
</form>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/requestEncoding.jsp
在页面中的文本框中输入"自行车",单击"提交"按钮,浏览器显示的结果如图9.9所示。
图9.9 使用<fmt:requestEncoding>标签设置HTTP请求消息的字符集编码
如果将requestEncoding.jsp页面中的<fmt:requestEncoding>标签注释掉,在文本框中输入"自行车"后,单击"提交"按钮,在页面中将会显示乱码。
如果读者使用的是Tomcat5.x及以后的版本,如果使用HTTP GET方法提交请求消息,Tomcat并不会考虑用户设置的HTTP请求消息的字符集编码,而只会采用ISO-8859-1字符集编码。因此,在这种情况下,通过<fmt:requestEncoding>标签或request.setCharacterRequest方法设置HTTP请求消息的字符集编码是没有任何意义的。要解决这种情况下的乱码问题,需要使用如下的代码:
String product = new String(request.getParameter("product")。
getBytes("ISO-8859-1"), "UTF-8");
读者可以将requestEncoding.jsp页面中<form>标签的method属性值改成get,看看会发生什么事情。
9.4.7 <fmt:timeZone>标签
<fmt:timeZone>标签用于设置时区,但该标签设置的时区只在其标签体中有效。该标签的value属性表示要设置的时区,该属性值可以是String类型,也可以是java.util.TimeZone类的对象实例。如果value属性值是表示时区名称的字符串,该字符串将由java.util.TimeZone.getTimeZone方法解析为java.util.TimeZone类的对象实例。如果value属性值是null或空字符串,<fmt:timeZone>标签就会采用格林威治标准时间(GMT)。
timeZone.jsp页面是一个使用<fmt:timeZone>标签设置时区的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<jsp:useBean id="now" class="java.util.Date"/>
格林威治标准时间(GMT)<br>
<fmt:timeZone value="">
<fmt:formatDate value="${now}" type="both" dateStyle="long" timeStyle="long"/>
</fmt:timeZone>
<hr>
GMT-2:00<br>
<fmt:timeZone value="GMT-2:00">
<fmt:formatDate value="${now}" type="both" dateStyle="long" timeStyle="long"/>
</fmt:timeZone>
在浏览器地址栏中输入如下的URL来测试timeZone.jsp页面:
http://localhost:8080/demo/chapter9/timeZone.jsp
9.4.8 <fmt:setTimeZone>标签
<fmt:setTimeZone>标签和<fmt:timeZone>标签的功能类似,也用于在JSP页面中设置时区,但<fmt:setTimeZone>标签可以将时区信息以java.util.TimeZone对象的形式保存在Web域中。
该标签会使用var属性值作为属性名将设置的时区信息以TimeZone对象的形式保存在scope属性指定的Web域中。如果未指定var属性,TimeZone对象保存在Web域中的属性名是javax.servlet.jsp.jstl.fmt.timeZone.scope,其中scope表示scope属性的值。如scope属性值是request,TimeZone对象保存在request域中的属性名是javax.servlet.jsp.jstl.fmt.timeZone.request.<fmt:setTimeZone>的其他使用细节和<fmt:timeZone>标签相同,读者可以参阅9.4.7节的内容。
setTimeZone.jsp页面是一个使用<fmt:setTimeZone>标签设置时区的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<jsp:useBean id="now" class="java.util.Date"/>
格林威治标准时间(GMT)<br>
<fmt:setTimeZone var="gmt" value="" scope="request"/>
<fmt:formatDate value="${now}" type="both" dateStyle="long" timeStyle="long"/>
<hr>
<fmt:setTimeZone value="GMT+4:00" scope="request"/>
从request域中的TimeZone对象获得displayName属性值<br>
${requestScope["javax.servlet.jsp.jstl.fmt.timeZone.request"].displayName}<br>
<fmt:formatDate value="${now}" type="both" dateStyle="long" timeStyle="long"/>
在浏览器地址栏中输入如下的URL来测试setTimeZone页面:
http://localhost:8080/demo/chapter9/setTimeZone.jsp
9.4.9 <fmt:formatNumber>标签
<fmt:formatNumber>标签用于对数字、货币和百分数按本地化信息或用户自定义格式进行格式化。在使用<fmt:formatNumber>标签时应注意如下几点:
如果指定scope属性,必须要指定var属性,否则<fmt:formatNumber>标签将抛出异常。
如果value属性值是null或空字符串,并且var属性值不为null,则<fmt:formatNumber>标签不输出任何内容,如果这时指定了var属性和scope属性,将scope属性指定的Web域中的var属性指定的属性删除。
如果pattern属性值是null或空字符串,该属性将被忽略。
如果指定pattern属性,则<fmt:formatNumber>标签会忽略type属性。
由于<fmt:formatNumber>标签内部使用了java.text.DecimalFormat类的对象实例来格式化数值,而pattern属性值就是DecimalFormat类的构造方法的第一个参数,因此,pattern属性指定的模式字符串必须符合DecimalFormat类要求的模式字符串的语法。
如果<fmt:formatNumber>标签无法格式化指定的数值,就会使用Object.toString方法直接输出该value属性的值。
formatNumber.jsp页面是一个使用<fmt:formatNumber>标签格式化数值的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
使用组分隔符格式化数值:
<fmt:formatNumber value="123456" /><hr>
将数值格式化为货币格式,中文(中国):
<fmt:formatNumber value="123456" type="currency" />
<hr>
将数值格式化为货币格式,英语(美国):
<fmt:setLocale value="en-US"/>
<fmt:formatNumber value="123456" type="currency" />
<hr>
将数值格式化为百分数形式(使用标签体指定要格式化的数值):
<fmt:formatNumber type="percent" minFractionDigits="1" >
0.346
</fmt:formatNumber>
<hr>
指定小数部分数字的最大位数(5)和最小位数(3):
<fmt:formatNumber maxFractionDigits="5" minFractionDigits="3">
3.12
</fmt:formatNumber>
<hr>
使用pattern属性指定模式字符串(#000.0000#),并将格式化结果保存在request域中:
<fmt:formatNumber pattern="#000.0000#" value="12.625"
var="myNumber" scope="request"/>
<!-- 使用EL从request域中取出格式化结果,并输出 -->
${requestScope.myNumber}
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/formatNumber.jsp
浏览器显示的结果如图9.10所示。
图9.10 使用<fmt:formatNumber>标签格式化数值
9.4.10 <fmt:parseNumber>标签
<fmt:parseNumber>标签的作用正好和<fmt:formatNumber>标签相反,它用于将一个按着本地信息格式化的数值、货币或百分数字符串解析为数值。使用<fmt:parseNumber>标签应注意如下几点:
如果value属性值为null或空字符串,<fmt:parseNumber>标签会删除scope属性指定的Web域中的var属性指定的域属性。如果value属性的值无法成功被解析,<fmt:parseNumber>标签会抛出异常。
如果parseLocale属性值为null或空字符串,该属性将被忽略。
如果pattern属性值为null或空字符串,该属性将被忽略。
如果指定pattern属性,则<fmt:parseNumber>标签会忽略type属性。
如果<fmt:parseNumber>无法确定解析所使用的本地信息,将会抛出JspException异常。异常信息中包含要解析的字符串。
如果指定var属性,并且var属性值不为null,<fmt:parseNumber>标签会将解析结果(Number对象)以var属性指定的域属性名保存在scope属性指定的域中。如果未指定var属性,或var属性值为null,<fmt:parseNumber>标签会调用Number.toString方法直接输出解析的结果。?如果指定scope属性,必须指定var属性,否则<fmt:parseNumber>标签将抛出异常。
parseNumber.jsp页面是一个使用<fmt:parseNumber>标签解析数值、货币和百分数字符串的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
解析货币字符串($1234.65),英语(美国):
<fmt:parseNumber value="$1234.65" type="currency" parseLocale="en-US"/>
<hr>
解析百分数字符串(20.6%):
<fmt:parseNumber value="20.6%" type="percent" />
<hr>
使用用户定义的模式字符串解析数值,并将解析的结果保存在request域中:
<fmt:parseNumber var = "myNumber" scope ="request" pattern="#.000#">
45.12
</fmt:parseNumber>
${requestScope.myNumber}
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/parseNumber.jsp
浏览器显示的结果如图9.11所示。
图9.11 使用<fmt:parseNumber>标签分析数值、货币和百分数字符串
9.4.11 <fmt:formatDate>标签
<fmt:formatDate>标签用于对日期和时间按本地化信息或按用户自定义格式进行格式化。使用<fmt:formatDate>标签应注意如下几点:
如果指定了var属性,<fmt:formatDate>标签将格式化的结果以var属性值作为属性名保存在scope属性指定的Web域中,如果未指定var属性,<fmt:formatDate>标签会直接输出格式化的结果。如果指定了scope属性,var属性必须指定。
如果value属性值为null,则不输出任何内容,如果这时指定了var属性和scope属性,则scope属性指定的Web域中的var属性指定的域属性将被删除。
如果timeZone属性的值是null或空字符串,<fmt:formatDate>标签会忽略该属性。
如果标签不能确定格式化日期和时间所采用的本地信息,则使用java.util.Date.toString()方法作为输出格式。
formatDate.jsp页面是一个使用<fmt:formatDate>标签格式化日期和时间和例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<jsp:useBean id="now" class="java.util.Date"/>
使用long格式对日期进行格式化:
<fmt:formatDate value="${now}" dateStyle="long"/>
<hr>
使用long格式对时间进行格式化:
<fmt:formatDate value="${now}" timeStyle="long" type="time"/>
<hr>
使用full格式对日期和时间进行格式化:
<fmt:formatDate value="${now}" dateStyle="full" timeStyle="full" type="both"/>
<hr>
使用自定义格式(包括毫秒)对日期和时间进行格式化:
<fmt:formatDate value="${now}" pattern="yyyy-MM-dd HH:mm:ss S"/>
<hr>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/formatDate.jsp
浏览器显示的结果如图9.12所示。
图9.12 使用<fmt:formatDate>标签格式化日期和时间
9.4.12 <fmt:parseDate>标签
<fmt:parseDate>标签用于将一个表示日期和时间的字符串解析成java.util.Date对象实例。使用<fmt:parseDate>标签应注意如下几点:
如果value属性值为null或空字符串,<fmt:parseDate>标签会删除scope属性指定的Web域中的var属性指定的域属性。如果value属性的值无法成功被解析,<fmt:parseDate>标签会抛出异常。
如果parseLocale属性值为null或空字符串,该属性将被忽略。
如果timeZone属性值为null或空字符串,该属性将被忽略。
如果pattern属性值为null或空字符串,该属性将被忽略。
如果指定pattern属性,<fmt:parseDate>标签就会忽略type、dateStyle和timeStyle属性。这时,<fmt:parseDate>标签就会按pattern属性指定的自定义格式解析字符串。pattern属性值必须符合日期和时间模式字符串,否则<fmt:parseDate>标签会抛出异常。
如果<fmt:parseDate>标签不能确定用于解析日期和时间字符串的本地信息,就会抛出JspException异常,异常信息中包含要解析的字符串。
parseDate.jsp页面是一个使用<fmt:parseDate>标签解析日期和时间字符串的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
使用long格式解析时间字符串<br>
时间字符串:下午07时14分23秒<br>
<fmt:parseDate value="下午07时14分23秒" timeStyle="long" type="time"/>
<hr>
使用full格式解析日期和时间字符串,并将解析结果保存在request域中<br>
日期和时间字符串:2008年10月31日 星期五 下午07时14分23秒CST<br>
<fmt:parseDate dateStyle="full" timeStyle="full" type="both" var="datetime">
2008年10月31日 星期五 下午07时14分23秒CST
</fmt:parseDate>
${datetime}
<hr>
使用pattern属性指定自定义格式解析日期和时间字符串<br>
日期和时间字符串:2008-1-1 20:12:44 123<br>
自定义格式:yyyy-MM-dd HH:mm:ss S<br>
<fmt:parseDate pattern="yyyy-MM-dd HH:mm:ss S" value="2008-1-1 20:12:44 123" />
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/parseDate.jsp
浏览器显示的结果如图9.13所示。
图9.13 使用<fmt:parseDate>标签解析日期和时间字符串
9.5 数据库标签库
为了简化JSP页面访问数据库的操作,JSTL中提供了一个负责操作数据库的标签库,JSP规范建议数据库标签库的前缀名为sql.这个标签库中对不同的数据库操作提供了相应的标签,主要包含设置数据源、执行查询语句、执行更新语句和事务处理等标签。
本节使用在第2章建立的mydb数据库来实验数据库标签库中的标签。为了测试设置数据源的标签,在本将为mydb数据库建立一个数据源。
在Java IDE中打开servers工程,在server.xml文件中找到path属性值为"/demo"的<Context>元素,向<Context>元素中添加<Resource>子元素以配置数据源,配置完的<Context>元素的代码如下:
<Context docBase="demo" path="/demo" reloadable="true"
crossContext="true" source="org.eclipse.jst.jee.server:demo">
<Resource name="jdbc/mydb" auth="Container" type="javax.sql.DataSource"
driverClassName="com.mysql.jdbc.Driver"
url="jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8"
username="root" password="1234" maxActive="200" maxIdle="50"
maxWait="3000" />
</Context>
使用下面的SQL语句为mydb数据库中的t_books表插入五条记录:
INSERT INTO mydb.t_books (id, name, isbn, author, price) VALUES
(1, '人月神话', '6787102165345', '布鲁克斯', 52),
(2, 'Ajax基础教程', '5643489212407', '阿斯利森', 73),
(3, 'Thinking in C++', '7111171152', 'Bruce Eckel', 66),
(4, 'SQL Server 2005技术详解', '712489876532', '王超', 72),
(5, 'Java网络基础', '765129876213', '赵宇', 61);
为了在Java IDE中可以使用MySQL驱动程序来访问MySQL数据库,需要将MySQL驱动程序的jar包复制到WEB-INF\lib目录中。
9.5.1 <sql:setDataSource>标签
<sql:setDataSource>标签用于创建一个javax.sql.DataSource对象实例,并将该对象实例保存在指定Web域中。使用<sql:setDataSource>标签应注意如下几点:
dataSource属性值不能为null,否则<sql:setDataSource>标签会抛出JspException异常。
如果指定var属性,<sql:setDataSource>标签会将DataSource对象实例保存在scope属性指定的Web域中的属性变量中,域属性名就是var属性的值,如果未指定var属性,保存在Web域中的DataSource对象实例的属性名是javax.servlet.jsp.jstl.sql.dataSource.scope,其中scope表示scope属性的值。
setDataSource.jsp页面是一个使用<sql:setDataSource>标签创建DataSource对象,并将该对象保存在page域的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%>
<!-- 使用JNDI数据源创建DataSource对象实例 -->
<sql:setDataSource dataSource="jdbc/mydb" var="dataSource1" />
<!-- 使用直接指定连接参数的方式创建DataSource对象实例 -->
<sql:setDataSource
url="jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8"
driver="com.mysql.jdbc.Driver" user="root" password="1234" var="dataSource2" />
9.5.2 <sql:query>标签
<sql:query>标签用于执行查询语句,并将查询返回的结果保存在指定的Web域中的某个域属性中。使用<sql:query>标签应注意如下几点:
如果<sql:query>标签执行url属性指定的查询语句失败,则会抛出异常。
dataSource属性值不能为null,否则<sql:query>标签会抛出JspException异常。
如果指定dataSource属性,<sql:query>标签就不能嵌套在<sql:transaction>标签内,如果在<sql:transaction>标签中使用<sql:query>标签,该标签的数据源从<sql:transaction>标签获得,并由<sql:transaction>标签来管理数据库的连接。
如果指定maxRows属性,该属性值必须大于或等于-1。
如果在查询语句中包含参数占位符"?" ,需要在<sql:query>标签体内使用<sql:param>标签或<sql:dateParam>标签为相应的查询参数赋值。
如果指定了startRow属性,除非使用"order by"子句对查询结果进行排序,否则并不能保证查询结果按着某个字段进行排序。
一个<sql:query>标签的sql属性或标签体中只能包含一条SQL查询语句,否则将抛出异常。如果想执行多条SQL查询语句,需要使用多个<sql:query>标签。
<sql:query>标签返回的查询结果是javax.servlet.jsp.jstl.sql.Result对象,Result是一个接口,该接口由org.apache.taglibs.standard.tag.common.sql.ResultImpl类实现。在Result接口中定义了一系列的方法来获得和数据库相关的信息,这些方法如下:
public SortedMap[] getRows():该方法返回查询结果中的所有的行,每一行是一个SortedMap对象,SortedMap对象中的每一个关键字(key)表示相应的列名,每个关键字对应的值就是相应的列值。
public Object[][] getRowsByIndex():该方法以二维数组形式返回查询结果中的所有的行,每一行是一个Object数组。Object数组中的每一个元素就是当前行的列值。通过该方法返回的查询结果可以快速定位到某一行。
public String[] getColumnNames():该方法返回查询结果集中的所有列名。
public int getRowCount():该方法返回查询结果集中的行数。
public boolean isLimitedByMaxRows():该方法返回查询结果集是否通过maxRows属性限制了返回记录的行数。该方法只有在指定了maxRows属性,并且maxRows属性值小于查询结果的实际行数才返回true,否则返回false.
query.jsp页面是一个使用<sql:query>标签查询t_books表中的记录的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%>
<!-- 创建DataSource对象实例 -->
<sql:setDataSource
url="jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8"
driver="com.mysql.jdbc.Driver" user="root" password="1234" var="mydb" />
<!-- 未指定查询参数 -->
<sql:query var="books1" dataSource="${mydb}" sql="select * from t_books" />
<!-- 指定查询参数 -->
<sql:query var="books2" dataSource="${mydb}"
sql="select * from t_books where id < ?">
<!-- 指定查询参数的值 -->
<sql:param value="4" />
</sql:query>
books1<br>
<table border="1">
<tr>
<!-- 对books1查询结果的列名进行迭代 -->
<c:forEach var="columnName" items="${books1.columnNames}">
<!-- 输出列名 -->
<th>${columnName}</th>
</c:forEach>
</tr>
<!-- 使用Result对象的rows属性返回books1查询结果 -->
<!-- 对books1查询结果的记录进行迭代 -->
<c:forEach var="book" items="${books1.rows}">
<!-- 输出books1查询结果 -->
<tr>
<td>${book.id}</td>
<td>${book.name}</td>
<td>${book.isbn}</td>
<td>${book.author}</td>
<td>${book.price}</td>
</tr>
</c:forEach>
</table>
<hr>
books2<br>
<table border="1">
<tr>
<!-- 对books2查询结果的列名进行迭代 -->
<c:forEach var="columnName" items="${books2.columnNames}">
<th>${columnName}</th>
</c:forEach>
</tr>
<!-- 使用Result对象的rowsByIndex属性返回books2查询结果 -->
<!-- 对books2查询结果的记录进行迭代 -->
<c:forEach var="book" items="${books2.rowsByIndex}">
<!-- 输出books2查询结果 -->
<tr>
<td>${book[0]}</td>
<td>${book[1]}</td>
<td>${book[2]}</td>
<td>${book[3]}</td>
<td>${book[4]}</td>
</tr>
</c:forEach>
</table>
在浏览器地址栏中输入如下的URL来测试query.jsp页面:
http://localhost:8080/demo/chapter9/query.jsp
9.5.3 <sql:update>标签
<sql:update>标签用于执行SQL中的更新语句,如insert、update、delete等,除此之外,DDL语句(如创建和删除数据库和数据表的语句)也可以使用<sql:update>标签执行。<sql:update>标签返回执行更新语句后影响的记录行数,如果更新语句影响的记录行数为0,则<sql:update>标签返回0.使用<sql:update>标签时注意如下几点:
如果SQL更新语句包含参数占位符"?",需要在<sql:update>标签中使用<sql:param>标签或<sql:dateParam>标签设置参数的值。
如果指定了var属性,<sql:update>标签会将返回的结果保存在scope属性指定的Web域的属性中,域属性名为var属性的值。如果未指定var属性,或var属性值为null,<sql:update>标签不会在Web域中保存返回的结果。
如果要同时执行多条更新语句(由多个<sql:update>标签执行),并且这些更新语句要在同一个事务中执行,就需要将这些<sql:update>标签放到<sql:transaction>标签的标签体中,而且<sql:update>标签不能指定dataSource属性,<sql:update>标签所需要的数据源由<sql:transaction>标签指定。
一个<sql:update>标签的sql属性或标签体中只能包含一条SQL更新语句或SQL DDL语句,否则将抛出异常。如果想执行多条SQL更新语句或SQL DDL语句,需要使用多个<sql:update>标签。
update.jsp页面是一个使用<sql:update>标签执行SQL更新语句和SQL DDL语句的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%>
<sql:setDataSource
url="jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8"
driver="com.mysql.jdbc.Driver" user="root" password="1234" var="mydb" />
<!-- 如果t_products表存在,删除该表 -->
<sql:update sql="DROP TABLE IF EXISTS mydb.t_products" dataSource="${mydb}" />
<!-- 创建t_products表 -->
<sql:update dataSource="${mydb}">
CREATE TABLE mydb.t_products
(
id int unsigned NOT NULL auto_increment,
name varchar(50) NOT NULL,
price int unsigned,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=UTF8;
</sql:update>
成功创建t_products表!<br>
<!-- 向t_products表中插入1条记录 -->
<sql:update var="insertCount" dataSource="${mydb}" scope="request">
insert into mydb.t_products(name, price) values("洗衣机", 1024)
</sql:update>
成功向t_products表中插入${insertCount}条记录!<br>
<!-- 向t_products表中插入1条记录 -->
<sql:update var="insertCount" dataSource="${mydb}" scope="request">
insert into mydb.t_products(name, price) values(?, ?)
<sql:param value="自行车"/>
<sql:param value="320"/>
</sql:update>
成功向t_products表中插入${insertCount}条记录!<br>
<!-- 将t_products表中的price字段值都加15 -->
<sql:update var="updateCount" dataSource="${mydb}" scope="request">
update mydb.t_products set price = price + ?
<sql:param value="15"/>
</sql:update>
成功更新t_products表中${updateCount}条记录!<br>
<sql:query var="products" dataSource="${mydb}"
sql="select * from mydb.t_products" />
<hr>
t_products表中的所有记录<br>
<table border="1">
<tr>
<c:forEach var="columnName" items="${products.columnNames}">
<th>${columnName}</th>
</c:forEach>
</tr>
<c:forEach var="product" items="${products.rows}">
<tr>
<td>${product.id}</td>
<td>${product.name}</td>
<td>${product.price}</td>
</tr>
</c:forEach>
</table>
在浏览器地址栏中输入如下的URL来测试update.jsp页面:
http://localhost:8080/demo/chapter9/update.jsp
9.5.4 <sql:transaction>标签
<sql:transaction>标签用于定义一个事务块,在该标签体中可以包含<sql:query>和<sql:update>子标签,如果<sql:transaction>标签体中的<sql:query>和<sql:update>标签中有一个执行SQL语句失败,整个事务回滚,否则,在<sql:transaction>标签结束时成功提交事务。在<sql:transaction>标签体中的<sql:query>和<sql:update>子标签不能指定dataSource属性,这两个标签将使用在<sql:transaction>标签中指定的数据源。
transaction.jsp页面是一个使用<sql:transaction>标签定义事务块,并在该事务块中插入和更新t_products表中的记录的例子,该例子可以通过不同的请求参数值使事务成功提交或发生回滚,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%>
<sql:setDataSource
url="jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8"
driver="com.mysql.jdbc.Driver" user="root" password="1234" var="mydb" />
<!-- 包含update.jsp,以便重新建立t_products表,并输出表中的初始数据 -->
<jsp:include page="update.jsp" />
<hr>
<!-- 使用catch标签捕捉由于事务回滚页抛出的异常 -->
<c:catch var="exception">
<!-- 使用transaction标签开始一个事务块 -->
<sql:transaction dataSource="${mydb}" isolation="read_committed">
<!-- 向t_products表中插入1条记录 -->
<sql:update var="insertCount" scope="request">
insert into mydb.t_products(name, price) values("U盘", 35)
</sql:update>
成功向t_products表中插入${insertCount}条记录!<br>
<!-- 向t_products表中插入1条记录 -->
<sql:update var="insertCount" scope="request">
insert into mydb.t_products(name, price) values(?, ?)
<!-- 设置SQL更新语句的参数值 -->
<sql:param value="笔记本电脑" />
<sql:param value="8250" />
</sql:update>
成功向t_products表中插入${insertCount}条记录!<br>
<!-- 将t_products表中的price字段值增加一个值,该值通过请求参数increment指定 -->
<sql:update var="updateCount" scope="request">
update mydb.t_products set price = price + ?
<!-- 通过increment请求参数指定price字段值的增量 -->
<sql:param value="${param.increment}" />
</sql:update>
成功更新t_products表中${updateCount}条记录!<br>
</sql:transaction>
</c:catch>
<hr>
<!-- 如果事务回滚,输出异常消息 -->
${exception.message}<p/>
<!-- 查询t_products表中的所有记录 -->
<sql:query var="products" dataSource="${mydb}"
sql="select * from mydb.t_products" />
经过事务块中的SQL更新语句处理后,t_products表中新的记录
<br>
<table border="1">
<tr>
<c:forEach var="columnName" items="${products.columnNames}">
<th>${columnName}</th>
</c:forEach>
</tr>
<c:forEach var="product" items="${products.rows}">
<tr>
<td>${product.id}</td>
<td>${product.name}</td>
<td>${product.price}</td>
</tr>
</c:forEach>
</table>
为了保证在访问transaction.jsp页面时t_products表已经被建立,并且t_products表中有初始的记录,在transaction.jsp页面中使用<jsp:include>标签包含了在9.5.3节实现的update.jsp页面。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/transaction.jsp?increment=12
浏览器显示的结果如图9.14所示。
图9.14 在<sql:transaction>事务块中执行SQL更新语句,并成功提交事务
将increment请求参数的值改成abc,浏览器将会显示如图9.15所示的结果。
图9.15 在<sql:transaction>标签定义的事务块中抛出异常,事务回滚
从图9.15所示的输出结果可以看出,由于在事务块中的update语句执行失败(price字段是数值类型,而increment请求参数指定的是字符串abc,因此会抛出异常),所以在<sql:transaction>标签中的所有执行过的更新都将回滚(在update语句执行之前,执行过两条insert语句),因此,使用<sql:query>标签查询出的t_products表的记录未发生任何变化,也就是说由于事务回滚,在事务块中的更新操作并未对t_products表进行任何更新。
9.5.6 <sql:dateParam>标签
<sql:dateParam>标签和<sql:param>标签类型,也用于设置SQL语句的参数值,但<sql:dateParam>标签设置的参数值的类型是java.util.Date.dateParam.jsp页面是一个使用<sql:dateParam>标签设置date类型的SQL参数值的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/sql" prefix="sql"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/fmt" prefix="fmt"%>
<sql:setDataSource
url="jdbc:mysql://localhost:3306/mydb?characterEncoding=UTF-8"
driver="com.mysql.jdbc.Driver" user="root" password="1234" var="mydb" />
<!-- 将表示日期的字符串解析成java.util.Date对象实例 -->
<fmt:parseDate var="myDate" pattern="yyyy-MM-dd">
2001-10-20
</fmt:parseDate>
<!-- 更新t_booksale表中的id等于2的记录 -->
<sql:update dataSource="${mydb}">
update mydb.t_booksale set amount = ?, saledate = ? where id = 2
<!-- 设置第一个SQL参数的值 -->
<sql:param>58 </sql:param>
<!-- 使用dateParam标签设置第二个SQL参数的值 -->
<sql:dateParam value="${myDate}" type="date"/>
</sql:update>
已经将id=2的记录的amount和saledate字段值分别修改成58和2001-10-20
在浏览器地址栏中输入如下的URL来测试dateParam.jsp页面:
http://localhost:8080/demo/chapter9/dateParam.jsp
9.6 XML标签库
为了简化JSP页面中对XML文档的处理,JSTL中提供了一个用于处理XML文档的标签库,JSP规范建议XML标签库的前缀名为x.
在XML标签库的标签中可以使用XPath表达式,XPath表达式是专门用于查询XML文档资料中的数据的表达式语言,读者可以从下面的网址查询XPath表达式的规范:
http://www.w3.org/TR/xpath
在XML标签中可以通过XPath表达式访问Web域中的域属性。XPath表达式所访问的Web域和EL中定义的隐含对象所访问的是Web域相同。表9.3列出了访问Web域中域属性的XPath表达式和获得Web域中域属性的方法的映射关系。
表9.3 访问Web域中域属性的XPath表达式和获得Web域中域属性的方法的映射
从表9.3可以看出,通过XPath表达式可以从request、session等Web域以及Cookie、请求参数、请求消息头中获得指定的属性值。如下面的XPath表达式从request域中获得id值,并在XML文档中检索id属性等于id域属性的<商品>元素:
/工厂/商品[@id=$requestScope.id]
在开始学习XML标签之前,先建立一个XML文档,该XML文档的文件名为products.xml.该XML文档的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<商场>
<商品id = "0001A">
<商品名称>自行车</商品名称>
<产地>上海</产地>
<单价>320</单价>
</商品>
<商品id = "0001B">
<商品名称>洗衣机</商品名称>
<产地>深圳</产地>
<单价>1024</单价>
</商品>
<商品id = "0002C">
<商品名称>笔记本电脑</商品名称>
<产地>厦门</产地>
<单价>6520</单价>
</商品>
</商场>
9.6.1 <x:parse>标签
<x:parse>标签用于解析XML文档,并将解析的结果对象保存在指定的Web域中的某个属性中。<x:parse>标签可以通过如下3种方式指定XML数据源:
在JSP页面中导入外部的XML文档,如可以通过9.3.12节介绍的<c:import>标签将XML文档导入JSP页面。
使用<c:set>标签设置XML文档。
在<x:parse>标签体中内嵌XML文档。
使用<x:parse>标签应注意如下几点:
xml属性是为XML规范保留的,建议读者尽量不要使用该属性,而要使用doc属性指定XML文档。
如果doc属性或xml属性的值为null或空字符串,则<x:parse>标签会抛出JspException异常。
如果filter属性值为null,<x:parse>标签会忽略该属性。
如果指定var属性,JSTL规范允许<x:parse>标签使用适当的类型保存解析结果,如果指定varDom属性,<x:parse>标签的解析结果必须是Document对象实例。在apache的JSTL实现中var属性和varDom属性是一样的。
parse.jsp页面是一个使用<x:parse>标签解析XML文档的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%>
<!-- 导入products.xml -->
<c:import url="products.xml" charEncoding="UTF-8" var="products" />
<!-- 解析products.xml文件中的XML文档 -->
<x:parse varDom="productDom" doc="${products}" />
<table border="1">
<tr>
<th>商品名称</th>
<th>产地</th>
<th>单价</th>
</tr>
<tr>
<!-- 使用XPath表达式输出products.xml中的相应信息 -->
<td><x:out select="$productDom//商品[@id='0001A']//商品名称" /></td>
<td><x:out select="$productDom//商品[@id='0001A']//产地" /></td>
<td><x:out select="$productDom//商品[@id='0001A']//单价" /></td>
</tr>
</table>
<hr>
<!-- 解析内嵌的XML文档 -->
<x:parse varDom="dom" >
<root>
<element1>
value1
</element1>
<element2>
value2
</element2>
</root>
</x:parse>
<!-- 输出内嵌的XML文档的元素内容,$dom表示从Web域中获得Document对象 -->
<x:out select="$dom//element1" /><br>
<x:out select="$dom//element2" />
在浏览器地址栏中输入如下的URL来测试parse.jsp页面:
http://localhost:8080/demo/chapter9/parse.jsp
9.6.2 使用Filter过滤XML文档
由于<x:parse>标签在解析XML文档时,会将整个XML文档都装入内存。如果<x:parse>标签解析的XML文档非常大,就会大量消耗系统资源。因此,在这种情况下,就需要在解析之前对XML进行过滤。<x:parse>标签提供了一个filter属性,通过该属性指定一个过滤器的对象实例,就可以达到过滤XML文档的目的。
【实例9-1】 使用Filter过滤XML文档
1. 实例说明
本实例使用Servlet类创建一个org.xml.sax.XMLFilter对象实例,并将该对象实例保存在request域中。在xmlFilter.jsp页面包含该Servlet,在使用<x:parse>解析XML文档时,将request域中的XMLFilter对象实例作为filter属性的值。在本例中仍然使用products.xml文件作为测试用的XML文档,并建立一个products.xsl文件作为过滤XML文档的XSLT文档。products.xsl文件可以将products.xml文件中所有单价小于1000的商品过滤掉。
2. 建立products.xsl文件
products.xsl文件是一个XSLT(Extensible Stylesheet Language Transformations)文档,XSLT是一种样式表转换语言,用于根据XML文档中的数据生成自定义格式的样式。在本例中使用XSLT生成XML文档的子集。products.xsl文件的内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<xsl:template match="/">s
<商场>
<xsl:for-each select="商场/商品[单价>=1000]">
<商品>
<商品名称><xsl:value-of select="商品名称"/></商品名称>
<产地><xsl:value-of select="产地"/></产地>
<单价><xsl:value-of select="单价"/></单价>
</商品>
</xsl:for-each>
</商场>
</xsl:template>
</xsl:stylesheet>
3. 测试products.xsl文件
实际上可以直接在XML文档中引用XSLT文档来测试该XSLT文档是否正确。在products.xml文件的第二行添加如下的代码来引用products.xsl文件:
<?xml-stylesheet type="text/xsl" href="products.xsl"?>
在浏览器地址栏输入如下的URL:
http://localhost:8080/demo/chapter9/products.xml
浏览器显示的结果如图9.16所示。
图9.16 测试products.xsl文件
从图9.16所示的显示结果可以看出,浏览器只显示出了单价不小于1000的商品。因此可以断定,products.xsl文件成功过滤了products.xml文件中的数据。
4. 编写MyXMLFilter类
MyXMLFilter类是一个Servlet类,用于创建XMLFilter对象实例,并将该对象实例保存在request域中。MyXMLFilter类的实现代码如下:
public class MyXMLFilter extends HttpServlet
{
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
try
{
// 获得products.xsl文件所在的本地路径
String path = this.getServletContext()。getRealPath("/chapter9");
// 创建SAXTransformerFactory对象实例
SAXTransformerFactory saxTransformerFactory =
(SAXTransformerFactory) TransformerFactory.newInstance();
// 创建封装products.xsl文件内容的StreamSource对象实例
StreamSource streamSource = new StreamSource(path + "/products.xsl");
// 创建XMLFilter对象实例
XMLFilter xmlFilter = saxTransformerFactory.newXMLFilter(streamSource);
// 将XMLFilter对象实例保存在request域中
request.setAttribute("xmlFilter", xmlFilter);
}
catch (Exception e)
{
System.out.println(e.getMessage());
}
}
}
5. 编写xmlFilter.jsp页面
xmlFilter.jsp页面导入了MyXMLFilter,并使用<x:parse>标签解析products.xml文件,该标签的filter属性值就是在MyXMLFilter类中创建的XMLFilter对象实例。xmlFilter.jsp页面的实现代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%>
<!-- 包含MyXMLFilter -->
<jsp:include page="MyXMLFilter"/>
<!-- 导入products.xml文件 -->
<c:import url="products.xml" charEncoding="UTF-8" var="products" />
<!-- 解析XML文档,并使用过滤器进行过滤 -->
<x:parse varDom="productDom" doc="${products}" filter="${xmlFilter}" />
<table border="1">
<tr>
<th>商品名称</th>
<th>产地</th>
<th>单价</th>
</tr>
<!-- 输出过滤后XML文档中的所有数据 -->
<x:forEach select="$productDom//商品">
<tr>
<td><x:out select="商品名称" /></td>
<td><x:out select="产地" /></td>
<td><x:out select="单价" /></td>
</tr>
</x:forEach>
</table>
6. 测试xmlFilter.jsp页面
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/xmlFilter.jsp
浏览器显示的结果如图9.17所示。
图9.17 使用XSLT过滤XML文档
从图9.17所示的输出结果可以看出,第一个商品(单价为320的自行车)被过滤掉了,只显示出单价大于1000的商品。
9.6.4 <x:set>标签
<x:set>标签用于计算XPath表达式,并将计算结果保存在Web域中的属性中。
x_set.jsp页面是一个使用<x:set>标签保存XPath表达式计算结果,并使用<x:out>标签输出计算结果的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%>
<c:import url="products.xml" charEncoding="UTF-8" var="products" />
<x:parse varDom="productDom" doc="${products}" />
id等于0002C的商品信息:
<x:set var="product1" select="$productDom//商品[@id='0002C']"/>
<!-- 从Web域中获得XPath计算结果,并输出 -->
<x:out select="$product1"/>
<hr>
id等于0001B的商品名称:
<x:set var="product2" select="$productDom//商品[@id='0001B']//商品名称"
scope="request"/>
<!-- 从Web域中获得XPath计算结果,并输出 -->
<x:out select="$product2"/>
在浏览器地址栏中输入如下的URL来测试x_set.jsp页面:
http://localhost:8080/demo/chapter9/x_set.jsp
9.6.5 XPath表达式的条件判断
XML标签库提供了一些用于条件判断的标签,如<x:if>、<x:when>等。这些标签将XPath表达式的计算结果转换成布尔类型的值再进行判断,如果XPath表达式的计算结果为true,则执行标签体的内容。将XPath表达式的计算结果转换成布尔类型的规则如下:
如果XPath表达式的计算结果是一个数值,只有在计算结果不是0或NaN时,转换的结果才为true.其中NaN是不能用数值表示的值,如正无穷大和负无穷大。
如果XPath表达式的结果是XML文档的节点集时,只有在节点集非空时,转换的结果才为true。
如果XPath表达式的结果是一个字符串,只有在非空字符串(长度为0的字符串)时,转换的结果才为true。
9.6.6 <x:if>标签
<x:if>标签可以构造简单的"if-then"结构的条件表达式,该标签计算一个XPath表达式,当计算结果为true就执行标签体的内容。如果指定了scope属性,必须指定var属性,否则<x:if>标签会抛出异常。
x_if.jsp是一个使用<x:if>标签进行条件判断的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%>
<c:import url="products.xml" charEncoding="UTF-8" var="products" />
<x:parse varDom="productDom" doc="${products}" />
<x:if select="$productDom//商品[@id='0002C']" var="result" scope="request" >
id等于0002C的商品已找到,商品信息:
<x:out select="$productDom//商品[@id='0002C']" />
<hr>
XPath表达式的计算结果:${result }
</x:if>
在浏览器地址栏中输入如下的URL来测试x_if.jsp页面:
http://localhost:8080/demo/chapter9/x_if.jsp
9.6.7 <x:choose>、<x:when>和<x:otherwise>标签
<x:choose>标签用于构成包含多个分支的条件判断表达式。每一个<x:when>标签表示一个条件分支,<x:otherwise>标签表示当所有的<x:when>标签的条件都不满足时,执行<x:otherwise>标签体的内容。<x:when>标签只有一个select属性,该属性是String类型,不支持动态属性值。select属性表示要计算的XPath表达式,该XPath表达式的计算结果被转换成布尔类型,如果计算结果为true,则执行<x:when>标签体的内容。<x:choose>、<x:when>和<x:otherwise>标签组合的其他规则与核心标签库中的<c:choose>、<c:when>和<c:choose>标签组合的规则相同,读者可以参考9.3.6节中的内容。
x_choose.jsp是一个使用<x:choose>标签进行多分支条件判断的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%>
<c:import url="products.xml" charEncoding="UTF-8" var="products" />
<x:parse varDom="productDom" doc="${products}" />
<x:choose>
<x:when select="$productDom//商品[@id=$param:id]">
id等于${param.id}的商品:<x:out select="$productDom//商品[@id=$param:id]" />
</x:when>
<x:otherwise>
未找到id等于${param.id}的商品!
</x:otherwise>
</x:choose>
在浏览器地址栏中输入如下的URL来测试x_choose.jsp页面:
http://localhost:8080/demo/chapter9/x_choose.jsp?id=0002C
如果将id请求参数改成其他在products.xml文件中不存在的值,如0003A,再次访问x_choose.jsp页面,就会输出如下的信息:
未找到id等于0003A的商品!
9.6.8 <x:forEach>标签
<x:forEach>标签用于计算一个XPath表达式,并将计算结果作为上下文(Context)来迭代标签体的内容。使用<x:forEach>标签应注意如下几点:
如果指定begin属性,该属性值必须大于或等于0.
如果指定end属性,并且该属性值小于begin属性的值,不迭代标签体中的内容。
如果指定step属性,该属性值必须大于或等于1.
如果select属性值为空字符串,<x:forEach>标签将抛出JspException异常。
x_forEach.jsp页面是一个使用<x:forEach>标签迭代XML文档中某个节点的子节点的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%>
<c:import url="products.xml" charEncoding="UTF-8" var="products" />
<x:parse varDom="productDom" doc="${products}" />
<table border="1">
<tr>
<th>ID</th>
<th>商品编码</th>
<th>商品名称</th>
<th>产地</th>
<th>单价</th>
</tr>
<!-- 使用forEach标签迭代商品节点中的子节点 -->
<x:forEach select="$productDom//商品" varStatus="status">
<tr>
<td>${status.count}</td>
<td><x:out select="@id" /></td>
<td><x:out select="商品名称" /></td>
<td><x:out select="产地" /></td>
<td><x:out select="单价" /></td>
</tr>
</x:forEach>
</table>
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/x_forEach.jsp
浏览器显示的输出结果如图9.18所示。
图9.18 使用<x:forEach>标签迭代"商品"节点的子节点
9.6.9 <x:transform>标签
<x:transform>标签根据指定的XSLT样式表对XML文档进行转换,在转换时不执行任何DTD验证或Schema验证。使用<x:transform>标签应注意如下几点:
xml属性和xmlSystemId属性是为XML规范保留了,建议读者不要使用这两个属性,而要使用doc属性和docSystemId属性。
如果要转换的XML文档或XSLT样式表文档为null或空字符串,<x:transform>标签将抛出JspException异常。
如果指定了result属性,转换结果将保存在Result对象中,如可以在创建Result对象时指定一个文件,这时<x:transform>标签会将转换结果保存在指定的文件中。
如果设置了var属性和scope属性,<x:transform>标签会将转换结果以org.w3c.doc.Document对象的形式保存在scope属性指定的Web域中,域属性名为var属性的值。
在实验<x:transform>标签之前先建立一个myProducts.xsl文件,该文件是XSLT样式表文档,代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<xsl:stylesheet version="1.0"
xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
<!-- 定义XSLT样式表参数,并指定默认值 -->
<xsl:param name="price">1000</xsl:param>
<xsl:template match="/">
<html>
<head>
<!-- 指定转换后的HTML文档采用UTF-8编码 -->
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
</head>
<body>
<table border="1">
<tr align="center">
<th>商品编码</th>
<th>商品名称</th>
<th>产地</th>
<th>单价</th>
</tr>
<!-- 迭代商品节点的所有满足条件的子节点 -->
<xsl:for-each select="商场/商品[单价>=$price]">
<tr>
<td>
<xsl:value-of select="@id" />
</td>
<td>
<xsl:value-of select="商品名称" />
</td>
<td>
<xsl:value-of select="产地" />
</td>
<td>
<xsl:value-of select="单价" />
</td>
</tr>
</xsl:for-each>
</table>
</body>
</html>
</xsl:template>
</xsl:stylesheet>
transform.jsp页面是一个使用<x:transform>标签根据XSLT样式表转换XML文档的例子,代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/xml" prefix="x"%>
<%@page import="javax.xml.transform.stream.StreamResult"%>
<!-- 导入XML文档(products.xml文件) -->
<c:import url="products.xml" charEncoding="UTF-8" var="products" />
<!-- 导入XSLT样式表文档(myProducts.xsl文件) -->
<c:import url="myProducts.xsl" charEncoding="UTF-8" var="products_xslt" />
<form>
最低单价:
<!-- 提交price请求参数,也是XSLT样式表的请求参数 -->
<input type="text" name="price" value="${param.price}" />
<input type="submit" value="提交" />
</form>
<!-- 根据XSLT样式表转换XML文档 -->
<x:transform doc="${products}" xslt="${products_xslt}">
<!-- 指定XSLT样式表参数值(该参数值就是相应的请求参数值)-->
<x:param name="price" value="${param.price}" />
</x:transform>
<hr>
<!-- 根据XSLT样式表转换XML文档,并将转换结果保存在Web域中 -->
<x:transform doc="${products}" xslt="${products_xslt}" var="trans" />
输出table元素的字符串值:
<x:out select="$trans//table" />
<%
String path = pageContext.getServletContext()。getRealPath("/");
StreamResult sr = new StreamResult(path + "products.html");
System.out.println(path + "products.html");
request.setAttribute("result", sr);
%>
<!-- 将转换结果保存在products.html文件中 -->
<x:transform doc="${products}" xslt="${products_xslt}" result="${result}" />
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter9/transform.jsp
在"最低单价"文本框中输入1000,单击"提交"按钮,将输出如图9.19所示的结果。
图9.19 使用<x:transform>标签转换XML文档
9.7 JSTL自定义函数
为了JSP页面中的字符串操作,JSTL中提供了一套EL自定义函数,这些函数可以完成大多数的字符串操作。如fn:contains函数判断一个字符串中是否包含指定的子字符串,fn:toLowerCase函数可以将字符串中的所有字符转换成小写形式。所有的自定义函数中的参数值如果为null,会按着空字符串处理。在使用JSTL自定义函数之前,需要使用taglib指定来引用JSTL自定义函数,代码如下:
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn"%>
关于本节的示例代码,请读者参阅fn.jsp页面。在浏览器地址栏中输入如下的URL可以访问fn.jsp页面:
http://localhost:8080/demo/chapter9/fn.jsp
9.7.1 fn:contains函数
fn:contains函数用于检测一个字符串中是否包含指定的子字符串,如果包含子字符串,返回true,否则返回false.fn:contains函数在比较两个字符是否相等时是大小写敏感的。
fn:contains函数有两个参数,第一个参数表示被检测的字符串,第二个参数表示子字符串。如果第一个参数值中包含第二个参数值,fn:contains函数返回true,否则返回false.fn:contains函数相当于判断fn:indexOf函数返回值是否为-1,如下面的代码所示:
fn:indexOf(string, substring) != -1
下面的代码是fn:contains函数的应用举例:
<!-- 返回true -->
${fn:contains("nokiaguy.blogjava.net", "nokiaguy")}
<!-- 返回false -->
${fn:contains("nokiaguy.cnblogs.com","Nokiaguy")}
<!-- 返回true -->
${fn:contains("nokiaguy.cnblogs.com", "")}
<!-- 返回true -->
${fn:contains("nokiaguy.blogjava.net", null)}
9.7.2 fn:contains Ignore Case函数
fn:containsIgnoreCase函数的功能和fn:contains函数类似,这两个函数的唯一区别是fn:containsIgnoreCase函数在比较两个字符时对大小写不敏感。读者可以使用下面的代码来代替fn:containsIgnoreCase函数的功能:
fn:contains(fn:toUpperCase(string), fn:toUpperCase(substring))
下面的代码是fn:containsIgnoreCase函数的应用举例:
<!-- 返回true -->
${fn:containsIgnoreCase("nokiaguy.blogjava.net", "nokiaguy")}
<!-- 返回true -->
${fn:containsIgnoreCase("nokiaguy.cnblogs.com","Nokiaguy")}
9.7.3 fn:startsWith函数
fn:startsWith函数用于检测一个字符串是否以一个指定的字符串开始,返回Boolean类型。fn:startsWith函数有两个参数,第一个参数表示被检测的字符串,第二个参数表示子字符串。如果第一个参数值以第二个参数值开始,fn:startsWith函数返回true,否则返回false.fn:startsWidth函数在比较两个字符时是大小写敏感的。
下面的代码是fn:startsWidth函数的应用举例:
<!-- 返回true -->
${fn:startsWith("nokiaguy.cnblogs.com","nokiaguy")}
<!-- 返回false -->
${fn:startsWith("nokiaguy.cnblogs.com","guy")}
<!-- 返回false -->
${fn:startsWith("nokiaguy.cnblogs.com","Nokia")}
9.7.4 fn:ends With 函数
fn:endsWith函数用于检测一个字符串是否以一个指定的字符串结束,返回Boolean类型。fn:endsWith函数有两个参数,第一个参数表示被检测的字符串,第二个参数表示子字符串。如果第一个参数值以第二个参数值结束,fn:endsWith函数返回true,否则返回false.fn:endsWidth函数在比较两个字符时是大小写敏感的。
下面的代码是fn:endsWidth函数的应用举例:
<!-- 返回true -->
${fn:endsWith("nokiaguy.cnblogs.com",".com")}
<!-- 返回false -->
${fn:endsWith("nokiaguy.cnblogs.com",".net")}
<!-- 返回false -->
${fn:endsWith("nokiaguy.cnblogs.com",".COM")}
9.7.5 fn:escapeXml函数
fn:escapeXml函数用于将字符串中的特殊字符按着表9.2所示的转换规则进行HTML编码转换,并返回转换后的字符串。fn:escapeXml函数有一个字符串参数,表示被转换的字符串。
下面的代码是fn:escapeXml函数的应用举例:
<a href='http://nokiaguy.blogjava.net'>nokiaguy.blogjava.net</a><br>
${fn:escapeXml("<a href='http://nokiaguy.blogjava.net'>
nokiaguy.blogjava.net</a>")}
在运行上面的代码后,第一行将被解释成HTML代码,而使用fn:escapeXml函数进行转换后,将在浏览器中按原样显示被转换的字符串,而发送到客户端的内容如下:
<a href=';
http://nokiaguy.blogjava.net'>nokiaguy.blogjava.net</a>;
9.7.6 fn:indexOf函数
fn:indexOf函数返回一个字符串中第一次出现指定子字符串的位置(从0开始),返回值为int类型。fn:indexOf函数有两个参数,第一个参数表示要检索的字符串,第二个参数表示子字符串。不管第二个参数值在第一个参数值中出现几次,fn:indexOf函数总是返回第一次出现的位置。如果第二个参数值在第一个参数值中未出现,fn:indexOf函数返回-1,如果第二个参数值为空字符串,fn:indexOf函数返回-1.
下面的代码是fn:indexOf函数的应用举例:
fn:indexOf函数演示<br>
<!-- 返回0 -->
${fn:indexOf("nokiaguy.cnblogs.com","nokiaguy")}
<!-- 返回16 -->
${fn:indexOf("nokiaguy.cnblogs.com",".com")}
<!-- 返回-1 -->
${fn:indexOf("nokiaguy.cnblogs.com","abcd")}
<!-- 返回0 -->
${fn:indexOf("nokiaguy.cnblogs.com","")}
9.7.7 fn:split
fn:split函数以指定字符串作为分隔符,将一个字会串分割成字符串数组并返回该数组。fn:split函数有两个参数,第一个参数表示被分割的字符串,第二个参数表示作为分隔符的字符串。fn:split函数使用java.util.StringTokenizer类分割字符串,得到的字符串数组不包含分隔符本身。如果第一个参数值是空字符串,返回的字符串数组只包含一个空字符串元素。如果第二个参数值为空字符串,返回的字符串数组只包含一个元素,该元素就是源字符串(第一个参数值)本身。
下面的代码是fn:split函数的应用举例:
<!-- 返回nokiaguy -->
${fn:split("nokiaguy.cnblogs.com",".")[0]} <br>
<!-- 返回nokia -->
${fn:split("nokiaguy.cnblogs.com","gy")[0]}<br>
<!-- 返回u -->
${fn:split("nokiaguy.cnblogs.com","gy")[1]}<br>
<!-- 返回nokiaguy.cnblogs.com -->
${fn:split("nokiaguy.cnblogs.com","")[0]}
9.7.9 fn:length函数
fn:length函数返回指定字符串、数组或集合的长度。fn:length函数只有一个参数,该参数值除了可以是字符串或数组外,还可以是<c:forEach>标签的items属性所支持的各种类型的集合,如java.util.Collection、java.util.Iterator、java.util.Map等。
下面的代码是fn:length函数的应用举例:
<%
java.util.Map<String, String> map = new java.util.HashMap<String, String>();
map.put("abc","xyz");
map.put("xyz","abc");
map.put("ccc","abcxyz");
String[] strArray = new String[]{};
request.setAttribute("map", map);
request.setAttribute("strArray", strArray);
%>
<!-- 返回8 -->
${fn:length("编译原理:55元")}
<!-- 返回2 -->
${fn:length(fn:split("superman#126.com", "#"))}
<!-- 返回0 -->
${fn:length(strArray)}
<!-- 返回3 -->
${fn:length(map)}
9.7.10 fn:replace函数
fn:replace函数用于将一个字符串中的某个子字符串替换成别外一个指定的字符串,并返回被替换后的字符串。fn:replace函数有三个参数,第一个参数表示源字符串,第二个参数表示被替换的子字符串,第三个参数表示要被替换成的字符串。fn:replace函数按着如下规则进行替换:
原字符串中出现的所有子字符串都被替换成了第3个参数的值。
如果第一个参数为空字符串,fn:replace函数返回空字符串。
如果第二个参数为空字符串,fn:replace函数返回原字符串(第一个参数的值)。
如果第三个参数为空字符串,fn:replace函数将在原字符串中删除第二个参数指定的子字符串,并返回删除后的原字符串。
下面的代码是fn:replace函数的应用举例:
<!-- 返回ababcdefgefg -->
${fn:replace("abcdefg","cd","abcdefg")}
<!-- 返回xyzbxyzcxyzd -->
${fn:replace("abacad","a","xyz")}
<!-- 返回bcd -->
${fn:replace("abacad","a","")}
<!-- 返回abacad -->
${fn:replace("abacad","","abc")}
9.7.11 fn:substring函数
fn:substring函数截取一个字符串的子字符串,并返回截取后的子字符串。fn:substring函数有三个参数。第一个参数表示原字符串,第二个参数表示要截取的子字符串在原字符串中的开始索引,第三个参数表示要截取的子字符串在原字符串中的结束索引。第二个参数和第三个参数都是Integer类型,它们的索引值都从0开始。
在使用fn:substring函数时应注意如下规则:
fn:substring函数截取的子字符串不包括原字符串中第3个参数指定的索引的字符。
如果第二个参数的值大于或等于原字符串中的字符个数,fn:substring函数返回空字符串。
如果第二个参数的值小于0,则将其值设为0.
如果第三个参数的值小于0,或大于原字符串中的字符个数,则将其值设为原字符串的长度,即截取的字符串是从第二个参数值开始往后的所有字符。
如果第三个参数值小于第二个参数值,fn:substring函数返回空字符串。
下面的代码是fn:substring函数的应用举例:
<!-- 返回bc -->
${fn:substring("abcdefg",1,3)}
<!-- 返回空字符串 -->
${fn:substring("abcdefg",20,3)}
<!-- 返回abc -->
${fn:substring("abcdefg",-2,3)}
<!-- 返回cdefg -->
${fn:substring("abcdefg",2,30)}
<!-- 返回空字符串 -->
${fn:substring("abcdefg",4,2)}
9.7.12 fn:substringAfter函数
fn:substringAfter函数用于截取一个字符串中第一次出现的子字符串的后面的子符串。fn:substringAfter函数有两个参数。第一个参数表示原字符串,第二个参数表示在原字符串中第一次出现的子字符串。fn:substringAfter函数按如下的规则截取子字符串。
如果第一个参数值是空字符串,fn:substringAfter函数返回空字符串。
如果第二个参数值是空字符串,fn:substringAfter函数返回源字符串(第一个参数值)。
如果第二个参数值不是第二个参数值的子符,fn:substringAfter函数返回空字符串。
下面的代码是fn:substringAfter函数的应用举例:
fn:substringAfter函数演示<br>
<!-- 返回defg -->
${fn:substringAfter("abcdefg","bc")}
<!-- 返回空字符串 -->
${fn:substringAfter("","bc")}
<!-- 返回abcdefg -->
${fn:substringAfter("abcdefg","")}
<!-- 返回空字符串 -->
${fn:substringAfter("abcdefg","xyz")}
9.7.13 fn:substringBefore函数
fn:substringBefore函数用于截取一个字符串中第一次出现的子字符串的前面的子符串。fn:substringBefore函数有两个参数。第一个参数表示原字符串,第二个参数表示在原字符串中第一次出现的子字符串。fn:substringBefore函数按如下的规则截取子字符串。
如果第一个参数值是空字符串,fn:substringBefore函数返回空字符串。
如果第二个参数值是空字符串,fn:substringBefore函数返回空字符串。
如果第二个参数值不是第二个参数值的子符,fn:substringBefore函数返回空字符串。
下面的代码是fn:substringBefore函数的应用举例:
<!-- 返回abc -->
${fn:substringBefore("abcdefg","de")}
<!-- 返回空字符串 -->
${fn:substringBefore("","bc")}
<!-- 返回空字符串 -->
${fn:substringBefore("abcdefg","")}
<!-- 返回空字符串 -->
${fn:substringBefore("abcdefg","xyz")}
第10章 简单标签
在JSP1.x规范中只定义了一种实现自定义标签的方法,通过这种方法实现的标签被称为传统标签。虽然传统标签完全可以胜任自定义标签的工作,但开发一个复杂的自定义标签需要考虑的东西太多,如传统标签有三个核心接口(Tag、IterationTag和BodyTag)以及其他一些相关的类,这将给开发工作带来很多的麻烦。因此,Sun在JSP2.0规范中定义了一种新的自定义标签:简单标签。
10.1 简单标签基础
简单标签的标签类必须实现javax.servlet.jsp.tagext.SimpleTag接口。SimpleTag接口中处理标签逻辑的方法只有一个doTag方法,该方法可以实现传统标签中的doStartTag、doAfterBody和doEndTag等方法的功能。为了简化编写简单标签的工作,JSP API提供了一个javax.servlet.jsp.tagext.SimpleTagSupport类,该类是SimpleTag接口的默认实现类。在通常情况下,简单标签的标签类只需要继承SimpleTagSupport类即可。
10.1.1 简单标签的基本原理
简单标签的标签类必须实现SimpleTag接口,但SimpleTag接口中只有一个处理标签逻辑的doTag方法,而且doTag方法没有返回值。doTag方法的定义如下:
public void doTag()throws javax.servlet.jsp.JspException,
java.io.IOException
在传统标签中需要通过doStartTag、doAfterBody和doEndTag方法的返回值来通知Web容器所何调用标签,例如,doStartTag方法返回EVAL_BODY_INCLUDE,Web容器会执行标签体的内容;doAfterBody方法返回EVAL_BODY_AGAIN,Web容器会再次执行标签体的内容;doEndTag方法返回EVAL_PAGE,Web容器会继续执行标签后面的内容。
由于doTag方法没有返回值,因此,通过返回值来控制处理标签的过程是行不通的。但仍然可以采用其他的方法来完成与传统标签同样的功能。
根据上面所述,传统标签的功能主要有如下几个:
是否执行标签体的内容
是否重复执行标签体的内容
是否执行标签后面的内容
前两个功能实际上是就是一个功能:执行标签体的内容。它们的区别只是执行一次和执行多次。从传统标签的工作原理可以得知,在传统标签中实际上是将执行标签体的工作交给了Web容器。但在简单标签中只能使用doTag方法来处理标签逻辑,而且该方法还没有返回值,因此,将执行标签体内容的权利交给Web容器并不容易。为了使编写简单标签更容易,可以采用另外一种方式来处理,这就是将执行标签体内容的代码写在doTag方法中。如果要这样做,就必须在doTag方法通过某种机制可以执行标签体的内容。
在JSP API中提供了一个javax.servlet.jsp.tagext.JspFragment类,通过JspFragment类的invoke方法可以调用标签体的内容。在SimpleTag接口中定义了两个方法:setJspBody和getJspBody方法。Web容器会将标签体的内容封装在JspFragment对象中,并调用SimpleTag接口的setJspBody方法将JspFragment对象传入标签类的对象实例,然后Web容器会调用标签类的doTag方法来处理标签逻辑。这样在doTag方法中就可以通过getJspBody方法获得JspFragment对象实例,并调用JspFragment类的invoke方法来执行标签体的内容。当然,如果想循环执行标签体的内容,只需要循环调用invoke方法即可。实际上,JSP引擎在翻译简单标签时,会将标签体的内容翻译成Java代码后,会再生成一个invoke方法,然后将这些代码放在invoke方法中。当调用JspFragment类的invoke方法时,就相当于调用了JSP引擎生成的invoke方法。如果读者查看由调用简单标签的JSP页面生成的Servlet源代码就可以看到这一切。
现在来解决最后一个问题:是否执行标签后面的内容。在传统标签中是通过doEndTag方法的返回值来通知Web容器是否执行标签后面的内容,而简单标签的doTag方法没有返回值,但可以通过抛出异常的方式来达到返回值得的效果。在简单标签中如果不想让Web容器执行标签后面的内容,可以在doTag方法中抛出javax.servlet.jsp.SkipPageException异常。Web容器会截获这个异常。如果在调用doTag方法时抛出异常,自然就会跳过标签后面的内容了。
从上面的描述可以知道简单标签分别通过如下两种方式来实现(循环)执行标签体的内容和忽略标签体的内容:
在doTag方法中通过调用JspFragment类的invoke方法来执行标签体的内容,将调用invoke方法的代码放到Java的循环语句中就可以实现循环调用标签体的内容的功能。
在doTag方法中通过SkipPageException异常的方式来实现忽略标签后面内容的功能。如果doTag方法不抛出异常,Web容器会继续执行标签后面的内容。
10.1.2 SimpleTag接口
SimpleTag接口是简单标签的核心接口,所有简单的标签类必须实现该接口。SimpleTag接口定义如下5个方法:
1. doTag方法
doTag方法用于完成标签处理逻辑。该方法可以完成传统标签中所有的逻辑,如迭代集合中的元素、修改标签体内容等。doTag方法的定义如下:
public void doTag() throws javax.servlet.jsp.JspException, java.io.IOException
在doTag方法中需要抛出SkipPageException异常来阻止Web容器继续执行标签后面的内容。SkipPageException是JspException类的子类,因此,在doTag方法中可以直接抛出SkipPageException异常。doTag方法抛出SkipPageException异常相当于传统标签中的doEndTag方法返回SKIP_PAGE常量的情况。
2. setParent方法
setParent方法用于将父标签的标签类的对象传入当前标签的标签类对象,该方法和Tag接口的setParent方法的作用和意义相同。setParent方法的定义如下:
public void setParent(JspTag parent)
从上面的方法定义可以看出,Tag接口的setParent方法和SimpleTag接口的setParent方法在一点不同,就是SimpleTag接口的setParent方法的参数类型是JspTag,而Tag接口的setParent方法的参数类型是Tag.由于JspTag是Tag和SimpleTag的父接口,因此,简单标签的父标签既可以是简单标签,也可以是传统标签,而传统标签的父标签只能是传统标签。
3. getParent方法
getParent方法用于返回当前标签的父标签的对象实例,该对象实例是通过setParent方法传入当前标签对象。getParent方法的定义如下:
public JspTag getParent()
4. setJspContext
setJspContext方法用于将表示当前JSP页面的PageContext对象传入当前标签的对象实例。setJspContext方法的定义如下:
public void setJspContext(JspContext pc)
JspContext是PageContext的父类。PageContext类必须依赖Servlet环境才能使用,但JspContext类并不依赖于Servlet环境。因此,虽然目前简单标签只能运行在Servlet环境中,但可以通过继承JspContext类的方式使简单标签在非Servlet环境中运行。
5. setJspBody方法
setJspBody标签用于将表示当前标签的标签体的JspFragment对象传入标签类的对象实例,该方法的定义如下:
public void setJspBody(JspFragment jspBody)
JSP API还提供了一个SimpleTagSupport类,该类对SimpleTag接口提供了默认的实现。SimpleTagSupport类中已经为JspTag、JspContext和JspFragment对象定义了相应的变量,但这些变量都是私有的,需要分别使用getJspTag、getJspContext和getJspBody方法来获得这三个对象。
10.1.3 JspFragment类
JspFragment类是在JSP2.0中新增加的,该类的对象实例表示JSP页面中一段不包含Java代码的JSP代码片段。
JSP引擎在翻译简单标签时,会将简单标签的标签体转换成相应的Java代码,并放在一个invoke方法中,然后会生成一个JspFragment类的子类,并使用该invoke方法的内容来实现JspFragment类的invoke方法。因此,调用JspFragment类的invoke方法就相当于调用由标签体转换而来的Java代码,也就是说,调用JspFragment类的invoke方法就是执行标签体的内容。如果想重复执行标签体的内容,就重复调用JspFragment类的invoke方法。
JspFragment类只定义了如下两个抽象方法:
1. invoke方法
invoke方法用于执行标签体的内容。该方法的定义如下:
public abstract void invoke(Writer out)
invoke方法有一个java.io.Writer类型的参数,如果指定该参数,invoke方法会将标签体的执行结果输出到指定的Writer对象的缓冲区中;如果该参数值为null,invoke方法会将标签体的执行结果输出到JspContext.getOut方法获得的Writer对象的缓冲区中。如果想修改标签体的执行结果,可以先将标签体的执行结果输出到指定的Writer对象中(如StringWriter对象、BufferedWriter对象等),然后修改这些对象中的内容,最后再通过JspContext.getOut方法获得将数据输出到客户端的JspWriter对象,并通过JspWriter对象将修改过的标签体执行结果发送的客户端。
2. getJspContext方法
getJspContext方法用于返回表示当前JSP页面的JspContext对象,该方法的定义如下:
public abstract JspContext getJspContext()
10.1.4 简单标签中方法的调用顺序
由于简单标签中的相关方法比较少,因此,简单标签中方法的调用顺序没有传统标签中方法的调用顺序复杂。简单标签和传统标签中的相同或相似的方法的调用顺序基本相同,例如,Web容器在创建标签类的对象实例后,首先会调用setJspContext方法,如果当前标签有父标签时,会调用setParent方法,然后会调用与标签属性相应的setter方法。简单标签在调用处理标签逻辑的方法(doTag)之前会调用setJspBody方法将表示标签体的JspFragment对象传入标签类的对象实例,最后是调用doTag方法。图10.1是简单标签中方法的调用顺序。
图10.1 简单标签中方法的调用顺序
10.2.1 迭代集合元素的简单标签
【实例10.1】 编写迭代集合元素的简单标签
1. 编写IteratorTag类
IteratorTag类的功能是实现一个可以迭代集合元素的简单标签。该类将标签处理逻辑都写在了doTag方法中,代码如下:
package chapter10;
import java.io.IOException;
import java.util.List;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.tagext.SimpleTagSupport;
import javax.servlet.jsp.JspContext;
public class IteratorTag extends SimpleTagSupport
{
private List items = null;
private String var;
private int begin = 0;
private int end = Integer.MAX_VALUE;
private int step = 1;
private String count = null;
// n表示当前已经迭代的集合元素的个数
private int n = 1;
// 省略了属性的setter方法
… …
@Override
public void doTag() throws JspException, IOException
{
// 如果标签属性满足迭代的条件,开始迭代集合中的元素
if (items != null && items.size() > 0 && begin < items.size()
&& begin <= end && step >= 1)
{
// 获得JspContext对象
JspContext jspContext = this.getJspContext();
// 获得JspFragment对象
JspFragment fragment = this.getJspBody();
// 在for循环中调用invoke方法对集合元素进行迭代
for (int i = begin; i <= end && i < items.size(); i += step)
{
// 将当前迭代的元素保存在page域中
jspContext.setAttribute(var, items.get(i));
// 如果设置了count属性,将当前已经迭代的集合元素个数保存在page域中
if (count != null)
jspContext.setAttribute(count, n++);
// 执行标签体的内容
fragment.invoke(null);
}
}
}
}
从上面的实现代码可以看出,简单标签和传统标签的setter方法部分完全一样,所不同的是简单标签是通过在doTag方法中使用循环调用invoke方法来迭代集合中的元素的,而传统标签是通过doAfterBody方法的返回值来依赖Web容器对集合元素进行迭代的。
2. 安装iterator标签
在jsp-simpletaglib.tld文件中添加如下的内容来安装iterator标签:
<tag>
<description>迭代集合元素的简单标签</description>
<name>iterator</name>
<tag-class>chapter10.IteratorTag</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>items</name>
<required>true</required>
<rtexprvalue>true</rtexprvalue>
</attribute>
<attribute>
<name>var</name>
<required>true</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>begin</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>end</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>step</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
<attribute>
<name>count</name>
<required>false</required>
<rtexprvalue>false</rtexprvalue>
</attribute>
</tag>
3. 编写iterator.jsp页面
iterator.jsp页面使用了简单标签iterator来迭代List对象中的元素。iterator.jsp页面的代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://nokiaguy.blogjava.net/simpletag" prefix="st"%>
<%
java.util.List<String> movies = new java.util.ArrayList<String>();
movies.add("007大破量子危机");
movies.add("独立日");
movies.add("变形金刚 ");
movies.add("星球大战之克隆战争");
movies.add("火星任务");
request.setAttribute("movies", movies);
%>
<center>
欧美电影<p/>
<select name="movies1" multiple="multiple" style="width: 150px; height: 150px">
<st:iterator items="${movies}" var="teleplay" count="count">
<option value="${count}">${teleplay}</option>
</st:iterator>
</select>
<select name="movies2" multiple="multiple" style="width: 150px; height: 150px">
<st:iterator items="${movies}" var="teleplay" count="count" begin="1" end="4" step="2">
<option value="${count}">${teleplay}</option>
</st:iterator>
</select>
</center>
4. 测试iterator标签
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter10/iterator.jsp
浏览器的输出效果如图10.2所示。
图10.2 使用简单标签iterator迭代集合中的元素
10.2.2 修改标签体内容的简单标签
在本节给出一个更复杂的简单标签(text2Table标签)的例子。text2Table标签可以将文本的内容转换成表格,列使用空格、tab等字符分隔,而行使用"\r\n"分隔。text2Table标签还可以将文本内容中的特殊的字符(如<、>、空格等)转换成字符实体编码(详见表9.3的内容)。text2Table标签有如下几个属性:
isFile:该属性指定标签体的内容是否为一个指定文本文件的相对路径。默认值是false.如果该属性为true,text2Table标签会将该相对路径指定的文本文件的内容转换成表格。
haveHeader:该属性指定是否将表格的第一行设置为表头(使用th元素设置)。默认值是false.
escapeXML:该属性指定是否将文本内容中的特殊字符进行HTML编码转换。默认值是true.
attributes:该属性指定了table元素中的所有属性和属性值。也就是说,text2Table标签会将attributes属性值作为<table … >中间的部分。
【实例10.2】 编写将文本内容转换成表格的简单标签
1. 程序说明
在本节实现的简单标签中通过调用JspFragment类的invoke方法将标签体的内容写入指定的StringWriter对象中,并从StringWriter对象中取得标签体的内容,对其修改后,再使用PageContext.getOut()。writer方法将修改后的标签体内容发送的客户端。简单标签的其他功能的实现代码和10.5.4节中的text2Table标签的实现代码相同。
2. 编写Text2TableTag类
Text2TableTag是一个简单标签类,在该类中将所有的修改标签体内容的代码都放在了doTag方法中。Text2TableTag类的代码如下:
package chapter10;
import java.io.IOException;
import java.io.BufferedReader;
import java.io.Reader;
import java.io.FileReader;
import java.io.StringReader;
import java.io.StringWriter;
import javax.servlet.jsp.PageContext;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.tagext.SimpleTagSupport;
public class Text2TableTag extends SimpleTagSupport
{
private boolean isFile = false;
private boolean haveHeader = false;
private boolean escapeXML = true;
private String attributes = null;
public void setAttributes(String attributes)
{
this.attributes = attributes;
}
public void setIsFile(boolean isFile)
{
this.isFile = isFile;
}
public void setEscapeXML(boolean escapeXML)
{
this.escapeXML = escapeXML;
}
public void setHaveHeader(boolean haveHeader)
{
this.haveHeader = haveHeader;
}
// HTML编码转换
private String filter(String message)
{
if (message == null)
return (null);
char content[] = new char[message.length()];
message.getChars(0, message.length(), content, 0);
StringBuffer result = new StringBuffer(content.length + 50);
for (int i = 0; i < content.length; i++)
{
switch (content[i])
{
case '<':
result.append("<");
break;
case '>':
result.append(">");
break;
case '&':
result.append("&");
break;
case '"':
result.append(""");
break;
case ' ':
result.append(" ");
break;
default:
result.append(content[i]);
}
}
return (result.toString());
}
@Override
public void doTag() throws JspException, IOException
{
Reader reader = null;
try
{
PageContext pageContext = (PageContext) this.getJspContext();
JspFragment fragment = this.getJspBody();
// 标签体的内容被写到StringWriter对象中,以便修改标签体的内容
StringWriter out = new StringWriter();
// 调用invoke方法,以获得标签体的内容
fragment.invoke(out);
if (isFile)
{
// 将标签体的内容转换成字符串
String url = out.toString()。trim();
// 获得文本文件的本地路径
String path =
pageContext.getServletContext()。getRealPath(url);
// 获得指向文本文件的Reader对象
reader = new BufferedReader(new FileReader(path));
}
else
{
// 根据文本内容获得Reader对象
reader = new StringReader(out.toString());
}
BufferedReader br = new BufferedReader(reader);
// 开始生成html代码
String html = "<table " + ((attributes == null) ? "" : attributes)
+ ">";
// 将生成的html代码发送到客户端
pageContext.getOut()。write(html);
String line = null;
int lineCount = 1;
while ((line = br.readLine()) != null)
{
if (line.trim()。equals(""))
continue;
// 如果escapeXML属性值是true,对标签体的内容进行HTML编码转换
if (escapeXML)
{
ine = filter(line);
}
String cellHtml = "td";
// 如果haveHeader属性值是true,将第一行作为表格头
if (lineCount == 1 && haveHeader)
{
cellHtml = "th";
}
ineCount++;
html = "<tr>";
String[] cells = line.split("\\s+");
for (String cell : cells)
{
html += "<" + cellHtml + ">" + cell + "</" + cellHtml + ">";
}
html += "</tr>";
// 将生成的html代码发送到客户端
pageContext.getOut()。write(html);
}
html = "</table>";
// 将生成的html代码发送到客户端
pageContext.getOut()。write(html);
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
3. 安装text2Table标签
text2Table标签需要在jsp-simpletaglib.tld文件中添加相应的配置代码才能在程序中使用,关于具体的配置代码,请读者查看随书光盘中的相关内容。
4. 编写text2table.jsp页面
text2table.jsp页面使用text2Table标签将标签体和文本文件中的内容转换成表格,并为text2Table标签增加了一层父标签(<c:set>),将text2Table标签生成的HTML代码保存在Web域中,并使用EL输出到客户端。text2table.jsp页面的代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://nokiaguy.blogjava.net/simpletag" prefix="st"%>
<%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c"%>
<center>
<!-- 将text2Table标签的转换结果保存在Web域中的table1属性中 -->
<c:set var="table1">
<st:text2Table attributes="border='1'" escapeXML="true">
aaa bbb ccc dd d
xxx yyy z zz
u uu v vv
ppp
</st:text2Table>
</c:set>
${table1}
<hr>
<!-- 将text2Table标签的转换结果保存在Web域中的table2属性中 -->
<c:set var="table2">
<st:text2Table attributes="border='1'" haveHeader="true">
产品ID 产品名称 单价 产地
0001 笔记本 6200 上海
0002 复印机 3200 北京
0002 传真机 1210 北京
0003 路由器 1267 广州
</st:text2Table>
</c:set>
${table2}
<hr>
<!-- 将text2Table标签的转换结果保存在Web域中的table3属性中 -->
<c:set var="table3">
<st:text2Table attributes="border='1'" isFile="true">
chapter11\table.txt
</st:text2Table>
</c:set>
${table3}
</center>
5. 测试text2Table标签
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter11/text2table.jsp
浏览器的输出效果如图10.3所示。
图10.3 使用简单标签text2Table将标签体和文本文件中的内容转换成表格
6. 程序总结
在text2table.jsp页面中使用了<c:set>标签作为text2Table标签的父标签,这时,在text2Table标签类中使用PageContext.getOut返回的不再是表示JSP页面的JspWriter对象,而是表示<c:set>标签体的JspWriter对象。因此,text2Table标签并不会直接将转换结果发送的客户端浏览器,而是将转换结果写到了表示<c:set>标签体的JspWriter对象的缓冲区中,再由<c:set>标签负责将这些内容保存在Web域中。实际上,其内部实现原理是:在调用JspFragment.invoke方法时,在invoke方法内部使用PageContext.pushBody方法将表示JSP页面的JspWriter对象压栈,并产生一个新的JspWriter对象,这个对象表示<c:set>的标签体。然后将这个新产生的JspWriter对象使用传统标签的setBodyContent方法转换标签类的对象实例,在invoke方法返回前,使用PageContext.popBody方法将表示JSP页面的JspWriter对象从堆栈中弹出,并让PageContext.getOut方法返回的JspWriter对象重新指向这个表示JSP页面的jspWriter对象。
10.2.3 使用JspFragment类型的属性
在简单标签中可以使用JspFragment类型的属性。如下面的JSP代码使用<jsp:attribute>标签为JspFragment类型的属性赋值,代码如下:
<st:fragment>
<jsp:attribute name="fragment1">
${4 + 5}
</jsp:attribute>
</st:fragment>
其中fragment1是fragment标签的属性,该属性的类型是JspFragment.JSP引擎在翻译fragment标签时会将<jsp:attribute>标签返回的数据封装在JspFragment对象中,并赋给相应的JspFragment类型的属性。如果使用<jsp:attribute>标签设置JspFragment类型的属性,标签体必须放在<jsp:body>标签内,否则JSP引擎在翻译标签时会抛出异常,如下面的代码所示:
<st:fragment>
<jsp:attribute name="fragment1">
${4 + 5}
</jsp:attribute>
<jsp:body>
这是标签体的内容
</jsp:body>
</st:fragment>
可以通过调用JspFragment类的invoke方法执行属性和标签体。
【实例10.3】 使用JspFragment类型的属性
1. 程序说明
在本示例实现的fragment标签的标签类中有两个JspFragment类型的属性:fragment1和fragment2.在doTag方法中调用了fragment1.invoke(null)和fragment2.invoke(null)方法分别执行了fragment1属性和fragment2属性。在JSP页面中使用<jsp:attribute>标签来设置这两个属性,因此,调用invoke方法就相当于执行<jsp:attribute>标签体的内容,并将执行结果输出到客户端。
2. 编写FragmentTag类
FragmentTag是一个简单标签类,负责执行fragment1和fragment2属性,并执行fragment标签体的内容。FragmentTag类的代码如下:
package chapter10;
import java.io.IOException;
import javax.servlet.jsp.JspContext;
import javax.servlet.jsp.JspException;
import javax.servlet.jsp.tagext.JspFragment;
import javax.servlet.jsp.tagext.SimpleTagSupport;
public class FragmentTag extends SimpleTagSupport
{
private JspFragment fragment1;
private JspFragment fragment2;
public void setFragment1(JspFragment fragment1)
{
this.fragment1 = fragment1;
}
public void setFragment2(JspFragment fragment2)
{
this.fragment2 = fragment2;
}
@Override
public void doTag() throws JspException, IOException
{
JspContext jspContext = this.getJspContext();
jspContext.getOut()。write("第一个属性(fragment1)的执行结果:");
// 执行fragment1属性
fragment1.invoke(null);
jspContext.getOut()。write("<br>");
jspContext.getOut()。write("第二个属性(fragment2)的执行结果:");
// 执行fragment2属性
fragment2.invoke(null);
jspContext.getOut()。write("<br>");
JspFragment fragment = this.getJspBody();
if(fragment != null)
{
jspContext.getOut()。write("标签体的执行结果:");
// 执行标签体的内容
fragment.invoke(null);
}
}
}
3. 安装fragment标签
在配置JspFragment类型的属性时需要使用<fragment>元素,而且该元素不能与<rtexprvalue>元素同时使用。在jsp-simpletaglib.tld文件中添加如下的内容来安装fragment标签:
<tag>
<description></description>
<name>fragment</name>
<tag-class>chapter11.FragmentTag</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>fragment1</name>
<required>true</required>
<fragment>true</fragment>
</attribute>
<attribute>
<name>fragment2</name>
<required>true</required>
<fragment>true</fragment>
</attribute>
</tag>
4. 编写fragment.jsp页面
fragment.jsp页面使用<jsp:attribute>标签设置了fragment标签的fragment1和fragment2属性,并在fragment标签中使用<jsp:body>标签增加了标签体。fragment.jsp页面的代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"%>
<%@ taglib uri="http://nokiaguy.blogjava.net/simpletag" prefix="st"%>
<jsp:useBean id="now" class="java.util.Date" />
<st:fragment >
<!-- 设置fragment1属性 -->
<jsp:attribute name="fragment1">
${(1+3) * 6}
</jsp:attribute>
<!- 设置fragment2属性 -->
<jsp:attribute name="fragment2">
${now}
</jsp:attribute>
<jsp:body>
这是fragment标签的标签体
</jsp:body>
</st:fragment>
5. 测试fragment标签
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter11/fragment.jsp
浏览器的输出信息如图10.4所示。
图10.4 使用<jsp:attribute>标签设置JspFragment类型的属性
10.3 简单标签和传统标签的差异和相同点
简单标签和传统标签有如下几点差异:
简单标签的标签类必须实现SimpleTag接口,而传统标签的标签类必须实现Tag接口。
简单标签并不直接执行标签体,而是将标签体的内容封装在JspFragment对象中,然后通过调用JspFragment类的invoke方法执行标签体的内容。传统标签通过doStartTag的返回值来通知Web容器是否执行标签体的内容。
简单标签通过在循环语句中调用invoke方法来达到迭代的目的,而传统标签需要通过doAfterBody方法的返回值来通知Web容器是否迭代执行标签体的内容。
简单标签通过在doTag方法中抛出SkipPageException异常的方式通知Web容器忽略标签后面的内容,而传统标签通过doEndTag方法的返回值来通知Web容器是否执行标签后面的内容。
在修改标签体的内容时,简单标签需要使用invoke方法将标签体的执行结果写入指定的Writer对象,然后从该Writer对象中取出执行结果,并修改这个执行结果,最后使用JspContext.getOut方法返回的JspWriter对象将修改后的执行结果输出到客户端。而传统标签需要从BodyContent对象中取出标签体的执行结果,并将修改后的执行结果通过由bodyContent.getEnclosingWriter方法返回的JspWriter对象输出的客户端。
简单标签支持JspFragment类型的属性,而传统标签不支持JspFragment类型的属性。
简单标签和传统标签有如下几个相同点:
简单标签和传统标签读取标签属性的方式是一样的,都需要在标签类中为标签属性提供相应的setter方法。
简单标签和传统标签都支持动态属性,也就是说,如果标签类实现了DynamicAttributes接口,Web容器就会调用DynamicAttributes接口的setDynamicAttribute方法依次将所有的动态属性传入标签类的对象实例。
10.4 小结
本章主要介绍了JSP2.0新提供了简单标签的实现方法。简单标签的标签类必须实现SimpleTag接口。在SimpleTag接口中只提供了一个doTag方法来处理标签逻辑,并通过调用表示标签体的JspFragment对象的invoke方法来控制标签体的执行。由于doTag方法没有返回值,因此,doTag方法需要抛出SkipPageException异常来忽略标签后面的内容。简单标签支持JspFragment类型的属性。在调用包含JspFragment类型属性的标签时,需要使用<jsp:body>标签指定标签体的内容。
第19章 整合Hibernate
Hibernate框架是目前非常流行的ORM框架。Hibernate框架可以实际对象和数据表记录之间的映射,而且Hibernate封装了很多流行的数据库,并为其提供了统一的访问接口,因此,使用Hibernate可以实现对数据库的透明访问。由于Struts 2和Hibenate的某些特性可以互相利用,因此,将Struts 2和Hibernate进行整合是一个非常好的想法。
19.1 Hibernate概述
Hibernate框架是一个非常强大的对象/关系持久化和查询框架。Hibernate框架的最终目标是使开发人员从95%的数据持久化工作中解脱出来(引自Hibernate的官方文档)。Hibernate通过XML格式的映射文件将数据记录映射到JavaBean对象中,并可以通过JavaBean对象来描述各种映射关系,这些关系包括联合(association)、继承(inheritance)、多态(polymorphism)、组合(composition)以及collections.除此之外,Hibernate还提供了一种在语法上类似SQL的HQL,通过HQL可以直接操作持久化对象(JavaBean对象)。
Hibernate框架的运行并不需要任何容器,Hibernate甚至可以在控制台程序中使用。而对于传统的ORM框架EJB来说,必须在EJB容器中才能运行,在这一点上,Hibernate要比EJB更灵活,更轻巧,因此,也可以将Hibernate称为轻量级的ORM框架。
19.3 整合Struts 2与Hibernate
在通常情况下,整合就意味着在一起使用。Struts 2是客户端表现层的框架,而Hibernate用于对数据库的操作,可以将Hibernate看作是数据层的框架。而实体Bean和数据记录进行映射,可以将实体Bean看作是数据持久层的组件。从这种划分上看,可以使用Hibernate来实现第14章的DAO层组件,而使用Struts 2来实现表现层的组件。除此之外,Struts 2的模型类和Hibernate的实体Bean十分相似,例如,在客户端录入了图书的信息,并将这些信息封装在模型类的对象实例中,而在服务端可以使用Hibernate的Session接口的save或saveOrUpdate方法直接将模型类的对象实例作为实体Bean的对象实例进行持久化。
【实例19.3】 整合Struts 2与Hibernate
1. 程序说明
本示例使用一个JSP页面提交一条图书信息,在服务端的Action类中使用Session接口的save方法将封装图书信息的模型类对象实例进行持久化(在t_book表中插入一条记录)。
2. 编写AddBookAction类
AddBookAction是一个Action类,负责持久化封装图书信息的模型类对象实例。AddBookAction类的代码如下:
package chapter19.action;
import org.hibernate.Session;
import org.hibernate.Transaction;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;
import chapter19.HibernateSessionFactory;
import chapter19.entity.Book;
public class AddBookAction extends ActionSupport implements ModelDriven<Book>
{
private Book book = new Book();
private String result;
public Book getModel()
{
return book;
}
public String getResult()
{
return result;
}
public String execute()
{
Session session = HibernateSessionFactory.getSession();
Transaction tx = session.beginTransaction();
// 持久化Book对象。Book对象既是模型类对象,也是实体Bean对象
session.save(book);
tx.commit();
session.close();
result = "成功添加记录";
return SUCCESS;
}
}
3. 配置AddBookAction类
在struts.xml文件中添加如下的代码来配置AddBookAction类:
<package name="struts2_chapter19" namespace="/chapter19"
extends="struts-default">
<action name="addBook" class="chapter19.action.AddBookAction">
<result name="success">/chapter19/add_book.jsp</result>
<result name="input">/chapter19/add_book.jsp</result>
</action>
</package>
4. 编写add_book.jsp页面
add_book.jsp页面用于显示图书信息录入页面,并将用户录入的信息提交给addBook.add_book.jsp页面的代码如下:
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib prefix="s" uri="/struts-tags"%>
<html>
<head>
<title>添加图书记录</title>
</head>
<body>
<!-- 显示Action错误 -->
<s:actionerror/>
<!-- 显示成功信息 -->
${result}
<s:form action="addBook" namespace="/chapter17">
<s:textfield label="书名" name="name" />
<s:textfield label="作者" name="author" />
<s:textfield label="ISBN" name="isbn" />
<s:textfield label="价格" name="price" />
<s:submit value="保存" />
</s:form>
</body>
</html>
5. 测试
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter19/add_book.jsp
在输入一条图书信息后,单击"保存"按钮,浏览器显示的效果如图19.2所示。
图19.2 录入并保存图书信息
19.4 小结
本章介绍了Hibernate框架的基础知识。Hibernate框架是目前比较流行的ORM框架。Hibernate并不需要运行在任何容器中,这就意味着Hibernate框架可以在任何类型的程序中使用。Hibernate框架提供了dialect来实现数据库的透明性,并提供了多种操作数据库的技术,如Session接口中定义的方法、标准查询API、HQL等。由于Hibernate负责完成和数据相关的工作,而Struts 2主要负责编写表现层的组件,将Struts 2和Hibernate进行整合正好可以发挥它们各自的优势。
第20章 整合Spring
Spring框架是目录比较流行的轻量级的Ioc容器框架。在Spring框架中提供了非常多的功能,该框架的主要功能包括装配JavaBean、Spring AOP、与各种持久化框架的整合(如Hibernate、EJB等)、事务处理等。在本章将介绍Spring的基础知识以及如何整合Struts 2、Spring与Hibernate框架。
20.1 Spring概述
Spring是一个开源的Ioc框架,该框架由Rod Johnson创建。Spring的主要目的是简化企业级开发。虽然EJB是当之无愧的企业级应用的王者,但EJB过于复杂,而有很多企业级的应用并不需要如此复杂的技术。最理想的应用模式是技术的复杂度和企业级应用的复杂度成正比,也就是说,越复杂的企业应用,实现起来也越复杂,越简单的企业应用,实现起来也应该越简单,而Spring框架正好可以满足这个要求。
Spring的主要特性如下:
Spring是一个非侵入(non-invasive)式的框架。Spring框架可以尽可能地减小程序代码对框架的依赖。Spring不仅可以对新系统进行配置,而且还可以对很多旧系统的未使用Spring的Java类进行配置。
提高代码的重用性。Spring框架的核心是装配JavaBean,而所有的装配规则都放在了XML格式的配置文件中,这样可以尽量避免在程序中硬编码,而且还可以在其他程序中重用配置文件中的装配信息。
提供了一致的编程接口。Spring框架可以在任何运行环境(如Web容器、桌面程序)下使用,但在这些运行环境中使用Spring的方式基本相同,也就是说,Spring提供了统一的编程接口来隔离应用程序代码和运行环境,从而使代码对运行环境的依赖最小化。
不同的框架之间切换更容易。Spring为相同类型的框架提供了统一的接口,例如,在Spring中可以使用不同的ORM框架,包括Hibernate、EJB等,但访问这些框架的接口是相同的,这样可以使开发人员*地选择这些框架,而不需要修改程序代码。
Spring不重造*。虽然Spring框架可以完成很多工作,如操作数据库、事务管理等,但Spring并没有自己实现这些应用,而是封装了第三方的实现。这么做的好处是尽可能地保护用户的投资,也就是说,开发人员仍然可以在Spring中使用旧的框架来实现自己应用程序。
20.2.1 Spring的下载和安装
读者可以从如下的网址来下载Spring的最新版本:
http://www.springsource.org/download
在笔者写作本书时,Spring框架的最新版本是2.5.6,在本书中也将采用这个Spring版本来编写示例程序。读者可以从上面的网址下载Spring的最新版本,也可以使用随书光盘中带的Spring2.5.6.
在Spring的发行包中有很多jar文件,但Spring将大多数jar包中的内容都放在了spring.jar文件中,因此,只需要在demo工程中引用该文件就可以使用Spring框架的大多数功能了。引用spring.jar文件的方法和引用Hibernate框架的jar文件类似。
20.2.2 Ioc模式概述
在现今的面象对象语言中,通常由两个或多个类进行配合来完成不同的工作。按着传统的作法,由一个类负责创建其他类的对象实例,并在该类中调用这些类的相应方法。如有两个类:Class1和Class2,在Class1内部创建了Class2的对象实例,并在Class1类的method1方法中调用了Class2类的method2方法,代码如下:
public class Class2
{
public String method2()
{
String email = null;
// 此处省略了method2方法中的其他代码
return email;
}
}
public class Class1
{
private Class2 class2;
// Class1的构造方法
public Class1()
{
// 创建Class2类的对象实例
class2 = new Class2();
}
public String method1()
{
// 此处省略了method1方法中的其他代码
… …
// 调用Class2类的method2方法
return class2.method2();
}
}
上面代码中的Class1和Class2以很普通的组合方式进行耦合。这种耦合方式通常没有什么问题,但如果遇到以下两种情况时,将会带来一些麻烦:
当测试Class1类的method1方法,暂时不想测试method1方法调用Class2类的method2方法的情况,也就是说,只想测试调用method2方法的语句前面的代码部分。但由于某种原因,无法修改Class1类的代码(可能是没有源代码,或是其他的原因)。这时也许我们会修改Class2类的代码,或是干脆就不采取任何措施。
如果Class2类的method2方法中的处理逻辑需要修改,或是需要换另外一套解决方案,这时,可能只有修改Class1类的method1方法中调用method2方法的代码才能做到。
发生上面尴尬的事情的根本原因就是Class1和Class2的耦合度过高。也就是说,在Class1类完全拥有了对Class2的控制权。这就象让某个人在地上挖一个坑,并且限制了这个人必须使用什么样的工具以及其他一些限制(如必须由这个人自己来完成,在挖坑时不能发出太大的响声等),那么这个挖坑的人就会被这些条条框框牢牢地固定住,也不利于其发挥自身的创造力。而如果不进行过多的限制,也就是说,只规定了所挖的坑的形状和深度,至于如何挖这个坑、用多少人、用什么工具,完全由负责挖坑的人来决定,这样负责完成工作的人将会获得更大的自主性。
同样的道理也可以用在面向对象的程序设计中。上面所描述的事件的核心思想就是"获得更大的自主性",也就是只规定了做什么,至于怎么做,完全由负责完成任务的主体自己来决定。从面向对象的各种基本组成元素来看,只有接口才最符合这种需求。接口只是定义了类中有什么方法,而方法的处理逻辑完全由实现该接口的类来完成。而调用接口的部分并不会限制实现接口的类如何来实现接口的方法。如下面的代码使用接口来重新描述了Class1和Class2的调用关系:
public interface MyInterface
{
public String method2();
}
public class Class2 implements MyInterface
{
public String method2()
{
String email = null;
// 此处省略了method2方法中的其他代码
return email;
}
}
public class Class1
{
private MyInterface myInterface;
// Class1的构造方法
public Class1(MyInterface myInterface)
{
this.myInterface = myInterface;
}
public String method1()
{
// 此处省略了method1方法中的其他代码
… …
// 调用Class2类的method2方法
return myInterface.method2();
}
}
在上面的代码中Class2类实现了MyInterface接口,而在Class1类中只涉及到了MyInterface接口,并且在method1方法中调用了MyInterface接口中的method2方法。
对于上面的实现,就可以非常容易地解决上述两个问题。如果只想测试调用method2语句前面的代码,可以在创建Class1类的对象实例时传入一个MyInterface接口的空实现,也就是该实现类的method2方法中除了一条return语句外,没有任何代码,这样在不修改Class1和Class2的代码的前提下就可以暂时将method2方法的功能关闭。关于第二个问题也可以采用类似的方法,如果method2方法中的处理逻辑改变,可以别外编写一个实现MyInterface接口的类,并在该类中的method2方法中实现相应的处理逻辑,然后在创建Class1类的对象实例时,将该类的对象实例传入Class1类的对象实例即可。
上面两种解决方案的一个根本区别就是创建Class2类的对象实例的任务是在Class1类的内容完成,还是在Class1类的外部完成。在第二种解决方案中通过接口的方式使得在Class1类的外部创建了Class2类的对象实例,并将该对象实例通过Class1类的构造方法传入Class1类的对象实例。这种编程模式称为Ioc(反向控制,Inversion of Control)模式,Ioc模式的核心思想就是利用接口解耦合。Ioc模式也是Spring框架的主要应用模式。
20.2.3 编写第一个基于Spring框架的程序
在本节将编写一个简单的基于Spring的程序,该程序使用Spring框架装配一个Helloworld类,并调用了该类的getGreeting方法。
【实例20.1】 编写第一个基于Spring框架的程序
1. 开发基于Spring框架的程序的基本步骤
读者可按如下的步骤开发一个基于Spring框架的程序:
编写被装配的JavaBean.
使用Spring装配文件来配置这些JavaBean.主要指定要装配的JavaBean的名称以及装配JavaBean时JavaBean属性的初始值。
使用org.springframework.context.ApplicationContext对象装载Spring装配文件。
使用ApplicationContext接口的getBean方法获得指定的JavaBean的对象实例。
2. 编写Helloworld类
Helloworld类是一个被装配的JavaBean.该类的代码如下:
package chapter20;
public class Helloworld
{
private String greeting;
public String getGreeting()
{
return greeting;
}
public void setGreeting(String greeting)
{
this.greeting = greeting;
}
}
3. 编写Spring装配文件
Spring框架的装配文件通常为applicationContext.xml,当然,也可以使用其他的文件名。该文件可以被放在任何路径下,在本例中将applicationContext.xml文件放在工程目录(demo)的src目录下。装配文件的代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframe work.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframe work.org/schema/tx/spring-tx-2.5.xsd">
<!-- 指定Helloworld类的装配信息 -->
<bean id="helloworld" class="chapter20.Helloworld">
<property name="greeting" value="bill gates"/>
</bean>
</beans>
在上面的配置代码中使用<bean>元素指定了Helloworld类的装配信息,其中id属性表示Helloworld类被装配时的引用名,也就是ApplicationContext接口的getBean方法的参数值;class属性表示JavaBean的名称(package.classname)。在<bean>元素中使用<property>元素设置了greeting属性的值。applicationContext.xml文件的<beans>元素是根元素,该元素的属性比较复杂,读者并不需要记忆这些内容,只需要将Spring框架的发行包中带的例子的相应内容复制过来即可,如本例中<beans>元素的内容实际上就是jpetstore程序中applicationContext.xml文件的相应内容。
4. 编写FirstSpring类
FirstSpring类创建了ApplicationContext对象,并调用了ApplicationContext接口的getBean方法获得了Helloworld类的对象实例。FirstSpring类的代码如下:
package chapter20;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationConte xt;
public class FirstSpring
{
public static void main(String[] args)
{
// 创建ApplicationContext对象,并装载applicationContext.xml文件
ApplicationContext context = new FileSystemXmlApplicationContext(
"src//applicationContext.xml");
// 获得Helloworld类的对象实例
Helloworld helloworld = (Helloworld) context.getBean("helloworld");
System.out.println(helloworld.getGreeting());
}
}
创建ApplicationContext对象有多种方式,如果需要指定本地的Spring装配文件,就需要使用FileSystemXmlApplicationContext类来创建ApplicationContext对象。运行FirstSpring程序后,将在控制台输出bill gates。
20.2.4 装配JavaBean
装配JavaBean是Spring框架最核心的功能。在20.2.3节给出了一个装配JavaBean的例子,在这个例子中设置了JavaBean的greeting属性的值。除了可以在装配过程中设置普通属性的值,也可以设置集合类型的属性以及构造方法的参数的值。
【实例20.2】 设置集合类型的属性和构造方法的参数的值
1. 编写MyJavaBean类
MyJavaBean类是被装配的JavaBean,在该JavaBean中包含了三个属性,这三个属性的类型分别为java.util.List、java.util.Map及String.其中String类型的属性只有getter方法,而MyJavaBean类的构造方法负责设置String类型的属性的值。MyJavaBean类的代码如下:
package chapter20;
public class MyJavaBean
{
private java.util.List<String> myList;
private java.util.Map<String, String> myMap;
private String name;
public MyJavaBean(String name)
{
this.name = name;
}
public java.util.List<String> getMyList()
{
return myList;
}
public void setMyList(java.util.List<String> myList)
{
this.myList = myList;
}
public java.util.Map<String, String> getMyMap()
{
return myMap;
}
public void setMyMap(java.util.Map<String, String> myMap)
{
this.myMap = myMap;
}
public String getName()
{
return name;
}
}
2. 配置MyJavaBean类
在applicationContext.xml文件中添加如下的内容来配置MyJavaBean:
<bean id="myJavaBean" class="chapter20.MyJavaBean">
<!-- 设置构造方法的参数值 -->
<constructor-arg value="bill gates" />
<!-- 设置myList属性的值 -->
<property name="myList">
<list>
<value>xyz</value>
<value>abc</value>
</list>
</property>
<!-- 设置myMap属性的值 -->
<property name="myMap">
<map>
<entry>
<key>
<value>author</value>
</key>
<value>赵明</value>
</entry>
<entry>
<key>
<value>isbn</value>
</key>
<value>12345678</value>
</entry>
</map>
</property>
</bean>
3. 编写ManualLoad类
ManualLoad类从装配信息中获得了MyJavaBean对象实例,并输出了相应属性的值。ManualLoad类的代码如下:
package chapter20;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.FileSystemXmlApplicationConte xt;
public class ManualLoad
{
public static void main(String[] args)
{
ApplicationContext context=new FileSystemXmlApplicationContext("src// appli cationContext.xml");
MyJavaBean myJavaBean = (MyJavaBean) context.getBean("myJavaBean");
System.out.println(myJavaBean.getName());
System.out.println(myJavaBean.getMyList()。toString());
System.out.println(myJavaBean.getMyMap()。toString());
}
}
4. 测试
运行ManualLoad程序后,在控制台输出的信息如下:
bill gates
[xyz, abc]
{author=赵明, isbn=12345678}
20.3 整合Struts 2、Spring与Hibernate
在19章介绍了如何整合Struts 2和Hibernate.但如果系统比较大时,需要将服务端的程序进行分层,例如,可以分为数据持久层、数据访问层、业务逻辑层。在这些层次涉及到了非常多的类和接口。如果在每一个Action中都创建这些类的对象实例,就会出现大量的代码冗余,并且不易维护。而Spring框架的精髓就是尽量减少程序中的硬编码,因此,可以将这些创建各层的类的对象实例的工作交给Spring来完成。
除此之外,在Struts 2中使用Spring和Hibernate需要指定Spring和Hibernate的配置文件的路径,而将配置文件的路径硬编码在程序中也不利于程序的维护,为了使在Struts 2中使用Hibernate和Spring更加透明,可以使用Struts 2提供的一个插件来自动寻找和处理Spring和Hibernate的配置文件。这样在Struts 2的Action类中就一定也看不到Spring和Hibernate的影子了。
【实例20.3】 Struts 2-Spring插件在整合SSH中的应用
1. 使用Struts 2-Spring插件前的准备工作
在Struts 2.1.6中的插件文件名为struts2-spring-plugin-2.1.6.jar.将该文件与spring.jar文件复制到WEB-INF\lib目录中。并在WEB-INF目录中建立一个applicationContext.xml文件,并添加XML文件名和<beans>元素的内容。struts2.xml和hibernate.cfg.xml文件仍然放在WEB-INF\classes目录中。
2. 配置ContextLoaderListener监听器
在web.xml文件中添加如下的代码来配置ContextLoaderListener监听器:
<listener>
<listener-class> org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
3. 配置SessionFactory和HibernateTemplate
在applicationContext.xml文件中添加如下的内容来配置SessionFactory和HibernateTemplate:
<bean id="sessionFactory"
class="org.springframework.orm.hibernate3.LocalSessionFactoryBean">
<!-- 指定了Hibernate的配置文件:hibenate.cfg.xml -->
<property name="configLocation" value="classpath:hibernate.cfg.xml">
</property>
</bean>
<!-- Hibenrate需要通过HibernateTemplate对象来使用Hibernate -->
<bean id="hibernateTemplate"
class="org.springframework.orm.hibernate3.HibernateTemplate">
<!--指定sessionFactory属性的值(sessionFactory属性的数据类型是Session Factory) -->
<!-- 其中ref属性的值是另一个被装配JavaBean的id属性值 -->
<property name="sessionFactory" ref="sessionFactory" />
</bean>
4. 配置Spring事务
在Spring中修改数据库中的数据需要在事务中完成。在applicationContext.xml文件中添加如下的内容来装配一系列和事务相关的JavaBean:
<bean id="transactionManager"class="org.springframework.orm.hibernate3.Hi bernateTransactionManager">
<property name="sessionFactory">
<ref bean="sessionFactory" />
</property>
</bean>
<bean id="transactionInterceptor" class="org.springframework.transaction.i nterceptor.TransactionIntercep tor">
<property name="transactionManager">
<ref bean="transactionManager" />
</property>
<!-- 指定被拦截的JavaBean的属性应采用的事务策略 -->
<property name="transactionAttributes">
<props>
<!-- 为了提高性能,属性的getter方法都采用只读的事务策略 -->
<prop key="get*">PROPAGATION_REQUIRED, readOnly</prop>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
5. 编写DAO接口和类
在本例中仍然使用第19章建立的Book类作为实体Bean,本例涉及到一个BookDAO接口和一个BookDAOImpl类。BookDAO接口的代码如下:
package chapter20.dao.interfaces;
import chapter19.entity.Book;
public interface BookDAO
{
public void save(Book book);
}
BookDAOImpl接口的代码如下:
package chapter20.dao;
import org.springframework.orm.hibernate3.HibernateTemplate;
import chapter20.dao.interfaces.BookDAO;
import chapter19.entity.Book;
public class BookDAOImpl implements BookDAO
{
protected HibernateTemplate template;
// template参数的值由Spring进行装配
public BookDAOImpl(HibernateTemplate template)
{
this.template = template;
}
public void save(Book book)
{
// 调用HibernateTemplate类的save方法持久化Book类的对象
template.save(book);
}
}
6. 编写Service接口和类
在本例中涉及到一个BookService接口和一个BookServiceImpl类。BookService接口的代码如下:
package chapter20.service.interfaces;
import chapter19.entity.Book;
public interface BookService
{
public void addBook(Book book);
}
BookServiceImpl类的代码如下:
package chapter20.service;
import chapter19.entity.Book;
import chapter20.service.interfaces.BookService;
import chapter20.dao.interfaces.BookDAO;
public class BookServiceImpl implements BookService
{
private BookDAO bookDAO;
// bookDAO参数的值由Spring进行装配
public BookServiceImpl(BookDAO bookDAO)
{
this.bookDAO = bookDAO;
}
public void addBook(Book book)
{
if (book.getName()。length() > 4)
{
bookDAO.save(book);
}
else
{
System.out.println("书名长度必须大于4个字符");
}
}
}
7. 编写ServiceManager类
ServiceManager类用于获得所有的Service类的对象实例,由于本例只涉及到一个Service类,因此,ServiceManager类中只有一个getBookService方法。ServiceManager类的代码如下:
package chapter20.service;
import chapter20.service.interfaces.BookService;
public class ServiceManager
{
private BookService bookService;
public BookService getBookService()
{
return bookService;
}
public void setBookService(BookService bookService)
{
this.bookService = bookService;
}
}
8. 装配JavaBean
在applicationContext.xml文件中添加如下的代码来装配本例所涉及到的JavaBean:
<!-- 用于自动创建DAO类的对象实例 -->
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAutoPro xyCreator">
<property name="beanNames">
<list>
<value>bookDAO</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>transactionInterceptor</value>
</list>
</property>
</bean>
<bean id="bookDAO" class="chapter20.dao.BookDAOImpl">
<constructor-arg>
<ref bean="hibernateTemplate" />
</constructor-arg>
</bean>
<bean id="bookService" class="chapter20.service.BookServiceImpl">
<constructor-arg>
<ref bean="bookDAO" />
</constructor-arg>
</bean>
<bean id="serviceManager" class="chapter20.service.ServiceManager">
<property name="bookService">
<ref bean="bookService" />
</property>
</bean>
9. 编写AddBookAction类
AddBookAction是一个Action类,负责获得BookService对象,并调用BookService接口的addBook方法将封装请求参数的Book对象作为实体Bean对象进行持久化(向t_books表中添加一条图书记录)。AddBookAction类的代码如下:
package chapter20.action;
import com.opensymphony.xwork2.ActionSupport;
import com.opensymphony.xwork2.ModelDriven;
import chapter19.entity.Book;
import chapter20.service.ServiceManager;
import chapter20.service.interfaces.BookService;
public class AddBookAction extends ActionSupport implements ModelDriven <Book>
{
private Book book = new Book();
private ServiceManager serviceManager;
private String result;
public Book getModel()
{
return book;
}
public void setServiceManager(ServiceManager serviceManager)
{
this.serviceManager = serviceManager;
}
public String getResult()
{
return result;
}
public String execute()
{
BookService bookService = serviceManager.getBookService();
bookService.addBook(book);
result = "成功添加记录";
return SUCCESS;
}
}
在AddBookAction类中有一个serviceManager属性,该属性的数据类型是ServiceManager.从上面的程序看,并没有去创建ServiceManager类的对象实例,而是直接使用了serviceManager.这是由于Struts 2-Spring插件在创建Action类的对象实例时,会将Spring装配的JavaBean对象实例赋给Action类中同属性名同数据类型的属性。如在本例的applicationContext.xml文件中有一个id属性为serviceManager的JavaBean,如果在AddBookAction类中有一个名为serviceManager的属性,并且该属性的数据类型是chapter18.service.ServiceManager,Struts 2-Spring插件就会将Spring装配的ServiceManager对象赋给AddBookAction类的serviceManager属性。
如果想在AddBookAction类中直接使用BookService对象,可以定义一个bookService属性,并且该属性的类型必须是chapter20.service.BookService,而且必须提供该属性的setter方法。如下面的代码所示:
public class AddBookAction extends ActionSupport implements ModelDriven <Book>
{
private BookService bookService;
public void setBookService(BookService bookService)
{
this.bookService = bookService;
}
public String execute()
{
bookService = serviceManager.getBookService();
bookService.addBook(book);
return SUCCESS;
}
}
10. 配置AddBookAction类
在struts.xml文件中添加如下的内容来配置AddBookAction类:
<package name="struts2_chapter20" namespace="/chapter20"
extends="struts-default">
<action name="addBook" class="chapter20.action.AddBookAction">
<result name="success">/chapter20/add_book.jsp
</result>
<result name="input">/chapter20/add_book.jsp
</result>
</action>
</package>
其中add_book.jsp页面的内容和第19章的add_book.jsp页面的内容类似,但要将<s:form>标签的namespace属性值改成"/chapter20"。
11. 测试
在浏览器地址栏中输入如下的URL:
http://localhost:8080/demo/chapter20/add_book.jsp
当输入正确的图书信息后,单击"保存"按钮,会得到和图19.2完全一样的输出效果。
第21章 用户登录注册系统
本章给出了一个简单而实用的示例,该示例实现了用户登录和注册的功能。本章主要使用JSP/Servlet技术来实现这个系统。在该系统中使用了前面介绍的过滤器技术将请求参数映射成JavaBean的对象实例,同时,本系统还提供了中文验证码的功能。读者通过对本系统的学习可以了解使用JSP/Servlet技术开发Web应用程序的方法和步骤。本章的源代码在随书光盘的entry目录中。
21.1 系统概述
本系统分为用户登录和注册两个功能。用户登录功能允许用户输入已经注册的用户名和密码,并且需要输入验证码。如果这三个值都输入正确,系统会提示登录成功,并进入主页面。其中验证码中的字符集合被保存在了一个文本文件中(code.txt),如该文件中可以输入中文和英文,在本例中主要使用了中文验证码。
用户注册功能和用户注册功能类似,只是输入的内容多了一些,在输入完相应的用户注册信息后,如果注册成功,在用户登录页面就可以使用刚注册的用户进行登录了。
本系统采用了典型的三层结构来实现,如下所示:
Web表现层:该层主要由JSP页面和Servlet组成。
业务逻辑层:该层由一些Service类组成,在这些类中直接访问数据层来获得相应的数据,并根据这些数据处理业务逻辑。
数据访问层:该层由DAO类组成,在这些类中使用JDBC访问数据库,并提供对数据库的通用功能,如查询数据、修改数据等。只有业务逻辑层直接和数据访问层打交道,而Web表现层则通过业务逻辑层来访问数据访问层。
为了简化编写代码的工作量,在本系统中将一些通用的功能(如获得数据库连接、创建数据访问层和业务逻辑层的类的对象实例)提了出来,单独放在了一个CommonServlet类中,该类是一个Servlet类,然后所有的Servlet类都继承CommonServlet.
Hibernate框架是目前非常流行的ORM框架。Hibernate框架可以实际对象和数据表记录之间的映射,而且Hibernate封装了很多流行的数据库,并为其提供了统一的访问接口,因此,使用Hibernate可以实现对数据库的透明访问。由于Struts 2和Hibenate的某些特性可以互相利用,因此,将Struts 2和Hibernate进行整合是一个非常好的想法。
21.3.1 编写User类
User类的对象实例用于封装登录页面和注册页面提交的请求参数,同时数据访问层也需要User类的对象实例来获得用户登录和注册的信息。User类中的属性和t_users表中的属性一一对应(除了t_users表中的id字段外,其他的字段都在User类中有相应的属性)。User类的实现代码如下:
public class User
{
// 与数据表字段和请求参数对应的属性
private String name;
private String password;
private String xm;
private String email;
private String validationCode;
public String getPassword_md5()
{
return Encrypter.md5(password);
}
// 此处省略了属性的getter和setter方法
… …
}
其中util.Encrypter类的md5方法可以利用MD5算法对字符串进行加密。在User类中虽然从请求参数接收的是明文的密码,但在password属性的getter方法中使用md5方法对明文密码进行加密,而数据访问层将通过password属性的getter方法获得用户提交的密码,因此,保存在t_users表中的密码是经过MD5加密后的密码字符串。util.Encrypter类的详细介绍参见21.7.1节的内容。
21.3.2 编写Common类
Common类用于封装其他的请求参数,但该类的对象实例中的内容并不会被保存在数据库中。在本系统中该类只有一个属性:path.该属性表示要转换的页面。
当用户未登录时,不能访问除了登录和注册页面的其他页面,因此,在本系统中将这些页面(也可以称为敏感页面)放在WEB-INF\jsp目录中,然后通过请求参数将要转换的页面提供给负责转入页面的Servlet,再由该Servlet负责转入到WEB-INF\jsp目录中的敏感页面。Common类中的path属性就封装了敏感页面的名称。Common类的实现代码如下:
package model;
public class Common
{
// 封装敏感页面名称的属性
private String path;
// 此处省略了path属性的getter和setter方法
… …
}
21.4 实现数据访问层和业务逻辑层
数据访问层只负责操作数据库,并提供一些通用的数据库操作方法供其他层使用。而业务逻辑层并不直接访问数据库,该层根据从数据访问层获得的数据来完成业务逻辑,并提供一些接口给Web表示层。数据访问层由以下两个类组成:
DAOSuport类:负责获得数据库连接。在该类中只是通过构造方法转入一个Connection对象,并不直接使用JDBC打开数据库连接。连接数据库的工作由CommonServlet类来完成(该类将在21.5.1节介绍)。
UserDAO类:负责操作t_users表中的数据。该类主要提供了将用户信息保存在t_users表中以及获得指定用户的密码(经过MD5算法加密后的密码)字符串。UserDAO类是DAOSupport类的子类。
业务逻辑层只涉及到一个UserService类,该类直接调用了UserDAO类的相应方法,并提供了保存用户信息和验证用户登录信息的功能。
21.4.1 编写DAOSupport类
DAOSupport类只有一个connection属性(没有getter和setter方法)和一个构造方法,该构造方法只有一个Connection类型的参数。用于将Connection对象转入DAOSupport类及其子类的对象实例。DAOSupport类的代码如下:
public class DAOSupport
{
protected java.sql.Connection connection;
public DAOSupport(Connection connection)
{
this.connection = connection;
}
}
其中connection属性被声明为protected,表示该属性可以在DAOSupport类子类中使用。
21.4.2 编写UserDAO类
UserDAO类是DAOSupport类的子类,负责读取和修改t_users表中的数据。UserDAO类的代码如下:
public class UserDAO extends DAOSupport
{
private User user;
public UserDAO(Connection connection, User user)
{
super(connection);
this.user = user;
}
// 返回model.User类的对象实例
public User getUser()
{
return user;
}
// 将用户注册信息保存在t_users表中(在该表插入一条记录)
public void Save() throws Exception
{
// 定义insert语句(带参数)
String sql = "insert into t_users(name, password_md5, xm, email) " +
"values(?,?,?,?)";
PreparedStatement pstmt = connection.prepareStatement(sql);
// 为SQL参数赋值
pstmt.setString(1, user.getName());
pstmt.setString(2, user.getPassword_md5());
pstmt.setString(3, user.getXm());
pstmt.setString(4, user.getEmail());
// 向t_users表中插入记录
pstmt.executeUpdate();
}
// 返回指定用户的密码字符串
public String getPasswordMD5() throws Exception
{
String sql = "select password_md5 from t_users where name = ?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, user.getName());
ResultSet rs = pstmt.executeQuery();
while(rs.next())
{
return rs.getString("password_md5");
}
return null;
}
}
在UserDAO类的构造方法中除了Connection类型的参数,还有一个model.User类型的参数。创建UserDAO对象时,必须为其指定model.User对象,因为UserDAO对象要操作的t_users表中的数据来自model.User对象,而model.User对象封装了用户登录或注册信息。
21.4.3 编写UserService类
UserService类处于业务逻辑层,在创建该类的对象实例时,必须通过构造方法将UserDAO类的对象传入UserService类的对象实例。UserService类的代码如下:
public class UserService
{
private UserDAO userDAO;
public UserService(UserDAO userDAO)
{
this.userDAO = userDAO;
}
public UserDAO getUserDAO()
{
return userDAO;
}
// 保存用户的注册信息
public void save() throws Exception
{
userDAO.Save();
}
// 验证用户登录时提交的用户名和密码是否正确,如果验证失败,抛出异常
public void verifyUser() throws Exception
{
String passwordMD5 = userDAO.getPasswordMD5();
if(passwordMD5 == null)
throw new Exception("<" + userDAO.getUser()。getName() + ">不存在!");
if(!passwordMD5.equals(userDAO.getUser()。getPassword_md5()))
throw new Exception("密码不正确!");
}
}
21.5 实现处理用户请求的Servlet
当JSP页面提交请求时,由Servlet处理这些请求。在本系统中涉及到如下4个处理用户请求的Servlet类:
CommonServlet:该类是本系统中所有Servlet类的父类。在该类中编写了一些通用的功能,如打开数据库连接、验证用户是否已登录等。
LoginServlet:该类负责处理用户登录请求。
RegisterServlet:该类负责处理用户注册请求。
EntryServlet:该类负责转入WEB-INF\jsp目录中的页面。当用户登录成功后,就会调用该Servlet转入到主页面。
21.5.1 编写CommonServlet类
CommonServlet类是一个抽象类,同时也是一个Servlet类。在该类中定义了一个抽象的execute方法,如果当前用户成功通过验证,在CommonServlet类的service方法中就会调用execute方法,因此,继承CommonServlet的子类只需要覆盖execute方法即可。
由于本系统包含了数据访问层和业务逻辑层,这两层中的类名都是有规律的。在数据访问层中的类(UserDAO类)的名称是数据持久化层中的实体类(User类)的名称后面加DAO.而业务逻辑层中的类的名称是数据持久化层中的实体类(User类)的名称后面加"Service".因此,可以在CommonServlet类中根据实体类的名称自动找到并创建这些类的对象实例。由于本系统通过FormFilter过滤器创建了封装请求参数的模型类对象(实体类对象),并将该对象保存在request域中,因此,可以根据该对象的类名来获得DAO和Service类的名称,并动态创建这些对象。CommonServlet类的实现代码如下:
public abstract class CommonServlet extends HttpServlet
{
// CommonServlet类的子类只需要在execute方法中编写代码即可,
// obj表示Service对象或模型类对象
protected abstract String execute(HttpServletRequest request,
HttpServletResponse response, Object obj);
// 将一个属性名转换成setter方法名,如name属性对应的setter方法名是setName
private String getSetter(String property)
{
String methodName = "set" + property.substring(0, 1)。toUpperCase()
+ property.substring(1);
return methodName;
}
// 获得连接数据库的Connection对象
private Connection getConnection() throws Exception
{
Class.forName("com.mysql.jdbc.Driver");
// 获得Connection对象
Connection conn = DriverManager.getConnection("jdbc:mysql://local ost/mydb?characterEncoding=UTF8","root", "1234");
return conn;
}
// 创建Service对象或模型类对象,并返回创建的对象,model参数表示封装用户请求参数
// 模型类对象
private Object LoadObject(HttpServletRequest request, Object model)
throws Exception
{
if (model == null)
return null;
try
{
// 获得DAO类名(package.classname)
String daoClassName = "dao." + model.getClass()。getSimpleName() + "DAO";
// 获得Service类名(package.classname)
String serviceClassName = "service."
+ model.getClass()。getSimpleName() + "Service";
Connection connection = getConnection();
// 装载DAO类
Class daoClass = Class.forName(daoClassName);
// 获得DAO类带参数的构造方法对象(Constructor对象)
Constructor constructor =daoClass.getConstructor(Connecti on.cla ss,model.getClass());
// 使用DAO类带参数的构造方法创建一个DAO对象
Object dao = constructor.newInstance(connection, model);
// 装载Service类
Class serviceClass = Class.forName(serviceClassName);
// 获得Service类带参数的构造方法对象(Constructor对象)
constructor = serviceClass.getConstructor(dao.getClass());
// 使用Service类带参数的构造方法创建一个Service对象
Object service = constructor.newInstance(dao);
return service;
}
catch (Exception e)
{
return model;
}
}
// 对字符串进行解码
protected String decode(HttpServletRequest request, String s)
throws UnsupportedEncodingException
{
String encoding = request.getCharacterEncoding();
if (encoding == null)
encoding = "ISO-8859-1";
s = new String(s.getBytes(encoding), "UTF-8");
return s;
}
// 核对验证码是否正确
private void checkValidationCode(HttpServletRequest request, Object model) throws Exception
{
// 如果模型类是model.User,则验证效验码
if (model instanceof model.User)
{
model.User user = (model.User) model;
// 获得用户提交的验证码
String validationCode = user.getValidationCode();
if (validationCode != null)
{
// 对验证码进行编码
// 从HttpSession对象中获得服务端生成的验证码
String vCode = (String) request.getSession()。getAttribute(
"vCode");
// 如果HttpSession对象中没有验证码,则抛出"Session失效"异常
if (vCode == null)
{
throw new Exception("Session过期,验证码失效!");
}
// 如果验证码不一致,抛出"验证码错误"异常
if (!validationCode.equals(vCode))
{
throw new Exception("验证码错误!");
}
}
}
}
// 核对用户是否已经登录,如果当前用户已经登录,返回true
private boolean checkLogin(HttpServletRequest request)
{
// 从web.xml文件中获得不进行核对我Servlet
String ignoreServlets = this.getServletContext()。getInitParameter ("ignoreS ervlets");
if (ignoreServlets != null)
{
String[] servlets = ignoreServlets.split(",");
String servlet = this.getServletName();
for (String s : servlets)
{
// 如果当前Servlet被忽略,则直接返回true
if (s.trim()。equals(servlet))
{
return true;
}
}
}
boolean isOK = false;
HttpSession session = request.getSession(false);
if (session != null)
{
String user = (String) session.getAttribute(String.valueOf(session
.getId()。hashCode()));
if (user != null)
{
isOK = true;
}
}
return isOK;
}
@Override
protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException
{
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
// 从request域中获得封装用户请求参数的模型类
Object model= request.getAttribute("#form");
try
{
checkValidationCode(request, model);
if (checkLogin(request) == false)
{
// 如果当前用户未登录,重定向到login.jsp
response.sendRedirect("login.jsp");
return;
}
String result = null;
// 调用当前Servlet的execute方法来处理客户端请求。
result = execute(request, response, LoadObject(request, model));
if (result != null)
out.println(result);
}
catch (Exception e)
{
out.println("{'message':'" + e.getMessage() + "'}");
}
}
}
代码说明:
CommonServlet类的子类只需要在execute方法中编写处理客户端请求的逻辑即可。execute方法除了有与service方法相同类型的参数外,还有一个Object类型的参数,该参数表示业务逻辑层的Service对象。
在CommonServlet类中通过动态创建Java对象的方式创建了UserDAO和UserService对象,这些功能是由LoadObject方法完成的。实际上,在LoadObject方法的最后一个参数就是由FormFilter过滤器创建的模型类对象,因此,可以根据该对象的类名(在本例中是User)来获得DAO和Service类的名称,并动态创建这些类的对象实例。
由于登录页面和注册页面不需要任何验证就可以访问,因此,在配置登录注册系统时,应在web.xml文件中指定不需要验证的Servlet.如果有多个不需要验证的Servlet,中间用逗号(,)分割。
下面是在web.xml文件中指定不需要验证的Servlet的代码:
<context-param>
<param-name>ignoreServlets</param-name>
<param-value>Login, Register</param-value>
</context-param>
21.5.2 编写LoginServlet类
oginServlet类负责处理用户登录请求,该类的代码如下:
public class LoginServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, Object obj)
{
UserService userService = (UserService) obj;
try
{
// 验证登录用户
userService.verifyUser();
HttpSession session = request.getSession();
session.setAttribute(String.valueOf(session.getId()。
hashCode()),
userService.getUserDAO()。getUser()。getName());
session.setMaxInactiveInterval(3600);
String json = "{'message':'登录成功','target':'main'}";
return json;
}
catch (Exception e)
{
return "{'message':'" +e.getMessage() + "'}";
}
}
}
在execute方法中首先通过obj参数获得Service对象,并利用该对象的verifyUser方法对当前登录用户信息进行验证。除此之外,在execute方法中还在session域保存了一个标志(属性名为当前HttpSession对象的hashcode,值为当前登录的用户名)。并将Session的有效时间设为1小时(3600秒)。如果在登录成功后,在1小时内再次访问主页面,就无需登录成直接进入主页面了。
由于在本系统中的Web表现层使用了prototype组件通过异步的方式来服务端进行通讯,该组件使用了json数据格式,因此,execute方法返回了成功处理登录请求的信息和在登录成功后要转入的页面名称(也就是Common类的path属性值)必须符合json的数据格式。
21.5.3 编写RegisterServlet类
RegisterServlet类负责处理用户注册信息,该类的代码如下:
public class RegisterServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, Object obj)
{
String json = "";
try
{
UserService userService = (UserService) obj;
userService.save();
json = "{'message':'注册成功'}";
}
catch (Exception e)
{
json = "{'message':\"" + e.getMessage() + "\"}";
}
return json;
}
}
在RegisterServlet类的execute方法中调用了UserService类的save方法将用户注册信息保存在t_users表中,并返回json格式的"注册成功"或异常信息。从LoginServlet和RegisterServlet类可以看出,通过使用三层的系统构架后,实际写在Servlet中的处理代码并不多。虽然CommonServlet类看上去比较复杂,但该类是通用的,只需要实现一次,在以后的应用中只需要继承该类即可。
21.5.4 编写EntryServlet类
EntryServlet类负责转入WEB-INF\jsp目录中的页面,该类的代码如下:
public class EntryServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, Object obj)
{
try
{
// 获得Common类的对象实例
Common common = (Common) obj;
// 从Common类的getPath方法中获得要转入的页面名称
String path = "WEB-INF/jsp/" + common.getPath() + ".jsp";
HttpSession session = request.getSession(false);
if(session != null)
{
// 将当前Session对象的hashcode保存在request域中
request.setAttribute("user",
session.getAttribute(String.valueOf(session
.getId()。hashCode())));
}
RequestDispatcher rd = request.getRequestDispatcher(path);
rd.forward(request, response);
}
catch (Exception e)
{
}
return null;
}
}
在EntryServlet类的execute方法中将当前用户名保存在request域中,在主页面(main.jsp)中将通过该域属性获得当前登录的用户名,并显示在页面中。
21.5.5 注册FormFilter类
FormFilter类是在11.2.1节实现的过滤器,本系统中需要重新注册该类,注册代码如下:
<filter>
<filter-name>FormFilter</filter-name>
<filter-class>filter.FormFilter</filter-class>
<init-param>
<param-name>formName</param-name>
<param-value>#form</param-value>
</init-param>
<init-param>
<param-name>/login</param-name>
<param-value>model.User</param-value>
</init-param>
<init-param>
<param-name>/register</param-name>
<param-value>model.User</param-value>
</init-param>
<init-param>
<param-name>/entry</param-name>
<param-value>model.Common</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>FormFilter</filter-name>
<servlet-name>Login</servlet-name>
<servlet-name>Register</servlet-name>
<servlet-name>Entry</servlet-name>
</filter-mapping>
第 21 章:用户登录注册系统作者:李宁 来源:希赛网 2014年03月10日
实现Web表现层
第 21 章:用户登录注册系统作者:李宁 来源:希赛网 2014年03月10日
编写login.jsp页面
21.6.2 编写login.jsp页面
ogin.jsp页面负责显示用户登录页面,并利用prototype组件向服务端提交登录信息。该页面的代码如下:
<%@ page language="java" pageEncoding="UTF-8"%>
<html>
<head>
<script src="./javascript/prototype.js" type="text/javascript">
</script>
<link type="text/css" rel="stylesheet" href="./css/style.css" />
<title>用户登录</title>
<script type="text/javascript">
// 刷新图像验证码
function refresh()
{
var img = document.getElementById("img_validation_code")
img.src = "validationCode?" + Math.random();
}
function check()
{
// 客户端验证代码
… …
ogin();
}
// 以异步的方式向服务端发送请求
function login()
{
// 定义要请求的Servlet
var url = 'login';
// 将form1表单域的值转换成请求参数
var params = Form.serialize('login_form');
// 创建一个Ajax.Request对象来发送请求
var myAjax = new Ajax.Request(url,
{
// 指定请求方法为POST
method:'post',
// 指定请求参数
parameters:params,
// 指定回调函数
onComplete: processResponse,
// 指定通过异步方式发出请求和接收响应信息
asynchronous:true
});
}
// 该方法为异步处理响应信息的函数
function processResponse(request)
{
// 将json格式的数据转换成JavaScript对象,
// 这些数据由LoginServlet类的execute方法返回
var obj = request.responseText.evalJSON();
alert(obj.message);
if(obj.target != undefined)
window.navigate("entry?path=" + obj.target);
}
</script>
</head>
<body>
<center>
<form name="login_form" action="login" method="post">
… …
</form>
</center>
</body>
</html>
在编写login.jsp页面时要注册,表单的name属性值要和User类的属性名称一致,否则FormFilter过滤器无法成功封装请求参数。
在上面代码中的refresh函数负责从服务端获得图像验证码,由于浏览器缓存的原因,每次访问validationCode时要加一个随机的请求参数。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/entry/login.jsp
浏览器显示的用户登录页面如图21.2所示。
图21.2 用户登录页面
当单击"登录"按钮后,如果输入的用户登录信息是正确的,系统会弹出一个提示登录成功的对话框,在关闭该对话框后,系统会转入main.jsp页面。
21.6.3 编写register.jsp页面
register.jsp负责显示用户注册页面,该页面的实现方法和login.jsp页面类似,只是需要用户录入的信息多了一些。如下面是register.jsp页面中表单的代码:
<form name="register_form" action="register" method="post">
<label style="color:red">*</label><label>用户名:</label>
<input type="text" id="name" class="input_list" name="name" />
<label style="color:red">*</label><label>密 码:</label>
<input type="password" id="password" class="input_list" name="password" />
<label>姓名:</label>
<input type="text" id="xm" class="input_list" name="xm" />
<label>电子邮件:</label>
<input type="text" id="email" class="input_list" name="email" />
<label style="color:red">*</label><label>验证码:</label>
<input type="text" id="validationCode" class="input_list"
name="validationCode" />
<input type="button" value="注册" onclick="check()" />
</form>
上面的代码只给出了register.jsp页面中提交注册信息的表单代码,其他的实现代码和login.jsp页面中的相应代码类似,读者可以参阅随书光盘中的源代码。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/entry/register.jsp
浏览器显示的用户注册页面如图21.3所示。
图21.3 用户注册页面
当用户单击"注册"按钮后,如果注册信息输入正确,浏览器会弹出一个提示注册成功的对话框,然后就可以转到登录页面利用刚才注册的用户进行登录了。
21.7.1 使用MD5算法对字符串进行加密
在Encrypter类中使用了java.security.MessageDigest类的digest方法对字符串进行加密,并使用sun.misc.BASE64Encoder类的encode方法将加密后的字节流转换成Base64编码格式,以便于将加密后的字符串保存在数据库中。Encrypter类的实现代码如下:
public class Encrypter
{
public static String md5(String s)
{
try
{
MessageDigest messageDigest = MessageDigest.getInstance("MD5");
sun.misc.BASE64Encoder base64Encoder = new sun.misc.BASE64Encoder();
return base64Encoder.encode(messageDigest
.digest(s.getBytes("utf8")));
}
catch (Exception e)
{
return s;
}
}
}
21.7.2 中文图像验证码
生成图像验证码实际上就是在服务端将验证码画在图像上,并将该图像发送到客户端。生成验证码的程序是一个Servlet类(ValidationCodeServlet),该类从WEB-INF\code.txt文件中读取随机生成验证码所需要的字符串,并随机从这些字符串选取3到5个字符,将其画在图像上,并将该图像的字节流输出的客户端。在本系统的code.txt文件中主要是中文字符,因此,生成的验证码大多是中文验证码。ValidationCodeServlet类的实现代码如下:
public class ValidationCodeServlet extends HttpServlet
{
// 保存用于随机生成验证码的字符串,从WEB-INF\code.txt文件读取
private static String codeChars = null;
private static Color getRandomColor(int minColor, int maxColor)
{
Random random = new Random();
int red = minColor + random.nextInt(maxColor - minColor);
int green = minColor + random.nextInt(maxColor - minColor);
int blue = minColor + random.nextInt(maxColor - minColor);
return new Color(red, green, blue);
}
@Override
protected void service(HttpServletRequest request,
HttpServletResponse response) throws ServletException, IOException
{
// 如果未加载验证码字符串,从code.txt文件中读取验证码字符串
if(codeChars == null)
{
FileInputStream fis =new FileInputStream(this.getServletontext ()。getRealPath("/WEB-INF/code.txt"));
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
String s = "";
while ((s = br.readLine()) != null)
{
codeChars = s;
}
}
// 获得验证码集合的长度
int charsLength = codeChars.length();
// 禁止客户端缓存网页
response.setHeader("ragma", "No-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
// 设置图形验证码的长和宽(验证码图像的大小)
int width = 150, height = 30;
BufferedImage image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
Graphics g = image.getGraphics();
Random random = new Random();
// 随机设置要填充的颜色
g.setColor(getRandomColor(190, 250));
// 填充图形背景
g.fillRect(0, 0, width, height);
// 随机设置字体颜色
g.setColor(getRandomColor(80, 160));
// 用于保存最后随机生成的验证码
StringBuilder validationCode = new StringBuilder();
String[] fontNames = new String[]{"宋体", "楷体", "隶书"};
// 随机生成3个到5个验证码
for (int i = 0; i < 3 + random.nextInt(3); i++)
{
// 随机设置当前验证码的字符的字体
g.setFont(new Font(fontNames[random.nextInt(3)], Font.ITALIC, height));
// 随机获得当前验证码的字符
char codeChar = codeChars.charAt(random.nextInt(charsLength));
validationCode.append(codeChar);
// 随机设置当前验证码字符的颜色
g.setColor(getRandomColor(10, 100));
// 在图形上输出验证码字符
g.drawString(String.valueOf(codeChar), 25 * i + 6,
height - random.nextInt(5));
}
HttpSession session = request.getSession();
// 设置session对象30分钟失效
session.setMaxInactiveInterval(30 * 60);
// 将验证码保存在session对象中,key为vcode
session.setAttribute("vCode", validationCode.toString());
g.dispose();
OutputStream os = response.getOutputStream();
ImageIO.write(image, "JPEG", os);
}
}
21.8 小结
本章给出了一个使用JSP/Servlet技术实现的用户登录注册系统。该系统使用了典型的三层构架,既数据访问层、业务逻辑层和Web表现层。在本系统中使用CommonServlet类来完成一些通用的功能,如打开数据库连接、创建数据访问层和业务逻辑层的组件等。除此之外,本系统还使用了MD5算法对用户密码进行加密,同时,本系统还支持中文图形验证码。
第22章 电子相册
本章给出一个完整的Web应用程序:电子相册系统。该系统主要使用JSP/Servlet技术实现。在本系统中的用户注册登录功能与第21章实现的注册登录功能类似,在本章将不再对这部分进行介绍,读者可以参阅第21章的相关内容。本章提供的电子相册系统是对第21章所涉及到的内容的延伸,读者可以通过对电子相册系统的学习更进一步了解使用JSP/Servlet技术开发相对完整的Web应用程序的方法和步骤。由于电子相册系统的代码比较多,为了节省篇幅,本章只给出了核心的实现代码,关于本系统详细的实现代码,读者可以查看随书光盘的album目录中的相关源码文件。
22.1 系统概述
本系统实现了电子相册的基本功能,除了用户注册和登录功能外,还包括如下几个功能:
创建相册
删除相册
显示当前用户的所有相册
修改指定相册的属性
将照片上传到指定的相册中
删除照片
本系统和第21章的注册与登录系统一样,也采用了三层构架来实现。程序的组成也和注册与登录系统类似,也就是说,系统分为数据持久层、数据访问层、业务逻辑层和Web表现层,其中数据持久层和数据访问层可以看作是数据层。
电子相册系统的主页面如图22.1所示。
图22.1 相册系统的主页面
在图22.1所示的页面上方显示了当前登录的用户。在该页面中显示了当前用户建立的三个相册,其中最后一个相册中并没有照片,因此显示了系统提供的默认图像。当鼠标放在相册附近时,会在下方显示"修改属性"和"删除"链接,分别用于修改相册的属性和删除当前相册。关于主页面的具体实现细节详见22.6.1节的内容。
22.2 数据库设计
电子相册系统使用了album数据库,在该数据库中包含了如下3个表:
t_users:保存注册用户信息。
t_albums:保存相册信息。
t_photos:保存上传照片信息。
其中t_users表的结构和第21章的注册与登录系统使用的t_users表的结构相同,读者可以参阅21.2节的内容来了解t_users表的详细情况。
t_albums表的结构如表22.1所示。
表22.1 t_ablums表的结构
t_photos表的结构如表22.2所示。
表22.2 t_photos表的结构
关于建立albums数据库和上述3个表的SQL语句,请读者参阅album目录中的album.txt文件。
22.3 实现数据持久层与数据访问层
在本例中的数据持久层的组件实际上就是一些JavaBean,这些JavaBean负责封装客户端的请求,并为数据访问层提供数据,也就是说,数据持久层组件就是数据的载体。数据访问层负责操作数据库中的数据。该层的组件是以DAO结尾的Java类,在电子相册系统中涉及到了三个DAO类:UserDAO类、AlbumDAO类和PhotoDAO类,其中UserDAO类和第14章介绍的UserDAO类基本相同,在本节不再对该类进行介绍。关于这个类的实现细节请读者参阅随书光盘中的源代码。在本节将着重介绍AlbumDAO类和PhotoDAO类的实现。
22.3.1 编写数据持久层组件
数据持久层组件主要包括四个类:Common类、User类、Album类和Photo类。其中Common和User类的具体实现和第21章的相关类一样,在本节不再介绍。
Album类封装了与相册相关的信息(包括t_albums表中的字段值)。该类实现代码如下:
public class Album
{
// 下面五个属性分别封装了相应的请求参数,以及t_albums表中的字段值
private int id;
private String name;
private String path;
private String description;
private int type;
// 该属性表示显示相册时显示的照片ID,也就是图15.1所示的主页面中显示的图像的ID
private int photoId;
// 此处省略了属性的getter和setter方法
… …
}
Photo类封装了与照片相关的信息(包括t_photos表中的字段值)。该类的实现代码如下:
public class Photo
{
// 封装了上传的所有照片的信息
private List<FileExt> upload;
// 封装了请求参数和t_photos表中字段的信息
private int Id;
private String userName;
private int albumId;
private String name;
private String contentType;
private java.util.Date uploadDate;
// 此处省略了属性的getter和setter方法
… …
}
22.3.2 编写数据访问层的AlbumDAO类
AlbumDAO类主要用于操作t_albums表中的数据。该类主要完成如下几个功能:
向t_albums表中增加记录。
删除t_albums表中指定的记录。
修改t_albums表中指定记录的字段值。
获得指定相册的显示照片ID.
获得指定相册中的部分照片的信息。
获得指定相册中全部照片的信息。
根据相册的ID装载其他的相册信息。
AlbumDAO类的代码如下:
public class AlbumDAO extends DAOSupport
{
private Album album;
public AlbumDAO(Connection connection, Album album)
{
super(connection);
this.album = album;
}
public Album getAlbum()
{
return album;
}
// 向t_ablums表中插入一条记录
public void save(String username) throws Exception
{
String sql = "insert into t_albums(name, path, description, type, user_name) values(?,?,?,?,?)";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, album.getName());
pstmt.setString(2, album.getPath());
pstmt.setString(3, album.getDescription());
pstmt.setInt(4, album.getType());
pstmt.setString(5, username);
pstmt.executeUpdate();
}
// 更新t_albums表中的指定记录
public void update() throws Exception
{
String sql = "update t_albums set name=?,description=?,type=? where id=?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, album.getName());
pstmt.setString(2, album.getDescription());
pstmt.setInt(3, album.getType());
pstmt.setInt(4, album.getId());
pstmt.executeUpdate();
}
// 删除t_albums表中的指定记录,同时删除t_photos表中属于被删除相册的照片记录
public void delete(String username) throws Exception
{
String sql = "delete from t_photos where album_id=?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, album.getId());
pstmt.executeUpdate();
sql = "delete from t_albums where id = ?";
pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, album.getId());
pstmt.executeUpdate();
}
// 获得指定用户建立的所有相册的信息
public List<Album> getAlbums(String username) throws Exception
{
String sql = "select * from t_albums where user_name=?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setString(1, username);
ResultSet rs = pstmt.executeQuery();
ist<Album> albums = new LinkedList<Album>();
while (rs.next())
{
Album album = new Album();
album.setId(rs.getInt("id"));
album.setName(rs.getString("name"));
album.setPath(rs.getString("path"));
album.setDescription(rs.getString("description"));
album.setType(rs.getInt("type"));
album.setPhotoId( getPhotoId(album.getId()));
albums.add(album);
}
return albums;
}
// 返回指定相册的显示照片ID
public int getPhotoId(int albumId) throws Exception
{
String sql = "select id from t_photos where album_id=? order by upload_date DESC limit 0, 1";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, albumId);
ResultSet rs = pstmt.executeQuery();
if (rs.next())
{
return rs.getInt("id");
}
return -1;
}
// 根据相册的id装载t_albums表中其他字段值
public void LoadAlbum() throws Exception
{
if(album != null)
{
int id = album.getId();
String sql = "select * from t_albums where id=?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, id);
ResultSet rs = pstmt.executeQuery();
while (rs.next())
{
album.setName(rs.getString("name"));
album.setPath(rs.getString("path"));
album.setDescription(rs.getString("description"));
album.setType(rs.getInt("type"));
return;
}
}
}
// 获得指定相册中所有的照片的信息,startRow参数表示开始的行,从0开始,
// count参数表示返回的记录数。这两个参数实际上就是MySQL中的limit子句的两个参数
public List<Photo> getPhotos(int startRow, int count) throws Exception
{
String limit = "";
if(startRow > 0 || count >= 0)
{
imit = "limit " + startRow + "," + count;
}
String sql = "select * from t_photos where album_id=? order by upload_date DESC " + limit;
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, album.getId());
ResultSet rs = pstmt.executeQuery();
ist<Photo> photos = new LinkedList<Photo>();
while (rs.next())
{
Photo photo = new Photo();
photo.setId(rs.getInt("id"));
photo.setAlbumId(rs.getInt("album_id"));
photo.setName(rs.getString("name"));
photo.setContentType(rs.getString("content_type"));
photo.setUserName(rs.getString("user_name"));
photo.setUploadDate(rs.getDate("upload_date"));
photos.add(photo);
}
return photos;
}
// 返回当前相册全部的相片
public List<Photo> getPhotos() throws Exception
{
return getPhotos(0, -1);
}
}
其中getPhotos方法的第一个重载形式可以用于分页处理,但在电子相册中并未使用该方法来进行分页,读者可以修改本书提供的源代码来增加分页功能,也可以参考下一章的Blog系统中的分页的实现。
22.3.3 编写数据访问层的PhotoDAO类
PhotoDAO类主要用于操作t_photos表中的记录。该类主要完成如下几个功能:
向t_photos表中插入记录。
删除t_photos表中指定的记录。
根据指定的照片ID装载其他的照片信息。
获得当前照片所在的相册的类型(t_albums表中的type字段值)。
获得当前照片所在的相册的保存路径(t_albums表中的path字段值)。
PhotoDAO类的代码如下:
public class PhotoDAO extends DAOSupport
{
private Photo photo;
public PhotoDAO(Connection connection, Photo photo)
{
super(connection);
this.photo = photo;
}
public Photo getPhoto()
{
return photo;
}
// 根据用户上传照片的数量,会向t_photos表中插入0至N条记录
public void save() throws Exception
{
ist<FileExt> files = photo.getUpload();
String sql = "insert into t_photos(user_name, album_id, name,content_type, upload_date) values(?,?,?,?,?)";
PreparedStatement pstmt = connection.prepareStatement(sql);
// 处理用户上传的照片,每张上传照片的信息作为一条记录被插入到t_photos表中
for (FileExt file : files)
{
pstmt.setString(1, photo.getUserName());
pstmt.setInt(2, photo.getAlbumId());
pstmt.setString(3, file.getNewFilename());
pstmt.setString(4, file.getContentType());
pstmt.setDate(5, new java.sql.Date((new java.util.Date())。get Time()));
pstmt.executeUpdate();
}
}
// 删除指定的照片记录
public void deletePhoto() throws Exception
{
oadPhoto();
String sql = "delete from t_photos where id=?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, photo.getId());
pstmt.executeUpdate();
}
// 获得指定相册的类型,该方法在浏览照片时使用
public int getAlbumType(int albumId) throws Exception
{
String sql = "select type from t_albums where id=?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, albumId);
ResultSet rs = pstmt.executeQuery();
while (rs.next())
{
return rs.getInt("type");
}
throw new Exception("不存在id为" + albumId + "的相册");
}
// 获得指定相册的保存路径(t_albums表中的path字段的值)
public String getAlbumPath(int albumId) throws Exception
{
String sql = "select path from t_albums where id=?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, albumId);
ResultSet rs = pstmt.executeQuery();
while (rs.next())
{
return rs.getString("path");
}
throw new Exception("不存在id为" + albumId + "的相册");
}
// 根据指定的照片的ID装载其他的照片信息
public void loadPhoto() throws Exception
{
String sql = "select * from t_photos where id=?";
PreparedStatement pstmt = connection.prepareStatement(sql);
pstmt.setInt(1, photo.getId());
ResultSet rs = pstmt.executeQuery();
if(rs.next())
{
photo.setName(rs.getString("name"));
photo.setAlbumId(rs.getInt("album_id"));
photo.setUserName(rs.getString("user_name"));
photo.setContentType(rs.getString("content_type"));
photo.setUploadDate(rs.getDate("upload_date"));
}
rs.close();
}
}
22.4.1 编写AlbumService类
AlbumService类主要用于处理和相册相关的业务逻辑,包括建立和删除相册(同时删除t_albums表和t_photos表中相关记录以及本地硬盘上的相关目录和文件)。除此之外,还封装了AlbumDAO类中的相应方法。AlbumService类的代码如下:
public class AlbumService
{
// 相册在本地的根目录
public static final String albumRoot = "albums";
private AlbumDAO albumDAO;
public AlbumService(AlbumDAO albumDAO) throws Exception
{
this.albumDAO = albumDAO;
}
public AlbumDAO getAlbumDAO()
{
return albumDAO;
}
//返回指定相册在本地的保存目录,该目录的规则是每个用户建立的相册都保存在该用户的目录中,
//用户目录使用用户名的hashcode作为目录名,每一个相册保存在t_albums表中的path字段
// 指定的目录中
public String getAlbumDir(String username)
{
String albumName = albumDAO.getAlbum()。getName();
return AlbumService.albumRoot + File.separator + username.hashCode()
+ File.separator + albumDAO.getAlbum()。getPath();
}
// 建立一个相册(同时向t_albums表中添加一条记录,并且在本地建立相应的相册目录)
public void addAlbum(String username) throws Exception
{
java.util.Random rand = new java.util.Random();
java.util.Date now = new java.util.Date();
// 生成相册保存在本地的目录名
String path = String.valueOf(now.getTime())
+ String.valueOf(rand.nextInt(1000000));
albumDAO.getAlbum()。setPath(path);
// 向t_albums表中插入记录
albumDAO.save(username);
File dir = new File(getAlbumDir(username));
// 在本地建立相册目录
dir.mkdirs();
}
// 删除指定的相册,同时删除本地的相册目录
public void deleteAlbum(String username) throws Exception
{
// 装载相册的相关信息
albumDAO.LoadAlbum();
// 开始删除相册目录中的照片文件和相册目录
File dir = new File(getAlbumDir(username));
if (dir.exists())
{
File[] files = dir.listFiles();
for (File file : files)
{
file.delete();
}
dir.delete();
}
// 删除t_albums表中的记录
albumDAO.delete(username);
}
public List<Album> getAlbums(String username) throws Exception
{
return albumDAO.getAlbums(username);
}
public int getAlbumType() throws Exception
{
albumDAO.LoadAlbum();
return albumDAO.getAlbum()。getType();
}
public List<Photo> getPhotos() throws Exception
{
return albumDAO.getPhotos();
}
// pageIndex参数表示当前显示的页索引,该参数值从1开始,
// pageCount参数表示每页最多显示的记录数
public List<Photo> getPhotos(int pageIndex, int pageCount) throws Exception
{
return albumDAO.getPhotos((pageIndex - 1) * pageCount, pageCount);
}
public void updateAlbum() throws Exception
{
albumDAO.update();
}
}
22.4.2 编写PhotoService类
PhotoService类主要用于处理和照片相关的业务逻辑,包括添加和删除照片(同时删除t_photos表中记录以及本地硬盘上的相应照片文件)。除此之外,还封装了PhotoDAO类中的相应方法。PhotoService类的代码如下:
public class PhotoService
{
private PhotoDAO photoDAO;
public PhotoService(PhotoDAO photoDAO) throws Exception
{
this.photoDAO = photoDAO;
// 装载照片的相关信息
photoDAO.loadPhoto();
}
public PhotoDAO getPhotoDAO()
{
return photoDAO;
}
// 获得保存在本地的照片文件路径
public String getPhotoDir() throws Exception
{
String photoDir =AlbumService.albumRoot+File.separator+ photoDAO.get Photo()。getUserName()。hashCode() +File.separator+ photoDAO.getAlbumPath(photo DAO.getPhoto()。getAlbumId()) + File.separator;
return photoDir;
}
// 增加照片文件(0个或多个),并将上传的照片文件保存在本地的相册目录中
public void addPhotos() throws Exception
{
List<FileExt> files = photoDAO.getPhoto()。getUpload();
String photoDir = getPhotoDir();
File dir = new File(photoDir);
if(!dir.exists())
{
dir.mkdirs();
}
for (FileExt file : files)
{
FileOutputStream fos = new FileOutputStream(photoDir
+ file.getNewFilename());
InputStream is = file.getFileStream();
byte[] buffer = new byte[8192];
int count = 0;
// 开始在本地保存上传文件,每次写入8K字节
while ((count = is.read(buffer)) > 0)
{
fos.write(buffer, 0, count);
}
is.close();
fos.close();
}
// 向t_photos表中插入相应的记录
photoDAO.save();
}
// 删除指定的照片(同时删除t_photos表中记录以及本地相册目录中照片文件)
public void deletePhoto() throws Exception
{
String path = getPhotoDir() + photoDAO.getPhoto()。getName();
File file = new File(path);
if(file.exists())
file.delete();
photoDAO.deletePhoto();
}
public int getAlbumType() throws Exception
{
return photoDAO.getAlbumType(photoDAO.getPhoto()。getAlbumId());
}
public String getContentType() throws Exception
{
return photoDAO.getPhoto()。getContentType();
}
}
22.5.1 编写CommonServlet类
CommonServlet类的功能与第21章的CommonServlet类基本相同,但本章的CommonServlet类主要做了如下的修改:
1. 将输出信息的代码单独放在了一个out方法中。如果在CommonServlet类的子类中需要改变输出信息的方式,可以覆盖out方法。out方法的代码如下:
protected void out(HttpServletResponse response, String text)
{
try
{
PrintWriter out = response.getWriter();
out.println(text);
}
catch (Exception e)
{
}
}
2. 在execute方法中加入了一个username参数,用于传递当前登录的用户名。execute方法的定义代码如下:
protected abstract String execute(HttpServletRequest request,
HttpServletResponse response, String username, Object obj);
3. 使用protected来修饰checkLogin方法,以便可以在CommonServlet类的子类中覆盖checkLogin方法。
22.5.2 创建相册
CreateAlbumServlet一个Servlet类,负责处理建立相册的请求,该类的代码如下:
public class CreateAlbumServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, String username, Object obj)
{
AlbumService albumService = (AlbumService) obj;
try
{
// 建立相册
albumService.addAlbum(username);
// 返回json格式的信息,当成功创建相册后,重定向到main.jsp页面
String json = "{'message':'成功建立相册','target':'main.jsp'}";
return json;
}
catch (Exception e)
{
return "{'message':\"" +e.getMessage() + "\"}";
}
}
}
第 22 章:电子相册作者:李宁 来源:希赛网 2014年03月11日
删除相册
22.5.3 删除相册
DeleteAlbumServlet是一个Servlet类,负责处理删除相册的请求,该类的代码如下:
public class DeleteAlbumServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, String username, Object obj)
{
AlbumService albumService = (AlbumService) obj;
String json = "";
try
{
// 删除相册
albumService.deleteAlbum(username);
json = "{'message':'成功删除相册'}";
}
catch (Exception e)
{
json = "{'message':'删除照片失败!'}";
e.printStackTrace();
}
return json;
}
}
22.5.4 获得当前用户创建的所有相册
GetAlbumsServlet类可以获得当前用户创建的所有相册的信息,并将这些信息以albums属性名保存在request域中。在图22.1所示的主页面将使用GetAlbumsServlet类来获得当前登录用户建立的所有相册信息,并以一定的形式显示在页面上。GetAlbumsServlet类的代码如下:
public class GetAlbumsServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, String username, Object obj)
{
AlbumService albumService = (AlbumService) obj;
try
{
// 获得当前用户建立的所有相册信息,并将这些信息以albums属性名保存在request域中
request.setAttribute("albums", albumService.getAlbums(username));
}
catch (Exception e)
{
return "{'message':\"" + e.getMessage() + "\"}";
}
return null;
}
}
22.5.5 获得指定相册的内容
GetAlbumServlet类可以获得指定相册的信息(Album对象),并将这些信息以album属性名保存在request域中。GetAlbumServlet类的代码如下:
public class GetAlbumServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, String username, Object obj)
{
AlbumService albumService = (AlbumService) obj;
try
{
获得指定相册的信息(Album对象),并将这些信息以album属性名保存在request域中
request.setAttribute("album", albumService.getAlbumDAO()。getAlbum());
}
catch (Exception e)
{
return "{'message':\"" + e.getMessage() + "\"}";
}
return null;
}
}
22.5.6 上传照片
UploadPhotoServlet类负责处理用户上传的照片信息。在上传完照片后,会重定向到当前相册的页面。UploadPhotoServlet类的代码如下:
public class UploadPhotoServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, String username, Object obj)
{
PhotoService photoService = (PhotoService) obj;
try
{
// 处理上传的照片(向t_photos表中添加记录,并将照片文件保存在本地
photoService.addPhotos();
// 在照片上传完成后,重定向到显示当前相册的照片的页面
response.sendRedirect("viewAlbum?id="
+ photoService.getPhotoDAO()。getPhoto()。getAlbumId()+
"&username=" + username);
}
catch (Exception e)
{
return e.getMessage();
}
return null;
}
}
22.5.7 删除照片
DeletePhotoServlet类负责删除指定的照片,包括t_photos表中的相应记录和保存在相册目录中的照片文件。该类的代码如下:
public class DeletePhotoServlet extends CommonServlet
{
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, String username, Object obj)
{
String json = "";
try
{
PhotoService photoService = (PhotoService) obj;
photoService.deletePhoto();
json = "{'message':'成功删除照片!'}";
}
catch (Exception e)
{
json = "{'message':'删除照片失败!'}";
e.printStackTrace();
}
return json;
}
}
22.5.8 浏览指定的照片
ViewPhotoServlet类负责读取本地的指定照片文件,并将照片文件以字节流形式发送到客户端。如果用户将当前照片所有的相册设为"公开"类型,非登录用户也可以使用ViewPhotoServlet类来查询照片的内容,因此,需要在ViewPhotoServlet类中覆盖CommonServlet类的checkLogin方法,当照片所在的相册的类型为"公开"时,checkLogin方法就会永远返回true,表示通过核对,这样任何访问者就都可以查看"公开"类型的相册中的照片了。由于输出照片需要使用response.getOutputStream,因此,需要覆盖CommonServlet类中的out方法,否则系统会由于同时调用了getOutputStream和getWriter方法而抛出异常。覆盖后的out方法为空即可。
ViewPhotoServlet类的代码如下:
public class ViewPhotoServlet extends CommonServlet
{
// 阻止调用response.getWriter方法
@Override
protected void out(HttpServletResponse response, String text)
{
}
@Override
protected boolean checkLogin(HttpServletRequest request, Object obj)
{
PhotoService photoService = (PhotoService) obj;
if(photoService != null)
{
try
{
int albumType = photoService.getAlbumType();
// 属性为"公开"的相册返回true
if(albumType == 1)
{
return true;
}
}
catch (Exception e)
{
}
}
// 如果相册的属性是"私有",仍然调用CommonServlet类的checkLogin方法
return super.checkLogin(request, obj);
}
@Override
protected String execute(HttpServletRequest request,
HttpServletResponse response, String username, Object obj)
{
PhotoService photoService = (PhotoService) obj;
try
{
Photo photo = photoService.getPhotoDAO()。getPhoto();
// 获得要读取照片文件的本地路径
String path = photoService.getPhotoDir() + photo.getName();
// 设置响应消息头的Content-Type字段值
response.setContentType(photoService.getContentType());
OutputStream out = response.getOutputStream();
InputStream is = new FileInputStream(path);
// 每次向客户端输出64K的字节
byte[] buffer = new byte[1024 * 64];
int count = 0;
// 开始在本地保存上传文件
while ((count = is.read(buffer)) > 0)
{
out.write(buffer, 0, count);
}
is.close();
}
catch (Exception e)
{
}
return null;
}
}
22.6.1 电子相册的主页面
main.jsp是电子相册的主页面。在main.jsp页面中使用了<c:import>标签导入了getAlbums,代码如下:
<c:import url="getAlbums"/>
getAlbums将当前登录用户建立的所有相册的信息保存在request域中(属性名为albums),因此,可以在main.jsp页面中导入getAlbums的语句后面的代码中从request域中获得相册的信息。并使用<c:forEach>标签迭代处理所有的相册,核心代码如下:
<c:forEach var="album" items="${albums}" varStatus = "status">
… …
<a href="viewAlbum?username=${user}&id=${album.id}">
<c:url value="viewPhoto?userName=${user}&albumId=${album.id}&id=${album.photoId}"
var="viewAlbum" />
<!-- 显示当前相册的照片,也就是相册中的最近上传的照片 -->
<!-- 如果相册中没有照片,则显示默认的图像文件(null.jpg) -->
<img id="img${status.index}" onload="imgLoad(this, ${status.index})" style="margin-top:5px;"
src="${(album.photoId==-1)?'images/null.jpg':viewAlbum}" />
</a>
… …
</c:forEach>
读者也可以使用album显示其他的相册信息,如${album.name}显示相册的名称。
在浏览器地址栏中输入如下的URL:
http://localhost:8080/album/login.jsp
当使用正确的用户名和密码登录后,就会显示类似图22.1所示的主页面。
22.6.2 建立相册的JSP页面
create_albums.jsp页面负责建立相册。在该页面中使用AJAX技术向createAlbum提交建立相册的信息,核心代码如下:
<form name="album_form" >
相册名称:<input type="text" id="name" class="input_list" name="name" />
相册描述:<textarea name="description" class="textarea"></textarea>
<input type="radio" name="type" checked="checked" value="1" >公开</input>
<input type="radio" name="type" value="2" >私有</input>
<input type="button" value="创建" onclick="check()" />
</form>
建立相册的JSP页面如图22.2所示。
图22.2 建立相册的JSP页面
22.6.4 显示相册中照片的JSP页面
view_album.jsp页面负责显示指定相册中的所有照片。在该页面中使用<c:import>标签导入了getPhotos来获得指定相册中所有的照片信息,并通过<c:forEach>标签来显示这些照片信息,代码如下:
<body onload="table.style.visibility='visible'">
… …
<table id="table" style="visibility: hidden">
<c:forEach var="photo" items="${photos}" varStatus="status">
<!-- 每行显示5张照片 -->
<c:if test="${status.index % 5 == 0}">
<tr>
<c:set var="index" value="${status.index}" />
</c:if>
<td valign="top" align="center"
style="padding-left: 10px; padding-right: 10px">
<div onmouseout="div${status.index}.style.visibility = 'hidden'"
onmouseover="div${status.index}.style.visibility = 'visible'">
<table height="210" width="180" style="border: double">
<tr align="center">
<td><label style="font-size: 12px">上传日期:</label><label
style="font-size: 12px; color: red">${photo.uploadDate }</label></td>
</tr>
<tr align="center">
<td><a
href="<c:url value ='/view_photo.jsp?userName=${param.username}&albumId=${param.id}&id=${photo.id}'/>">
<!-- 显示当前照片 -->
<img id="img${status.index}" style="margin-top: 5px"
onload="imgLoad(this,${status.index})"
src="<c:url value ='/viewPhoto?userName=${param.username}&albumId=${param.id}&id=${photo.id}'/>" />
</a></td>
</tr>
<tr align="center" height="20">
<td><c:if test="${user==param.username}">
<div id="div${status.index}" style="visibility: hidden"><label
style="font-size: 12px"><a
href="javascript:deletePhoto('${photo.id}')">删除</a></label> <label
style="font-size: 12px"><a
href="<c:url value ='/view_photo.jsp?userName=${param.username}&albumId=${param.id}&id=${photo.id}'/>">原图</a></label>
</c:if>
</div>
</td>
</tr>
</table>
</div>
</td>
<c:if test="${(index - 5) > 0 && (index - 5) % 4 == 0}">
</tr>
</c:if>
</c:forEach>
</table>
</body>
… …
显示指定相册的照片的页面如图22.4所示。
图22.4 显示相册中的所有的照片
当鼠标放在照片及四周时,在相应照片的下方将显示"删除"和"原图"链接。分别用来删除当前照片和按着原始尺寸显示照片。
该页面也可以在未登录时显示属性为"公开"的相册中的照片。在Firefox的地址栏中输入如下的URL:
http://localhost:8080/album/viewAlbum?id=43&username=nokiaguy
在访问上面的URL后,将显示如图22.5所示的页面。
图22.5 在未登录的情况下显示属性为"公开"的相册中的照片
有了数据源与数据源视图之后,就可以定义一个 Analysis Services 多维数据集了。下面将使用“多维数据集向导”,建立一个简单的多维数据集。在建立第一个多维数据集之前,笔者会首先手工建立两个维度:Dim Product.dim 和 Dim Customer.dim,具体步骤如下:
1、在解决方案资源管理器中,选择节点“维度”,单击右键选择“新建维度”,进入“欢迎使用维度向导”页面,直接单击“下一步”按钮。
2、选择创建方法。本例中选择“使用现有表”作为维度的基础。单击“下一步”按钮,如图17-12所示。
图17-12 “选择创建方法”对话框
3、指定源信息。本例中的具体选择为:数据源视图默认使用“Adventure Works DW”;主表选择“DimProduct”;键列和名称列默认为“ProductKey”,如图17-13所示。单击“下一步”按钮。
图17-13 “指定源信息”对话框
4、选择维度属性。在该步骤中,系统默认选择了“Product Key”。用户可以根据具体的需求把其他属性加入到选择中去。本例中,选择了“English Product Name”。单击 “下一步”按钮,如图17-14所示。
图17-14 “选择维度属性”对话框
5、完成向导。维度会默认被命名为“Dim Product”。单击“完成”按钮。
至此,维度Dim Product.dim,建立完成。重复上述5个步骤,同样建立维度Dim Customer.dim。维度Dim Customer基于表DimCustomer,属性全选。下面开始使用“多维数据集向导”建立多维数据集,具体步骤如下:
1、在解决方案资源管理器中,选择“多维数据集”,单击右键选择“新建多维数据集”,进入多维数据集向导欢迎页,单击“下一步”按钮。
2、选择创建方法,如图17-15所示。使用默认值:使用现有表,创建基于数据源中一个或多个表的多维数据集。单击“下一步”按钮。
图17-15 “选择创建方法”对话框
3、选择度量值组表。度量值组表(Measure group tables),也被称做事实表,通常包含比如销售量等一些具体的数据。选择刚才建立的Adventure Works DW数据源视图(本例中只有唯一一个数据源视图,所以默认选择了),然后单击“建议”按钮,表FactInternetSales和FactInternetSalesReason会被选中,本示例只选择了FactInternetSales作为度量值组表,如图17-16所示。单击“下一步”按钮。
图17-16 “选择度量值组表”对话框
4、选择度量值。默认全选,如图17-17所示。单击“下一步”按钮。
图17-17 “选择度量值”对话框
5、选择现有维度,如图17-18所示。选择前面建立好的两个维度Dim Product和Dim Customer,单击“下一步”按钮。
图17-18 “选择现有维度”对话框
6、选择新维度,如图17-19所示。默认选定所有系统生成的新维度,单击“下一步”按钮。
图17-19 “选择新维度”对话框
7、完成向导,如图17-20所示。多维数据集命名为“Adventure Works DW”,单击“完成”按钮。至此,多维数据集添加完成。
图17-20 “完成向导”对话框
第 23 章:Blog系统作者:李宁 来源:希赛网 2014年03月11日
系统概述
第23章 Blog系统
本章给出了一个使用SSH整合方式实现的Blog系统。其中SSH中的第一个S表示Struts 2,第二个S表示Spring,H表示Hibernate.在本系统中,使用Struts 2的Action代替了传统Web应用程序中的Servlet,在操作数据库方法使用了目前非常流行的ORM框架Hibernate,而Spring框架通过自身的优势将Struts 2和Hibernate有机地结合在一起,以使Struts 2、Spring和Hibernate形成三位一体的三层结构体系。读者通过对本章提供的Blog系统的学习可以掌握使用Struts 2 + Spring + Hibernate整合的方式开发三层结构的Web应用程序的步骤和方法。由于篇幅所限,本章只给出了Blog系统的核心源代码,读者可以在随书光盘的blog目录中找到Blog系统的完整源代码。
23.1 系统概述
本系统实现了Blog系统的基本功能,这些功能主要包括:
用户注册
用户登录
发布Blog
浏览当前登录用户录入的Blog列表(如果Blog太多,会分页显示)
编辑以前录入的Blog
删除以前录入的Blog
对Blog进行评论
统计Blog的阅读人数和评论人数
设置并显示标题和子标题
除此之外,在Blog系统中还使用了JSP版的FCKEditor组件在浏览器页面中显示录入和编辑Blog的编辑器,FCKEditor组件生成的Web版的编辑器可以编辑非常复杂的文档,关于FCKEditor组件的详细介绍请读者参阅23.7节中的内容。
下面让我们先睹为快,看看本章实现的Blog系统最终的效果是什么。图23.1是Blog系统的主页面。
图23.1 Blog系统的主页面
23.3.1 编写User类
User类与t_users表对应,User类的大多数属性与t_users表中的字段对应,该类的代码如下:
public class User
{
private int id;
private String name;
// 封装客户端提交的password请求参数
private String password;
private String passwordMd5;
private String xm;
private String email;
// 封装客户端提交的validationCode请求参数
private String validationCode;
private String title;
private String subTitle;
public void setPassword(String password)
{
this.password = password;
this.passwordMd5 = Encrypter.md5(password);
}
// 此处省略了属性的getter和setter方法
… …
}
User类除了封装t_users表中的字段值外,还封装了客户端提交的请求参数,因此,实体Bean也可以作为模型类使用。在上面的代码的password属性的setter方法中在设置password请求参数的同时,使用Encrypter类的md5方法对明文密码进行加密,并赋给了passwordMd5属性。这个passwordMd5属性与t_users表中的password_md5字段对应。
在WEB-INF\classes\entity目录中建立一个User.hbm.xml文件,该文件配置了User类与t_users表的映射关系,代码如下:
<hibernate-mapping>
<class name="entity.User" table="t_users">
<id name="id" column="id" type="int">
<generator class="native" />
</id>
<property name="name" column="name"/>
<property name="passwordMd5" column="password_md5" />
<property name="xm" column="xm" />
<property name="email" column="email" />
<property name="title" column="title" />
<property name="subTitle" column="sub_title" />
</class>
</hibernate-mapping>
23.3.2 编写Blog类
Blog类与t_blogs表对应,Blog类的大多数属性与t_blogs表中的字段对应,该类的代码如下:
public class Blog
{
private int id;
private User user;
private String title;
private String content;
private String blogAbstract;
private String path;
// 封装了用于访问Blog内容的完整Web路径
private String fullPath;
private int replyCount;
private int viewCount;
private java.util.Date postDate;
// 此处省略了属性的getter和setter方法
… …
}
在WEB-INF\classes\entity目录中建立一个Blog.hbm.xml文件,该文件配置了Blog类与t_blogs表的映射关系,代码如下:
<hibernate-mapping>
<class name="entity.Blog" table="t_blogs">
<id name="id" column="id" type="int">
<generator class="native" />
</id>
<many-to-one name="user" column="user_id" class="entity.User"
cascade="all" lazy="false" />
<property name="title" column="title" />
<property name="blogAbstract" column="abstract" />
<property name="path" column="path" />
<property name="replyCount" column="reply_count" />
<property name="viewCount" column="view_count" />
<property name="postDate" column="post_date" />
</class>
</hibernate-mapping>
在上面的代码中使用了<many-to-one>标签来配置Blog对象与User对象的多对一关系,其中lazy属性值为false时表示当Hibernate创建Blog对象时,会自动装载与该Blog对象相关的User对象,并将该对象实例赋给Blog类的user属性。
23.3.3 编写Reply类
Reply类与t_replies表对应,Reply类的大多数属性与t_replies表中的字段对应,该类的代码如下:
public class Reply
{
private int id;
private int blogId;
private String replyUsername;
private java.util.Date replyDate;
private String replyFilename;
// 封装了用于访问回复内容的完整Web路径
private String replyFullPath;
private String replyContent;
// 此处省略了属性的getter和setter方法
… …
}
在WEB-INF\classes\entity目录中建立一个Reply.hbm.xml文件,该文件配置了Reply类与t_replies表的映射关系,代码如下:
<hibernate-mapping>
<class name="entity.Reply" table="t_replies">
<id name="id" column="id" type="int">
<generator class="native" />
</id>
<property name="blogId" column="blog_id" />
<property name="replyUsername" column="reply_user_name" />
<property name="replyDate" column="reply_date" />
<property name="replyFilename" column="reply_filename" />
</class>
</hibernate-mapping>
23.3.4 配置Hibernate
Hibernate需要一个配置文件(通常为hibernate.cfg.xml)来配置连接数据库的信息,以及引用上面三个。hbm.xml文件。在WEB-INF\classes目录中建立一个hibernate.cfg.xml文件,内容如下:
<hibernate-configuration>
<session-factory>
<property name="connection.username">root</property>
<property name="connection.url">
jdbc:mysql://localhost/blog?characterEncoding=UTF8
</property>
<property name="dialect">
org.hibernate.dialect.MySQLDialect
</property>
<property name="show_sql">true</property>
<property name="connection.password">1234</property>
<property name="connection.driver_class">
com.mysql.jdbc.Driver
</property>
<mapping resource="entity\User.hbm.xml" />
<mapping resource="entity\Blog.hbm.xml" />
<mapping resource="entity\Reply.hbm.xml" />
</session-factory>
</hibernate-configuration>
23.4.1 编写DAOSupport类
DAOSupport类是所有的DAO类的父类,由于所有的DAO对象都需要HibernateTemplate对象,因此,在DAOSupport类中通过构造方法将HibernateTemplate对象传入了DAO对象中。DAOSupport类的代码如下:
public class DAOSupport
{
protected HibernateTemplate template;
public DAOSupport(HibernateTemplate template)
{
this.template = template;
}
}
23.4.2 编写操作用户信息的DAO组件
操作用户信息的DAO组件由UserDAO接口和UserDAOImpl类组成。虽然UserDAO接口并不是必须的,但为了使系统有更好的扩展性,笔者建议使用接口来降低系统中各个类之间的耦合度。
UserDAO接口定义了操作用户信息的方法,该接口的代码如下:
public interface UserDAO
{
// 保存用户信息
public void save(User user) throws Exception;
// 根据用户名装载用户信息(User对象)
public User findByName(String name) throws Exception;
}
UserDAOImpl类是UserDAO接口的实现类,该类的代码如下:
public class UserDAOImpl extends DAOSupport implements UserDAO
{
public UserDAOImpl(HibernateTemplate template)
{
super(template);
}
// 如果没有指定的用户名,返回null
@Override
public User findByName(String name) throws Exception
{
// 使用HibernateTemplate类的find方法执行HQL语句
List<User> users = template.find("from User where name=?", name);
if(users.size() > 0)
return users.get(0);
return null;
}
@Override
public void save(User user) throws Exception
{
template.saveOrUpdate(user);
}
}
23.4.3 编写操作Blog信息的DAO组件
操作用Blog信息的DAO组件由BlogDAO接口和BlogDAOImpl类组成。在BlogDAO接口中定义了保存、删除Blog记录的方法,除此之外,还定义了一些更新和查询的方法。BlogDAO接口的代码如下:
public interface BlogDAO
{
// 保存Blog信息
public void save(Blog blog) throws Exception;
// 通过Blog的id装载Blog信息
public Blog findById(final int id) throws Exception;
// 返回指定用户的部分Blog信息,由start和count参数确定返回Blog信息的数量
public List<Blog> getBlogs(final User user, final int start, final int count);
// 更新t_blogs表中的view_count字段值(递增1)
public void incViewCount(final int id) throws Exception;
// 更新t_blogs表中的reply_count字段值(递增1)
public void incReplyCount(final int id) throws Exception;
// 删除指定的Blog信息
public void delete(final Blog blog) throws Exception;
// 获得指定用户录入的Blog总数
public long getBlogCount(User user) throws Exception;
}
BlogDAOImpl类是BlogDAO接口的实现类,该类的代码如下:
public class BlogDAOImpl extends DAOSupport implements BlogDAO
{
public BlogDAOImpl(HibernateTemplate template)
{
super(template);
}
@Override
public Blog findById(final int id) throws Exception
{
return (Blog) template.execute(new HibernateCallback()
{
public Object doInHibernate(Session session)
throws HibernateException, SQLException
{
Blog blog = (Blog)session.get(Blog.class, id);
return blog;
}
});
}
@Override
public void save(Blog blog) throws Exception
{
template.saveOrUpdate(blog);
}
@Override
public List<Blog> getBlogs(final User user, final int start, final int count)
{
return (List<Blog>) template.execute(new HibernateCallback()
{
public Object doInHibernate(Session session)
throws HibernateException, SQLException
{
Query query = session.createQuery("from Blog where user=? order by postDate DESC");
query.setEntity(0, user);
query.setFirstResult(start);
query.setMaxResults(count);
return query.list();
}
});
}
@Override
public void incViewCount(int id) throws Exception
{
template.bulkUpdate("update Blog set viewCount = viewCount + 1 where id = ?", id);
}
@Override
public void incReplyCount(int id) throws Exception
{
template.bulkUpdate("update Blog set replyCount = replyCount + 1 where id = ?", id);
}
@Override
public void delete(final Blog blog) throws Exception
{
template.execute(new HibernateCallback()
{
public Object doInHibernate(Session session)
throws HibernateException, SQLException
{
Query query = session.createQuery("delete from Reply where blogId = ?");
query.setInteger(0, blog.getId());
query.executeUpdate();
session.delete(blog);
return null;
}
});
}
@Override
public long getBlogCount(User user) throws Exception
{
List count = template.find("select count(*) from Blog where user=?", user);
return ((Long)count.get(0))。longValue();
}
}
要注意的是,在上面的代码中的findById、getBlogs和delete方法中使用了HibernateTemplate类的execute方法执行了相关的数据库操作。由于Spring的HibernateTemplate类的其他方法,如find方法,在执行完后会自动关闭Hibernate的Session对象,因此,如果两个或多个操作需要在同一个Session中完成,就不能直接使用HibernateTemplate类的find等方法,例如,将Blog.hbm.xml文件中的<many-to-one>标签的lazy属性值设为proxy,则在装载Blog对象时,Blog对象对应的User对象并不马上装载,而要等到调用User对象的属性时才会被装载。但访问Blog和User对象必须要在同一个Session中进行,在这时就只能使用HibernateTemplate类的execute方法来完成这个工作。由于execute方法接收一个HibernateCallback类型的参数,通过实现HibernateCallback接口的doInHibernate方法,可以直接使用Hibernate的Session对象,而只有doInHibernate方法执行完后,Session对象才会关闭,因此,在doInHibernate方法中可以在同一个Session中完成多项工作。
23.4.4 编写操作回复信息的DAO类
操作回复信息的DAO组件由ReplyDAO接口和ReplyDAOImpl类组成。在ReplyDAO接口中定义了保存和获得指定Blog的所有回复信息的方法,ReplyDAO接口的代码如下:
public interface ReplyDAO
{
// 保存回复信息
public void save(Reply reply) throws Exception;
// 获得指定Blog的所有回复信息
public List<Reply> getReplies(int blogId) throws Exception;
}
ReplyDAOImpl类是ReplyDAO接口的实现类,该类的代码如下:
spublic class ReplyDAOImpl extends DAOSupport implements ReplyDAO
{
public ReplyDAOImpl(HibernateTemplate template)
{
super(template);
}
@Override
public List<Reply> getReplies(int blogId) throws Exception
{
return (List<Reply>) template.find(
"from Reply where blogId=? order by replyDate ASC", blogId);
}
@Override
public void save(Reply reply) throws Exception
{
template.save(reply);
}
}
23.5.1 编写与用户相关的Service组件
为了尽可能地降低系统之间的类的耦合度,业务逻辑层组件也由接口和相应的实现代码组成。UserService接口和UserServiceImpl类用于处理与用户相关的数据及业务逻辑。UserService接口的代码如下:
public interface UserService
{
// 添加用户信息
public void addUser(User user, String root) throws Exception;
// 获得指定的用户信息
public User getUser(String username) throws Exception;
// 验证登录用户的信息是否有效
public boolean verifyUser(User user) throws Exception;
// 更新用户的标题和子标题
public void editTitle(User user, HttpSession session) throws Exception;
}
UserServiceImpl类是UserService接口的实现类,该类的代码如下:
public class UserServiceImpl implements UserService
{
private UserDAO userDAO;
private Config config;
// userDAO参数表示UserDAO类的对象实例,
//config参数表示一个从Spring配置文件装载的Config对象,该对象封装了一些系统级的信息
public UserServiceImpl(UserDAO userDAO, Config config)
{
this.userDAO = userDAO;
this.config = config;
}
@Override
public void addUser(User user, String root) throws Exception
{
// 向t_users表中添加一条用户记录
userDAO.save(user);
// 下面是业务逻辑代码
// 为当前建立的用户创建一个保存该用户发布的blog及回复的目录
String userPath = root + config.getRoot() + File.separator
+ user.getName()。hashCode() + File.separator;
File dir = new File(userPath);
if (!dir.exists())
dir.mkdirs();
}
@Override
public boolean verifyUser(User user) throws Exception
{
User dbUser = userDAO.findByName(user.getName());
if (dbUser == null)
return false;
// 验证登录用户的用户名和密码是否正确
if (user.getName()。equals(dbUser.getName())
&& user.getPasswordMd5()。equals(dbUser.getPasswordMd5()))
{
return true;
}
return false;
}
@Override
public User getUser(String username) throws Exception
{
return userDAO.findByName(username);
}
@Override
public void editTitle(User user, HttpSession session) throws Exception
{
// 根据用户名获得一个User对象
User dbUser = userDAO.findByName(user.getName());
// 从Session中获得用户名,如果为null,则该用户未登录,不更新标题和子标题
String username = (String)session.getAttribute("user");
if(username == null) return;
if(dbUser == null) return;
if(!username.equals(dbUser.getName())) return;
dbUser.setTitle(user.getTitle());
dbUser.setSubTitle(user.getSubTitle());
// 更新t_users表中的title和sub_title字段值
userDAO.save(dbUser);
}
}
要注意的是,由于在Service对象中需要用到一个或多个DAO对象,因此,这些DAO对象通常作为Service类构造方法的参数传入Service对象,当然,也可以作为Service类的属性来传入Service对象,读者可以根据具体情况选择相应的方法向Service对象传入DAO对象。
23.5.2 编写与Blog相关的Service组件
与Blog相关的Service组件由BlogService接口和BlogServiceImpl类组成。BlogService接口的代码如下:
public interface BlogService
{
// 添加一个Blog
public void addBlog(Blog blog, HttpSession session,
ServletContext servletContext) throws Exception;
// 修改Blog的内容,如标题、正文、摘要等
public void editBlog(Blog blog,HttpSession session,
ServletContext servletContext) throws Exception;
// pageIndex表示当前显示页的索引,从1开始pageCount表示当前页显示的最大blog数
public List<Blog> getBlogs(String username, int pageIndex, int pageCount)
throws Exception;
// 获得指定id的Blog信息,如果incViewCount属性为true时,
// t_blogs表的view_count字段值增1
public Blog getBlog(int id, boolean incViewCount) throws Exception;
// 删除指定的Blog
public void deleteBlog(Blog blog, HttpSession session,ServletContext servletContext) throws Exception;
// 获得当前用户发布的Blog的总数
public long getBlogCount(String username) throws Exception;
}
BlogServiceImpl类是BlogService接口的实现类,该类的代码如下:
public class BlogServiceImpl implements BlogService
{
private BlogDAO blogDAO;
private UserDAO userDAO;
private Config config;
// BlogServiceImpl对象需要使用BlogDAO和UserDAO对象,
// 因此,通过构造方法将这两个对象传入BlogServiceImpl对象
public BlogServiceImpl(BlogDAO blogDAO, UserDAO userDAO, Config config)
{
this.blogDAO = blogDAO;
this.userDAO = userDAO;
this.config = config;
}
@Override
public void addBlog(Blog blog, HttpSession session,
ServletContext servletContext) throws Exception
{
String username = (String) session.getAttribute("user");
if (username == null)
return;
User user = userDAO.findByName(username);
if (user == null)
return;
java.util.Random rand = new java.util.Random();
java.util.Date now = new java.util.Date();
String path = String.valueOf(now.getTime())
+ String.valueOf(rand.nextInt(1000000));
// 转换标题中的特殊符号
blog.setTitle(Html.filter(blog.getTitle()));
blog.setUser(user);
blog.setPath(path);
blog.setBlogAbstract(Html.filter(blog.getBlogAbstract()));
blog.setPostDate(now);
// 向t_blogs表中添加一条Blog记录
blogDAO.save(blog);
// 获得Blog正文内容保存在本地的文件路径,并向该文件写入Blog正文
String blogPath = servletContext.getRealPath("/") + config.getRoot()
+ File.separator + username.hashCode() + File.separator + path
+ File.separator;
File dir = new File(blogPath);
if (!dir.exists())
dir.mkdirs();
java.io.FileOutputStream fos = new java.io.FileOutputStream(blogPath
+ "content.txt");
java.io.OutputStreamWriter osw = new java.io.OutputStreamWriter(fos,
"UTF-8");
osw.write(blog.getContent());
osw.close();
fos.close();
}
@Override
public void editBlog(Blog blog,HttpSession session,
ServletContext servletContext) throws Exception
{
Blog myBlog = blogDAO.findById(blog.getId());
String username = (String)session.getAttribute("user");
if(username == null) return;
if(myBlog == null) return;
if(!username.equals(myBlog.getUser()。getName())) return;
myBlog.setTitle(blog.getTitle());
myBlog.setBlogAbstract(blog.getBlogAbstract());
blogDAO.save(myBlog);
String blogPath = servletContext.getRealPath("/") + config.getRoot()
+ File.separator + myBlog.getUser()。getName()。hashCode() + File.separator + myBlog.getPath()
+ File.separator;
File dir = new File(blogPath);
if (!dir.exists())
dir.mkdirs();
java.io.FileOutputStream fos = new java.io.FileOutputStream(blogPath
+ "content.txt");
java.io.OutputStreamWriter osw = new java.io.OutputStreamWriter(fos,
"UTF-8");
osw.write(blog.getContent());
osw.close();
fos.close();
}
@Override
public List<Blog> getBlogs(String username, int pageIndex, int pageCount) throws Exception
{
User user = userDAO.findByName(username);
if(user == null) return null;
return blogDAO.getBlogs(user, (pageIndex - 1) * pageCount, pageCount);
}
@Override
public Blog getBlog(int id, boolean incViewCount) throws Exception
{
Blog blog = blogDAO.findById(id);
String fullPath = config.getRoot() + "/" + blog.getUser()。getName()。hashCode() + "/" + blog.getPath() + "/content.txt";
blog.setFullPath(fullPath);
if(incViewCount)
blogDAO.incViewCount(id);
return blog;
}
@Override
public void deleteBlog(Blog blog,HttpSession session, ServletContext servletContext) throws Exception
{
Blog myBlog = blogDAO.findById(blog.getId());
String username = (String)session.getAttribute("user");
if(username == null) return;
if(myBlog == null) return;
if(!username.equals(myBlog.getUser()。getName())) return;
String blogPath = servletContext.getRealPath("/") + config.getRoot()
+ File.separator + myBlog.getUser()。getName()。hashCode() + File.separator + myBlog.getPath()
+ File.separator;
File dir = new File(blogPath);
if(dir.exists())
{
File[] files = dir.listFiles();
for(File file: files)
file.delete();
dir.delete();
}
blogDAO.delete(blog);
}
@Override
public long getBlogCount(String username) throws Exception
{
User user = userDAO.findByName(username);
if(user == null) return 0;
return blogDAO.getBlogCount(user);
}
}
23.5.3 编写与回复相关的Service组件
与回复相关的Service组件由ReplyService接口和ReplyServiceImpl类组成。ReplyService接口的代码如下:
public interface ReplyService
{
// 添加回复信息
public void addReply(Reply reply, HttpSession session,
ServletContext servletContext) throws Exception;
// 获得指定Blog ID的全部回复信息
public List<Reply> getReplies(int blogId) throws Exception;
}
ReplyServiceImpl类是ReplyService接口的实现类,该类的代码如下:
public class ReplyServiceImpl implements ReplyService
{
private BlogDAO blogDAO;
private ReplyDAO replyDAO;
private Config config;
public ReplyServiceImpl(BlogDAO blogDAO, ReplyDAO replyDAO, Config config)
{
this.blogDAO = blogDAO;
this.replyDAO = replyDAO;
this.config = config;
}
@Override
public void addReply(Reply reply, HttpSession session,
ServletContext servletContext) throws Exception
{
Blog blog = blogDAO.findById(reply.getBlogId());
if(blog == null) return;
String replyPath = servletContext.getRealPath("/") + config.getRoot()
+ File.separator + blog.getUser()。getName()。hashCode() + File.separator + blog.getPath()
+ File.separator;
File dir = new File(replyPath);
if (!dir.exists())
dir.mkdirs();
java.util.Random rand = new java.util.Random();
java.util.Date now = new java.util.Date();
String replyFilename = String.valueOf(now.getTime())
+ String.valueOf(rand.nextInt(1000000)) + ".txt";
reply.setReplyDate(now);
reply.setReplyFilename(replyFilename);
reply.setReplyContent(util.Html.filter(reply.getReplyContent()));
// 向t_replies表中添加一条回复记录
replyDAO.save(reply);
java.io.FileOutputStream fos = new java.io.FileOutputStream(replyPath +
replyFilename);
java.io.OutputStreamWriter osw = new java.io.OutputStreamWriter(fos,
"UTF-8");
// 向本地文件写入回复的内容
osw.write(reply.getReplyContent());
osw.close();
fos.close();
// 将t_blogs表中的reply_count字段值增1
blogDAO.incReplyCount(blog.getId());
}
@Override
public List<Reply> getReplies(int blogId) throws Exception
{
Blog blog = blogDAO.findById(blogId);
if(blog == null) return null;
List<Reply> replies = replyDAO.getReplies(blogId);
String replyPath=config.getRoot()+"/"+blog.getUser()。getName()。hash Code()+"/" + blog.getPath()
+ "/";
for(Reply reply: replies)
{
reply.setReplyFullPath(replyPath + reply.getReplyFilename());
}
return replies;
}
}
23.5.4 编写ServiceManager类
ServiceManager类负责前面编写的三个Service类的对象实例,该类也需要在Spring的配置文件中装配。ServiceManager类的代码如下:
public class ServiceManager
{
private UserService userService;
private BlogService blogService;
private ReplyService replyService;
public UserService getUserService()
{
return userService;
}
public void setUserService(UserService userService)
{
this.userService = userService;
}
public BlogService getBlogService()
{
return blogService;
}
public void setBlogService(BlogService blogService)
{
this.blogService = blogService;
}
public ReplyService getReplyService()
{
return replyService;
}
public void setReplyService(ReplyService replyService)
{
this.replyService = replyService;
}
}
23.6 配置Spring
在编写完数据访问层组件和业务逻辑层组件后,需要在Spring配置文件中配置这些组件。在WEB-INF目录中建立一个applicationContext.xml文件,并输入如下的内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans … …
<bean id="sessionFactory" class="org.springframework.orm.hibernate3.Lo calSessionFactoryBean">
<property name="configLocation" value="classpath:hibernat e.cfg.xm l">
</property>
</bean>
<bean id="hibernateTemplate" class="org.springframework.orm.hiberna te3.HibernateTemplate">
<property name="sessionFactory" ref="sessionFactory" />
</bean>
<bean id="transactionManager"
class="org.springframework.orm.hibernate3.HibernateTransactionManager">
<property name="sessionFactory">
<ref bean="sessionFactory" />
</property>
</bean>
<bean id="transactionInterceptor" class="org.springframework.transact ion.interceptor.TransactionInterceptor">
<property name="transactionManager">
<ref bean="transactionManager" />
</property>
<property name="transactionAttributes">
<props>
<prop key="get*">PROPAGATION_REQUIRED, readOnly</prop>
<prop key="*">PROPAGATION_REQUIRED</prop>
</props>
</property>
</bean>
<bean class="org.springframework.aop.framework.autoproxy.BeanNameAuto ProxyCreator">
<property name="beanNames">
<list>
<value>userDAO</value>
<value>blogDAO</value>
<value>replyDAO</value>
</list>
</property>
<property name="interceptorNames">
<list>
<value>transactionInterceptor</value>
</list>
</property>
</bean>
<bean id="userDAO" class="dao.UserDAOImpl">
<constructor-arg>
<ref bean="hibernateTemplate" />
</constructor-arg>
</bean>
<bean id="blogDAO" class="dao.BlogDAOImpl">
<constructor-arg>
<ref bean="hibernateTemplate" />
</constructor-arg>
</bean>
<bean id="replyDAO" class="dao.ReplyDAOImpl">
<constructor-arg>
<ref bean="hibernateTemplate" />
</constructor-arg>
</bean>
<bean id="userService" class="service.UserServiceImpl">
<constructor-arg>
<ref bean="userDAO" />
</constructor-arg>
<constructor-arg>
<ref bean="config" />
</constructor-arg>
</bean>
<bean id="blogService" class="service.BlogServiceImpl">
<constructor-arg>
<ref bean="blogDAO" />
</constructor-arg>
<constructor-arg>
<ref bean="userDAO" />
</constructor-arg>
<constructor-arg>
<ref bean="config" />
</constructor-arg>
</bean>
<bean id="replyService" class="service.ReplyServiceImpl">
<constructor-arg>
<ref bean="blogDAO" />
</constructor-arg>
<constructor-arg>
<ref bean="replyDAO" />
</constructor-arg>
<constructor-arg>
<ref bean="config" />
</constructor-arg>
</bean>
<bean class="org.springframework.beans.factory.config.PropertyPlaceho lderConfigurer">
<property name="location">
<value>WEB-INF\blog.properties</value>
</property>
</bean>
<bean id="config" class="util.Config">
<property name="root" value="${root}"/>
</bean>
<bean id="serviceManager" class="service.ServiceManager">
<property name="userService">
<ref bean="userService" />
</property>
<property name="blogService">
<ref bean="blogService" />
</property>
<property name="replyService">
<ref bean="replyService" />
</property>
</bean>
</beans>
23.7 安装和配置FCKEditor组件
FCKEditor组件用于生成Web版的内容编辑器。该组件支持很多开发Web应用程序的语言或技术,如PHP、。net、JSP等。在Blog系统中使用了JSP版的FCKEditor组件来生成编辑Blog正文的编辑器。下面是安装和配置FCKEditor组件的基本步骤:
1. 将FCKEditor目录复制到Web根目录。
2. 配置FCKEditor组件中的ConnectorServlet和SimpleUploaderServlet类。关于具体的配置代码请读者参阅web.xml文件中的内容。
3. 由于FCKEditor也需要使用Servlet,因此,需要将Struts 2的Filter的URL匹配模式改成如下的形式,否则FCKEditor的Servlet会不起作用。
<filter>
<filter-name>struts2</filter-name>
<filter-class> org.apache.struts2.dispatcher.FilterDispatcher
</filter-class>
</filter>
<filter-mapping>
<filter-name>struts2</filter-name>
<url-pattern>*.action</url-pattern>
</filter-mapping>
4. 在struts.xml文件中映射connector,代码如下:
<action name="connector">
<result>
/FCKeditor/editor/filemanager/browser/default/connectors/jsp/connector
</result>
</action>
5. 修改fckconfig.js文件。该文件中FCKEditor目录中,打开该文件,修改上传文件和浏览服务端文件的链接,代码如下:
FCKConfig.LinkBrowserURL=FCKConfig.BasePath+"filemanager/browser/default/browser.html?Connector=connector.action" ;
FCKConfig.ImageBrowserURL=FCKConfig.BasePath+"filemanager/browser/default/browser.html?Type=Image&Connector=connector.action" ;
FCKConfig.FlashBrowserURL=FCKConfig.BasePath+"filemanager/browser/default/browser.html?Type=Flash&Connector=connector.action" ;
6. 在Web根目录中建立一个UserFiles目录,并建立三个子目录:File、Flash和Image.这三个目录用于保存用户上传的文件。
7. 根据需要修改SimpleUploaderServlet类和ConnectorServlet类中的代码。在上述三个目录中为每个用户单独建立了一个目录来保存当前用户上传的文件。关于具体的代码请读者参阅本章的源代码。
23.8.1 编写ModelAction类
ModelAction类是所有Action类的父类,在该类中实现了所有Action类都要用到的功能,如实现ModelDriven接口、获得HttpSession对象等。该类的代码如下:
// 在ModelAction类中使用了泛型来指定Model类
public class ModelAction<Model> extends ActionSupport implements
ModelDriven<Model>,ServletRequestAware, ServletContextAware
{
protected Model model;
protected ServiceManager serviceManager;
protected String result;
protected HttpServletRequest request;
protected HttpSession session;
protected ServletContext context;
@Override
public void setServletContext(ServletContext context)
{
this.context = context;
}
@Override
public Model getModel()
{
return model;
}
public void setServiceManager(ServiceManager serviceManager)
{
this.serviceManager = serviceManager;
}
@Override
public void setServletRequest(HttpServletRequest request)
{
this.request = request;
this.session = request.getSession();
}
// 该方法用于检查验证码是否正确,用于登录和注册页面
@Override
public void validate()
{
if (model == null)
return;
try
{
Method method = model.getClass()。getMethod("getValidationCode");
String validationCode = (String)method.invoke(model);
if(validationCode.equals("")) return;
Object obj=ActionContext.getContext()。getSession()。get("vCode ");
String vCode = (obj != null) ? obj.toString() : "";
if(!vCode.equals(validationCode))
{
this.addFieldError("validationCode", "验证码错误");
}
}
catch (Exception e)
{
}
}
public String getResult()
{
return result;
}
}
在ModelAction类中实现了ModelDriven接口,因此,所有从ModelAction继承的类都必须指定一个Model类。Model类只需要在ModelAction的子类的构造方法中指定即可。
23.8.2 用户注册
在本例中通过RegisterAction类来处理用户注册请求。RegisterAction是一个Action类,代码如下:
public class RegisterAction extends ModelAction<User>
{
public RegisterAction()
{
// 创建模型类的对象实例
model = new User();
}
public String execute()
{
UserService userService = serviceManager.getUserService();
try
{
String root = context.getRealPath("/");
userService.addUser(model, root);
result = "用户注册成功";
return SUCCESS;
}
catch (Exception e)
{
result = e.getMessage();
e.printStackTrace();
}
return INPUT;
}
}
RegisterAction类的配置代码如下:
<action name="register" class="action.RegisterAction">
<result name="success">/WEB-INF/jsp/register.jsp</result>
<result name="input">/WEB-INF/jsp/register.jsp</result>
</action>
其中register.jsp显示了用户注册页面。该页面使用了<s:form>及其他的Struts 2标签来生成若干用于录入用户注册信息的表单域,代码如下:
<s:form id="registerForm" action="register">
<s:textfield name="name" label="用户名" required="true" cssClass="input_list" />
<s:password name="password" required="true" cssClass="input_list" label="密码" />
<s:textfield name="xm" cssClass="input_list" label="姓名" />
<s:textfield name="email" cssClass="input_list" label="电子邮件" />
<s:textfield name="validationCode" required="true" cssClass="input_list" label="验证码" />
<s:submit value="注册" />
</s:form>
23.8.3 用户登录
Blog系统使用LoginAction类来处理用户登录请求,该类的代码如下:
public class LoginAction extends ModelAction<User>
{
public LoginAction()
{
model = new User();
}
public String execute()
{
try
{
UserService userService = serviceManager.getUserService();
// 验证登录用户的信息是否合法
if(userService.verifyUser(model))
{
result = "用户登录成功";
session.setAttribute("user", model.getName());
// session的有效时间为24小时
session.setMaxInactiveInterval(24 * 2600);
return SUCCESS;
}
}
catch (Exception e)
{
e.printStackTrace();
}
result = "用户登录失败";
return INPUT;
}
}
LoginAction类的配置代码如下:
<action name="login" class="action.LoginAction">
<result name="success" type="redirectAction">main</result>
<result name="input">/WEB-INF/jsp/login.jsp</result>
</action>
当用户成功登录后,会重定向到Blog系统的主页面(通过main.action访问)。login.jsp是用户登录的页面,该页面的实现代码与register.jsp页面类似,读者可以查看本书提供的源代码以获得更详细的实现细节。
23.8.4 实现Blog系统的主页面
由于Blog系统的主页面需要显示当前用户发布的所有的Blog(超过10个Blog会分页显示),因此,需要在运行主页面的过程中从服务端获得这些Blog信息。在本例中首先使用了GetBlogsAction类来获得Blog信息以及其他相关的信息,然后在main.jsp页面中获得并输出这些信息。GetBlogsAction类的代码如下:
public class GetBlogsAction extends ModelAction<Blogs>
{
private User user;
public GetBlogsAction()
{
model = new Blogs();
}
public User getUser()
{
return user;
}
public String execute()
{
try
{
BlogService blogService = serviceManager.getBlogService();
UserService userService = serviceManager.getUserService();
String username = (String) session.getAttribute("user");
if (model.getUsername() != null
&& !model.getUsername()。trim()。equals(""))
username = model.getUsername();
model.setCount(blogService.getBlogCount(username));
int div = (int) model.getCount() / model.getEveryPageCount();
// 获得Blog可显示的页面(每页显示10个Blog,包括Blog摘要)
model.setPageCount((model.getCount() % model.getEveryPageCount() == 0)?div:div+1);
model.setBlogs(blogService.getBlogs(username,model.getPage Index(),
model.getEveryPageCount()));
user = userService.getUser(username);
return SUCCESS;
}
catch (Exception e)
{
e.printStackTrace();
}
return INPUT;
}
}
其中Blogs是一个模型类,封装了当前用户发布的Blog信息以及其他相关的信息,该类的代码如下:
public class Blogs
{
private String username;
// 当前页的索引(从1开始)
private int pageIndex = 1;
// 每页的记录数
private int everyPageCount = 10;
// Blog记录数
private long count;
// 返回的Blog可分成多少页显示
private int pageCount;
// 封装用户发布的Blog信息
private List<Blog> blogs;
// 此处省略了属性的getter和setter方法
… …
}
GetBlogsAction类的配置代码如下:
<action name="main" class="action.GetBlogsAction">
<result name="success">/WEB-INF/jsp/main.jsp</result>
<result name="input">/WEB-INF/jsp/main.jsp</result>
</action>
其中main.jsp为Blog系统的主页面。该页面使用了<s:iterator>标签迭代GetBlogsAction类返回的Blog信息,并显示Blog标题、Blog摘要、发布时间等信息,以及进行分页处理。核心代码如下:
<!- 显示Blog标签、Blog摘要及其他信息 -->
<s:iterator id="blog" value="blogs">
<a href="viewBlog.action?id=${blog.id}"> ${blog.title}</a><hr>
<div style="width: 800px; font-size: 12px">
摘要:${blog.blogAbstract}</div>
<p/>posted @ <fmt:formatDate value="${blog.postDate}"
pattern="yyyy-MM-dd HH:mm:ss" />
${blog.user.name} 阅读(${blog.viewCount}) | 评论
(${blog.replyCount})
<!-- 如果用户未登录,不显示"编辑"和"删除"链接 -->
<c:if test="${(sessionScope.user != null) && (param.username == sessionScope.user || param.username == null)}">
| <a href="editBlog.action?id=${blog.id}">编辑</a> |
<a href="javascript:deleteBlog(${blog.id})">删除</a>
</c:if><hr><p/>
</s:iterator>
<!-- 显示页码 -->
<div style="width: 800px; text-align: center">
<!-- 如果页数大于1(超过10个Blog),才进行分页 -->
<c:if test="${model.pageCount > 1}">
<c:forEach begin="1" end="${model.pageCount}" varStatus="status">
<c:set var="userparam" value="&username=${param.username}" />
<a href="main.action?pageIndex=${status.count}${(param.username ==null||param.username=='')?'':userparam }">${status.count}</a>
</c:forEach>
</c:if>
</div>
main.jsp页面的显示效果如图23.1所示。
23.8.5 发布与编辑Blog信息
发布与编辑Blog信息分别由AddBlogAction和EditBlogAction类来处理。AddBlogAction类调用了BlogService接口的addBlog方法来发布Blog信息,EditBlogAction类调用了BlogService接口的editBlog方法来发布Blog信息。关于这两个类的实现细节,请读者参阅本书提供的相应源代码。
addblog.jsp是发布Blog信息的页面,该页面实际上通过一个表单来提交用户录入的Blog信息。在该页面中使用如下的代码来生成Blog内容编辑器:
<%
util.WebEditor.createFCKEditor(request, "800", "400");
%>
上面的代码实际上调用了FCKEditor组件的相关类来生成编辑器的客户端代码。该类的实现代码如下:
public class WebEditor
{
public static void createFCKEditor(HttpServletRequest request, String width, String height)
{
createFCKEditor(request, width, height, "");
}
public static void createFCKEditor(HttpServletRequest request, String width, String height, String value)
{
String path = request.getContextPath();
FCKeditor oFCKeditor;
// 定义一个属性来使Action通过request来获得FCKeditor编辑器中的值
oFCKeditor = new FCKeditor(request, "content");
FCKeditorConfigurations con = new FCKeditorConfigurations();
oFCKeditor.setConfig(con);
oFCKeditor.setBasePath(path + "/FCKeditor/");
oFCKeditor.setWidth(width);
oFCKeditor.setHeight(height);
oFCKeditor.setInstanceName("content");
// 设置FCKeditor编辑器打开时的默认值
oFCKeditor.setValue(value);
request.setAttribute("editor", oFCKeditor.create());
}
}
从上面的代码可以看出,createFCKEditor方法有两个重载形式,在addblog.jsp页面中使用了第一种重载形式,而第二种重载形式需要指定一个编辑器默认显示的值。在editblog.jsp页面中需要使用这个重载形式将Blog原来的正文作为默认值显示在编辑器中,代码如下:
<%
util.WebEditor.createFCKEditor(request, "800", "400",
(String)pageContext.getAttribute("content"));
%>
发布和编辑Blog的页面的显示效果类似,如图23.2是录入Blog的页面。
图23.2 录入Blog的页面
23.8.6 添加与显示回复信息
添加回复信息由AddReplyAction来完成。该类调用了ReplyService接口的addReply方法向t_replies表中添加回复记录,并将回复的内容保存在本地相应的文件中。
当用户单击图23.1所示的页面中的某个Blog标题时,就会显示该Blog的内容。每一个Blog标题都会链接到viewBlog.action上,这个Action对应于GetBlogAction类。在该类中同时获得了当前Blog的信息以及所有的回复信息。GetBlogAction类的代码如下:
public class GetBlogAction extends ModelAction<Blog>
{
private Blog blog;
private List<Reply> replies;
private boolean incViewCount = false;
public GetBlogAction()
{
model = new Blog();
}
public Blog getBlog()
{
return blog;
}
public List<Reply> getReplies()
{
return replies;
}
public void setIncViewCount(boolean incViewCount)
{
this.incViewCount = incViewCount;
}
public String execute()
{
try
{
BlogService blogService = serviceManager.getBlogService();
ReplyService replyService = serviceManager.getReplyService();
// 获得Blog的内容
blog = blogService.getBlog(model.getId(), incViewCount);
// 获得Blog的所有回复信息
replies = replyService.getReplies(blog.getId());
return SUCCESS;
}
catch (Exception e)
{
e.printStackTrace();
}
return INPUT;
}
}
GetBlogAction类的配置代码如下:
<action name="viewBlog" class="action.GetBlogAction">
<result name="success">/WEB-INF/jsp/viewblog.jsp</result>
<result name="input">/WEB-INF/jsp/viewblog.jsp</result>
// 每次访问该Action,t_blogs表中的view_count字段值都加1s
<param name="incViewCount">true</param>
</action>
viewblog.jsp页面使用<c:forEach>标签迭代了当前Blog的回复信息,代码如下:
<c:forEach var="reply" items="${replies}" varStatus="status">
<label style="font-size:12px">#${status.count} 楼
<fmt:formatDate value="${reply.replyDate}"
pattern="yyyy-MM-dd HH:mm:ss" />
${reply.replyUsername}<br>
<c:import url="//${reply.replyFullPath}" charEncoding="UTF-8"/>
</label><p/>
</c:forEach>
关于更详细的代码,请读者参阅随书光盘中的viewblog.jsp文件中的内容。图23.3是在viewblog.jsp页面中录入回复信息的页面。
图23.3 录入回复信息的页面
23.9 小结
本章给出了一个基于SSH的Blog系统。该系统使用三层架构实现。在第15章实现的电子相册系统也使用了三层构架,但在电子相册系统中很多工作都是由开发人员来做的。而在基于SSH整合的开发模式中,这些基础的工作(如装配JavaBean,自动为Action类的属性赋值)都由Spring以及相关的组件代劳了。因此,使用SSH来实现三层构架的Web应用程序是非常容易的,而且层次清晰。