1
数据库系统实现
实
验
指
导
书
齐心 彭彬
计算机工程与软件实验中心
2016 年 3 月
2
目 录
实验一、JDBC 应用程序设计(2 学时) ........................................................ 7
1、实验目的 ............................................................................. 7
2、实验性质 ............................................................................. 7
3、实验导读 ............................................................................. 7
3.1、JDBC 介绍......................................................................... 7
3.2、JDBC 中常用的类和接口 ............................................................. 8
3.3、数据库连接 ...................................................................... 11
3.4、数据库操作 ...................................................................... 14
4、实验内容 ............................................................................ 20
5、实验总结 ............................................................................ 24
实验二、GUI 图形用户界面设计(2 学时)..................................................... 25
1、实验目的 ............................................................................ 25
2、实验性质 ............................................................................ 25
3、实验导读 ............................................................................ 25
3.1、Swing 的基本概念 ................................................................. 25
3.2、Swing 和 MVC 设计模式 ............................................................. 27
3.3、Swing 的单线程模型 ............................................................... 28
3.4、Swing 程序设计练习 ............................................................... 30
3.5、Swing 基本控件和窗口 ............................................................. 37
3.6、Swing 容器....................................................................... 57
3.7、Swing 高级控件................................................................... 62
4、实验内容 ............................................................................ 74
5、实验总结 ............................................................................ 82
实验三、简单 Web 应用程序设计(2 学时) .................................................... 83
1、实验目的 ............................................................................ 83
2、实验性质 ............................................................................ 83
3、实验导读 ............................................................................ 83
3.1、Web 基础知识..................................................................... 83
3.2、HTML 基础知识.................................................................... 88
3.3、开发环境的搭建 .................................................................. 92
3.4、Servlet 技术..................................................................... 97
3.5、Filter 技术..................................................................... 105
3.6、JSP 技术........................................................................ 120
3.7、EL 表达式....................................................................... 124
3.8、JSTL 核心标记库 ................................................................. 125
3.9、中文乱码解决方案 ............................................................... 132
4、实验内容 ........................................................................... 136
5、实验总结 ........................................................................... 148
实验四、存储子程序(2 学时) ............................................................. 149
3
1、实验目的 ........................................................................... 149
2、实验性质 ........................................................................... 149
3、实验导读 ........................................................................... 149
3.1、PL/SQL 基础知识 ................................................................. 149
3.2、游标 ........................................................................... 154
3.3、存储子程序 ..................................................................... 156
3.4、包 ............................................................................. 160
3.5、序列 ........................................................................... 164
4、实验内容 ........................................................................... 165
5、实验总结 ........................................................................... 167
实验五、触发器(2 学时) ................................................................. 168
1、实验目的 ........................................................................... 168
2、实验性质 ........................................................................... 168
3、实验导读 ........................................................................... 168
3.1、触发器的概念 ................................................................... 168
3.2、触发器的分类 ................................................................... 168
3.3、触发器的作用 ................................................................... 168
3.4、创建触发器的规则 ............................................................... 169
3.5、触发器的语法 ................................................................... 169
3.6、查询触发器信息 ................................................................. 176
3.7、其它触发器相关语法 ............................................................. 177
3.8、实验方案讨论 ................................................................... 177
4、实验内容 ........................................................................... 181
5、实验总结 ........................................................................... 182
实验六、C/S 架构应用程序(3 学时) ....................................................... 183
1、实验目的 ........................................................................... 183
2、实验性质 ........................................................................... 183
3、实验导读 ........................................................................... 183
3.1、数据库应用系统架构 ............................................................. 183
3.2、两层 C/S 架构的实现 ............................................................. 184
3.3、SDI 界面的设计与实现 ............................................................ 184
3.4、MDI 界面的设计与实现 ............................................................ 185
3.5、BLOB 数据类型的处理 ............................................................. 195
4、实验内容 ........................................................................... 200
5、实验总结 ........................................................................... 202
实验七、B/S 架构应用程序(3 学时) ....................................................... 203
1、实验目的 ........................................................................... 203
2、实验性质 ........................................................................... 203
3、实验导读 ........................................................................... 203
3.1、B/S 架构开发步骤 ................................................................ 203
3.2、Servlet 3.0 新特性详解 ......................................................... 203
4
3.3、BLOB 数据类型的处理 ............................................................. 212
3.4、分页显示技术的实现 ............................................................. 214
3.5、JQuery.Validate 客户端验证 ...................................................... 221
4、实验内容 ........................................................................... 225
5、实验总结 ........................................................................... 227
5
【实验相关说明】
实验中用到的学生-课程数据库表结构如下所示:
---学生表
CREATE TABLE Student(
SNO VARCHAR2(7) PRIMARY KEY,
Sname VARCHAR2(18),
SSex CHAR(3),
Sage SMALLINT,
Sdept VARCHAR2(50),
SAvgGrade NUMBER(3,0),
SPicture BLOB
);
---课程表
CREATE TABLE Course(
Cno VARCHAR2(4) PRIMARY KEY,
Cname VARCHAR2(50),
Cpno VARCHAR2(4),--前导课
Ccredit NUMBER(2,0),
FOREIGN KEY (Cpno) REFERENCES Course(Cno)
);
---选课表
CREATE TABLE SC(
Sno VARCHAR2(7),
Cno VARCHAR2(4),
Grade NUMBER(3,0),
PRIMARY KEY(Sno,Cno),
FOREIGN KEY (Sno) REFERENCES Student(Sno),
FOREIGN KEY (Cno) REFERENCES Course(Cno)
);
测试数据:
insert into student(sno,sname,ssex,sage,sdept) values('9512101','李勇','男',19,'计算机系');
insert into student(sno,sname,ssex,sage,sdept) values('9512102','刘晨','男',20,'计算机系');
insert into student(sno,sname,ssex,sage,sdept) values('9521101','张立','男',22,'信息系');
insert into student(sno,sname,ssex,sage,sdept) values('9521102','吴宾','女',21,'信息系');
insert into student(sno,sname,ssex,sage,sdept) values('9531101','钱小平','女',18,'数学系');
insert into student(sno,sname,ssex,sage,sdept) values('9531102','王大力','男',19,'数学系');
insert into course values('1','数据处理',null,2);
insert into course values('2','数学',null,2);
6
insert into course values('3','操作系统','1',3);
insert into course values('4','C 语言程序设计','1',4);
insert into course values('5','数据结构','4',4);
insert into course values('6','数据库','5',4);
insert into course values('7','信息系统','6',4);
insert into sc values('9512101','1',92);
insert into sc values('9512101','2',85);
insert into sc values('9512101','3',88);
insert into sc values('9512102','2',90);
insert into sc values('9512102','3',100);
7
实验一、JDBC 应用程序设计(2 学时)
1、实验目的
(1)掌握 JDBC 中常用的类和接口。
(2)掌握数据库操作的步骤。
(3)掌握利用 JDBC 技术实现对数据的添加、修改、删除和查询。
2、实验性质
验证性实验
3、实验导读
3.1、JDBC 介绍
JDBC (Java Database Connectivity) 是 Sun 公司给出的一个基于 Java 语言访问关系数据库的接口标准,
这个标准基于 X/Open SQL Call Level Interface ,并与 SQL 92 入门级标准兼容。JDBC 制定了统一的访问各
类关系数据库的标准接口,为各个数据库厂商提供了标准接口的实现。
3.1.1、JDBC 驱动程序
(1)JDBC 体系结构
图 1-1 JDBC 体系结构
图 1-1 中 JDBC Driver 由数据库厂商依据 SUN 公司发布的 JDBC API 接口实现(注意:厂商在实现 JDBC
API 标准时往往有个性化的内通融)。除非用户要用到开发商驱动程序某些特有的功能,一般情况下,开发
者的 JAVA 代码应依据标准 JDBC API 来编写。
(2)JDBC 驱动程序类型
①JDBC-ODBC 桥驱动程序+ODBC 驱动程序。其中桥驱动程序由 JDK 中提供,使用这种驱动程序的
应用还要求用户在本地安装 ODBC 驱动程序和配置 ODBC 数据源。
Java Application
JDBC API
JDBC Driver Manager
JDBC Driver
中间件 数据源
8
②本地 API 部分 JAVA 实现驱动程序。这种类型的驱动程序把客户机上的 JDBC 调用转换为对 Oracle、
Sybase、Informix、DB2 或其它 DBMS 的调用。使用这种驱动程序要求用户安装特定数据库厂商的驱动程序。
③JDBC-NET 纯 java 驱动程序。这种驱动程序将 JDBC 调用转换为与 DBMS 无关的网络协议,这种协议又
被某个服务器转换为一种 DBMS 协议。使用这种驱动程序要求一个网络上的中间件服务器。
④本地协议纯 java 驱动程序。这种类型的驱动程序将 JDBC 调用直接转换为 DBMS 所使用的网络协议,
一般具有较好的性能。
(3)Oracle JDBC 驱动程序
ORACLE 提供基于类型 2 和类型 4 的 JDBC 驱动程序。两类驱动均可以运行于客户端和服务器端,如
图 1-2 所示。
第四类驱动称为 Thin 驱动;第二类驱动称为 OCI 驱动。
(4)JDBC URL 与 Oracle JDBC URL
JDBC 用 JDBC URL 来标识数据库,格式为“jdbc:<子协议>:<子名称>”,其中子协议、子名称均由数
据库厂商自己规定。
基于数据源的 JDBC URL 为 jdbc:odbc:<数据资源名称>[;<属性名>=<属性值>],如:
使 JDBC+ODBC 桥驱动的 JDBC URL:
jdbc:odbc:qeor7
jdbc:odbc:wombat; CacheSize=20; ExtensionCase=LOWER
jdbc:odbc:qeora; UID=kgh; PWD=fodey
使用 Thin 驱动的 JDBC URL 为:
jdbc:oracle:thin:[user/password]@[host][:port]:SID 或
jdbc:oracle:thin:[user/password]@//[host][:port]/SID
其中 port 缺省为 1521,Oracle 10g Express 版本的 SID 为 XE。
图 1-2 ORACLE 驱动架构
3.2、JDBC 中常用的类和接口
说明:JDBC 中常用的类和接口都是位于 java.sql 包中,在 使用时导入即可。
3.2.1、DriverManager 类
9
DriverManager 类用来管理数据库中的所有驱动程序,是 JDBC 的管理层,作用于用户和驱动程序之间,
跟踪可用的驱动程序,并在数据库的驱动程序之间建立连接。DriverManager 类中的方法都是静态方法,所
以在程序中无须对它进行实例化,直接通过类名就可以调用。DriverManager 类的常用方法如表 1-1 所示:
表 1-1 DriverManager 类的常用方法
方法 功能描述
getConnection(String url, String
user, String password)
试图建立到给定数据库 URL 的连接。方法中指定 3 个入口参数,依次
是连接数据库的 URL、用户名、密码
setLoginTimeout(int seconds) 设置驱动程序试图连接到某一数据库时将等待的最长时间,以秒为单
位。
println(String message) 将一条消息打印到当前 JDBC 日志流中
3.2.2、Connection 接口
Connection 接口代表与特定的数据库的连接。要对数据表中的数据进行操作,首先要获取数据库连接。
Connection 实例就像在应用程序与数据库之间开通了一条通道,如图 1-3 所示:
图 1-3 Connection 接口连接示意图
通过 DriverManager 类的 getConnection()方法可获取 Connection 对象。Connection 接口的常用方法如表
1-2 所示:
表 1-2 Connection 接口的常用方法
方法 功能描述
createStatement() 创建一个 Statement 对象来将 SQL 语句发送到数据库
prepareStatement(String sql)
创建一个 PreparedStatement 对象来将参数化的 SQL 语句发送到数据
库
prepareCall(String sql) 创建一个 CallableStatement 对象来调用数据库存储过程
getAutoCommit() 获取此 Connection 对象的当前自动提交模式
setAutoCommit(boolean
autoCommit)
将此连接的自动提交模式设置为给定状态
commit()
使所有上一次提交/回滚后进行的更改成为持久更改,并释放此
Connection 对象当前持有的所有数据库锁
rollback()
取消在当前事务中进行的所有更改,并释放此 Connection 对象当前持
有的所有数据库锁
createBlob() 构造实现 Blob 接口的对象
close()
立即释放此 Connection 对象的数据库和 JDBC 资源,而不是等待它们
被自动释放
3.2.3、Statement 接口
应用程序
Connection 实例 数据库
10
Statement 实例用于在已经建立连接的基础上向数据库发送 SQL 语句。该接口用于执行静态的 SQL 语
句。例如:执行 INSERT、UPDATE、DELETE 语句可调用该接口的 executeUpdate()方法,执行 SELECT 语
句可调用该接口的 executeQuery()方法。
Statement 实例可以通过 Connection 实例的 createStatement()方法获取。获取 Statement 实例的代码如下:
Connection conn = DriverManager.getConnection(url, username, password);
Statement statement = conn.createStatement();
Statement 接口的常用方法如表 1-3 所示:
表 1-3 Statement 接口的常用方法
方法 功能描述
execute(String sql) 执行给定的 SQL 语句,该语句可能返回多个结果
executeQuery(String sql) 执行给定的 SQL 语句,该语句返回单个 ResultSet 对象
executeUpdate(String sql)
执行给定 SQL 语句,该语句可能为 INSERT、UPDATE 或 DELETE
语句,或者不返回任何内容的 SQL 语句(如 SQL DDL 语句)
close()
立即释放此 Statement 对象的数据库和 JDBC 资源,而不是等待该对
象自动关闭时发生此操作
3.2.4、PreparedStatement 接口
向数据库发送一个 SQL 命令,数据库中的 SQL 解释器负责把 SQL 语句生成底层的内部命令,然后执
行该命令完成相关的数据操作。如果不断地向数据库提交SQL语句肯定会增加数据库中SQL解释器的负担,
从而影响执行的速度。对于 JDBC,可以通过 Connection 对象的 prepareStatement(String sql)方法对 SQL 语
句进行编译预处理,生成数据库底层的内部命令,并将该命令封装在 PreparedStatement 对象中。通过调用
该对象的相应方法执行底层数据库命令,这样应用程序能针对连接的数据库将 SQL 语句解释为数据库底层
的内部命令,然后让数据库执行这个命令,这样可以减轻数据库的负担,从而提高访问数据库的速度。
PreparedStatement 接口继承 Statement,用于处理已编译的 SQL 语句,预编译语句通常具有一个或多个
IN 参数,每个 IN 参数用一个问号(“?”)来表示。IN 参数的值在该语句被 PreparedStatement 执行之前,
通过适当的 setXXX()方法提供。PreparedStatement 可提供更好地数据操作效率。
PreparedStatement pstmt=con.prepareStatemnet(“UPDATE EMPLOYEES SET SALARY=? WHERE ID=?
“);
......
pstmt.setBigDecimal(1,153833.00) --设置第一个“?”,既 SALARY= 153833.00
pstmt.setInt(2,110592) --设置第二个“?”,既 ID=110592
继承自 Statement 的三种方法 execute,executeQuery 和 executeUpdate 已被更改,不再需要参数。
setXXX()方法用来提供参数值,其中 XXX 应与参数类型一致。例如,如果参数是长整型,则使用 setLong。
setXXX()方法的第一个参数 parameterIndex 为要设置的参数索引,第二个参数是设置给该参数的值,length
为流中字节数。
void setNull(int parameterIndex, int sqlType)
void setBoolean(int parameterIndex,boolean x)
void setByte(int parameterIndex,byte x)
void setShort(int parameterIndex,short x)
void setInt(int parameterIndex,int x)
void setLong(int parameterIndex,int x)
void setFloat(int parameterIndex,fload x)
void setDouble(int parameterIndex,double x)
11
void setBigDecimal(int parameterIndex.BigDecimal x)
void setString(int parameterIndex,String x)
void setBytes(int parameterIndex,byte x[])
void setDate(int parameterIndex,java.sql.Date x)
void setTime(int parameterIndex,javat.sql.Time x)
void setTimestamp(int parameterIndex,java.sql.Timestamp x)
void setAseiiStram(int parameterIndex, java.io.InputStream x,int length)t
void setUnicodeStream(int parameterIndex,java.io.InputStream x,int length)
void setBinaryStream(int parameterIndex, java.io.InputStream x,int length)
void setRef(int parameterIndex, Ref x)
void setBlob(int parameterIndex,Blob x)
void setcolb(int parameterIndex,Clob x)
void setArray(int parameterIndex, Array x)
3.2.5、Callablestatement 接口
CallableStatement 接口继承自 PreparedStatement,用于调用存储子程序。
CallableStatement cs1 = con.prepareCall("{CALL Abc()}");// 调用无参存储过程
CallableStatement cs2 = con.prepareCall("{CALL Abc(?, ?, ...,?)}"); // 调用有参存储过程
//调用存储函数
CallableStatement cs4 = con.prepareCall("{? = CALL Abc(?,?,...,?)}");
3.2.6、ResultSet 接口
ResultSet 接口类似于一张数据表,用来暂时存放数据库查询操作所获得的结果集。ResultSet 实例具有
指向当前数据库行的指针,指针开始的位置在查询结果集第一条记录的前面。在获取查询结果集时,可通
过 next()方法将指针向下移。如果存在下一行该方法会返回 true,否则返回 false。
ResultSet 接口提供了从当前行检索不同类型列值的 getXXX()方法,通过该方法的不同重载形式,可实
现分别通过列的索引编号和列的名称检索列值。
3.3、数据库连接
如果要访问数据库,首先要加载数据库驱动,数据库驱动只需在第一次访问数据库时加载一次,然后
在每次访问数据库时创建一个 Connection 实例,获取数据库连接,这样就可以执行操作数据库的 SQL 语句。
最后在完成数据库操作时,释放与数据库的连接。
3.3.1、加载数据库驱动
Sun 公司提供了 JDBC 技术用于与数据库建立联系,但需要由数据库提供商实现这些接口,即提供数据
库驱动。
由于不同的数据库厂商实现的 JDBC 接口不同,因此就产生了不同的数据库驱动包。数据库驱动包里
包含了一些类,其负责与数据库建立连接,把一些 SQL 语句传到数据库里边去。例如 Java 程序实现与 SQL
Server2000 数据库建立连接,需要在程序中加载驱动包 msbase.jar、mssqlserver.jar、msutil.jar,与 MySQL
数据库建立连接需要在程序加载驱动包 mysql-connection-java.jar,与 Oracle 数据库建立连接需要在程序加载
驱动包 ojdbc6.jar 等(本课程采用 Oracle 数据库,因此后续实验内容以 Oracle 数据库为主)。
将下载的数据库驱动文件添加到项目中后,首先需要加载数据库驱动程序,才能进行数据库操作。加
载数据库驱动程序的常用方式有以下两种:
12
方式一:调用 Class 类的静态方法 forName()。
Class.forName(String driverManager)
forName()方法参数用于指定要加载的数据库驱动,加载成功,会将加载的驱动类注册给 DriverManager。
如果加载失败,则会抛出 ClassNotFoundException 异常。
加载 Oracle 数据库驱动的代码如下:
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
Connection conn = DriverManager.getConnection(url, user, password);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
方式二:采用 Oracle 数据源的方式
Oracle数据库厂商提供了专门的数据库驱动包ojdbc6.jar,在oracle.jdbc.pool包下有一个专门的ORACLE
数据源类 OracleDataSource,用来创建数据库连接。
try {
OracleDataSource ds = new OracleDataSource();
ds.setURL(jdbcUrl);
conn = ds.getConnection(userid, password);
} catch (SQLException ex) {
ex.printStackTrace();
}
3.3.2、创建数据库连接
在进行数据库操作时,只需要在第一次访问数据库时加载数据库驱动。以后每次访问数据时,创建一
个 Connection 对象后即可执行操作数据库的 SQL 语句。通过 DriverManager 类的 getConnection()方法可创
建 Connection 实例。
conn = DriverManager.getConnection(url, user, password);
url:指定连接数据库的 url
user:指定连接数据库的用户
password:指定连接数据库的密码
例 1:创建类 GetConn,该类实现了与 Oracle 数据库的连接。
import java.sql.*;
public class GetConn {
Connection conn;
public Connection getConnection() {
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
System.out.println("数据库驱动加载成功");
// 定义连接数据库的 URL
String url = "jdbc:oracle:thin:@localhost:1521:xe";
String user = "hr";
13
String password = "oracle";
conn = DriverManager.getConnection(url, user, password);
if(conn != null) {
System.out.println("数据库连接成功");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (SQLException e) {
e.printStackTrace();
}
return conn;
}
public static void main(String[] args) {
GetConn getConn = new GetConn();
getConn.getConnection();
}
}
3.3.3、向数据库发送 SQL 语句
建立数据库连接的目的是与数据库进行通信,实现方式为执行 SQL 语句,但 Connection 实例并不能执
行 SQL 语句。此时需要通过 Connection 接口的 createStatement()方法获取 Statement 对象。
try {
Statement state = conn.createStatement();
} catch (SQLException e) {
e.printStackTrace();
}
3.3.4、获取查询结果集
Statement 接口的 executeUpdate()方法或 executeQuery()方法可以执行 SQL 语句。executeUpdate()方法用
于执行数据的插入、修改或删除操作,返回影响数据库记录的条数;executeUpdate()方法用于执行 SELECT
查询语句,将返回一个 ResultSet 类型的结果集。通过遍历查询结果集的内容,可以获取语句执行的查询结
果。
获取查询结果集的代码如下:
ResultSet rs = state.executeQuery("select * from student") ;
ResultSet 对象具有指向当前数据行的光标。最初,光标被置于第一行之前,可以通过该对象的 next()
方法将光标移动到下一行;如果 ResultSet 对象没有下一行时,则 next()方法返回 false。所以,可以在 while
循环中使用 next()方法迭代结果集。
循环遍历查询结果集的代码如下:
while(rs.next()) {
String sno = rs.getString("sno") ;
String sname = rs.getString("sname") ;
14
int sage = rs.getInt("sage") ;
}
3.3.5、关闭连接
在进行数据库访问时,Connection、Statement、ResultSet 实例都会占用一定的系统资源,因此在每次访
问数据库后,及时地释放这些对象占用的资源是一个很好的编程习惯。Connection、Statement、ResultSet
实例都提供了 close()方法用于释放对象占用的数据库和 JDBC 资源。
关闭 Connection、Statement、ResultSet 对象的实例如下:
resultSet.close() ;
statement.close() ;
connection.close() ;
如果通过 DriverManager 类的 getConnection()方法获取 Connection 实例,那么通过关闭 Connection 实例,
就可以同时关闭 Statement 实例与 ResultSet 实例。当采用数据库连接池时,close()方法并没有释放 Connection
实例,而是将其放入连接池中,又可被其他连接调用。此时,如果没有调用 Statement 与 ResultSet 实例的
close()方法。它们在 Connection 中会越来越多,虽然 JVM(Java 虚拟机)会定时清理缓存,但当数据库连
接达到一定数量时,清理不够及时,就会严重影响数据库和计算机的运行速度。
3.4、数据库操作
建立数据库连接并不是操作数据库的真正目的,真正的目的是对数据表中的数据进行添加、修改、删
除和查找操作。本节将以操作 Oracle 数据库为例,介绍几种常见的数据操作。
在编写 JDBC 应用程序时,经常要书写建立数据库连接和关闭连接的操作代码,如果每次在进行数据
库添加、修改、删除和查找操作时都书写一遍代码,将会造成代码冗余,不利于代码的维护。因此,我们
可以编写单独的类来封装相关的数据库连接和关闭操作,示例代码如下:
import java.sql.*;
import oracle.jdbc.pool.OracleDataSource;
public class DatabaseBean {
public static Connection getConnection() throws SQLException {
String jdbcUrl = "jdbc:oracle:thin:@localhost:1521/xe";
String userid = "hr";
String password = "oracle";
// Oracle 数据库厂商提供了专门的数据库驱动包 ojdbc6.jar
// 这里使用 ORACLE 的数据源 OracleDataSource 来创建数据库连接
// 在使用时需要引入相应的包
OracleDataSource ds = new OracleDataSource();
ds.setURL(jdbcUrl);
return ds.getConnection(userid, password);
}
public static void close(ResultSet rs, Statement st, Connection conn) {
try {
if (rs != null) {
15
rs.close();
}
if (st != null) {
st.close();
}
if (conn != null) {
conn.close();
}
} catch (SQLException ex) {
ex.printStackTrace();
}
}
}
3.4.1、添加数据
执行静态的 SQL 语句需要使用 Statement 接口。该接口的 execute()方法与 executeUpdate()方法都可以执
行 INSERT 语句,实现添加数据操作。
1、通过 execute()方法实现数据添加
execute()方法执行 INSERT 语句,结果返回 false。如果想获取影响数据表中数据的行数,需要调用
Statement 接口的 getUpdateCount()方法。
boolean execute(String sql) throws SQLException
sql:任意的 SQL 语句
例 1:应用 execute()方法实现向 student 表中添加数据。
import java.sql.*;
public class Insert {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
conn = DatabaseBean.getConnection();
stmt = conn.createStatement();
stmt.execute("insert into student(sno,sname,ssex,sage,sdept) values('9531103','张
三','男',20,'数学系')");
// 将操作数据影响的行数输出
System.out.println("添加数据的行数为:" + stmt.getUpdateCount());
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
DatabaseBean.close(null, stmt, conn);
}
}
}
16
2、通过 executeUpdate()方法实现数据添加
Statement 接口的 executeUpdate()方法可执行 INSERT 语句,并返回受影响数据的行数。该方法也可执
行 CREATE TABLE、ALTER TABLE 等 DDL 语句,此时返回值为 0。
int executeUpdate(String sql) throws SQLException
sql:任意 INSERT、UPDATE、DELETE 或不返回任何结果的 DDL 语句
例 2:应用 executeUpdate()方法实现向 student 表中添加数据。
import java.sql.*;
public class Insert {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
conn = DatabaseBean.getConnection();
stmt = conn.createStatement();
int count = stmt.executeUpdate("insert into student(sno,sname,ssex,sage,sdept)
values('9531104','张三','男',20,'数学系')");
// 将操作数据影响的行数输出
System.out.println("添加数据的行数为:" + count);
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
DatabaseBean.close(null, stmt, conn);
}
}
}
3.4.2、删除数据
在程序中删除数据库表中的数据,也可通过 Statement 接口的 execute()方法与 executeUpdate()方法实现。
在删除数据操作时,如果 DELETE 语法没有 WHERE 子句指定删除数据的条件,会将数据表中的全部数据
删除。
例 3:将学号为 9531104 的学生选课记录内容删除。
import java.sql.*;
public class Delete {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
conn = DatabaseBean.getConnection();
stmt = conn.createStatement();
int count = stmt.executeUpdate("delete from sc where sno='9531104'");
// 将操作数据影响的行数输出
System.out.println("删除数据的行数为:" + count);
17
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
DatabaseBean.close(null, stmt, conn);
}
}
}
3.4.3、修改数据
在程序中修改数据库表中的数据,也可通过 Statement 接口的 execute()方法与 executeUpdate()方法实现。
例 3:将学号为 9531103 的学生的年龄由 20 岁改为 19 岁。
import java.sql.*;
public class Update {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
try {
conn = DatabaseBean.getConnection();
stmt = conn.createStatement();
int count = stmt.executeUpdate("update student set sage=19 where sno='9531103'");
// 将操作数据影响的行数输出
System.out.println("修改数据的行数为:" + count);
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
DatabaseBean.close(null, stmt, conn);
}
}
}
3.4.4、查询数据
执行查询数据表中数据的操作,比执行添加、删除、修改操作要麻烦一些。要获取 SELECT 语句返回
的查询结果,必须通过遍历查询结果集(ResultSet 对象)。
例 4:查询性别为男的学生记录,将查询结果在控制台上输出。
import java.sql.*;
public class Query {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = DatabaseBean.getConnection();
18
stmt = conn.createStatement();
rs = stmt.executeQuery("select * from student where ssex='男'");
while(rs.next()) {
String sno = rs.getString("sno");
String sname = rs.getString("sname");
String ssex = rs.getString("ssex");
int sage = rs.getInt("sage");
String sdept = rs.getString("sdept");
System.out.println("学号:" + sno + " 姓名:" + sname + " 性别:" + ssex + " 年龄:
" + sage + " 系部:" + sdept);
}
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
DatabaseBean.close(rs, stmt, conn);
}
}
}
3.4.5、模糊查询
模糊查询是比较常见的一种查询方式。例如,要查询“9512XXX”类似学号的学生信息,最佳方式就
是使用模糊查询。进行模糊查询需要使用关键字“LIKE”。在使用“LIKE”关键字进行模糊查询时,可以
使用通配符“%”来代替 0 个或多个字符,可使用下划线“_”来代替一个字符。
例 5:查询“9512XXX”类似学号的学生信息,并在控制台输出显示。
import java.sql.*;
public class Query {
public static void main(String[] args) {
Connection conn = null;
Statement stmt = null;
ResultSet rs = null;
try {
conn = DatabaseBean.getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery("select * from student where sno like '9512%'");
while(rs.next()) {
String sno = rs.getString("sno");
String sname = rs.getString("sname");
String ssex = rs.getString("ssex");
int sage = rs.getInt("sage");
String sdept = rs.getString("sdept");
System.out.println("学号:" + sno + " 姓名:" + sname + " 性别:" + ssex + " 年龄:
" + sage + " 系部:" + sdept);
}
19
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
DatabaseBean.close(rs, stmt, conn);
}
}
}
3.4.6、JDBC 事务处理与设置隔离级别
通过 JDBC 的 Connection 对象,可设置数据库连接的事务提交模式和事务隔离级别。
JDBC 提供四种隔离级别,分别用常数 TRANSACTION_READ_UNCOMMITTED、
TRANSACTION_READ_COMMITTED、TRANSACTION_REPEATABLE_READ、
TRANSACTION_SERIALIZABLE 表示。设置语法为:
Connection.setTransactionLevel(XXX) ;
一个 Connection 可以执行多个数据库操作。缺省情况下,JDBC 立即向数据库提交操作的结果。通过
Connection.setAutoCommit(false)可把多个操作组成一个事务单元,整体提交和回滚,以在 Java 代码端实现
事务处理。
建议把这些操作封装到一个存储过程,在存储过程中实现事务处理。
JDBC 处理事务的流程如图 1-4 所示。
图 1-4 JDBC 事务处理流程
3.4.7、JDBC 关于数据库和结果集的元数据
Connection 对象的 getMetaData()方法返回数据库元数据对象 DatabaseMetaData,通过该对象可返回数据
源的一般信息、数据源支持的特性、数据源的限制、数据库对象信息、事务支持特性等。
ResultSet 对象的 getMetaData()方法返回结果集元数据对象 ResultsetMetaData,通过该对象可以获取结
果集的相关信息:列数目(getColumnCount())、列名称(getColumnName(i))、列数据类型名
(getColumnTypeName(i))等。
20
4、实验内容
利用 JDBC 技术完成学生信息管理系统中学生表 student 数据的添加、删除、修改和查询。为了方便对
学生管理系统进行管理和维护,在编写项目代码之前,需要定制好项目的系统文件夹组织结构。不同的 Java
包存放不同的窗体、公共类、工具类或者图片资源等,这样可以规范系统的整体架构。学生信息管理系统
的文件夹组织结构如图 1-5 所示:
图 1-5 项目文件结构
实验步骤:
1、按照上图所示文件结构,新建工程项目 JDBCTest,在项目中导入 JDBC 驱动包 ojdbc.jar。
2、在 com.stu.model 包中新建 Student.java 类,用于构建学生表模型。
public class Student {
private String sno;
private String sname;
private String ssex;
private int sage;
private String sdept;
public Student() {
}
public Student(String sno, String sname, String ssex, int sage, String sdept) {
this.sno = sno;
this.sname = sname;
this.ssex = ssex;
this.sage = sage;
this.sdept = sdept;
}
//用 get 和 set 方法封装数据成员
21
【补充代码…】
}
3、在 com.stu.util 包中新建 DatabaseBean.java 类,用于封装相关的数据库连接和关闭操作,代码编写请
参看 3.4 节数据库操作内容。
4、在 com.stu.dao 包中新建 IStudentDao.java 数据访问接口,用于封装相关的数据库操作方法。
public interface IStudentDao {
public List<Student> getAllStudent();
public Student getStudent(String sno);
public boolean findStudent(String sno);
public boolean insertStudent(Student stu);
public boolean updateStudent(Student stu);
public boolean deleteStudent(String sno);
}
5、在 com.stu.dao.impl 包中新建 IStudentDao.java 接口的数据访问实现类 StudentDaoImpl.java,用于对
IstudentDao 接口中相关数据库操作方法的具体实现。
public class StudentDaoImpl implements IStudentDao {
Connection conn = null;
Statement stmt = null;
PreparedStatement psmt = null;
ResultSet rs = null;
@Override
public List<Student> getAllStudent() {
List<Student> students = new ArrayList<Student>();
try {
conn = DatabaseBean.getConnection();
stmt = conn.createStatement();
rs = stmt.executeQuery("select sno,sname,ssex,sage,sdept from student");
while (rs.next()) {
Student stu = new Student();
stu.setSno(rs.getString("sno"));
stu.setSname(rs.getString("sname"));
stu.setSsex(rs.getString("ssex"));
stu.setSage(rs.getInt("sage"));
stu.setSdept(rs.getString("sdept"));
students.add(stu);
}
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
22
DatabaseBean.close(rs, stmt, conn);
}
return students;
}
@Override
public Student getStudent(String sno) {
Student stu = new Student();
try {
conn = DatabaseBean.getConnection();
psmt = conn.prepareStatement("select sno,sname,ssex,sage,sdept from student where
sno=?");
psmt.setString(1, sno);
rs = psmt.executeQuery();
while (rs.next()) {
stu.setSno(rs.getString("sno"));
stu.setSname(rs.getString("sname"));
stu.setSsex(rs.getString("ssex"));
stu.setSage(rs.getInt("sage"));
stu.setSdept(rs.getString("sdept"));
}
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
DatabaseBean.close(rs, psmt, conn);
}
return stu;
}
@Override
public boolean findStudent(String sno) {
【补充代码…】
}
@Override
public boolean insertStudent(Student stu) {
try {
String sql = "insert into student(sno,sname,ssex,sage,sdept) values(?,?,?,?,?)";
conn = DatabaseBean.getConnection();
psmt = conn.prepareStatement(sql);
psmt.setString(1, stu.getSno());
psmt.setString(2, stu.getSname());
psmt.setString(3, stu.getSsex());
psmt.setInt(4, stu.getSage());
23
psmt.setString(5, stu.getSdept());
psmt.executeUpdate();
return true;
} catch (SQLException ex) {
ex.printStackTrace();
} finally {
DatabaseBean.close(rs, stmt, conn);
}
return false;
}
@Override
public boolean updateStudent(Student stu) {
【补充代码…】
}
@Override
public boolean deleteStudent(String sno) {
【补充代码…】
}
}
6、在 com.stu.util 包中新建 DaoFactory.java 工厂类(这里用到了设计模式中的工厂模式),用于生成数
据操作访问对象。
public class DaoFactory {
public static IStudentDao getStudentDao() {
return new StudentDaoImpl();
}
}
7、在 com.stu.test 包中新建 Main.java 测试类,用来测试数据访问接口中定义的相关方法的正确性。
public class Main {
public static void main(String[] args) {
// 显示结果
displayStudents("原始");
Student stu = new Student("9512104", "张三", "男", 20, "计算机系");
// 插入数据之前需要先判断学号是否重复
if(DaoFactory.getStudentDao().findStudent(stu.getSno())) {
System.out.println("系统信息:学号为 " + stu.getSno() + " 的学生信息已经存在!");
return;
}
24
// 插入学生数据
if(DaoFactory.getStudentDao().insertStudent(stu))
System.out.println("系统信息:数据插入成功!");
// 显示结果
displayStudents("插入");
// 修改学生数据
stu.setSage(18);
if(DaoFactory.getStudentDao().updateStudent(stu))
System.out.println("系统信息:数据修改成功!");
// 显示结果
displayStudents("修改");
// 删除学生数据
if(DaoFactory.getStudentDao().deleteStudent("9512104"))
System.out.println("系统信息:数据删除成功!");
// 显示结果
displayStudents("删除");
}
private static void displayStudents(String state) {
List<Student> students = DaoFactory.getStudentDao().getAllStudent();
System.out.println("**************************************");
System.out.println(state + "学生记录后结果为:");
for (Student s : students) {
System.out.print("学号:" + s.getSno());
System.out.print(" 姓名:" + s.getSname());
System.out.print(" 性别: " + s.getSsex());
System.out.print(" 年龄:" + s.getSage());
System.out.println(" 系部:" + s.getSdept());
}
}
}
5、实验总结
请书写你对本次实验有哪些实质性的收获和体会,以及对本次实验有何良好的建议?
25
实验二、GUI 图形用户界面设计(2 学时)
1、实验目的
(1)掌握基于 Swing 的图形用户界面设计方法。
(2)掌握 Jtable 组件和表格模型的使用方法。
(3)掌握利用可视化开发工具设计图形用户界面的方法。
2、实验性质
验证性实验
3、实验导读
3.1、Swing 的基本概念
在 Swing 出现前,Java 开发人员使用 AWT(Abstract Window Toolkit,抽象窗口工具包)来实现基本的
界面。AWT 库的本质是将界面组件(例如菜单、按钮等)直接映射到本机操作系统上所对应的组件。这样
做虽然能够得到所谓的“本地外观”,但最大的缺点就是提供的组件非常有限,且灵活性差、扩展性差、容
易出错。
为了解决这些问题,Sun 在 1996 年与 Netscape 通力合作,推出了 Swing 这一“100% 纯 Java”的界面
工具包。与 AWT 不同,Swing 完全抛弃了本机系统的绘图接口,而是用 Java 2D 在窗口上画出组件。虽然
以这种“模拟”方式进行的绘图可能造成 Swing 界面比本机界面响应要慢(不过随着硬件的进步和 Sun 的
努力,这种速度差距已在缩小),但却有着 AWT 无可比拟的优势:
Swing 提供了大量丰富易用的组件,并具有极高的可扩展性。
Swing 极少依赖于底层系统,因此能在不同平台上表现一致。
提供了多个外观,可*切换,还包括一个可定制的外观框架。
不过 Swing 的到来并没有带来 AWT 的消亡,Swing 仍然要依靠 AWT 来获取系统事件,例如敲键盘、
点鼠标等等。
3.1.1、Swing 组件继承关系
26
图 2-1 Swing 组件的继承关系图
图中矩形表示 AWT 组件,圆角矩形的是 Swing 组件。可以看出,Swing 是从 AWT 扩展而来(但也有
一些不兼容的地方)。绝大多数 Swing 组件都是 JComponent 的子类,而窗口类型的 Swing 组件则继承自
Window。
Swing 组件名称都以字母“J”开头,这既是 Swing 的标识,也是为了和 AWT 中的同类组件相区别。
继承自 JComponent 的组件都是纯 Java 实现的,所以被称为轻量级组件,而其它组件则是重量级的。
3.1.2、Swing 组件一览
下面的几个表是对 Swing 各组件和容器的总结,你可在后面练习的时候查阅该表以供参考:
表 2-1 Swing 控件
名称 类 说明
标签 JLabel 显示文本,支持 HTML。
按钮 JButton 普通按钮。
切换按钮 JToggleButton 可在“压下”和“弹起”两种状态间切换。
复选框 JCheckBox 一种特殊的切换按钮,用于多项选择。
单选按钮 JRadioButton 另一种特殊的切换按钮,用于单项选择。
组合框 JComboBox 下拉菜单的实现。
翻选框 JSpinner 可在一系列连续数值中上下翻动选择。
列表 JList 呈现一系列选项。
菜单 JMenu 用于容纳多个菜单项的弹出窗口。
菜单项 JMenuItem 普通菜单项,是一种特殊的按钮。
复选框菜单项 JCheckboxMenuItem 功能和复选框相同的菜单项。
单选按钮菜单项 JRadioButtonMenuItem 功能和单选按钮相同的菜单项。
菜单栏 JMenuBar 通常位于窗口顶部,菜单的集合。
分隔符 JSeparator 分隔组件的线条。
工具栏 JToolBar 通常位于菜单栏下方,提供快捷按钮等。
文本字段 JTextField 单行纯文本编辑器。
口令字段 JPasswordField 用于输入口令,不显示实际的输入。
格式化文本字段 JFormattedTextField 能控制输入内容的模式。
文本区 JTextArea 多行纯文本编辑器。
编辑器窗格 JEditorPane 多行文本编辑器,支持 HTML 和富文本。
文本窗格 JTextPane 除了 HTML 和富文本,还能插入 Swing 组件。
进度栏 JProgressBar 显示进度。
滑块 JSlider 以滑动的方式选择值。
表 2-2 Swing 窗口
名称 类 说明
窗体 JFrame Swing 程序的主窗口。
对话框 JDialog 窗体的子窗口。
表 2-3 Swing 容器
27
名称 类 说明
面板 JPanel 最基本的容器。
滚动窗格 JScrollPane 给其它组件添加滚动功能。
选项卡窗格 JTabbedPane 用选项卡来切换显示的内容。
分隔窗格 JSplitPane 将两个组件水平或垂直分隔。
分层窗格 JLayeredPane 其中的组件具有“深度”、可重叠。
桌面窗格 JDesktopPane 多文档窗口。
内部窗体 JInternalFrame 添加到桌面窗格中的轻量级窗体。
3.2、Swing 和 MVC 设计模式
设计模式是前人在开发过程中总结出来的程序结构设计经验,能帮助我们编写可扩展且易维护的软件。
Swing 采用了经典的“模型-视图-控制器”(Model-View-Controller,简称 MVC)设计模式。MVC 设计模式
其实是一种由组合模式、装饰者模式、策略模式、观察者模式等多个“*”设计模式组成的复合设计
模式。有兴趣的读者可以参考这方面的书籍。
在真实的程序中,组件往往负责显示数据,这些数据可能来自文件、数据库、互联网等等。我们可能
需要针对特定的数据显示多个视图,例如报表中的柱状图、饼图等;用户操作可能更新界面中的数据,后
台数据需要和它们保持同步;后台数据可能变换,从而界面需要即时更新。要协调这些交互,解决方案就
是从组件中分离出三个不同的方面:模型、视图和控制器:
“模型”存储数据;
“视图”将“模型”中的数据显示出来;
“控制器”处理用户操作,并更新“模型”和“视图”。
Swing 的 MVC 可用下图来表示:
模型
视图 控制器
更新视图
触发事件
图 2-2 模型、视图和控制器之间的关系
下面根据上图,来详细分析一下这三个方面是如何相互作用的。
首先应用程序启动,从模型读取数据,显示在视图上。用户在视图上的操作触发 Swing 事件,由控制
器进行处理:如果该事件更新了后台数据,则由控制器修改模型,并给视图发出通知,视图重新从模型读
取数据并更新自己;否则控制器将处理结果直接返回给视图,并做必要的更新。
如果拿刚才说的报表作为例子,报表程序从数据库读取数据并生成统计信息图。用户从界面修改数据,
这一事件被控制器捕获,于是模型被更新了,模型随后更新数据库。接着,控制器通知视图模型更改这一
事件,视图便通过模型读取新数据,显示新的统计图。也可能用户只是更改了一些显示设置,不需要和后
28
台数据打交道,这时控制器直接更新视图就可以了。
多数时候,操作 Swing 组件不会直接去控制模型和视图,因为每个 Swing 组件类都提供了一些方法帮
助我们间接访问它们。例如 JTextArea(文本区域)的模型是一个 Document 对象,里面保存了 JTextArea 中
的文本信息,但我们通常不通过直接修改 Document 来修改文本,而是调用 setText 和 append 等方法,由它
们帮我们操作 Document 对象;对于视图也是一样,我们用键盘或鼠标移动光标位置的时候,编辑文本的时
候,甚至都不用程序员调用任何方法,视图就自动更新了。
3.3、Swing 的单线程模型
这一节讲解 Swing 和线程的关系。现实中的程序可能非常复杂,所以必须处理好 Swing 在多线程环境
下的问题,才能开发出高响应度的程序。
在开发 Swing 程序的时候,有三类线程需要处理:
1. 初始线程(Initial Thread)。该线程将执行程序的初始化代码。
2. 事件指派线程(Event Dispatch Thread,简称 EDT,也翻译成“事件调度线程”)。所有的侦听器事
件处理代码和绝大部分调用 Swing 方法的代码都在这个线程上执行。
3. 工人线程(Worker Thread)。也就是“后台线程”,用来在后台执行比较耗时的任务。
这三类线程都不需要你去显示地创建——它们已经由运行时和 Swing 框架提供了,这对开发多线程环
境下的 Swing 程序提供了极大的帮助。下面逐个分析这些线程。
3.3.1、初始线程
对 Swing 来说,初始线程是 SwingUtilities.invokeLater 的 Runnable 参数的任务。
SwingUtilities.invokeLater 的作用是导致其内部的代码在“事件指派线程”上执行。接下来就讨论“事
件指派线程”。
3.3.2、事件指派线程
在 Swing 程序里,执行 main 方法的主线程一般生命周期很短。它只是将界面的初始化安排到事件指派
线程上,然后就退出了。之后的界面响应工作就由事件指派线程来完成。
大多数 Swing 方法都不是“线程安全”的,从多个线程调用它们很容易造成线程或内存冲突。只能在
事件指派线程上调用它们。所有的事件在事件指派线程上形成一个事件队列,然后被逐个执行:
事件队列
事
件
指
派
线
程
Swing 操作请求
Swing 操作请求
Swing 操作请求
以并发方式发送请求 以串行方式处理请求
图 2-3 事件队列的处理
该过程可以被看作一个“生产者-消费者”模式,产生 Swing 操作请求的线程都是“生产者”,它们把请
求放进事件队列,然后由事件指派线程逐个消费。
29
同时,这也暗示如果处理某个事件需要的时间太长,那么排在它后面的事件将无法得到及时处理,从
而造成界面失去响应:
图 2-4 在事件指派线程上执行耗时任务
下一个小节会介绍如何使用 SwingWorker 来处理耗时任务。另外,也有小部分 Swing 方法是“线程安
全”的,这些方法都在 API 文档里有特别说明。
为什么 Swing 会被设计成这样的“单线程模型”?因为从经验来看,任何对设计线程安全的界面库的
尝试都面临了一些基础结构上的棘手问题。所以单线程模型几乎成了界面库的“规范”:不光是 Swing,其
它流行的界面库(例如 GTK+、Qt、MFC 等等)也是如此。单线程模型的好处就是程序结构会比较简单,
避免了复杂的线程同步处理。坏处就是程序员一不注意就把耗时任务直接放到事件指派线程上执行。
3.3.3、工人线程
前面提到,耗时任务不能在事件指派线程上执行,它们应当作为后台任务放到工人线程中执行。
一个典型的工人线程对界面有下列影响:
在任务进行时间或更新界面以显示任务进度。
任务完成后给出提示,或对界面做最终更改。
从 Java SE 6 开始,工人线程可以通过扩展 javax.swing.Swingworker<T, V> 来方便地创建。SwingWorker
是一个抽象范型类,类型参数 T 表示任务运行结果的类型,V 表示中间结果的类型。SwingWorker 的子类必
须实现 doInBackground 方法,该方法执行后台计算。调用 execute 方法将使后台计算开始执行,该方法会立
即返回。此外还有很多有用的方法,请参考 API 文档。
下面是 SwingWorker 的示意图:
A
B
30
图 2-5 SwingWorker 的运行方式
可以很明显地看出,工人线程承担了绝大部分繁重任务,事件指派线程只需不时处理一些低耗时的中
间结果以及最终结果,所以不会出现阻塞现象。
关于使用 SwingWorker 还有一点必须记住,那就是 SwingWorker 被设计为只执行一次,也就是说
SwingWorker 对象不可重复使用,每次都要重新构造一个。
最后引用 API 文档里的两个关于用 Swing 开发多线程程序事的约束条件,希望你能时刻记住:
不应该在事件指派线程上运行耗时任务。否则应用程序将无响应。
只能在事件指派线程*问 Swing 组件。
3.4、Swing 程序设计练习
图形用户界面设计可以采用任何一款文本编辑器来进行编写设计,但一款好的可视化开发工具,可以
大大提高程序员开发程序的效率,同时加快应用程序设计的进度。这里我们推荐使用 Netbeans 7.4 这款可视
化开发工具来进行程序设计。
下面我们用多个练习项目,并结合 NetBeans GUI 生成器来学习 Swing 编程基础和组件。请读者在开始
学习每个小节的时候,在 NetBeans 里打开对应的练习项目,根据讲解,尝试操作一番,然后根据讲解和代
码进行学习。以下内容所用到的示例代码如下:
code.rar
3.4.1、第一个 Swing 程序
本节中,我们逐步创建一个简单的 Swing 程序,然后对其进行分析(源代码在“MyFirstSwing”项目中)。
请启动你的 NetBeans,然后进行以下步骤:
1. 从菜单栏上选择“文件”“新建项目”按钮,将出现“新建项目”对话框。
2. 在“类别”中选择“Java”,“项目”中选择“Java 应用程序”,单击“下一步”。
3. 在“项目名称”中输入“MyFirstSwing”,选择好“项目位置”,再在“创建主类”中输入
“org.myorg.MyFirst”,并选中“设置为主项目”。
4. 在 main 方法中输入下面的代码:
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFrame frame = new JFrame("你好,Swing!");
frame.setLayout(new FlowLayout());
frame.add(new JButton("这是我的第一个 Swing 程序!"));
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
5. 在代码编辑器中单击鼠标右键,选择“修复导入”。
6. 单击工具栏上的“运行主项目”按钮,就能看到下面的运行结果:
31
图 2-6 第一个 Swing 程序
在这个程序里,我们创建了一个窗体(JFrame,也叫“框架”),并添加了一个按钮(JButton)。因为没
有添加事件处理代码,所以单击按钮不会产生任何反应。在 Swing 里,窗体是应用程序的顶层窗口,一个
Swing 程序至少要有一个窗体。一般来说,关闭窗体就意味着程序的结束。
下面开始逐行分析这段代码。
SwingUtilities.invokeLater(new Runnable() {
public void run() {
首先,调用 SwingUtilities 里的一个静态方法 invokeLater 来对界面进行初始化。如果查看源代码,就会
发现它实际上调用了 EventQueue.invokeLater。生成界面的操作都放在其 Runnable 参数的 run 方法里。
为什么不把 Swing 组件的初始化代码直接放到主线程里呢?
有经验的读者也许已经发现,有些网上或教材里的 Swing 程序把这些代码直接丢在 main 方法里,而且
运行起来似乎也没什么异常。实际上这样做存在隐患。如果把 Swing 的初始化代码放到主线程里,就可能
导致一些操作 Swing 的代码直接在主线程里被执行,而另一些被发送到事件指派线程,两者有可能产生冲
突,造成一些很奇怪且难以调试的错误。SwingUtilities.invokeLater 保证初始化代码在事件指派线程上执行,
从而避免了这种冲突。
JFrame frame = new JFrame("你好,Swing!");
初始化一个标题为“你好,Swing!”的窗体。
frame.setLayout(new FlowLayout());
将窗体的布局方式设为 FlowLayout(流布局),添加到窗体中的组件将沿水平方向居中放置,如果再也
放不下更多的组件就换行。不设置布局也可以,但那样的话按钮将撑满整个窗口,就不好看了。
在以前,Swing 的布局本来也属于重点内容,但现在有了 NetBeans GUI 生成器的帮助,就不再需要花
费过多的时间在布局上。因此,本章不会介绍 Swing 布局,如果你确实想了解,请参阅 API 文档以及 The Java
Tutorials(Sun 公司的官方 Java SE 教程,可免费下载)中的相关内容。
frame.add(new JButton("这是我的第一个 Swing 程序!"));
向窗体添加一个按钮。
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
这一行是说,如果窗体被关闭,程序就退出。
默认情况下,关闭窗体实际上只是将窗体隐藏,而程序仍在后台运行。为了证明这一点,来做一个小
实验。将这行代码注释掉,重新编译并运行程序,关闭窗口。我们会看见,NetBeans 右下角的任务进度栏
仍然在运行。这时只能单击进度栏右边的“×”按钮强制结束程序。
frame.pack();
调整窗体的大小以便适应其内部的组件和布局。如果想自定义窗体大小,则可以调用 setSize(int width,
int height) 方法。
frame.setLocationRelativeTo(null);
将窗体的位置放在屏幕正中。setLocationRelativeTo(Component c) 方法用来将一个窗口的位置相对于另
外某个组件居中,如果传入 null,则相对整个屏幕居中。
frame.setVisible(true);
显示窗体。
好,到现在,程序已经成功了!但是,是否觉得按钮和 Windows 的风格不一致?所以,接下来将要讨
32
论的内容就是给界面更换皮肤。
3.4.2、外观感觉
Swing 从体系结构上就被设计成能切换界面的外观感觉(Look And Feel,简称 L&F),也就是平常所说
的“换肤”。“外观”表示组件的静态样式(例如按钮的形状、颜色等),“感觉”表示组件的动态样式(例
如单击按钮时按钮的变化)。
针对不同的平台,Swing 提供了不同的外观感觉,可以用下面的代码来测试你安装的 JRE 支持哪些外
观感觉(源代码在“LafInfos”项目中)。
LookAndFeelInfo[] infos = UIManager.getInstalledLookAndFeels();
for (LookAndFeelInfo info : infos) {
System.out.println(info.getName()+ " - " + info.getClassName());
}
在 Windows XP 或更高版本上将得到下面的输出:
Metal - javax.swing.plaf.metal.MetalLookAndFeel
CDE/Motif - com.sun.java.swing.plaf.motif.MotifLookAndFeel
Windows - com.sun.java.swing.plaf.windows.WindowsLookAndFeel
Windows Classic - com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel
默认情况下,Swing 将使用 Metal 外观感觉(也叫 Java 外观感觉)。有些外观感觉是跨平台的(如 Metal),
有些则只对应特定平台(如 Windows)。
有两种方式来更改应用程序的外观感觉。
一种是在 JRE 安装目录下的 lib 文件夹里新建一个 swing.properties 文件,并添加属性 swing.defaultlaf。
这个属性的值就是上面输出的那些类名之一,它将成为该 JRE 中运行的 Swing 程序的默认外观感觉。例如:
swing.defaultlaf=com.sun.java.swing.plaf.windows.WindowsLookAndFeel
这种方法的最大缺点就是其作用范围仅限于当前 JRE,如果换台电脑运行的话,就没效果了。
第二种是调用 UIManager.setLookAndFeel 来动态设置外观感觉,参数仍然是上面得到的那些类名之一。
现在拿上一节中的程序来试验一下。在 main 方法的开头添加下面的代码(源代码在“MyFirstSwing2”项目
中):
try {
UIManager.setLookAndFeel(
“com.sun.java.swing.plaf.windows.WindowsLookAndFeel”);
} catch (Exception ex) {
// 这里不需要对异常进行任何处理,如果发生异常,系统会自动将外观感觉设置为 Metal。
}
运行程序,看看结果:
图 2-7 第一个 Swing 程序(Windows 外观感觉)
你是不是也得到了 Windows 风格的按钮?(实际效果和你安装的 Windows 版本有关)请再试试其它的
外观感觉。
UIManager 还提供了一个静态方法 getSystemLookAndFeelClassName 来获取本机外观感觉的类名,其返
回值因系统而异。如果将上面的那段代码换成:
33
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
那么在不同的平台下将产生不同的效果。
如果要在程序运行过程中更改外观感觉,则要在调用 UIManager.setLookAndFeel 后应立即调用
SwingUtilities.updateCompenentTreeUI(Component c),如果传入参数为窗体对象,则整个程序的外观感觉都
会被更新。
3.4.3、事件侦听器
在上一节的程序中,按钮没有任何作用,因为没添加事件处理代码。这一节将介绍 Swing 的事件侦听
器。我们采用同一个例子,先手写代码,然后用 NetBeans GUI 生成器来简化工作。
新建“ListenerBasics”项目,然后手动输入下面代码:
public class ListenerBasics {
public static void main(String[] args) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
}
SwingUtilities.invokeLater(new Runnable() {
public void run() {
JFrame frame = new ListenerBasicsFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(300, 150);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
}
class ListenerBasicsFrame extends JFrame {
private JLabel label;
private JButton button;
private int times;
public ListenerBasicsFrame() {
label = new JLabel("点击次数:" + times);
button = new JButton("点我!");
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
34
times++;
label.setText("点击次数:" + times);
}
});
JPanel panel = new JPanel(new GridLayout(2, 1, 6, 6));
panel.setBorder(BorderFactory.createEmptyBorder(10, 10, 10, 10));
panel.add(label);
panel.add(button);
add(panel);
setTitle("Swing 侦听器基础");
}
}
现在请运行“ListenerBasics”项目,并多次单击“点我!”按钮,应该看到标签上的计数不断增加(源
代码在“ListenerBasics”项目中):
图 2-8 单击按钮的效果
该程序最重要的一段是给按钮添加操作事件(例如单击按钮或按回车键)的侦听器:
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
times++;
label.setText("点击次数:" + times);
}
});
因为处理代码比较简单,这里采用了匿名内部类。
ActionListener 是一个接口,其中只有一个方法 actionPerformed(ActionEvent e)。ActionEvent 封装了所发
生的操作事件的相关信息。它是 EventObject 的子类,所有的事件对象都继承自 EventObject。EventObject
只定义了一个 getSource 方法,用来得到发出该事件的对象。
任何 Swing 组件都可以有自己的事件侦听器,并且个数不受限制。也就是说,还可以在按钮上再添加
一个 ActionListener,这两个 ActionListener 不会冲突,它们会按照添加的顺序来执行。我们还可以继续添加
其它的侦听器,例如侦听键盘事件的 KeyListener,鼠标事件的 MouseListener 等等。
有些侦听器方法较多,但是很少需要全部实现,所以 Swing 还引入了一些“适配器”。适配器是所对应
侦听器的一个实现类,只不过所有的方法体为空。用户可以扩展适配器,按需覆盖其中的方法,减少代码
量。例如 MouseListener 定义了五个方法,很可能你只想实现 mouseClicked,你就扩展 MouseAdapter 类,
然后只覆盖它的 mouseClicked 方法就行了。当然,由于 Java 的单继承模式,有时候你的类需要继承其它类,
35
这时候便无法使用 MouseAdapter,只能老老实实实现 MouseListener。
好,接下来用 NetBeans GUI 生成器(项目代号“Matisse”)重做刚才的程序。
NetBeans GUI 生成器的目的是让你摆脱烦人的布局代码,而专注于程序本身的逻辑。在它出现前,程
序员需要花费大量的时间去处理复杂的界面布局。NetBeans 从 5.0 开始,引入了一种简单易用且专业的布
局方式(已正式成为 Java SE 6 的一部分),它让你能够*排列组件,并自动处理调整大小的行为。它极
大地降低了学习曲线,缩短了开发时间。
后面的例子中我们都将采用 GUI 生成器,所以请务必仔细阅读并跟着动手操作。请启动你的 NetBeans,
然后按照下面的步骤进行(源代码在“ListenerBasics2”项目中):
选择“文件”“新建项目”,将弹出“新建项目”对话框。
在“类别”列表中选择“Java”,“项目”列表中选择“Java 应用程序”,单击“下一步”。
填写“项目名称”为“ListenerBasics”,取消选择“创建主类”,单击“完成”。
在“项目”窗口中,右键单击该项目的节点,选择“新建”>“JFrame 窗体”,将弹出“新建 JFrame
窗体”对话框。
填写“类名”为“ListenerBasicsFrame”,“包”为“org.myorg”,单击“完成”,将会在中间的代码
窗口中出现设计视图,右边会出现“组件面板”。
在“属性”窗口中,将窗体的“title”属性设为“Swing 侦听器基础”。
从“组件面板”的“Swing 控件”下,将一个“标签”拖到设计视图上,这时会出现参考线以及提
示信息帮助你定位:
图 2-9 设计视图中的参考线
再将一个“按钮”拖进去:
图 2-10 根据参考线继续添加组件
适当调整各组件的大小:
36
]
图 2-11 调整组件大小
按住“Ctrl”键,用鼠标选中标签和按钮两个组件,单击右键,同时选中“自动调整大小”下的“水
平”和“垂直”。
在设计视图的工具栏上,单击“源”按钮,将切换到源代码。
在 ListenerBasicsFrame 类里添加一个整型的 times 字段,如下图所示:
图 2-12 添加字段
切换回“设计”视图,选中标签,在“属性”窗口中,单击“text”属性右边的省略号按钮,将出
现“text”对话框。
将“使用以下内容设置 jLabel1 的 text 属性”改为“定制代码”,然后填入下面的代码,单击“确
定”:
图 2-13 定制代码
在按钮上单击右键,选择“编辑文本”,将按钮文本改为“点我!”。
双键按钮,NetBeans 将会自动为按钮添加 ActionListener。
在生成的 jButton1ActionPerformed 方法里,输入下面的代码:
times++;
jLabel1.setText("点击次数:" + times);
在 main 方法里加入前面学习的设置本机外观感觉的代码并修复导入(本章后面的例子中都会用这
段代码):
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
} catch (Exception ex) {
37
}
最后,让窗体在屏幕中居中,将 invokeLater 中的代码改成:
JFrame frame = new ListenerBasicsFrame();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
运行程序,应该得到和前面相同的结果。
Swing 的基础就介绍到这里。下面以练习的方式学习 Swing 组件、容器以及一些 Swing 的设计理念。
3.5、Swing 基本控件和窗口
3.5.1、标签
标签(JLabel)直接继承自 JComponent,用来显示文本信息。从 J2SE 5.0 开始,标签支持简单的 HTML,
从而极大地丰富了其表现内容。
请打开“Labels”项目并观察运行结果:
图 2-14 各种标签
第一个标签是最普通的,只有一行文本:
jLabel1 = new JLabel(“左对齐的标签”);
第二个标签在初始化之后调用 setHorizontalAlignment 将文本的水平对齐方式设为居中:
jLabel2.setHorizontalAlignment(SwingConstants.CENTER);
GUI 生成器产生的代码可能比较乱,而且采用类的全限定名,因此后面列举的代码都会去掉包名。
另外还有个 setVerticalAlignment 方法用来设置垂直对齐方式。
如果手写代码,可以直接用构造方法 JLabel(String text, int horizontalAlignment) 同时指定文本和水平对
齐方式。
第三个标签带有一个图标:
jLabel3.setIcon(new ImageIcon(
getClass().getResource("/org/myorg/netbeans.png")));
最后一个标签用 HTML 来格式化文本:
jLabel4.setText("<html><h3>用 HTML 格式化的标签<h3><p>可以有多行文本<br/><font face=\"黑体\"
color=\"blue\" size=\"20\"><i>还可以设置字体的样式</i></font></p></html>");
38
注意,如果用了 HTML,则最好不要再调用 setHorizontalAlignment 或者 setFont 之类的方法来设置样式,
以免产生混乱。
3.5.2、按钮
这个小结里,将学习四种按钮,包括前面已经见过的普通按钮(JButton),还有切换按钮(JToggleButton,
也较“开启/关闭按钮”)、复选框(JCheckBox)和单选按钮(JRadioButton)。按钮上的文本也可以像 JLabel
一样用 HTML 格式化。
本小节的“Buttons”项目演示了这几种按钮:
图 2-15 各种按钮
JButton
“生成随机数”按钮是一个 JButton,它添加了一个 ActionListener,每按一次,就会生成一个随机数,
显示在右边的标签上:
图 2-16 生成随机数
代码如下:
randomLabel.setText(Math.random() + "");
“显示/隐藏文本”是一个 JToggleButton。所谓 toggle,就是在两种状态下相互切换。JToggleButton 具
有“压下”和“弹起”两种状态,这通过调用 isSelected 方法来判断,true 表示“压下”,false 表示“弹起”。
在这个程序中,“显示/隐藏文本”按钮初始状态为压下,如果按一下,就会切换成弹起状态,再按一下又变
回压下,如此循环。
下图是“显示/隐藏文本”按钮“弹起”的效果:
39
图 2-17 “显示/隐藏文本”按钮的“弹起”效果
我们在 JToggleButton 上添加的是 ChangeListener 而非 ActionListener。因为有可能通过编程的方式改变
按钮的状态,这不会引发操作事件,所以要用 ChangeListener 来侦听状态的改变。ChangeListener 只有一个
方法 stateChanged。
处理代码检查“显示/隐藏文本”按钮的状态并分别处理:
if (showTextToggleButton.isSelected()) {
nbLabel1.setText("NetBeans");
} else {
nbLabel1.setText("");
}
JcheckBox 和 JRadioButton
接下来是下面的两个 JCheckBox 和两个 JRadioButton,请尝试选择它们,并注意上方文本的位置变化:
图 2-18 JCheckBox 和 JRadioButton 的效果
复选框就像多项选择题,而单选按钮每次只能选一个。它们都是 JToggleButton 的子类,只是用途不一
样。
JRadioButton 必须被添加到 ButtonGroup,才能编成一组选项:
ButtonGroup group = new ButtonGroup();
Group.add(radio1);
Group.add(radio2);
// 继续添加更多 JRadioButton。
ButtonGroup 不是 Swing 组件,所以还需要额外的代码将 JRadioButton 添加到父级组件中。
在 NetBeans 中创建单选按钮组的步骤如下:
1. 将各“单选按钮”拖到设计视图上所需的位置。
2. 将一个“按钮组”拖到设计视图上的任意位置。
3. 针对每个“单选按钮”,将“属性”窗口中的“buttonGroup”设为第 2 步中的按钮组。
JCheckBox 和 JRadioButton 和 JToggleButton 一样用 ChangeListener 来判断状态并分别处理,在本例中
是设置上方 JLabel 的对齐方式,例如“水平居中”JCheckBox 的代码:
if (hCenterCheckBox.isSelected()) {
nbLabel2.setHorizontalAlignment(SwingConstants.CENTER);
} else {
nbLabel2.setHorizontalAlignment(SwingConstants.LEADING);
40
}
另外,本程序用了四个面板(JPanel)。面板是 Swing 中最简单的容器类组件,它能够容纳各种 Swing
组件,能够嵌套。我们常将组件按相关性归类,放到不同面板上,再用这些面板拼接成整个界面。
这些面板都设置了标题边框。事实上可以对任何 Swing 组件设置边框,通常的做法是调用 BorderFactory
里的一系列静态方法,NetBeans 的“属性”窗口提供了用来快速设置边框的“border”属性。
3.5.3、MVC 设计模式
后面的练习中将会用到 MVC,所以先做个练习,加深印象。我们将让多个 Swing 组件共享一个模型,
并根据模型的更改做出更新。
请运行“SwingMVC”项目,在左边输入几行文字,然后单击“确认模型更改”按钮,应当看到右边的
三个组件都得到了相同的数据:
图 2-19 同步模型
事实上,只需在“确认模型更改”按钮侦听器里添加下面几行代码就可达到效果:
String text = modelTextArea.getText();
String[] model = text.split("\n");
comboBox.setModel(new DefaultComboBoxModel(model));
spinner.setModel(new SpinnerListModel(model));
list.setListData(model);
相信到此为止你已经对 MVC 有了一个较直观的概念,不过 MVC 的作用还不限于此,它还允许组件实
现可插拔的外观,从而改变自己默认的渲染方式——也就是后面几个高级 Swing 组件中要学习的“渲染器”。
3.5.4、组合框、翻选框和列表
组合框(JComboBox)就是常说的下拉列表组件,翻选框(JSpinner)常用于选择年份等连续性的数值,
列表(JList)直接将各选项列出来以供选择。
下图是本小节的项目“ComboSpinner”:
41
图 2-20 组合框、翻选框和列表
单击“注册”按钮将会根据所选内容显示一个对话框:
图 2-21 注册成功
如果调整出生年月使年龄低于 18 岁,则会弹出警告对话框提示注册失败:
图 2-22 注册失败
单击“注册”按钮后,事件处理代码会提取数据、分析年龄并显示结果:
int year = (Integer) yearSpinner.getValue();
int month = monthComboBox.getSelectedIndex() + 1;
Calendar c = Calendar.getInstance();
int age = c.get(Calendar.YEAR) - year;
if (c.get(Calendar.MONTH) < month) {
age--;
}
if (age < 18) {
JOptionPane.showMessageDialog(this, "未成年人不得注册!", "警告",
JOptionPane.WARNING_MESSAGE);
} else {
42
JOptionPane.showMessageDialog(this,
"感谢你花时间进行注册!\n 你将加入“"
+ clubList.getSelectedValue() + "”俱乐部。",
"注册成功", JOptionPane.INFORMATION_MESSAGE);
}
这里用到的对话框将在后面的“对话框”中介绍。
JComboBox
首先你可能会注意到,两个分别用来选择月份和食物的 JComboBox,其中一个不可编辑,另一个可编
辑,这通过 setEditable 方法来控制。JComboBox 的构造方法允许传入一个 Object 数组或 Vector 进行初始化,
其中的元素会显示成一个列表以供选择。
下面的代码可产生“兴趣爱好”组合框:
hobbyComboBox = new JComboBox(new String[] {"下棋", "绘画", "钓鱼", "游戏"});
Combo.setEditabled(true);
IDE 生成的代码用到了 ComboBoxModel:
hobbyComboBox.setModel(new DefaultComboBoxModel(new String[] {"下棋", "绘画", "钓鱼", "游戏"}));
Model(模型)用来存储组件的数据,这已经在前面的“Swing 和 MVC 设计模式”中介绍过。
JComboBox 主要有两个方法来获得所选项:getSelectedIndex 方法用来获取当前选择的索引,第一项的
索引为 0;getSelectedItem 方法直接返回所选元素代表的对象。
JSpinner
JSpinner 实际上由两个箭头按钮和一个 JFormattedTextField 组成,所以对非法值的处理方式和
JFormattedTextField 相同(这里是如果输入非法值,则自动设回上次合法的值)。JFormattedTextField 在后面
的“文本组件”中介绍。
JSpinner 在使用的时候需要设置其取值范围和步长:
yearSpinner.setModel(new SpinnerNumberModel(1980, 1970, 2000, 1));
四个参数分别代表默认值、最小值、最大值和步长。JDK 还提供了多种 SpinnerModel,可在 GUI 生成
器中通过“属性”窗口里的“model”来更改。getValue 方法返回当前设定的值。
实际上,JSpinner 不一定要连续的数值,也可以像 JComboBox 一样使用:
spinner.setModel(new SpinnerListModel(new String[] {"下棋", "绘画", "钓鱼", "游戏"}));
JList
JList 可以勉强看作一个“被展开”且不可以编辑的 JComboBox,构造方法和 JComboBox 类似,并且
也提供了作用相同的 getSelectedIndex 和 getSelectedValue 方法。
不同的是,JList 可以用 setSelectionMode 来设置选择模式,参数共有三种情况:
ListSelectionModel.SINGLE_SELECTION
一次只能选一项。本例采用了这种模式。
ListSelectionModel.SINGLE_INTERVAL_SELECTION
一次只能选择连续的一项或多项。这种情况下要调用 getMinSelectionIndex 和 getMaxSelectionIndex 来取得
选择范围,且 getSelectionIndex 和 getMinSelectionIndex 返回相同的值。
ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
可以任意单选或多选,这是默认值。getSelectedIndices 将返回所选项的数组。
3.5.5、菜单和工具栏
菜单(JMenu)和工具栏(JToolBar)是传统应用程序中的常见组件,通常位于主窗体的顶部。
43
和往常一样,先看看本小节项目“MenuToolBar”:
图 2-23 嵌套的菜单
“选项”菜单出现了我们已经熟悉的复选框和单选按钮:
图 2-24 复选框和单选按钮样式的菜单项
下面的文本区域里有右键弹出菜单(文本区域在后面的“文本组件”中介绍):
图 2-25 弹出菜单
工具栏可以浮动:
44
图 2-26 浮动工具栏
JMenu
如你所见,JMenu 可被添加到菜单栏(JMenuBar)或弹出菜单(JPopupMenu)里,并且可以嵌套。
创建菜单的时候,我们用 add 方法将向菜单的末尾添加一个菜单项(JMenuItem),用 addSeparator 方法
添加分隔符。
在练习项目里,共出现了 JMenuItem、JCheckboxMenuItem 和 JRadioButtonMenuItem 三种菜单项。从
API 文档可以发现 JMenuItem 继承自 AbstractButton,后两个是 JMenuItem 的子类。所以,完全可以将
JMenuItem 看作按钮的一种形式,而 JCheckboxMenuItem 和 JRadioButtonMenuItem 则分别对应 JCheckBox
和 JRadioButton。JMenu 也是 JMenuItem 的子类,所以 JMenu 可以嵌套添加,以便生成子菜单。
JMenuItem 提供了几个方法设置一些常用的菜单项属性(其中大部分 JButton 也支持)。例如“新建”
菜单项的代码:
// 设置快捷键 Ctrl+N。
jMenuItem2.setAccelerator(KeyStroke.getKeyStroke(
KeyEvent.VK_N, InputEvent.CTRL_MASK));
// 设置图标。
jMenuItem2.setIcon(new ImageIcon(
getClass().getResource("/org/myorg/netbeans.png")));
// 设置助记符,这样可以通过 Alt+N 来使用该菜单项。
jMenuItem2.setMnemonic('N');
快捷键和助记符的区别在于,前者始终有效,而后者只在该菜单项可见时才有效。
如果使用 JMenuBar,就在构造好 JMenu 以后,把它添加到 JMenuBar,然后调用 JFrame.setJMenuBar
方法来显示它。
如果使用 JPopupMenu,可以直接添加 JMenuItem,然后用 JComponent.setComponentPopupMenu 方法将
它附加到组件上,默认情况下右键单击组件就会显示弹出菜单。
JToolBar
JToolBar 和 JMenu 一样,用 add 方法在其末尾添加组件。有些组件(例如按钮)在 JToolBar 上有特殊
的外观。
JToolBar 可以在窗口上浮动,也可以停靠到窗口四周,setFloatable(boolean b) 方法用来控制这一属性。
3.5.6、文本组件
文本组件都是 JTextComponent 的子类,用于文本编辑。JTextComponent 提供了与文本编辑相关的大量
方法,例如处理光标、复制粘贴、文本高亮等等。我们一般都使用它的子类,包括文本字段(JTextField)、
口令字段(JPasswordField)、格式化文本字段(JFormattedTextField)、文本区域(JTextArea)、编辑器窗格
(JEditorPane)和文本窗格(JTextPane)。本小节将介绍前面四种,编辑器窗格和文本窗格将在“高级文本
组件”中介绍。
请运行“TextComponents”项目,在各个文本组件中输入一些值,并单击“统计字数”按钮,应该看到
下面的结果:
45
图 2-27 统计字符
JTextField
JTextField 是一个简单的单行文本编辑组件,getText 是其最常用的方法。如果查阅 API 文档,会发现这
样一个构造方法:
public JTextField(int columns)
这里的 columns 不是最大字符数,而是被用来计算 JTextField 显示的宽度。Swing 目前没有提供简单的
方法来限制字符输入的数量。
JPasswordField
JPasswordField 常用于输入口令,它不会显示所输入的真实字符,而是一串相同的回显字符。回显字符
的默认值和外观有关,可通过 setEchoChar 方法来设置回显字符,如果传入“(char) 0”,则会强制显示出输
入的真实字符,和 JTextField 一样。
和其它文本组件不同,JPasswordField 的 getText 方法被标记为“已过时”,应该调用 getPassword 方法
来获取输入的口令,它返回一个 char 数组而非 String 对象。
JFormattedTextField
JFormattedTextField 可以对输入的文本进行格式化,在用户进行输入后就根据 setFormatterFactory 方法
指定的格式化器来判断用户输入是否满足要求。本例使用了 JDK 所提供的整数格式化器:
formattedTextField.setFormatterFactory(new DefaultFormatterFactory(
new NumberFormatter(NumberFormat.getIntegerInstance())));
一旦设置了格式化器,就可以通过 getValue 方法获取根据该格式化器的策略格式化文本后得到的对象。
如果在本例中输入字符串“123”,getValue 就返回整数 123(Integer 对象)。
在 GUI 生成器里,可以通过“属性”窗口中的“formatterFactory”属性设置格式化器。
如果用户输入不合法的字符串如何处理呢?setFocusLostBehavior 方法定义了在其失去焦点时如何来处
理用户输入的文本,参数共有四种可能:
JFormattedTextField.REVERT
无论输入是否合法,都恢复成 getValue 所得到的值。很少使用。
JFormattedTextField.COMMIT
46
直接提交当前值,如果格式不对就抛出异常。
JFormattedTextField.COMMIT_OR_REVERT
格式合法就提交,否则就恢复成 getValue 所得到的值。这是默认的行为,也最经常使用。本例就采用了这
一默认值。
JFormattedTextField.PERSIST
不检查用户的输入,表现就像 JTextField 一样。
JTextArea
JTextArea 用于多行文本编辑,用下面的代码来指定换行策略:
// 自动换行。
textArea.setLineWrap(true);
// 根据单词进行换行。
textArea.setWrapStyleWord(true);
其它
这几个文本组件都是基于纯文本的,所以不能像标签那样用 HTML 来格式化文本。后面将会介绍的
JEditorPane 和 JTextPane 支持文本的格式化。
如果想侦听文本组件中文本的更改,需要在它对应的 Document 对象上添加 DocumentListener。下面的
代码将实时响应一个 JTextField 的文本更改:
textField.getDocument().addDocumentListener(new DocumentListener() {
// 实现 DocumentListener 中的方法。
});
Document 对象和在前面见到的 Model 一样,保存了文本组件中的文本信息,它的地位已在前面的“Swing
和 MVC 设计模式”中介绍过。
本程序中还涉及到了另外两个 Swing 组件:分隔符(JSeparator)和滚动窗格(JScrollPane)。JSeparator
是一条水平或垂直的分割线,在前面的菜单里间接用过;JScrollPane 包装一个实现了 Scrollable 接口的组件,
为其添加滚动条。GUI 生成器将自动为实现了 Scrollable 的行组件添加 JScrollPane。
3.5.7、SwingWorker
后面的练习中将用到 SwingWorker,所以下面通过一个实例来详细说明 SwingWorker 的用法,请结合前
面的“Swing 的单线程模型”进行学习。
假设有个任务,要找出某个文件夹下面所有的 Java 源文件(*.java),然后把搜索结果显示到界面上。
搜索整个文件夹(包括子文件夹)通常都会耗费较多时间,正好可以让 SwingWorker 派上用场。
请运行“JavaSourceSearcher”项目,选择一个包含 Java 源文件(*.java)的文件夹(包含越多越好),
然后单击“搜索”,后台的 SwingWorker 就会开始执行,并不时更新界面:
47
图 2-28 搜索 Java 源文件
在搜索过程中,“搜索”按钮被禁用,这是为了防止两个不同的搜索任务发生冲突。但是单击“浏览文
件夹”仍然可以打开文件选择器,甚至还可以在文本区域中选择文本。这说明搜索工作确实是在后台进行,
并没有干扰界面的响应。
下面开始分析代码,请打开 org.myorg.JavaSourceSearcher。
ActionHandler 负责处理单击按钮事件。如果接收到的操作命令为“Browse”(对应“浏览文件夹”按钮),
就弹出文件选择器,并将用户选择的文件夹路径写到文本字段里。
然后是“搜索”按钮的处理代码。首先根据文本字段里的内容构造出一个 File 对象,然后检查它是否
表示一个目录且可读,如果是则:
1. 禁用“搜索”按钮:
searchButton.setEnabled(false);
2. 清空文本区域:
resultTextArea.setText("");
3. 调度后台任务:
SwingWorker searchTask = new SearchTask(directory);
searchTask.execute();
否则就显示一条错误信息。
最后也是最关键的,就是 SearchTask 的实现。首先是类声明:
private class SearchTask extends SwingWorker<Void, String> {
注意其中的 Void。因为不需要返回任何结果,第一个参数类型事实上可以为任何类型,只要
doInbackground 返回 null 就可以了。
略过简单的构造方法,接下来就是必须实现的 doInbackground:
protected Void doInBackground() throws Exception {
doSearch(directory);
return null;
}
doInbackground 在自己单独的线程里执行,所以不会直接干扰界面,它把工作交给了 doSearch:
private void doSearch(File dir) {
48
File[] files = dir.listFiles();
for (File file : files) {
if (file.isDirectory()) {
doSearch(file);
} else {
String path = file.getPath();
if (path.endsWith(".java")) {
publish(path);
}
}
}
}
doSearch 用递归的方式遍历 dir 及其所有子目录,并分析文件的扩展名,一旦遇到“*.java”就将该文
件的路径用 publish 发送出去。发送出去的这些数据将由 process 方法处理(它在事件指派线程上运行):
@Override
protected void process(List<String> chunks) {
StringBuilder stringBuilder = new StringBuilder();
for (String s : chunks) {
stringBuilder.append(s + "\n");
}
resultTextArea.append(stringBuilder.toString());
}
你可能注意到,每次调用 publish 方法只发送了一个字符串,为什么 process 的参数是 List 类型呢?因
为这两个方法是异步执行的,为了改进性能,process 以比较“偷懒”的方式工作,多次调用 publish 发送的
数据可能会被 process 一次处理。
还有一点,我们没有对 List 里的字符串每次都调用 resultTextArea.append 方法,而是先用一个
StringBuilder 来“合并”所有的字符串,再一次性附加到 resultTextArea 后面。这种做法能减少 resultTextArea
的重画次数,从而提高执行效率。
后台任务完成后,将执行 done 方法(也在事件指派线程上执行):启用“搜索”按钮并给出搜索结束
的提示信息:
@Override
protected void done() {
searchButton.setEnabled(true);
JOptionPane.showMessageDialog(JavaSourceSearcherFrame.this,
"已完成搜索。", "完成", JOptionPane.INFORMATION_MESSAGE);
}
3.5.8、进度栏和滑块
我们在安装软件的时候经常能看见进度栏(JProgress),它指当前任务完成的状态;滑块(JSlider)则
是音量控制之类的用途所少不了的。
现在来看看这两个组件的 Swing 实现(“ProgressBarSlider”项目):
49
图 2-29 进度栏和滑块
第一个进度栏不断滚动。第二个进度栏需要单击“开始”按钮才滚动一次:
图 2-30 “一次性”滚动栏
滑块的作用有点像 JSpinner,用来选择一个范围内的数值:
图 2-31 滑块
JProgreeBar
JProgreeBar 在构造的时候可以指定方向、最小值和最大值。默认构造方法将生成一个横向、范围是 0~100
的 JProgreeBar。
上面的两个 JProgressBar,一个不断滚动(称为“不确定状态”),另一个则需要单击“开始”按钮才能
滚动一次,然后停下(称为“确定状态”),这通过 setIndeterminate 进行设置。
“开始”按钮控制第二个 JProgreeBar 时用了 SwingWorker。这里在后台先将进度栏重置,然后每 100
毫秒增加 10% 的任务进度:
setProgress(0);
while (getProgress() < 100) {
Thread.sleep(100);
setProgress(getProgress() + 10);
}
接着由 SwingWorker 的属性更改侦听器来更新进度栏的进度:
progressBar2.setValue((Integer) evt.getNewValue());
setValue 的参数取值在构造时指定的最小值和最大值之内,如果超出范围,则会根据情况被重设为最小
50
值或最大值。
为了在滚动时显示进度,在初始化的时候调用了
progressBar2.setStringPainted(true);
JSlider
JSlider 的构造方法也可以定义方向、最小值和最大值,并且还可以定义初始值。默认构造方法将生成
一个横向、范围是0~100且初始值为50的JSlider。JSlider在初始化的时候可以设置很多属性,本例中的JSlider
初始化代码如下:
// 设置主刻度(长线)的间距。
slider.setMajorTickSpacing(20);
// 设置次刻度(短线)的间距。
slider.setMinorTickSpacing(10);
// 绘制主刻度值。
slider.setPaintLabels(true);
// 绘制刻度。
slider.setPaintTicks(true);
另外还有两个常用方法:setPaintTrack 设置是否绘制滑道;setSnapToTicks 设置是否在滑动滑块时自动
对齐刻度。
JSlider 初始化之后,剩下的代码就基本上是添加 ChangeListener,然后在 stateChanged 方法中用 getValue
取得新值。
3.5.9、对话框
虽然对话框(JDialog)和 JFrame 一样都是重量级窗口组件,但它们除了包含的方法不一样,用法也不
同。
使用 JDialog,可以写一个类扩展它,然后像 JFrame 那样添加组件。Swing 还提供了多个类来直接创建
一些常用的对话框,例如前面讲组合框系列组件的时候已经用到了一个简单的消息对话框。
“Dialogs”项目演示了各种各样的对话框:
图 2-32 各种对话框
选项窗格
选项窗格(JOptionPane)虽然可以实例化,但它提供的一系列创建标准对话框静态方法才是重点。这
些方法一共分为四类,我们一个一个地来看。
51
“消息 1”和“消息 2”按钮演示了 showMessageDialog,它向用户显示一个消息对话框:
图 2-33 警告消息
代码:
JOptionPane.showMessageDialog(this, "前方有陷阱,当心!", "警告",
JOptionPane.WARNING_MESSAGE);
图 2-34 有自定义图标的普通消息
代码:
JOptionPane.showMessageDialog(this, "你正在使用 NetBeans。", "消息",
JOptionPane.PLAIN_MESSAGE, nbIcon);
showMessageDialog 的第一个参数指定父组件,这可以使它相对父组件居中;如果为 null,则在屏幕上
居中。第二个参数是消息内容,第三个是标题。第四个定义消息类型,它的实际作用就是显示不同的消息
图标。如果你对默认图标不满意,还可以在最后指定自己的图标。
你也许已经发现,在消息对话框关闭前是无法访问主窗体的。这种阻止你访问顶层窗口的对话框叫做
模态对话框(或“有模式对话框”)。与之对应的则是非模态对话框(或“无模式对话框”),这一般要通过
JDialog 来创建。
接下来是“确认”按钮带来的确认对话框:
图 2-35 确认对话框
代码:
int option = JOptionPane.showConfirmDialog(this, "决定了吗?", "确认",
JOptionPane.YES_NO_OPTION, JOptionPane.INFORMATION_MESSAGE);
if (option == JOptionPane.OK_OPTION) {
JOptionPane.showMessageDialog(this, "答对了,加十分!", "恭喜",
JOptionPane.INFORMATION_MESSAGE, nbIcon);
52
} else {
JOptionPane.showMessageDialog(this, "那就再想想吧。", "……",
JOptionPane.INFORMATION_MESSAGE, nbIcon);
}
容易看出,showConfirmDialog 和 showMessageDialog 的区别在第三个参数,并且它有返回值。第三个
参数指定对话框中要显示的按钮。该方法在关闭对话框的时候返回,通过返回值来检查哪个按钮被按下了。
“选项”按钮演示的选项对话框实际上就是确认对话框的扩展:
图 2-36 选项对话框
代码:
String[] options = new String[] {"什么事?", "等会儿。"};
int option = JOptionPane.showOptionDialog(this, "有空吗?", "确认",
JOptionPane.DEFAULT_OPTION, JOptionPane.INFORMATION_MESSAGE,
nbIcon, options, options[0]);
// 处理选择结果。
options 的长度将决定按钮个数。返回值是用户所选选项的索引;如果直接关闭了对话框,则返回
CLOSED_OPTION。
“输入 1”和“输入 2”按钮演示了输入对话框:
图 2-37 输入对话框 1
代码:
String name = JOptionPane.showInputDialog(this, "请输入姓名:", "第一步",
JOptionPane.INFORMATION_MESSAGE);
// 处理输入的姓名。
53
图 2-38 输入对话框 2
代码:
tring[] values = new String[] {"英语", "法语", "日语"};
String language = (String) JOptionPane.showInputDialog(this, "请选择外语:",
"第二步", JOptionPane.INFORMATION_MESSAGE, nbIcon, values, values[0]);
// 处理选择的外语。
如果在参数里指定了选项,则显示一个组合框,否则显示一个文本字段。返回所选或所输入的值。
文件选择器
单击“打开文件”按钮将出现文件选择器(JFileChooser):
图 2-39 文件选择器
代码:
int option = fileChooser.showOpenDialog(this);
if (option == JFileChooser.APPROVE_OPTION) {
fileTextField.setText(fileChooser.getSelectedFile().getName());
}
JFileChooser 还有个 showSaveDialog 方法,不同的仅仅是对话框标题和按钮文本。
在显示对话框前,可以设置 JFileChooser 的一些属性,常用的方法有:
setFileSelectionMode(int mode)
设置文件选择模式,参数的可能值有 JFileChooser.FILES_ONLY(只能选择文件,这是默认值)、
JFileChooser.DIRECTORIES_ONLY(只能选择目录)和 JFileChooser.FILES_AND_DIRECTORIES
(文件和目录都可以选择)。
setCurrentDirectory(File dir)
设置显示对话框时的目录。
addChoosableFileFilter(FileFilter filter) 和 setFileFilter(FileFilter filter)
添 加 或 设 置 文 件 过 滤 器 , 用 来 限 制 用 户 只 能 选 择 某 种 类 型 的 文 件 。 Swing 提 供 了
FileNameExtensionFilter 类来根据文件扩展名进行过滤。例如,我们想只允许用户选择 HTML 文件:
54
FileFilter filter = new FileNameExtensionFilter("HTML 文件", "html", "htm");
fileChooser.addChoosableFileFilter(filter);
setAcceptAllFileFilterUsed(boolean b)
是否将所有文件用作一个文件过滤器。这样即使设置了文件过滤器,仍然通过文件类型中的“所
有文件”选择其它类型的文件。
setFileHidingEnabled(boolean b)
是否显示隐藏文件。
setMultiSelectionEnabled(boolean b)
是否可以同时选择多个文件。getSelectedFiles() 将返回所选的文件数组。
颜色选择器
单击“选择颜色”按钮将打开颜色选择器(JColorChooser,也叫拾色器):
图 2-40 颜色选择器
代码:
Color color = JColorChooser.showDialog(this, "选择颜色", Color.WHITE);
colorTextField.setBackground(color);
可见 JColorChooser 的用法和 JFileChooser 不同,因为我们用它的静态方法来显示对话框。
三个参数分别表示父组件、标题和初始颜色。返回用户所选颜色。
进度监视器
单击“开始任务”将激活一个进度监视器(ProgressMonitor),它能产生一个带 JProgressBar 的进度对
话框,必须等进度完成或任务取消才会关闭:
55
图 2-41 进度监视器
ProgressMonitor 构造方法如下:
progressMonitor = new ProgressMonitor(
this, "正在安装 NetBeans", "已完成:0%", 0, 100);
第一个参数为进度对话框的父组件;第二个参数是要显示的描述信息,构造之后不能更改;第三个是
可以在进度中更改的提示信息;最后两个是进度的最小值和最大值。
接下来和前面的 JProgressBar 类似,用 SwingWorker 来运行后台任务并不时更新进度:
progressMonitor.setProgress(progress);
progressMonitor.setNote("已完成:" + progress + "%");
进度达到最大值 100 时,进度对话框自动关闭,这时又可以操作主界面了。如果在进行过程中单击“取
消”按钮,则会关闭该对话框,但不会自动结束 SwingWorker 的任务。所以需要手工检测状态并处理:
progressMonitor.setProgress(progress);
progressMonitor.setNote("已完成:" + progress + "%");
if (progressMonitor.isCanceled()) {
task.cancel(true);
JOptionPane.showMessageDialog(DialogFrame.this, "已取消安装 NetBeans。",
"取消", OptionPane.INFORMATION_MESSAGE, nbIcon);
} else if (task.isDone()) {
JOptionPane.showMessageDialog(DialogFrame.this, "NetBeans 已安装完毕!",
"完成", JOptionPane.INFORMATION_MESSAGE, nbIcon);
}
对话框
对话框(JDialog)在构造后的使用方法和 JFrame 几乎相同,这里主要看前面提到的模态和非模态对话
框。本例分别创建了一个模态和一个非模态对话框:
56
图 2-42 模态对话框
代码:
modalDialog = new JDialog(this, true);
图 2-43 非模态对话框
代码:
modalessDialog = new JDialog(this);
差别仅仅在构造方法的第二个参数上(默认为非模态)。如果主窗体关闭,该程序所属的所有非模态对
话框也会关闭。
打印
单击“打印”按钮将出现打印对话框:
57
图 2-44 打印对话框
任何 JComponent 都支持打印,但文本组件提供了一个更方便的方法:
boolean printed = printTextField.print();
if (printed) {
JoptionPane.showMessageDialog(this, “已完成打印。”, “打印”,
JoptionPane.INFORMATION_MESSAGE);
} else {
JoptionPane.showMessageDialog(this, “你取消了打印。”, “打印”,
JoptionPane.INFORMATION_MESSAGE);
}
如果找不到打印机,则会返回一条错误信息。
3.6、Swing 容器
Swing 容器用来容纳其它 Swing 组件(包括容器),对它们进行组织和布局。在前面的例子中,我们已
经多次用到了最基本的一个 Swing 容器——面板(Jpanel)。下面学习其它几种 Swing 容器。
3.6.1、选项卡窗格
选项卡窗格(JtabbedPane)允许用户在通过单击一系列选项卡(也称为“标签”)来切换显示的组件。
本小节将用下面的“TabbedPane”项目来介绍选项卡的基本用法:
58
图 2-45 选项卡浏览
这个程序有两个选项卡,它们的标题一个带图标,一个是红色字体。单击“教程”选项卡后,界面切
换到另一个选项窗格,它的选项卡位于底部,同样可以进行切换:
图 2-46 嵌套的选项卡
下面来看代码。先用 JTabbedPane 的 addTab 方法添加带图标的“欢迎”选项卡:
mainTabbedPane.addTab(“欢迎”, new ImageIcon(
getClass().getResource(“/org/myorg/netbeans.png”)), welcomePanel);
同样添加第二个选项卡,注意向第三个参数传入了另一个 JTabbedPane:
59
tutorialTabbedPane = new JtabbedPane();
tutorialTabbedPane.setTabPlacement(JtabbedPane.BOTTOM);
// 此处省略向 tutorialTabbedPane 添加选项卡的代码。
mainTabbedPane.addTab(“<html><span style=\”color: red\”> 教 程 </span></html>”,
tutorialTabbedPane);
setTabPlacement(int tabPlacement) 用来设置选项卡出现的位置。在添加选项卡的时候,我们用 HTML
对标题设置了红色。这暗示,选项卡组件是一个 Jlabel。事实上,你可以用 setTabComponentAt(int index,
Component component) 来自定义选项卡组件,这将覆盖 addTab 中指定的标题。例如,你可为选项卡组件增
加一个关闭按钮,以便在单击时调用 remove 方法来移除选项卡和对应的组件。
3.6.2、分隔窗格
分隔窗格(JsplitPane)分隔两个(只能两个)组件。但是因为组件可以嵌套,分隔更多组件很简单。
下图是本小节的“SplitPane”项目:
图 2-47 分隔窗格
单击分割条上的小箭头,可以折叠所指向的组件,再次单击可恢复。
单击左边列表中的项,右边的组件会随之切换,其中“样例项目”对应了另一个分隔组件:
60
图 2-48 嵌套的分隔窗格
setOrientation(int orientation) 用 来 设 置 JSplitPane 的 分 隔 方 向 , 而 是 否 可 折 叠 组 件 通 过
setOneTouchExpandable(boolean newValue) 来设置。
JsplitPane 设置分隔组件的方法共四个,也可以说只有两个:setLeftComponent 和 setTopComponent 都可
以用来设置左边和上面的组件;setRightComponent 和 setButtomComponent 都可以用来设置右边和下面的组
件。提供四个方法是为了名称上的直观。
3.6.3、分层窗格
分层窗格(JlayeredPane)为添加到其中的组件添加了“深度”的概念,这样组件就可以互相重叠。深
度值越大,组件越靠前。JlayeredPane 提供了几个默认的深度:
默认层(DEFAULT_LAYER)
调色板层(PALETTE_LAYER)
模态层(MODAL_LAYER)
弹出层(POPUP_LAYER)
拖动层(DRAG_LAYER)
61
图 2-49 JLayeredPane 提供的默认深度
大多数组件都位于最底部的默认层。
浮动工具栏等位于调色板层。
模态对话框等位于模态层。
弹出窗口等位于弹出层。
拖动层可用于显示拖动组件时的特效。
“LayeredPane”项目演示了这几个层。单击任何一个色板,它就将提升为最顶层色板,而先前的最顶
层色板将分配到被单击的色板原来所在的层:
图 2-50 深度的切换
这是切换深度的代码:
Component top = layeredPane.getComponentsInLayer(JLayeredPane.DRAG_LAYER)[0];
int layer = layeredPane.getLayer(c); // c 是被单击的色板。
layeredPane.setLayer(c, JLayeredPane.DRAG_LAYER);
layeredPane.setLayer(top, layer);
顺便提一下,JFrame 也具有分层,自底向上依次是根窗格(JRootPane)、分层窗格(JLayeredPane)、
内容窗格(Container 对象)和玻璃窗格(Component 对象),大部分组件都位于内容窗格。
3.6.4、桌面窗格和内部窗体
桌面窗格(JDesktopPane)用来实现多文档界面或虚拟桌面,它是 JLayeredPane 的子类,可以添加多个
内部窗体(JInternalFrame)。
请先运行下这次的练习项目“DesktopPane”,尝试建立多个内部窗体:
62
图 2-51 多文档界面
开始,JDesktopPane 只在右下角有一个“工具箱”JInternalFrame。设置各种属性,并单击“新建内部
窗体”,JDesktopPane 的左上角便会新增 JInternalFrame。
下面是添加 JInternalFrame 的代码:
JInternalFrame internalFrame = new JInternalFrame(titleTextField.getText(),
resizableCheckBox.isSelected(), closableCheckBox.isSelected(),
maxmizableCheckBox.isSelected(), iconifiedCheckBox.isSelected());
// 关闭时销毁以便释放内存。
internalFrame.setDefaultCloseOperation(JInternalFrame.DISPOSE_ON_CLOSE);
internalFrame.add(new JLabel(contentTextField.getText()));
// 或者用 setSize 设置大小。必须设置,否则大小为 0。
internalFrame.pack();
// 默认添加到坐标 (0, 0),可通过 setLocation 方法更改。
desktopPane.add(internalFrame);
internalFrame.setVisible(true);
我们看到,JInternalFrame 可以设置的很多属性是 JFrame 办不到的,因为 JInternalFrame 是轻量级的,
它只是被显示到 JDesktopPane 里各层上的组件。
3.7、Swing 高级控件
本节要学习的是剩下的几个 Swing 高级控件。之所以叫高级,是因为使用的难度稍大一些;但同时它
63
们也带来了更丰富的功能。
3.7.1、编辑器窗格和文本窗格
编辑器(JEditorPane)可编辑纯文本、富文本(RTF)和超文本(HTML)三种格式。文本窗格(JTextPane)
是它的子类,增加了添加图片、Swing 组件等功能。
运行“EditorTextPane”项目,首先可以看到一个用 JEditorPane 制作的网页浏览器:
图 2-52 网页浏览器
切换到“文本窗格”,将出现一个带有很多样式的 JTextPane,单击“下载 NetBeans IDE 6.1”按钮会将
你带到 NetBeans IDE 6.1 的下载页面:
64
图 2-53 多样式的 JTextPane
JEditorPane
控制 JEditorPane 的代码非常简单:
try {
editorPane.setPage(urlTextField.getText());
} catch (IOException ex) {
editorPane.setText("<html><head></head><body><span style=\"color: red\"> 无 法 连 接 。
</span></body></html>");
}
Swing 对 HTML 的渲染能力有限,所以很多样式无法现实或者显示混乱。本例中没有处理单击超链接
的事件,所以不能真正浏览网页。你可以向 JEditorPane 添加一个 HyperlinkListener 来侦听这种事件。
JTextPane
JTextPane 的代码稍微复杂一些,不过基本思想很简单:首先初始化一些样式,然后多次同时添加文本
和样式。下面来看具体操作。
首先准备好文本:
String[] strings = new String[] {
"\n",
"NetBeans IDE",
" 完全能够满足您开发各类应用程序的需求!它可以在 Windows、Linux、"
+ "Mac OS X 和 Solaris 操作系统上运行。",
"同时还是免费的开放源代码软件。\n\n",
"\n"
};
每个段落对应一个 String 对象,如果是非文本段落,就用一个换行符,但千万不要用空字符串,这会
造成图片等无法显示。
接下来是所需样式的名称数组:
String[] styles = {
"image", "key", "plain", "bold", "button"
};
然后初始化样式:
StyledDocument doc = textPane.getStyledDocument();
initStyles(doc);
initStyles 方法如下:
Style defaultStyle = StyleContext.getDefaultStyleContext()
.getStyle(StyleContext.DEFAULT_STYLE);
// 常规文本。
Style plain = doc.addStyle("plain", defaultStyle);
// 关键字。
Style style = doc.addStyle("key", plain);
StyleConstants.setFontSize(style, 16);
StyleConstants.setBold(style, true);
StyleConstants.setForeground(style, Color.RED);
65
// 粗体字。
style = doc.addStyle("bold", plain);
StyleConstants.setBold(style, true);
// 图像。
style = doc.addStyle("image", plain);
StyleConstants.setIcon(style, new ImageIcon(
getClass().getResource("/org/myorg/logo.gif")));
// 按钮。
style = doc.addStyle("button", plain);
StyleConstants.setComponent(style, nbButton);
基本上就是 addStyle,再 StyleConstants.setXXX 的操作,参数已经很直观地说明了作用,所以不再赘述。
最后用一个循环添加文本和样式:
for (int i = 0; i < strings.length; i++) {
try {
doc.insertString(doc.getLength(), strings[i], doc.getStyle(styles[i]));
} catch (BadLocationException ex) {
ex.printStackTrace();
}
}
3.7.2、树
要显示一个层次关系分明的一组数据,用树状图表示能给用户一个直观而易用的感觉,JTree 类如同
Windows 的资源管理器的左半部,通过点击可以"打开"、"关闭"文件夹,展开树状结构的图表数据。JTree 也
是依据 M-V-C 的思想来设计的,JTree 的主要功能是把数据按照树状进行显示,其数据来源于其它对象。
例 1:下面是一棵包含六个分枝点的树的例子,来演示 JTree 的实现过程。
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;
class Branch {
//DefaultMutableTreeNode 是树的数据结构中的通用节点,节点也可以有多个子节点。
DefaultMutableTreeNode r;
public Branch(String[] data) {
r = new DefaultMutableTreeNode(data[0]);
//给节点 r 添加多个子节点
for (int i = 1; i < data.length; i++) {
r.add(new DefaultMutableTreeNode(data[i]));
}
}
//返回节点
public DefaultMutableTreeNode node() {
66
return r;
}
}
public class Trees extends JPanel {
String[][] data = {
{"Colors", "Red", "Blue", "Green"},
{"Flavors", "Tart", "Sweet", "Bland"},
{"Length", "Short", "Medium", "Long"},
{"Volume", "High", "Medium", "Low"},
{"Temperature", "High", "Medium", "Low"},
{"Intensity", "High", "Medium", "Low"}
};
static int i = 0; //I 用于统计按钮点击的次数
DefaultMutableTreeNode root, child, chosen;
JTree tree;
DefaultTreeModel model;
public Trees() {
setLayout(new BorderLayout());
root = new DefaultMutableTreeNode("root");//根节点进行初始化
tree = new JTree(root);//树进行初始化,其数据来源是 root 对象
add(new JScrollPane(tree));//把滚动面板添加到 Trees 中
model = (DefaultTreeModel) tree.getModel();//获得数据对象 DefaultTreeModel
JButton test = new JButton("Press me");//按钮 test 进行初始化
test.addActionListener(new ActionListener() {//按钮 test 注册监听器
public void actionPerformed(ActionEvent e) {
if (i < data.length) {//按钮 test 点击的次数小于 data 的长度
child = new Branch(data[i++]).node();//生成子节点
chosen = (DefaultMutableTreeNode) //选择 child 的父节点
tree.getLastSelectedPathComponent();
if (chosen == null) {
chosen = root;
}
model.insertNodeInto(child, chosen, 0);//把 child 添加到 chosen
}
}
});
test.setBackground(Color.blue);//按钮 test 设置背景色为蓝色
test.setForeground(Color.white);//按钮 test 设置前景色为白色
JPanel p = new JPanel();//面板 p 初始化
p.add(test);//把按钮添加到面板 p 中
add(p, BorderLayout.SOUTH);//把面板 p 添加到 Trees 中
67
}
public static void main(String args[]) {
JFrame jf = new JFrame("JTree demo");
//把 Trees 对象添加到 JFrame 对象的*
jf.getContentPane().add(new Trees(), BorderLayout.CENTER);
jf.setSize(350, 500);
jf.setLocationRelativeTo(null);
jf.setVisible(true);
}
}
“Tree”项目演示了一个文件夹浏览器:
图 2-54 文件夹浏览器
JTree 是由节点构成的,最顶层的节点叫根节点。每个节点可包含多个子节点,没有子节点的节点称为
叶结点。一个节点是否为子节点会影响到它的显示方式。
对于节点,一般用 DefaultMutableTreeNode,它的构造方法要传入一个存储该节点对应的数据的对象,
显示为该对象转化为字符串(调用 toString 方法)的结果。
初始化这个 JTree 的代码如下:
// 每次只能选择一个节点。
directoryTree.getSelectionModel().setSelectionMode(
TreeSelectionModel.SINGLE_TREE_SELECTION);
// 根节点。
DefaultMutableTreeNode root = new DefaultMutableTreeNode("我的电脑");
// 列出系统的所有根目录。
File[] rootDirectories = File.listRoots();
FileSystemView fsv = FileSystemView.getFileSystemView();
68
for (File rootDirectory : rootDirectories) {
// 添加节点。因为 File 的 toString 方法不是我们想要的结果,所以自定义一个 Directory
// 类进行封装,它的 toString 方法返回文件的名称。
root.add(new DefaultMutableTreeNode(new Directory(
rootDirectory, fsv.getSystemDisplayName(rootDirectory))));
}
// 设置树模型。我们采用容易使用的 DefaultTreeModel,并覆盖它的 isLeaf 方法,这样所有的
// 节点都显示为文件夹图标。
directoryTree.setModel(new DefaultTreeModel(root) {
@Override
public boolean isLeaf(Object node) {
return false;
}
});
// 显示根节点“我的电脑”。
directoryTree.setRootVisible(true);
一开始并没有装入整个系统上的文件夹(那也是不可能的),所以需要在展开节点的时候查询对应文件
夹的子目录,再动态插入子节点。方法是添加一个 TreeWillExpandListener,在它的 treeWillExpand 方法里进
行处理:
// 从事件对象获取被展开的节点对象。
DefaultMutableTreeNode expandedNode =
(DefaultMutableTreeNode) evt.getPath().getLastPathComponent();
// 获取节点上存储的数据。
Directory dir = (Directory) expandedNode.getUserObject();
// 列出子目录。
File[] subDirs = dir.getDirectory().listFiles(new FileFilter() {
public boolean accept(File pathname) {
return pathname.isDirectory();
}
});
// 根据子目录,添加子节点。
if (subDirs != null) {
for (File subDir : subDirs) {
expandedNode.add(new DefaultMutableTreeNode(new Directory(subDir)));
}
}
最后是按钮的处理代码:
// 获得当前选中的节点。
DefaultMutableTreeNode selectedNode =
(DefaultMutableTreeNode) directoryTree.getLastSelectedPathComponent();
if (selectedNode != null) {
Directory dir = (Directory) selectedNode.getUserObject();
// 排除掉根节点。因为根节点没有存储目录数据。
if (dir != null) {
69
try {
Desktop.getDesktop().open(dir.getDirectory());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
3.7.3、表格
表格(JTable)是 Swing 新增加的组件,主要功能是把数据以二维表格的形式显示出来。Jtable 基于 MVC
模型工作,由接口 TableModel 向表格提供数据。
public interface TableModel
{
//返回模型的行数,JTable 在渲染窗口时用其来确定有多少行要显示。
public int getRowCount();
//返回模型的列数
public int getColumnCount();
//依据索引返回列名,用以初始化表头。列名可以不唯一。
public String getColumnName(int columnIndex); /
/依据索引返回列对象的超类的数据类型
public Class<?> getColumnClass(int columnIndex);
//返回指定的单元格是否可编辑
public boolean isCellEditable(int rowIndex, int columnIndex);
//返回指定单元格的值
public Object getValueAt(int rowIndex, int columnIndex);
//给指定单元格赋值
public void setValueAt(Object aValue, int rowIndex, int columnIndex);
//给表注册监听器
public void addTableModelListener(TableModelListener l);
//去掉表的监听器
public void removeTableModelListener(TableModelListener l);
}
使 用 表 格 , 最 好 先 生 成 一 个 MyTableModel 类 型 的 对 象 来 表 示 数 据 , 这 个 类 是 从 抽 象 类
AbstractTableModel 中继承来的,抽象类 AbstractTableModel 实现了上述 TableModel 接口,并提供了几个监
听器。要使用该类,需要实现以下三个方法:
Public int getRowCount();
Public int getColumnCount();
Public Object getValueAt(int row, int cloumn);
因为 Jtable 会从这个对象中自动获取表格显示所必需的数据,AbstractTableModel 类的对象负责表格大
小的确定(行、列)、内容的填写、赋值、表格单元更新的检测等等一切跟表格内容有关的属性及其操作。
JTable 类生成的对象以该 TableModel 为参数,并负责将 TableModel 对象中的数据以表格的形式显示出来。
此外,JDK 还提供了 DefaultTableModel,它继承自 AbstractTableModel,但当 JTable 与 JScrollPane 结
合时,性能比 AbstarctTableModel 差(其要求所有数据均在内存,但 AbstractTableModel 仅要求 JScrollPane
窗口内的数据在内存即可)。一般有大量数据显示时,用 AbstractTableModel。
70
一般把 JTable 放在与 JScrollPanel 里运行,如果 TableModel 中有关于列标题的数据,则自动显示列标
题,否在显示缺省的列标题。如果视口小于表格的需求,则自动在垂直方向上出现滚动条,而在水平方向
上没有滚动条。要水平滚动条出现,需要调用 setAutoResizeMode(AUTO_RESIZE_OFF)。
如果 JTable 不在 JscrollPane 中运行,则不会自动出现表头。如果需要表头,要独立创建,并结合布局
管理器,才能完成。
container.setLayout(new BorderLayout());
container.add(table.getTableHeader(), BorderLayout.PAGE_START);
container.add(table, BorderLayout.CENTER);
要实现表格排序,有两种方法:
①、直接调 table.setRowSorter(new TableRowSorter(model)),为表设置一个排序对象;
②、直接调用 setAutoCreateRowSorter(true),使用缺省的排序器。
JTable 的构造函数:
⑴、JTable(Object[][] rowData, Object[] columnNames)
⑵、JTable(Vector rowData, Vector columnNames)
这两种构造函数创建的表格有如下约束:自动支持单元格可编辑,所有的数据均以字符串形式显示在
窗口中,所有的数据需要存放在数组或向量中。要避开上述约束,就需要创建自己的表模型。
一般通过继承 AbstractTableModel 或者 DefauktTableModel 创建自己的模型。
继承 AbstractTableModel,常把标题数据放到一个 String[]中,而把表格数据放到 Object[][]中。
JTable 的下列方法可供用户处理行列选择。
Int getSelectedRowCount|ColumnCount:返回选择的行|列数
如果没有排序,下列方法返回的索引值对应模型中索引
Int[] getSelectedRows|Columns:返回选择的行|列索引数组
Int getSelectedRow|Column:返回单个的索引值
如果提供排序功能,要转换成模型中的索引
jtable.convertRowIndexToModel(jt.getSelectedRow()),
jt.convertColumnIndexToModel(jt.getSelectedCol());
例 2:表格组件应用示例
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.JScrollPane;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import java.awt.*;
import java.awt.event.*;
public class TableDemo extends JFrame {
private boolean DEBUG = true;
public TableDemo() { //实现构造方法
super("RecorderOfWorkers"); //首先调用父类 JFrame 的构造方法生成一个窗口
MyTableModel myModel = new MyTableModel();//myModel 存放表格的数据
JTable table = new JTable(myModel);//表格对象 table 的数据来源是 myModel 对象
71
table.setPreferredScrollableViewportSize(new Dimension(500, 70));//表格的显示尺寸
//产生一个带滚动条的面板
JScrollPane scrollPane = new JScrollPane(table);
//将带滚动条的面板添加入窗口中
getContentPane().add(scrollPane, BorderLayout.CENTER);
addWindowListener(new WindowAdapter() {//注册窗口监听器
public void windowClosing(WindowEvent e) {
System.exit(0);
}
});
}
//把要显示在表格中的数据存入字符串数组和 Object 数组中
class MyTableModel extends AbstractTableModel {
//表格中第一行所要显示的内容存放在字符串数组 columnNames 中
final String[] columnNames = {
"First Name",
"Position",
"Telephone",
"MonthlyPay",
"Married"
};
//表格中各行的内容保存在二维数组 data 中
final Object[][] data = {
{"Wangdong", "Executive", "01068790231", new Integer(5000), new Boolean(false)},
{"LiHong", "Secretary", "01069785321", new Integer(3500), new Boolean(true)},
{"LiRui", "Manager", "01065498732", new Integer(4500), new Boolean(false)},
{"ZhaoXin", "Safeguard", "01062796879", new Integer(2000), new Boolean(true)},
{"ChenLei", "Salesman", "01063541298", new Integer(4000), new Boolean(false)}
};
//下述方法是重写 AbstractTableModel 中的方法,其主要用途是被 JTable 对象调用,
//以便在表格中正确的显示出来。程序员必须根据采用的数据类型加以恰当实现。
//获得列的数目
@Override
public int getColumnCount() {
return columnNames.length;
}
//获得行的数目
@Override
public int getRowCount() {
return data.length;
}
//获得某列的名字,而目前各列的名字保存在字符串数组 columnNames 中
72
@Override
public String getColumnName(int col) {
return columnNames[col];
}
//获得某行某列的数据,而数据保存在对象数组 data 中
@Override
public Object getValueAt(int row, int col) {
return data[row][col];
}
//判断每个单元格的类型
@Override
public Class getColumnClass(int c) {
return getValueAt(0, c).getClass();
}
//设置表格可编辑状态
@Override
public boolean isCellEditable(int row, int col) {
if (col < 2) {
return false;
} else {
return true;
}
}
//改变某个数据的值
@Override
public void setValueAt(Object value, int row, int col) {
if (DEBUG) {
System.out.println("Setting value at " + row + "," + col + " to " + value
+ " (an instance of "
+ value.getClass() + ")"
);
}
if (data[0][col] instanceof Integer && !(value instanceof Integer)) {
try {
data[row][col] = new Integer(value.toString());
fireTableCellUpdated(row, col);
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(TableDemo.this, "The " + getColumnName(col) +
" column accepts only integer values.");
}
} else {
data[row][col] = value;
fireTableCellUpdated(row, col);
}
73
if (DEBUG) {
System.out.println("New value of data:");
printDebugData();
}
}
private void printDebugData() {
int numRows = getRowCount();
int numCols = getColumnCount();
for (int i = 0; i < numRows; i++) {
System.out.print(" row " + i + ":");
for (int j = 0; j < numCols; j++) {
System.out.print(" " + data[i][j]);
}
System.out.println();
}
System.out.println("--------------------------");
}
}
public static void main(String[] args) {
TableDemo frame = new TableDemo();
frame.setSize(500, 250);
//将窗体置于屏幕*显示
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
下图为另一个项目“Table”的运行结果:
74
图 2-55 太阳系表
这个表共有五列,第一列是字符串,第二列是图片,第三和第四列是小数,这四列都是不可编辑的。
第五列是可编辑的。
这个程序的大部分工作都在 GUI 生成器里直接输入完成,只有第二列进行了特殊处理:
final JLabel[] images = new JLabel[] {
new JLabel(new ImageIcon(getClass().getResource("/org/myorg/sun.jpg"))),
......
};
ssTable.getColumnModel().getColumn(1).setCellRenderer(new TableCellRenderer() {
public Component getTableCellRendererComponent(
JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column) {
return images[row];
}
});
这里出现了渲染器(Renderer)。渲染器能极大扩展 JTable 的丰富性——它返回一个 Component 对象,
也就是说任何组件都可以成为 JTable 的单元格。前面学习的 JComboBox 和 JList 中也有渲染器的存在。
JTable 还有编辑器(Editor)的概念,也就是说能自定义编辑单元格的方式。这两个概念的细节都超出
了本章的范围,你可以参阅 API 文档以及 The Java Tutorials 得到更多信息。
4、实验内容
利用可视化开发工具完成如图 2-56 所示应用程序界面设计,程序相关功能说明如下:
查询功能:在查询的学号文本框中输入学号,点击“查询”,可以查询到指定学号的学生记录,并显示
在表格当中。
添加功能:点击“添加记录”按钮,弹出添加学生记录模态对话框,填入学生相关信息后,点击确定
按钮添加学生记录,同时更新主窗体表格数据。
75
修改功能:在表格中选择要修改的学生记录,在右边学生信息中会显示选择学生的信息,当修改了学
生信息内容后,点击“更新记录”按钮,保存修改后的学生信息,同时更新表格数据内容。
删除功能:在表格中选择要删除的学生记录,然后选择“删除记录”按钮,删除前应给用户是否删除
的提示,当选择确定删除后,删除指定学生记录内容,同时更新表格数据内容。
程序采用分层结构进行设计,不同功能的类采用包进行组织管理,其中模型类和 JDBC 数据访问操作
内容请参看实验一内容。
图 2-56 程序界面图
实现步骤:
1、新建项目 StuMis,按照功能建立包层次结构。结构可参考下图。
图 2-57 包层次结构图
包结构说:
com.stu.dao 负责管理数据访问接口
com.stu.dao.impl 负责管理数据访问接口实现类
com.stu.model 负责管理模型数据类
com.stu.ui 负责管理用户界面类
com.stu.util 负责管理系统工具类
2、在 com.stu.model 包中新建 Student.java 类,用于构建学生表模型。
3、在 com.stu.util 包中新建 DatabaseBean.java 类,用于封装相关的数据库连接和关闭操作,代码编写请
参看实验一数据库操作内容。
4、在 com.stu.dao 包中新建 IStudentDao.java 数据访问接口,用于封装相关的数据库操作方法。
5、在 com.stu.dao.impl 包中新建 IStudentDao.java 接口的数据访问实现类 StudentDaoImpl.java,用于对
IstudentDao 接口中相关数据库操作方法的具体实现。
6、在 com.stu.util 包中新建 DaoFactory.java 工厂类(这里用到了设计模式中的工厂模式),用于生成数
据操作访问对象。
7、在 com.stu.model 包中新建 StudentTableModel.java 表格模型类,负责表格大小的确定(行、列)、内
76
容的填写、赋值、表格单元更新的检测等等一切跟表格内容有关的属性及其操作。参考代码如下:
public class StudentTableModel extends AbstractTableModel {
//表格中第一行所要显示的内容存放在字符串数组 columnNames 中
final String[] columnNames = {"学号","姓名","性别","年龄","系部"};
//表格中各行的内容保存在集合列表中
List<Student> rowsData;
// 表格模型默认构造方法
public StudentTableModel() {
rowsData = DaoFactory.getStudentDao().getAllStudent();
}
// 表格模型带参数构造方法
public StudentTableModel(String sno) {
rowsData = new ArrayList<Student>();
rowsData.add(DaoFactory.getStudentDao().getStudent(sno));
}
// 获得行的数目
@Override
public int getRowCount() {
return rowsData.size();
}
// 获得列的数目
@Override
public int getColumnCount() {
return columnNames.length;
}
//获得某行某列的数据,而数据保存在集合列表 rowsData 中
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
switch (columnIndex) {
case 0:
return rowsData.get(rowIndex).getSno();
case 1:
return rowsData.get(rowIndex).getSname();
case 2:
return rowsData.get(rowIndex).getSsex();
case 3:
return rowsData.get(rowIndex).getSage();
case 4:
77
return rowsData.get(rowIndex).getSdept();
case 5:
return rowsData.get(rowIndex).getSavgGrade();
default:
return null;
}
}
//获得某列的名字,而目前各列的名字保存在字符串数组 columnNames 中
@Override
public String getColumnName(int column) {
return columnNames[column];
}
}
8、在“项目”窗口中,右键单击 com.stu.ui 包节点,选择“新建”>“JFrame 窗体”,新建一个名为“MainFrame”
的 Jframe 主窗体。点击中间界面“源”按钮,切换到代码编辑视图,可以发现生成的窗体已经为我们写好
了大部分代码。其中代码含义说明如图 2-4-3 所示。
图 2-58 主窗体界面代码
9、然后按照图 2-56 所示设计应用程序界面,并修改相关组件的名称和属性值。注意:由于学号的唯一
性,不允许用户修改,因此,在主界面右侧的学号文本框应将 editable 属性设为 false。
78
10、打开“MainFrame”主窗体,点击中间界面“源”按钮,切换到代码编辑视图,在程序代码底部添
加 private StudentTableModel stuModel = new StudentTableModel();构造一个表格模型对象,用来维护主窗体
表格模型数据。
public class MainFrame extends javax.swing.JFrame {
…….
// End of variables declaration
private StudentTableModel stuModel = new StudentTableModel();
}
11、回到“MainFrame”主窗体设计界面,选择界面中的 JTable 对象,然后在右边的属性窗口中选择
“model”属性,点击右侧 按钮,进入到设置表格模型对话框,在列表框中选择“定制代码”(如图 2-4-4
所示),然后在文本框中输入刚才定义的“stuModel”表模型变量。
图 2-59 Jtable 模型属性设置
12、设置 JTable 对象属性:在主窗体界面选择表格组件,在右边的属性窗口中更改“selecttionModel”
属性为:单一选择。选择“tableHeader”属性,点击右侧 按钮,在表头编辑器对话框中,去掉“允许调整
大小”和“允许重新排序”两个选项,使表格不允许调整大小和重新排序。
13、在表格中选择一条记录,然后在主窗体右边学生信息中会显示相应学生的信息。此功能的实现是
为表格组件添加“mouseClicked”事件。鼠标右键选择表格,在弹出的菜单中依次选择“事件”>“Mouse” >
“mouseClicked”,会进入到表格单击事件代码的编写界面。参考代码如下:
private void studentTableMouseClicked(java.awt.event.MouseEvent evt) {
// 获取表格选定行的索引;如果没有选定的行,则返回 -1
int rowNum = studentTable.getSelectedRow();
if (rowNum >= 0) {
// 获取所选择行的第 0 列的值,也就是学号的值
String sno = (String) studentTable.getValueAt(rowNum, 0);
if (snoText.getText().trim().equals(sno)) {
return;
}
String sname = (String) studentTable.getValueAt(rowNum, 1);
String ssex = (String) studentTable.getValueAt(rowNum, 2);
int sage = (int) studentTable.getValueAt(rowNum, 3);
String sdept = (String) studentTable.getValueAt(rowNum, 4);
snoText.setText(sno);
snameText.setText(sname);
if ("男".equals(ssex)) {
maleRadioButton.setSelected(true);
} else {
79
femaleRadioButton.setSelected(true);
}
sageText.setText(String.valueOf(sage));
sdeptText.setText(sdept);
}
}
14、查询功能实现:鼠标右键选择“查询”按钮,在弹出的菜单中依次选择“事件”>“Action” >
“actionPerformed”,会进入到“查询”按钮的事件代码编写界面。“查询”按钮事件参考代码如下:
private void queryButtonActionPerformed(java.awt.event.ActionEvent evt) {
// 获取要查询的学生学号
String sno = snoQueryText.getText().trim();
if ("".equals(sno)) {
stuModel = new StudentTableModel();
} else {
stuModel = new StudentTableModel(sno);
}
// 设置表格模型
studentTable.setModel(stuModel);
snoQueryText.setText("");
}
15、更新记录功能实现:鼠标右键选择“更新记录”按钮,在弹出的菜单中依次选择“事件”>“Action” >
“actionPerformed”,会进入到“更新记录”按钮的事件代码编写界面。“更新记录”按钮事件参考代码如下:
private void updateStudentButtonActionPerformed(java.awt.event.ActionEvent evt) {
String sno = snoText.getText().trim();
String sname = snameText.getText().trim();
String ssex;
if (maleRadioButton.isSelected()) {
ssex = maleRadioButton.getText();
} else {
ssex = femaleRadioButton.getText();
}
String sage = sageText.getText().trim();
String sdept = sdeptText.getText().trim();
if ("".equals(sno) || sno == null) {
JOptionPane.showMessageDialog(this, "请选择一条要修改的学生记录!");
return;
}
// 验证修改信息的有效性
if (!validateData(sno, sname, ssex, sage, sdept)) {
return;
}
80
Student stu = new Student(sno, sname, ssex, Integer.parseInt(sage), sdept);
//如果数据验证正确,则插入学生记录内容
if (!DaoFactory.getStudentDao().updateStudent(stu)) {
JOptionPane.showMessageDialog(this, "学生记录更新失败,请检查输入的数据内容!");
}
// 数据发生变化,更新表格模型
stuModel = new StudentTableModel();
studentTable.setModel(stuModel);
}
// 验证输入信息的有效性
private boolean validateData(String sno, String sname, String ssex, String sage, String
sdept) {
if ("".equals(sno) || !Tools.isNumeric(sno) || sno.length() != 7) {
JOptionPane.showMessageDialog(this, "请输入正确的学号,并且学号为数字!");
snoText.requestFocus();
return false;
}
if ("".equals(sname)) {
JOptionPane.showMessageDialog(this, "请输入学生姓名!");
snameText.requestFocus();
return false;
}
try {
if (Integer.parseInt(sage) < 15 || Integer.parseInt(sage) > 45) {
JOptionPane.showMessageDialog(this, "学生年龄范围在 15 到 45 岁之间!");
sageText.requestFocus();
return false;
}
} catch (NumberFormatException e) {
JOptionPane.showMessageDialog(this, "请输入正确的年龄!");
sageText.requestFocus();
return false;
}
if ("".equals(sdept)) {
JOptionPane.showMessageDialog(this, "请输入系部名称!");
sdeptText.requestFocus();
return false;
}
return true;
}
16、删除记录功能实现:鼠标右键选择“删除记录”按钮,在弹出的菜单中依次选择“事件”>“Action” >
“actionPerformed”,会进入到“删除记录”按钮的事件代码编写界面。“删除记录”按钮事件参考代码如下:
private void deleteStudentButtonActionPerformed(java.awt.event.ActionEvent evt) {
int rowNum = studentTable.getSelectedRow();
81
if (rowNum >= 0) {
String sno = snoText.getText().trim();
int choise = JOptionPane.showConfirmDialog(this, "是否要删除学生的信息内容", "提
示", JOptionPane.YES_NO_OPTION);
if (choise == JOptionPane.YES_OPTION) {
if (!DaoFactory.getStudentDao().deleteStudent(sno)) {
JOptionPane.showMessageDialog(this, "学生信息删除失败,请与系统管理员联系!");
}
stuModel = new StudentTableModel();
studentTable.setModel(stuModel);
}
} else {
JOptionPane.showMessageDialog(this, "请选择要删除的学生信息!");
}
snoText.setText(null);
snameText.setText(null);
maleRadioButton.setSelected(true);
sageText.setText(null);
sdeptText.setText(null);
}
17、当要添加新的学生记录时,将会弹出一个添加学生记录对话框。首先,我们需要设计一个添加学
生记录对话框界面。在“项目”窗口中,右键单击 com.stu.ui 包节点,依次选择“新建”>“其他…”,在弹
出的“新建文件”对话框“类别”中选择“Swing GUI 窗体”,在右边“文件类型”中选择“确定/取消对话
框样例窗体”,新建一个自带“确定/取消”按钮的名为“AddStudentDialog”对话框窗体。点击中间界面“源”
按钮,切换到代码编辑视图,可以发现生成的窗体已经为我们写好了大部分代码。然后按照图 2-4-1 中添加
学生记录对话框所示设计应用程序界面,并修改相关组件的名称和属性值。修改“确定”按钮事件代码如
下所示:
private void okButtonActionPerformed(java.awt.event.ActionEvent evt) {
Student stu = new Student();
String sno = snoText.getText().trim();
String sname = snameText.getText().trim();
String ssex;
if (maleRadioButton.isSelected()) {
ssex = maleRadioButton.getText();
} else {
ssex = femaleRadioButton.getText();
}
String sage = sageText.getText().trim();
String sdept = sdeptText.getText().trim();
if (!validateData(sno, sname, ssex, sage, sdept)) {
return;
}
if (DaoFactory.getStudentDao().findStudent(sno)) {
82
JOptionPane.showMessageDialog(this, "指定学号的学生记录已经存在,请重新输入!");
return;
}
stu.setSno(sno);
stu.setSname(sname);
stu.setSsex(ssex);
stu.setSage(Integer.parseInt(sage));
stu.setSdept(sdept);
//如果数据验证正确,则插入学生记录内容
if (!DaoFactory.getStudentDao().insertStudent(stu)) {
JOptionPane.showMessageDialog(this, "学生记录插入失败,请检查输入的数据内容!");
} else {
doClose(RET_OK);
}
}
18、添加记录功能实现:鼠标右键选择“添加记录”按钮,在弹出的菜单中依次选择“事件”>“Action” >
“actionPerformed”,会进入到“添加记录”按钮的事件代码编写界面。“添加记录”按钮事件参考代码
如下:
private void addStudentButtonActionPerformed(java.awt.event.ActionEvent evt) {
AddStudentDialog dialog = new AddStudentDialog(this, true);
dialog.setLocationRelativeTo(this);
dialog.setVisible(true);
if (AddStudentDialog.RET_OK == dialog.getReturnStatus()) {
stuModel = new StudentTableModel();
studentTable.setModel(stuModel);
}
}
19、调试应用程序,完善程序相关功能。
5、实验总结
请书写你对本次实验有哪些实质性的收获和体会,以及对本次实验有何良好的建议?
83
实验三、简单 Web 应用程序设计(2 学时)
1、实验目的
(1)掌握 HTML5 和 CSS3 的基本语法。
(2)掌握 JSP 和 SERVELT 应用程序的编写方法。
(3)掌握通过可视化开发工具完成简单 WEB 应用程序的构建、设计和部署的方法。
2、实验性质
验证性实验
3、实验导读
3.1、Web 基础知识
Web 是 World Wide Web (缩写为 WWW) 的简称,也成为万维网。简单来说,Web 是建立在 Internet(国
际互联网,也称因特网)之上的一种应用,它用 URI (Uniform Resource Identifier,统一资源标识符) 来标识
分布在世界各地机器上的资源,如文本、图像等,客户可以根据 URI 来访问这些机器上的资源,以实现全
球网络资源的共享。
如今随着互联网技术的飞速发展,人们对网络的依赖不断加深。其中,浏览器是一个连接 Internet 主
要的工具,我们熟悉的浏览器包括微软的 IE,Mozilla Firefox,Google Chrome 等等。那么浏览器的真正作
用是什么呢?它是如何工作的呢?
浏览器是万维网(Web)服务的客户端浏览程序,可向万维网(Web)服务器发送各种请求,并对从服
务器发来的超文本信息和各种多媒体数据格式进行解释、显示和播放。
我们通过浏览器在互联网上浏览新闻,看电影,购物等这些行为看似是顺理成章的事。其实,这一切
的行为都是浏览器通过与远在各地的 Web 服务器进行交互。为了交互的进行,它们需要共同遵守一定的协
议来控制,这就是 HTTP (Hypertext Transport Protocol),超文本传输协议,一种详细规定了浏览器和 web 服
务器之间互相通信的规则,通过因特网传送万维网文档的数据传送协议。
3.1.1、Web 应用程序的运行原理
在传统的 Web 应用程序开发中,需要同时开发客户端和服务器端的程序。由服务器端的程序提供基本
的服务,客户端是提供给用户的访问接口,用户可以通过客户端的软件访问服务器提供的服务。这种 Web
应用程序的开发模式就是传统的 C/S 开发模式。在这种模式中,由服务器端和客户端的共同配合来完成复
杂的业务逻辑。例如以前的网络软件中,一般都会采用这种模式,而且现在的网络游戏中,一般还会采用
这种 Web 开发模式。在这些 Web 应用程序中,都是需要用户安装客户端才可以使用的。
在目前的 Web 应用程序开发中,一般情况下会采用另一种开发模式。在这种开发模式中,不再单独开
发客户端软件,客户端只需要一个浏览器即可。这个浏览器在每个操作系统中都是自带的,软件开发人员
只需专注开发服务器端的功能,用户通过浏览器就可以访问服务器提供的服务。这种开发模式就是当前流
行的 B/S 架构。在这种架构中,只需要开发服务器端的程序功能,而无须考虑客户端软件的开发,客户通
过一个浏览器就可以访问应用系统提供的功能。这种架构是目前 Web 应用程序的主要开发模式,例如各大
门户网站、各种 Web 信息管理系统等,使用 B/S 架构加快了 Web 应用程序开发的速度,提高了开发效率。
3.1.2、C/S 架构开发模式
84
在 Web 应用程序的开发中,存在着两种开发模式,一种是传统的 C/S 架构,另一种是近些年兴起的 B/S
架构。
所谓的 C/S 架构,就是客户端/服务器端的架构形式。在这种架构方式中,多个客户端围绕着一个或者
多个服务器,这些客户端安装在客户机上,负责用户业务逻辑的处理,在服务器端仅仅对重要的过程和数
据库进行处理和存储,每个服务器端都分担着服务器的压力,这些客户端可以根据不同的用户的需求进行
定制。C/S 这种架构方式的出现大大提高了 Web 应用程序的效率,给软件开发带来革命性的飞跃。但是,
随着时间的推移,C/S 架构的弊端开始慢慢显现。在 C/S 架构中,系统部署的时候需要在每个用户的机器上
安装客户端,这样的处理方式带来很大的工作量,而且在 C/S 架构中,软件的升级也是很麻烦的一件事情,
哪怕是再小的一点改动,都得把所有的客户端全部修改更新。这些致命的弱点决定了 C/S 结构的命运。在
C/S 架构模式流行一段时间以后,逐渐被另一种 Web 应用系统的架构方式所代替。这种新的 Web 软件架构
的模式就是 B/S。
3.1.3、B/S 架构开发模式
B/S 架构就是浏览器/服务器的架构形式。在这种架构方式中,采取了基于浏览器的策略,简化了客户
端的开发工作。在 B/S 架构的客户机中,不用安装客户端软件,只要有通用的浏览器工具,就可以访问服
务器端提供的服务,这些浏览器工具都是遵循着相同的协议规范,所以 B/S 结构的客户端在各种系统环境
中都已经实现。而且,在浏览器访问服务器的过程中,使用的是 HTTP 协议,所以这种方式非常容易就可
以穿过防火墙的限制。 在 B/S 结构的服务器端,也不用处理通信相关的问题,这些问题都由 Web 服务器提
供,Web 服务器处理用户的 HTTP 请求,开发人员只需要专注开发业务逻辑功能即可。而且软件的部署和
升级维护也变得非常简单。只需要把开发的 Web 应用程序部署在 Web 服务器中即可,而客户端根本不需要
做任何改动,这是在 C/S 架构中无法实现的。
但是 B/S 架构也有自身的一些缺点,例如界面元素单调。在 B/S 结构的程序中,失去了桌面应用程序
丰富的用户界面,程序在交互性上没有 C/S 架构那么人性化。 在 C/S 和 B/S 两种架构之间,并没有严格的
界限,两种架构没有好坏之分,使用这两种架构都可以实现系统的功能。开发人员可以根据实际的需要进
行选择。例如需要丰富的用户体验,那就选择 C/S 架构。在目前的网络游戏中,基本都是选择 C/S 架构。
如果更偏重的是功能服务方面的实现,就需要选择 B/S 架构,这也正是目前绝大部分管理应用系统采用的
软件架构方法。
3.1.4、HTTP 协议原理
HTTP 协议是一种通信协议。它允许将 HTML (超文本标记语言)从 Web 服务器传送到 Web 客户
端浏览器。因此需要 Web 服务器和 Web 客户端浏览器都支持该协议。当浏览器向 Web 服务器发送一个
请求, Web 服务器在接受到这个请求后,会返回一个响应给浏览器。这个请求包含一个请求页面的名字和
请求页面的信息等。返回的响应包含被请求页面的信息以及服务器的一些信息等。
它的具体请求、响应格式如图 3-1 所示:
图 3-1 HTTP 协议请求、响应格式
85
当用户在浏览器地址栏中输入某个 URL 地址,或单击网页上一个超链接,或提交网页上的 Form 表
单后,浏览器将生成请求消息发送给服务器。服务器收到请求后,将生成响应消息回送给浏览器。浏览器
发出的请求信息和 Web 服务器回送的响应信息都叫 HTTP 消息,HTTP 消息是有一定严格规定的格式。
浏览器提交给 Web 服务器的 FORM 表单内容和从 Web 服务器上获取的网页内容仅仅是 HTTP 消息中
的一部分数据,浏览器与 Web 服务器传递的信息中还包括一般用户所看不到的一些其他“隐藏”信息。
3.1.5、HTTP 请求格式
HTTP 协议对浏览器所发出的请求 request 格式由以下三部分组成:
第一部分是 request line。包括请求的方法、所请求资源的名字以及所使用的协议
第二部分是 request headers。它包含浏览器的一些信息。
第三部分是 body。其中 request headers 与 body 之间有个空行。
它的具体结构如图 3-2 所示:
图 3-2 HTTP 请求格式
其中,METHOD 表示请求的方法,如“POST”、“GET”。Path-to-resource 表示请求的资源。
HTTP/version-number 表示 HTTP 协议的版本号。
一个完整的请求消息包括:一个请求行、若干消息头,以及实体内容,其中的一些消息头和实体内容
都是可选的,消息头和实体内容之间要用空行隔开(即只有回车和换行两个字符的一个单独行)。下面是一
个 HTTP 请求消息的内容:
GET /book/java.html HTTP/1.1
Accept: */*
Accept-Language: en-us
Connection: Keep-Alive
Host: localhost
Referer: http://localhost/links.asp
User-Agent: Mozilla/4.0 (compatible; MSIE 5.5; Windows NT 5.0)
Accept-Encoding: gzip, deflate
[空行]
上面的请求消息中的第一行为请求行,请求行后面的内容都属于消息头部分。虽然该请求消息中没有
实体内容(正文),但紧接着消息头部分之后的一个空行必不可少,这个空行表示消息头部分已经结束。浏
览器使用 GET 方式发送类似上面的不包含实体内容的请求消息;只有使用 POST、PUT 和 DELETE 方式
的请求消息中才可以包含实体内容。
3.1.6、HTTP 响应格式
HTTP 协议对 Web 服务器所返回的响应 response 也有具体的格式规定,和 request 一样,response 也
86
分为三部分:
第一部分是 response line。包括请求的方法、所请求资源的名字以及所使用的协议
第二部分是 response headers。它包含浏览器的一些信息。
第三部分是 body。其中 response headers 与 body 之间也有个空行。
它的具体结构如图 3-3 所示:
图 3-3 HTTP 响应格式
其中,HTTP/version-number 表示 HTTP 协议的版本号。Statuscode 表示服务器返回的状态码。Message
表示服务器返回的状态消息。
一个完整的响应消息包括一个状态行、若干消息头,以及实体内容。与请求消息一样,响应消息中的
一些消息头和实体内容也都是可选的,消息头和实体内容之间也要用空行隔开。下面是一个 HTTP 响应消
息的内容:
HTTP/1.1 200 OK
Server: Microsoft-IIS/5.0
Date: Thu, 13 Jul 2000 05:46:43 GMT
Content-Length: 2291
Content-Type: text/html
Set-Cookie: ASPSESSIONIDQQGGGNCG=LKLDFFKCINFLDMFHCBCBMFLJ; path=/
Cache-control: private
[空行]
<HTML>
<BODY>
……
上面的响应消息中的第一行为状态行,紧跟状态行后面的内容是消息头部分,接着是一个空行,然后
是实体内容。在通常情况下,响应消息中都包含实体内容,响应消息的实体内容就是网页文档的内容。上
图中返回的状态码是 200,状态信息是 OK。表示服务器响应成功,请求被成功的完成,所请求的资源被
发送到客户端。
响应状态码用于表示服务器对请求的各种不同处理结果和状态,它是一个三位的十进制数。响应状态
码可归为 5 种类别,使用最高位为 1 到 5 来进行分类,如下所示:
1xx:表示成功接收请求,要求客户端继续提交下一次请求才能完成整个处理过程
2xx:表示成功接收请求并已完成整个处理过程
3xx:重定向--要完成请求必须进行更进一步的操作
4xx:客户端错误--请求有语法错误或请求无法实现
5xx:服务器端错误--服务器未能实现合法的请求
常见状态代码、状态描述、说明:
200 OK //客户端请求成功
87
400 Bad Request //客户端请求有语法错误,不能被服务器所理解
401 Unauthorized //请求未经授权,这个状态代码必须和 WWW-Authenticate 报头域一起使用
403 Forbidden //服务器收到请求,但是拒绝提供服务
404 Not Found //请求资源不存在,eg:输入了错误的 URL
500 Internal Server Error /服务器发生不可预期的错误
503 Server Unavailable //服务器当前不能处理客户端的请求,一段时间后可能恢复正常
3.1.7、Content Type、MIME 类型和字符编码
服务器在接受到请求后,必须能识别要发送的信息类型,比如图片、txt 文本、excel 表格还是其他的
形式。要需要知道网页的编码方式是什么。因此,在 response headers 中定义的 Content-Type 就是用于定
义网络文件的类型以及网页字符的编码。用于决定浏览器以什么形式、什么编码读取这个文件。
MIME(Multipurpose Internet Mail Extensions),即多功能 Internet 邮件扩充服务。它是一种多用途网际
邮件扩充协议, 是描述消息内容类型的因特网标准,服务器会通过这种手段来告诉浏览器它所发送的这些
多媒体数据是什么类型的,需要用何种程序来打开这种文件。在 HTTP 中,MIME 类型被定义在 Content-Type
header 中。最常用的 MIME 类型如下表所示:
表 3-1 常用的 MIME 类型
名称 MIME 类型
超文本标记语言(.html) text/html
普通文本(.txt) text/plain
Microsoft Word(.doc) application/msword
PDF 文档(.pdf) application/pdf
AVI 文件(.avi) video/x-msvideo
JPEG 图形(.jpeg, .jpg) image/jpeg
PNG 图像(.png) Image/png
字符的编码方式有很多种。有的支持中文显示,有的支持英文显示,其中最常见的字符集编码类型如
下表所示:
表 3-2 常见的字符编码类型
charset 类型 字符集编码类型
ISO-8859-1 拉丁语系 1
Big5 繁体中文
UTF-8 通用子集转化格式(8 位)
ISO-2022-JP 日本语
ISO-2022-KR 韩国语
GBK 简体中文(兼容 GB2312)
GB2312 汉字国标码
3.1.8、GET 和 POST 方式传递参数
在 URL 地址后面可以附加一些参数,每个参数由参数名和参数值组成,参数名与参数值之间用等号
(=)分隔,各个参数之间用&分隔,URL 地址与整个参数部分之间用问号(?)分隔,如下所示:
http://www.myweb.com/servlet/ParamsServlet?param1=abc¶m2=xyz
在一个 URL 地址后面附加参数有很大的用处,这使得 URL 地址所指向的 Web 资源可以根据参数值
的不同返回不同的结果内容,例如,我们可以通过访问某一个 URL 地址来分别读取 BBS 中的各个帖子,
88
那就需要将帖子的编号作为参数分别附加到这个 URL 地址后面。
当用户在浏览器地址栏中直接输入某个 URL 地址或单击网页上一个超链接时,浏览器将使用 GET 方
式发送请求。如果将网页上的<form>表单的 method 属性设置为“GET”或没有设置 method 属性(<form>
表单的默认值为“GET”),当用户提交表单时,浏览器也将使用 GET 方式发送请求。
如果浏览器请求的 URL 中有参数部分,在浏览器生成的请求消息中,参数部分将附加在请求行中的
资源路径后面。例如,对于如下的 URL 地址:
http://www.myweb.com/servlet/ParamsServlet?param1=abc¶m2=xyz
浏览器发送给 www.myweb.com 服务器的请求消息如下所示:
GET /servlet/ParamsServlet?param1=abc¶m2=xyz HTTP/1/1
Host: www.myweb.com
当使用 GET 方式提交表单内容时,浏览器将各个表单字段元素及其数据按照 URL 参数的格式附加在
请求行中的资源路径后面。使用 GET 方式传送的数据量是有限制的,一般限制在 1KB 以下。
如果将网页上的<form>表单的 method 属性设置为“POST”,当用户提交表单时,浏览器将使用 POST
方式提交表单内容,并把各表单字段元素及其数据作为 HTTP 消息的实体内容发送给 Web 服务器,而不
是作为 URL 地址的参数传递,因此,使用 POST 方式传送的数据量要比使用 GET 方式传送的数据量大
得多。在使用 POST 方式向服务器传递数据时,还必须将 Content-Type 消息头设置为
“application/x-www-form-urlencoded”,将 Content-Length 消息头设置为实体内容的长度,如下所示:
POST /servlet/ParamsServlet HTTP/1.1
Host:
Content-Type: application/x-www-form-urlencoded
Content-Length: 28
[空行]
param1=abc¶m2=xyz
对于这种 POST 方式传递的请求消息,Web 服务器端的程序可以采用获取 URL 后面的参数一样的方
式来获取各表单字段元素的数据。
3.1.9、Web 服务器汇总
下面简单的介绍 B/S 结构中常用的 WEB 服务器:
❑ IIS 是微软提供的一种 Web 服务器,提供对 ASP 语言的良好支持,通过插件的安装,也可以提供
对 PHP 语言的支持。
❑ Apache 服务器是由 Apache 基金组织提供的一种 Web 服务器,其特长是处理静态页面,对于静态
页面的处理效率非常高。
❑ Tomcat 也是 Apache 基金组织提供的一种 Web 服务器,提供对 JSP 和 Servlet 的支持。通过插件的
安装,同样可以提供对 PHP 语言的支持,但是 Tomcat 只是一个轻量级的 Java Web 容器,像 EJB 这样的服
务在 Tomcat 中是不能运行的。
❑ JBoss 是一个开源的重量级的 Java Web 服务器。在 JBoss 中,提供对 J2EE 各种规范的良好支持,
而且 JBoss 通过了 Sun 公司的 J2EE 认证,是 Sun 公司认可的 J2EE 容器。
❑ 另外,J2EE 的服务器还有 BEA 的 Weblogic 和 IBM 的 WebSphere 等,这些产品的性能都是非常优
秀的,可以提供对 J2EE 的良好支持。用户可以根据自己的需要选择合适的服务器产品。
3.2、HTML 基础知识
HTML(Hypertext Markup Language),即超文本标记语言,是用于描述网页文档的一种标记语言。它
是一种规范,一种标准,通过标记符号来标记要显示网页的各个部分。所以在学习 Web 开发之前,首先要
掌握 HTML 的相关基础知识。HTML 的大致结构如图 3-4 所示:
89
图 3-4 HTML 页面结构
由于 HTML 基础知识内容涉及较多,本节只做简单介绍,详细内容可以参考以下几个推荐的网站,对
于学习有很大的帮助。
慕课网 http://www.imooc.com/course/list
W3school http://www.w3school.com.cn/index.html
3.2.1、文本域
在 HTML 的标签库中有 <form></form>这样一个成对标签,它用于向一个目标地址提交一些数据。在
这个标签中,我们可以设计文本框、单选框、复选框和按钮等等一些元素用于获取数据,这些元素都称为
表单元素。下面介绍几个最常用的表单元素。
在 HTML 页面最常用的就是一个单行的文本域了。当用户要在表单中键入字母、数字等内容时,就会
用到文本域。它的语法格式如下,在浏览器页面显示的效果如下图所示:
<form>
First name:
<input type="text" name="firstname" />
<br />
Last name:
<input type="text" name="lastname" />
</form>
3.2.2、密码域
图 3-5 显示效果
在 HTML 页面的文本域中,当 type 类型指定为 password 时即为密码域,当在密码域中键入字符时,
浏览器将使用*来代替这些字符。在浏览器页面显示的效果如下图所示
<form>
用户:<input type="text" name="user">
<br />
密码:<input type="password" name="password">
</form> 图 3-6 显示效果
3.2.3、label 标签
<label> 标签为 input 元素定义标注(标记)。
label 元素不会向用户呈现任何特殊效果。不过,它为鼠标用户改进了可用性。如果您在 label 元素内
点击文本,就会触发此控件。就是说,当用户选择该标签时,浏览器就会自动将焦点转到和标签相关的表
单控件上。<label> 标签的 for 属性应当与相关元素的 id 属性相同。
<form>
90
<label for="male">Male</label>
<input type="radio" name="sex" id="male" />
<br />
<label for="female">Female</label>
<input type="radio" name="sex" id="female" />
</form>
3.2.4、单选按钮
当用户从若干给定的的选择中选取其一时,就会用到单选框。对于一组元素,必须保证它们的 name 属
性值相同。示例代码如下所示,在浏览器上显示的效果如图 3-7 所示:
<form>
<input type="radio" name="sex" value="male" /> Male
<br />
<input type="radio" name="sex" value="female" /> Female
</form>
图 3-7 显示效果
3.2.5、复选按钮
当用户需要从若干给定的选择中选取一个或若干选项时,就会用到复选框。对于同一组值,必须要保
证它们的 name 值相同。示例代码如下所示,在浏览器上显示的效果如图 3-8 所示:
<form>
<input type="checkbox" name="bike" />
I have a bike
<br />
<input type="checkbox" name="car" />
I have a car
</form>
图 3-8 显示效果
3.2.6、下拉列表
当用户需要从下拉框中选择一个值时,就会用到下拉列表。select 元素可创建单选或多选菜单。
<select> 元素中的<option>标签用于定义列表中的可用选项。注意:以下代码的加字符底纹的部分表示
当前项为下拉列表的预设项。示例代码如下所示,在浏览器上显示的效果如图 3-9 所示:
<form>
<select name="cars">
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="fiat" selected="selected">Fiat</option>
<option value="audi">Audi</option>
</select>
</form>
图 3-9 显示效果
3.2.7、文本域
<textarea> 标签定义多行的文本输入控件。文本区中可容纳无限数量的文本,其中的文本的默认字体是
等宽字体(通常是 Courier)。可以通过 cols 和 rows 属性来规定 textarea 的尺寸,不过更好的办法是使用
CSS 的 height 和 width 属性。示例代码如下所示,在浏览器上显示的效果如图 3-10 所示:
91
<textarea rows="10" cols="30">
The cat was playing in the garden.
</textarea>
图 3-10 显示效果
3.2.8、表单的动作属性(Action)和确认按钮
当用户单击确认按钮时,表单的内容会被传送到另一个文件。表单的动作属性定义了目的文件的文件
名。由动作属性定义的这个文件通常会对接收到的输入数据进行相关的处理。示例代码如下所示,在浏览
器上显示的效果如图 3-11 所示:
<form name="input" action="html_form_action.jsp" method="get">
Username:
<input type="text" name="user" />
<input type="submit" value="Submit" />
</form>
图 3-11 显示效果
3.2.9、综合案例应用
<!DOCTYPE html>
<html>
<head>
<title>表单示例程序</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
</head>
<body>
<form action="register.jsp" method="GET">
<input type="hidden" name="type" value="1"/>
名字:<input type="text" name="name" value="你的名字"/>
<br/><br/>
密码:<input type="password" name="pass" />
<br/><br/>
选择喜欢的颜色:<br/>
<input type="radio" name="color" value="red" checked="checked"/>红色
<input type="radio" name="color" value="green"/>绿色
<input type="radio" name="color" value="blue"/>蓝色
<br/><br/>
选择爱好的运动:<br/>
<input type="checkbox" name="sport" value="basked"/>篮球
<input type="checkbox" name="sport" value="foot"/>足球
<input type="checkbox" name="sport" value="pingpong"/>乒乓球
<br/><br/>
选择要睡觉的时间:
92
<select name="sleep_time">
<option value="short">6 小时</option>
<option value="medium" selected="selected">8 小时</option>
<option value="long">10 小时</option>
</select>
<br/><br/>
个人简介:<br/>
<textarea rows="5" cols="20">在此填写简介</textarea>
<br/><br/>
<input type="submit" value="提交" />
<input type="reset" value="重置" />
</form>
</body>
</html>
页面运行效果如图 3-12 所示:
图 3-12 页面运行效果
3.3、开发环境的搭建
在本地计算机上随便创建一个 web 页面,用户是无法访问到的,但是如果启动 tomcat 服务器,把 web
页面放在 tomcat 服务器中,用户就可以访问了。这说明什么问题?
1、不管什么 web 资源,想被远程计算机访问,都必须有一个与之对应的网络通信程序,当用户来访问
时,这个网络通信程序读取 web 资源数据,并把数据发送给来访者。
2、WEB 服务器就是这样一个程序,它用于完成底层网络通迅。使用这些服务器,We 应用的开发者只
需要关注 web 资源怎么编写,而不需要关心资源如何发送到客户端手中,从而极大的减轻了开发者的开发
工作量。
如果需要进行 Java Web 开发,还需要安装 Web 服务器,这里选择 Tomcat 服务器。Tomcat 服务器是由
Apache 开源组织开发并维护的,能够支持 JSP 和 Servlet 开发使用,而且 Tomcat 服务器是免费产品,并且
提供了其源代码。本实验推荐使用 Tomcat7 版本。
Tomcat 官方站点:http://jakarta.apache.org
下载 Tomcat 安装程序包:http://tomcat.apache.org/
93
tar.gz 文件是 Linux 操作系统下的安装版本
exe 文件是 Windows 系统下的安装版本
zip 文件是 Windows 系统下的压缩版本
Tomcat 的默认服务端口是 8080,可以通过管理 Tomcat 配置文件来改变该服务端口,甚至可以通过修
改配置文件让 Tomcat 同时在多个端口提供服务。
94
Tomcat 的配置文件都放在 conf 目录下,使用无格式的编辑器打开 conf 下的 server.xml 文件,找到如
下代码:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
其中, port=“8080” 就是 Tomcat 提供 Web 服务的端口。
如果 Tomcat 服务程序处于启动状态,当访问 http://localhost:8080/ 时,浏览器提示“该页无法显示”
或者返回的不是 Tomcat 提供的首页,这很可能是因为 Tomcat 服务程序所使用的网络监听端口号已被其
他网络服务程序或 Web 服务程序占用,导致 Tomcat 服务程序并没有真正正常启动运行。
我们可以进入命令行窗口,执行 netstat –nao 命令,查看 TCP 监听端口列表中是否包含 Tomcat 的
Web 服务绑定的监听端口。如果在下图中看见了为 Tomcat 的 Web 服务绑定的监听端口,这说明该端口
已经被其他网络程序所使用,查看被占用端口对应的 PID。例如,本例中是“3844”,然后进入到任务管理
器,切换到进程选项卡,在 PID 一列查看“3844”对应的进程是谁。
图 3-13 查看端口号
3.3.1、配置 Tomcat 网站根目录
Tomcat 默认网站根目录为<Tomcat 安装目录>\webapps\ROOT(注意:ROOT 必须是大写),此时可以
通过 http://localhost:8080/ 访问到 Tomcat 自带的 Tomcat Manager。
如果想把 Tomcat 的默认网站根目录修改成自己指定的目录,比如:F:/MyWeb。这样以后把自己写的
index.jsp 放到该目录下,就能通过 http://localhost:8080/index.jsp 来访问我的 F:/MyWeb/index.jsp 文件。其实
就是修改 conf 目录中的 server.xml。
找到</Host>标签,在之前加入这样一行:
<Context path="" docBase="F:/MyWeb" debug="0" reloadable="true" crossContext="true" />
重启 Tomcat,OK。
对上面语句做下解释:该句是设置 Tomcat 的虚拟路径,书写语法是<Context path=“虚拟目录”docBase=
“实际目录”debug=“0”reloadable=“true”crossContext=“true”/>,将网站实际根目录映射到了 F:/MyWeb,
更改了网站根目录的映射。
3.3.2、配置 Tomcat 网站虚拟子目录
如果要配置 Web 站点的虚拟子目录,要映射为 http://localhost:8080/myweb/index.jsp,有两种配置方法。
方法一
在 Tomcat 中设置虚拟子目录的最基本方式就是在<Tomcat 主目录>/conf/server.xml 文件中设置
95
<Context>元素。在 server.xml 中,<Context>元素必须嵌套在<Host>元素之中,一个<Host>元素表示一
个 Web 站点,其中可以包含多个<Context>子元素,每个<Context>子元素分别对应该站点下的一个虚
拟 Web 目录。 <Context>元素中指定的虚拟 Web 子目录名称与本地文件系统的目录名称没有必然的
关联,两者的名称可以不一致,例如,只要在<Tomcat 主目录>/conf/server.xml 文件的<Host>元素中增
加如下一条语句:
<Context path=“/myweb" docBase="F:\MyWeb" debug="0" />
即可将本地计算机文件系统上的 F:\MyWeb 目录映射成 Web 站点的 /myweb 虚拟子目录。<Context>
元素的 docBase 除了可以指向一个目录外,它还可以指向一个 war (Web Application Archive)文件。
在%tomcat%\conf\server.xml 中添加 context,找到</Host>标签,在之前加入这样一行:
<Context path=“/myweb" docBase="F:/MyWeb" debug="0" reloadable="true" crossContext="true"
/>
方法二
除了可以在 server.xml 文件中使用<Context>元素来设置 Web 应用的虚拟目录外,还可以将
server.xml 文件中的<Host>元素的 autoDeploy 属性设置为 TRUE,然后让 Tomcat 在启动 时自动按
如下几种方式来创建 Web 应用程序的虚拟目录。
如果<Tomcat 主目录>/conf/<引擎名>/<主机名>目录中的 XML 文件中包含<Context>元素设置,这
些<Context>元素与直接包含在 server.xml 文件中<Context>元素具有同样的作用效果,例如,<Tomcat
主目录>/conf/Catalina/localhost 目录中的 myweb.xml 文件。
如果<Host>元素指定的 appBase 目录下的子目录中包含 WEB-INF/web.xml 文件,这些子目录将
被自动设置成各自独立的 Web 应用程序,其虚拟路径就是在该子目录的名称前面加上“/”。
如果<Host>元素指定的 appBase 目录中包含 war 文件,这些 war 文件将被自动设置成各自独立
的 Web 应用程序,其虚拟路径就是该文件名称(不含 .war 后缀部分)前面加上“/”。如果<Host>元
素的 unpackWARs 属性设置为了 true,这些 war 文件在第一次使用时将被自动展开成同名的目录(不
含 .war 后缀部分)形式;如果以后更新了 war 文件,一定要记住将以前展开的同名目录删除掉,否
则更新不会生效。
在%tomcat%\conf\Catalina\localhost 中添加 xml 文件加以配置,文件名取决于 path 的值,本例中的名
字为:myweb.xml
<?xml version="1.0" encoding="UTF-8"?>
<!-- 自己配置的虚拟子目录,通过 http://localhost:8088/myweb/index.html 来访问 -->
<Context path="/myweb" docBase="F:/myweb" debug="0" reloadable="true" crossContext="true"
/>
说明:
Tomcat 启动时,会根据如下顺序访问:首先是到%tomcat%\conf\Catalina\localhost 目录下去察看有没有
存在主目录或虚拟目录的 xml 文件,如果有 xml 文件,就按 xml 里的路径进行访问,如果没有 xml 文件,
就到 server.xml 文件里去察看是否配置 context 标签,如果配置了 context 标签,则打开 context 里指定的路
径,如果 server.xml 里没有配置 context 标签,则返回访问错误页面。
3.3.3、设置 Web 站点的虚拟子目录 —— 案例
1. 在 d:\ 盘下创建一个 myweb 目录,接着在 myweb 目录下新建一个 test.html 文件,并在这个
test.html 文件中写上如下一行内容:这是 d:\myweb 目录中的 test.html 文件
2. 将本地计算机文件系统的 d:\myweb 目录映射成 Web 站点的 /myweb 虚拟子目录。用记事本打开
<Tomcat 主目录>/conf/server.xml 文件,紧接着</Host>上面新增加如下一行内容:<Context path=“/myweb"
docBase=“d:\myweb" debug="0" />
重 新 启 动 Tomcat 后 , 在 本 地 计 算 机 上 的 IE 浏 览 器 地 址 栏 中 输 入
96
http://127.0.0.1:8080/myweb/test.html,可以查看到浏览器中显示的网页文档内容为 d:\myweb\test.html 文件
中的内容。这个实验说明,文件系统的 d:\myweb 目录已经成功地映射成 Web 站点的 /myweb 虚拟子目
录。
3. 在 server.xml 文件中紧接着上面新增加的内容下面,再增加如下一行内容:
<Context path=“/myweb/test" docBase=“d:\myweb" debug="0" />
重 新 启 动 Tomcat 后 , 在 本 地 计 算 机 上 的 IE 浏 览 器 地 址 栏 中 输 入
http://127.0.0.1:8080/myweb/test/test.html ,在浏览器中也能显示出 d:\myweb\test.html 文件中的内容。这个
实验说明,文件系统的 d:\myweb 目录被同时映射成 Web 站点的多个虚拟子目录,并且虚拟子目录名称可
以是多级目录结构的形式。
4. 按下面的步骤将 d:\myweb 目录打包成 d:\myweb.jar 文件:
① 在命令行窗口中,使用 cd 命令进入 d:\myweb 目录;
② 执行命令:jar –cvf myweb.war .
其中最后的“.”代表当前目录,上面的命令表示要将 d:\myweb 目录中的所有内容压缩到 myweb.war
文件中,但不包含 myweb 目录本身。
将 myweb.war 文件移动到 d:\ 中。
③ 然后在 server.xml 文件中紧接着</Host>上面新增加如下一行内容:
<Context path=“/myweb" docBase=“d:\myweb.war" debug="0" />
重新启动 Tomcat,在本地计算机上的 IE 浏览器地址栏中输入 http://127.0.0.1:8080/myweb/index.html,
浏览器中显示 d:\myweb.war 压缩包中的 index.html 文件的内容。
5. 将 d:\myweb 目录及其中的所有内容复制到<Tomcat 主目录>/webapps 目录中,重新启动 Tomcat
后,在本地计算机上的 IE 浏览器地址栏中输入 http://127.0.0.1:8080/myweb/test.html,浏览器提示找不到网
页文档的错误信息。在<Tomcat 主目录>/webapps/myweb 目录中创建一个名为 WEB-INF 的子目录后,重
新启动 Tomcat,接着在本地计算机上的 IE 浏览器地址栏中还是输入 http://127.0.0.1:8080/myweb/test.html,
浏览器这次显示 test.html 文件的内容。这说明只有<Tomcat 主目录>/webapps 目录下的子目录中包含
WEB-INF 子目录(如果 WEB-INF 目录中不存在 web.xml 文件时,Tomcat 将使用默认的 web.xml 文件
内容)。更确切的说,在包含 WEB-INF/web.xml 文件时,该子目录才会被自动设置成一个 Web 应用程序,
其虚拟路径就是在该子目录的名称前面加上“/”。
6. 将 d:\myweb.war 文件复制到 <Tomcat 主目录>/webapps 目录中,稍后即可看到 tomcat 将该 war
文件解压成了一个名为 myweb 的目录,接着在本地计算机上的 IE 浏览器地址栏中还是输入
http://127.0.0.1:8080/myweb/test.html ,浏览器显示 myweb.war 压缩包中的 test.html 文件的内容。这说明放
入<Tomcat 主目录>/webapps 目录下的 war 文件将被自动设置成一个 Web 应用程序,其虚拟路径就是该
war 文件名称(不带扩展名)的前面加上“/”。在这种情况下,war 文件对应的目录中可以不包含 WEB-INF
子目录和 WEB-INF/web.xml 文件。
3.3.4、Tomcat 配置问题
修改 Server.xml
Tomcat 的配置文件都放在 conf 目录下,使用无格式的编辑器打开 conf 下的 server.xml 文件,找到如
下代码:
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding=“UTF-8" />
修改的目的:为了解决使用 HTTP Get 方法传递中文参数乱码的问题。这种情况只适用于可以修改服务
器配置信息的时候采用,而当无法修改服务器配置信息时,则应该通过程序代码解决。
97
修改 context.xml
使用无格式的编辑器打开 conf 下的 context.xml 文件:
把<Context> 修改成 <Context reloadable="true">
修改的目的:当 Web 应用中的文件或者 web.xml 文件修改后,Tomcat 服务器会自动重新加载当前
Web 应用,避免重新启动 Tomcat。这个修改会对 Tomcat 的运行性能有影响,如果把 Tomcat 作为产品阶
段所使用的服务器,最好修改成<Context reloadable=“false">
3.3.5、JavaWeb 应用的组成结构
开发 JavaWeb 应用时,不同类型的文件有严格的存放规则,否则不仅可能会使 web 应用无法访问,还
会导致 web 服务器启动报错
JavaWeb 项目标准的组成结构
WebRoot →Web 应用所在目录,一般情况下虚拟目录要配置到此文件夹当中。
WEB-INF:此文件夹必须位于 WebRoot 文件夹里面,而且必须以这样的形式去命名,字母都要大写。
┝web.xml:配置文件,有格式要求,此文件必须以这样的形式去命名,并且必须放置到 WEB-INF
文件夹中。
web.xml 的格式可以直接从 Tomcat 中参考得到:找到 Tomcat 目录下的 webapps\ROOT\WEB-INF 这个
目录下的 web.xml 文件,把这个文件拷贝到我们新建的 WEB-INF 文件夹中,并修改这个 web.xml 文件,把
里面的注释删除掉,只留下如下所示的代码即可。
3.4、Servlet 技术
Servlet 技术是 Sun 公司提供的一种实现动态网页的解决方案,它是基于 Java 编程语言的 Web 服务
器端编程技术,主要用于在 Web 服务器端获得客户端的访问请求信息和动态生成对客户端的响应消息。
Servlet 技术也是 JSP 技术(另外一种动态网页开发技术)的基础。
一个 Servlet 程序就是一个实现了特殊接口的 Java 类,它由支持 Servlet 的 Web 服务器(具有
Servlet 引擎)调用和启动运行。一个 Servlet 程序负责处理它所对应的一个或一组 URL 地址的访问请求,
并用于接收客户端发出的访问请求信息和产生响应内容。
Applet 是用于浏览器端的 Java 小程序,在浏览器端被解释执行,用于在 HTML 网页中实现一些桌面
应用程序的功能,称为“小应用程序”。Servlet 是用于 Web 服务器端的 Java 小程序,它在 Web 服务器
端被解释执行,用于处理客户端的请求和产生动态网页内容。源于 Applet 的命名,这种 Web 服务器端的
Java 小程序就被命名为了 Servlet。与 Applet 相对应,Servlet 可以被称之为“小服务程序”。
Servlet 与普通 Java 程序相比,只是输入信息的来源和输出结果的目标不一样,所以,普通 Java 程序
所能完成的大多数认为,Servlet 程序都可以完成。Servlet 程序具有如下的一些基本功能:
获取客户端通过 HTML 的 FORM 表单提交的数据和 URL 后面的参数信息
98
创建对客户端的响应消息内容
访问服务器端的文件系统
连接数据库并开发基于数据库的应用
调用其他 Java 类
Sun 公司定义了一套专门用于开发 Servlet 程序的 Java 类和接口,这些类和接口提供 Servlet 程序开
发中所涉及的各种功能,它们统称为 Servlet API (Servlet Application Programming Interface)。Servlet 引
擎与 Servlet 程序之间采用 Servlet API 进行通信,因此, Servlet 引擎与 Servlet 程序都需要用到 Servlet
API ,事实上,一个 Servlet 程序就是一个在 Web 服务器端运行的调用了 Servlet API 的 Java 类。
最新版本的 Java Servlet 开发工具包已经被集成到了 Sun 公司的 J2EE (即 Java 企业级版本) 开发工
具包中,这些开发工具包都可以从 http://java.sun.com 站点上下载到。由于支持 Servlet 的 Web 服务器软
件都会自带 Servlet API 的 Jar 包,所以一般不用专门下载 Java Servlet 开发工具包。各种支持 Java Servlet
的 Web 服务器所提供的用于包装 Servlet API 的 Jar 包的文件名称可能各不一样。例如,在 Tomcat 7.x
中,其名称为<tomcat 的安装目录>\lib\servlet-api.jar。
3.4.1、编写与编译 Servlet 程序
一个 Servlet 程序就是一个在 Web 服务器端运行的特殊 Java 类,这个特殊的 Java 类必须实现
javax.servlet.Servlet 接口,Servlet 接口定义了 Servlet 引擎与 Servlet 程序之间通信的协议约定。为了简化
Servlet 程序的编写, Servlet API 中也提供了一个实现 Servlet 接口的最简单的 Servlet 类,其完整名称
为 javax.servlet.GenericServlet,这个类实现了 Servlet 程序的基本特征和功能。
Servlet API 中还提供了一个专用于 HTTP 协议的 Servlet 类,其名称是 javax.servlet.http.HttpServlet,它是
GenericServlet 的子类,在 GenericServlet 类的基础上进行了一些针对 HTTP 特点的扩充。为了充分利用
HTTP 协议的功能,一般情况下,都应让自己编写的 Servlet 类继承 HttpServlet 类,而不是继承
GenericServlet。
查看 HttpServlet 类的帮助文档,可以看到其中有一个名为 service 的方法,当客户端每次访问一个
Servlet 程序时, Servlet 引擎都将调用这个方法来进行处理,我们自己编写的 Servlet 程序通常只需要在
HttpServlet 类的基础上覆盖这个方法。 service 方法接受两个参数:一个是用于封装 HTTP 请求消息的对
象,其类型为 HttpServletRequest;另一个是代表 HTTP 响应消息的对象,其类型为 HttpServletResponse。
调用 HttpServletResponse 对象的 getWriter 方法可以获得一个文本输出流对象,向这个流对象中写入的数
据将作为 HTTP 响应消息的实体内容部分发送给客户端。
3.4.2、编写与编译 Servlet 程序的过程
1. 在本地系统目录 f:\myweb 中创建一个名称为 WEB-INF 的子目录,然后在该子目录中创建一个名
称为 classes 的子目录,myweb 目录结构如下所示:
\myweb
|—— index.html
|—— \WEB-INF
|—— web.xml
|—— \classes
|—— \com
|—— \test
|—— HelloServlet.java
2. 在 tomcat7\conf\Catalina\localhost 目录下新建 myweb.xml,将本地计算机文件系统的 f:\myweb 目录
映射成 Web 站点的 /myweb 虚拟子目录,文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
99
<!-- 自己配置的虚拟目录,通过 http://localhost:8088/myweb/index.html 来访问 -->
<Context path="/myweb" docBase="F:/myweb" debug="0" reloadable="true" crossContext="true"
/>
3. 编写一个继承 HttpServlet 类的 HelloServlet 类,它对 HttpServlet 类中的 service 方法进行了覆盖,
代码如下所示:
package com.test;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
public class HelloServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("<font size=30 color=red>HelloServlet</font><br>");
out.println("<marquee>" + new java.util.Date() + "</marquee>");
out.println("</html>");
}
}
4. 使用 javac 命令编译这个源文件,在编译 Servlet 程序前,需设置环境变量
CLASSPATH = .;C:\tomcat7\lib\servlet-api.jar;C:\tomcat7\lib\jsp-api.jar;
然后在命令行窗口输入如下命令进行编译:
F:\myweb\WEB-INF\classes\com\qixin>javac HelloServlet.java
5. HTML 文件可以直接用浏览器打开并查看运行结果,但是,Servlet 程序必须通过 Web 服务器和
Servlet 引擎来启动运行。 Servlet 程序的存储目录有特殊要求,通常需要存储在<Web 应用程序目
录>\WEB-INF\classes\目录中。另外,Servlet 程序必须在 Web 应用程序的 web.xml 文件中进行注
册和映射其访问路径,才可以被 Servlet 引擎加载和被外界访问。
6. 注册 Servlet。在 web.xml 文件中,一个<servlet>元素用于注册一个 Servlet, <servlet>元素中包含有
两个主要的子元素:<servlet-name>和<servlet-class>,它们分别用于设置 Servlet 的注册名称和指定
Servlet 的完整类名,如下所示:
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.test.HelloServlet</servlet-class>
</servlet>
7. 映射 Servlet。在 web.xml 文件中,一个<servlet-mapping>元素用于映射一个已注册的 Servlet 的一
个对外访问路径,客户端将使用 Servlet 所映射的对外访问路径来访问 Servlet,而不是使用 Servlet
名称来访问 Servlet 。<servlet-mapping>元素中包含有两个子元素: <servlet-name>和<url-pattern>,
它们分别用于指定 Servlet 的注册名称和设置 Servlet 的对外访问路径,如下所示:
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
其中, <servlet-name>子元素的设置值是前面某个<servlet>元素的<servlet-name>子元素中设置的某个
Servlet 的注册名,<url-pattern>子元素中的访问路径必须以正斜杠(/)开头,这个正斜杠(/)表示当前 Web 应用
100
程序的根目录,而不是整个 Web 站点的根目录。
同一个 Servlet 可以被映射到多个 URL 上,即多个<servlet-mapping>元素的<servlet-name>子元素的设
置值可以是同一个 Servlet 的注册名。在 Servlet 映射到的 URL 中也可以使用*通配符,但只能有两种固
定格式:一种格式是以“*.扩展名”,在*前面不能有目录分隔符“/”;另一种格式是以正斜杠(/)开头并以“/*”
结尾。
8. 完整的 web.xml 文件内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app>
<servlet>
<servlet-name>HelloServlet</servlet-name>
<servlet-class>com.test.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>HelloServlet</servlet-name>
<url-pattern>/HelloServlet</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>test.html</welcome-file>
</welcome-file-list>
</web-app>
3.4.3、缺省 Servlet
如果某个 Servlet 的映射路径仅仅为一个正斜杠(/),那么这个 Servlet 就成为当前 Web 应用程序的缺
省 Servlet 。凡是在 web.xml 文件中找不到匹配的<servlet-mapping>元素的 URL,它们的访问请求都将交
给缺省 Servlet 处理。
在<tomcat 的安装目录>\conf\web.xml 文件中,注册了一个名为 org.apache.catalina.servlets.DefaultServlet
的 Servlet,并将这个 Servlet 设置为了缺省 Servlet。由于<tomcat 的安装目录>\conf\web.xml 文件的设置信
息对该服务器上的所有 Web 应用程序都起作用,所以,服务器上的所有 Web 应用程序的缺省 Servlet 都
是 org.apache.catalina.servlets.DefaultServlet。
当访问 Tomcat 服务器中的某个静态 HTML 文件和图片时,实际上是在访问这个缺省 Servlet,而这
个缺省 Servlet 的处理方式通常是把静态资源中的内容按字节原封不动地读出来,然后再按字节流原封不动
传递给客户端,并且生成一些响应消息头字段,例如,根据静态资源的扩展名所映射的 MIME 类型生成
Content-Type 头字段,根据静态资源的大小生成 Content-Length 头字段。
在<tomcat 的安装目录>\conf\web.xml 文件中注释掉设置缺省 Servlet 的<servlet-mapping>元素,如下所
示:
<!--
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
-->
重新启动 Tomcat 后,用浏览器访问前面曾经访问过的静态 HTML 页面,这时浏览器再也无法得到正
常的静态 HTML 页面内容了。但是,浏览器仍然可以成功访问映射到其他 Servlet 的 URL 地址,例如,
在浏览器地址栏中输入如下地址:
101
http://localhost:8088/myweb/HelloServlet
仍然可以获得 HelloServlet 的响应结果。
3.4.4、Servlet 的生命周期
(1) 加载和实例化
Servlet 容器装载和实例化一个 Servlet。创建出该 Servlet 类的一个实例。
(2) 初始化
在 Servlet 实例化完成之后,容器负责调用该 Servlet 实例的 init() 方法,在处理用户请求之前,来做
一些额外的初始化工作。
(3) 处理请求
当 Servlet 容器接收到一个 Servlet 请求时,便运行与之对应的 Servlet 实例的 service() 方法,service()
方法再派遣运行与请求相对应的 doXX(doGet,doPost) 方法来处理用户请求。
(4) 销毁
当 Servlet 容器决定将一个 Servlet 从服务器中移除时 ( 如 Servlet 文件被更新 ),便调用该 Servlet
实例的 destroy() 方法,在销毁该 Servlet 实例之前,来做一些其他的工作。
其中,(1)(2)(4) 在 Servlet 的整个生命周期中只会被执行一次。
上面的第(2)步过程一般是在 Web 服务器重新启动后,针对该 Servlet 的第一次访问时完成的,但也可
以让 Web 应用程序启动时就完成这个过程。在 web.xml 文件中定义的<servlet>元素中可以嵌套一个名为
<load-on-startup>的子元素,这个子元素用于指定 Servlet 被装载的时机和顺序。如果<load-on-startup>元素
中的内容被设置为 0 或一个正整数,它指定该 Servlet 应该在 Web 应用程序启动时就被实例化和调用它的
init()方法,且这个数字越小, Servlet 被装载的时间也越早,相同数字的 Servlet 之间的装载顺序由 Servlet
引擎自己决定。<load-on-startup>元素中的数值大小可用于解决两个 Servlet 之间的依赖关系,如果一个
Servlet 必须先于另一个 Servlet 装载,这个数值就非常有意义了。
因为在 Servlet 的整个生命周期内, Servlet 只被初始化一次,而对一个 Servlet 的每次访问请求都导
致 Servlet 引擎调用一次 Servlet 的 service 方法,所以,在 Servlet 的整个生命周期内,它的 init 方法只
被调用一次,但它的 service 方法可能被调用多次。对于每次访问请求, Servlet 引擎都会创建一个新的
HttpServletRequest 请求对象和 HttpServletResponse 响应对象,然后将这两个对象作为参数传递给它调用的
Servlet 的 service() 方法。在 service() 方法内部首先从请求对象中获得请求信息,接着处理请求和访问其
他资源以获得需要返回的信息,然后调用响应对象的方法将响应内容写入到 Servlet 引擎的缓冲区中,再由
Web 服务器发送给客户端。
如果编程人员对某个已被装载的 Servlet 进行了修改,除非重新启动 Tomcat,否则,客户端以后访问
到的 Servlet 实例对象还是修改前的 Servlet 版本。这是因为 Servlet 在服务器的运行期间只会被加载一次,
尽管在硬盘上已经覆盖掉了原来的 Servlet 文件,但服务器内存中运行的还是原来的 Servlet 程序代码。如
果每次修改 Servlet 后,都需要重新启动 Tomcat 才能看到修改后的运行效果,这显然是很不方便的事情,
因此, Tomcat 提供了是否自动重新装载被修改的 Servlet 的配置选项。在<Tomcat 安装主目
录>\conf\server.xml 或<Tomcat 安装主目录>\ conf\Catalina\localhost 目录下的 .xml 文件中,可以使用
<Context>元素来设置每个独立的 Web 应用程序的配置信息, <Context>元素有一个 reloadable 属性,当它
设置值为 true 时,Tomcat 将监视该 Web 应用程序的 /WEB-INF/classes 和 /WEB-INF/lib 目录下的类是
否发生了改变,然后自动重新装载那些发生了改变的类,并在 Tomcat 所允许的命令行窗口中显示出重新
装 载 的 有 关 提 示 信 息 。 reloadable 属 性 的 默 认 设 置 值 为 false 。 例 如 : <Context path="/myweb"
docBase="F:/myweb" debug="0" reloadable="true" crossContext="true" />
让 Tomcat 不断地执行这样的检测工作,显然需要额外的运行开销,从而降低了系统的运行效率,所
以在程序作为产品真正上线运行时,不要将 Web 应用程序的 reloadable 属性设置为 true。
3.4.5、Servlet 的初始化参数的作用
102
假设某个 Servlet 要被多个公司去使用,这个 Servlet 运行时需要显示出正在使用该 Servlet 的公司的
名称,显然在编写 Servlet 程序时无法确定它将被哪个(或哪些)公司使用,所以,公司名称不能被硬编码
进 Servlet 程序中。
如果某个公司使用这个 Servlet 时,在配置文件通过参数的形式将公司名传递给 Servlet,这个问题就迎
刃而解了。在 web.xml 配置文件中为 Servlet 设置参数的情况如下所示:
<servlet>
<servlet-name>ConfigTestServlet</servlet-name>
<servlet-class>ConfigTestServlet</servlet-class>
<init-param>
<param-name>Corporation</param-name>
<param-value>湖北汽车工业学院</param-value>
</init-param>
</servlet>
3.4.6、ServletConfig 接口
Servlet 程序是发布到 Web 应用程序中运行的,此 Web 应用程序就称之为 Servlet 容器。Servlet 引擎
将代表 Servlet 容器的对象和 Servlet 的配置参数信息一并封装到一个称为 ServletConfig 的对象中,并在初
始化 Servlet 实例对象时传递给该 Servlet 。
ServletConfig 接口则用于定义 ServletConfig 对象需要对外提供的方法,以便 Servlet 程序中可以调用
这些方法来获取有关信息,例如,获取代表 Servlet 容器的对象、获取在 Web.xml 文件中为 Servlet 设置的
友好名称和初始化参数等。
getInitParameterNames 方法
在 web.xml 文件中可以为 Servlet 设置若干个初始化参数,getInitParameterNames 方法用于返回一个
Enumeration 集合对象,该集合对象中包含在 web.xml 文件中为当前 Servlet 设置的所有初始化参数的名称。
如果在 web.xml 文件中进行了如下设置:
<servlet>
<servlet-name>ConfigTestServlet</servlet-name>
<servlet-class>com.test.ConfigTestServlet</servlet-class>
<init-param>
<param-name>firstname</param-name>
<param-value>zhang</param-value>
</init-param>
<init-param>
<param-name>lastname</param-name>
<param-value>san</param-value>
</init-param>
</servlet>
那么,在 ConfigTestServlet 中调用 ServletConfig.getInitParameterNames 方法返回的 Enumeration 集合对象
中将包含两个元素,即内容分别为“firstname”和“lastname”的两个字符串。
getInitParameter 方法
此方法用于返回在 web.xml 文件中为 Servlet 所设置的某个名称的初始化参数的值,如果指定名称的
初始化参数不存在,则返回值为 NULL。例如,对于上面的 ConfigTestServlet 这个 Servlet 的设置情况,
将“firstname”作为参数传递给 getInitParameter 方法时,得到的返回结果为“zhang”。
getServletName 方法
103
此方法用于返回在 web.xml 文件中为 Servlet 所注册的名称。例如,对于上面的 ConfigTestServlet 这
个 Servlet 的设置情况,getServletName 方法返回的结果为“ConfigTestServlet”。对于没有在 web.xml 文
件中注册的 Servlet, getServletName 方法返回的结果为该 Servlet 的类名。
getServletContext 方法
在 Servlet 程序中,每个 Web 应用程序(Servlet 容器)都用一个各自的 ServletContext 对象来表示,
ServletConfig 对象中包含了 ServletContext 对象的引用,getServletContext 方法用于返回 ServletConfig 对
象中所包含的 ServletContext 对象的引用。
例 1:编写一个名为 ConfigTestServlet 的 Servlet 程序
public class ConfigTestServlet extends HttpServlet {
public void service(HttpServletRequest request, HttpServletResponse response) throws
ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.println("<html>");
out.println("Servlet 名称为:" + getServletName() + "<br>");
Enumeration e = getInitParameterNames();
out.println("下面是为 Servlet 设置的初始化参数:" + "<br>");
while(e.hasMoreElements()) {
String key = (String)e.nextElement();
String value = getInitParameter(key);
out.println(" " + key + " = " + value + "<br>");
}
ServletContext context = getServletContext();
String path = context.getRealPath("");
out.println("当前 Web 应用程序的本地目录为:" + path + "<br>");
out.println("</html>");
}
}
在 web.xml 文件中的相应位置处增加如下两段内容:
<servlet>
<servlet-name>ConfigTestServlet</servlet-name>
<servlet-class>com.test.ConfigTestServlet</servlet-class>
<init-param>
<param-name>firstname</param-name>
<param-value>zhang</param-value>
</init-param>
<init-param>
<param-name>lastname</param-name>
<param-value>san</param-value>
</init-param>
</servlet>
……
<servlet-mapping>
<servlet-name>ConfigTestServlet</servlet-name>
104
<url-pattern>/ConfigTestServlet</url-pattern>
</servlet-mapping>
启动或重新启动 Tomcat,在浏览器地址栏中输入如下地址:
http://localhost:8088/myweb/ConfigTestServlet
浏览器中显示的结果如下所示。
Servlet 名称为:ConfigTestServlet
下面是为 Servlet 设置的初始化参数:
lastname = san
firstname = zhang
当前 Web 应用程序的本地目录为:F:\myweb
3.4.7、Servlet 的工作原理
图 3-14 Servlet 工作原理
当客户端浏览器向服务器请求一个 Servlet 时,服务器收到该请求后,首先到容器中检索与请求匹
配的 Servlet 实例是否已经存在。若不存在,则 Servlet 容器负责加载并实例化出该类 Servlet 的一
个实例对象,接着容器框架负责调用该实例的 init() 方法来对实例做一些初始化工作,然后 Servlet
容器运行该实例的 service() 方法。
若 Servlet 实例已经存在,则容器框架直接调用该实例的 service() 方法。service() 方法在运行时,
自动派遣运行与用户请求相对应的 doXX() 方法来响应用户发起的请求。
通常,每个 Servlet 类在容器中只存在一个实例,每当请求到来时,则分配一条线程来处理该请求。
3.4.8、业务流程控制
在 Servlet 中有两种方法可实现业务流程的转向。一般的 Servlet 在接收到用户请求后,都单独完成处理
请求和返回响应的工作。但实际应用中经常需要将请求转交给其他资源,常见的方法有重定向和请求转发。
1、重定向:HTTP 协议规定,当响应状态码为 302 时,表示当前请求的资源地址已改变,这时消息头
通常(但不强制)还附带 Location:URL 项表示资源的最新地址,浏览器可往该地址重新发送请求。这种通
知浏览器向另一个地址重新发送请求的响应称为重定向。HttpServletResponse 接口提供了 sendRedirect()方法
用来进行重定向,它的原型如下:
void sendRedirect(String location)
105
如果 location 以斜杠开头,表示它相对于请求地址的服务器根目录;当 location 不以斜杠开头,则表示
它相对于请求地址的当前目录。服务器根据此请求寻找资源并发送给客户,它可以重定向到任意 URL,不
能共享 request 范围内的数据。
例如: response.sendRedirect("demo.jsp");//重定向到 demo.jsp
详解:假设浏览器访问 servlet1,而 servlet1 想让 servlet2 为客户端服务。此时 servlet1 调用 sendRedirect()
方法,将客户端的请求重新定向到 Servlet2。接着浏览器访问 servlet2,servlet2 对客户端请求作出反应。浏
览器 URL 的地址栏改变。
注意:由于 sendRedirect 方法会重新设置状态码、消息头,因此,调用这个方法前不允许有数据已经返
回给客户端,否则这个方法会抛出非法状态异常(IllegalStateException)。
2、请求转发:请求转发的处理和重定向不同,它是在服务器内部进行转发,浏览器并不知情。有两种
方式获得转发对象(RequestDispatcher):一种是通过 HttpServletRequest 的 getRequestDispatcher()方法获得,
一种是通过 ServletContext 的 getRequestDispatcher()方法获得;请求转发后,以前在 request 范围中存放的变
量不会失效,就像把两个页面拼到了一起。 例如:
方法一:
request.getRequestDispatcher ("demo.jsp"). forward(request, response);//转发到 demo.jsp
方法二:
ServletContext sc = getServletContext();
sc.getRequestDispatcher("demo.jsp").forward(request, response); //转发到 demo.jsp
详解:假设浏览器访问 servlet1,而 servlet1 想让 servlet2 为客户端服务。此时 servlet1 调用 forward()方
法,将请求转发给 servlet2。但是调用 forward()方法,对于浏览器来说是透明的,浏览器并不知道为其服务
的 Servlet 已经换成 Servlet2,它只知道发出了一个请求,获得了一个响应。浏览器 URL 的地址栏不变。
注意:调用 forward 方法时,一定不能有任何数据已经返回到客户端,否则该方法会抛出非法状态异常。
如果输出流设置了缓冲区,并且有数据在缓冲区未返回,则该方法执行前,Web 容器会自动清空缓冲区的
数据。一般可以将 forward 方法看成是 Servlet 处理流程中的最后一个步骤。
3.5、Filter 技术
Filter 也称之为过滤器,它是 Servlet 技术中最激动人心的技术,WEB 开发人员通过 Filter 技术,对 web
服务器管理的所有 web 资源:例如 Jsp, Servlet, 静态图片文件或静态 html 文件等进行拦截,从而实现一些
特殊的功能。例如实现 URL 级别的权限访问控制、过滤敏感词汇、压缩响应信息等一些高级功能。
Servlet API 中提供了一个 Filter 接口,开发 web 应用时,如果编写的 Java 类实现了这个接口,则把这
个 java 类称之为过滤器 Filter。通过 Filter 技术,开发人员可以实现用户在访问某个目标资源之前,对访问
的请求和响应进行拦截,如图 3-15 所示:
图 3-15 Filter 接口拦截过程
Filter 接口中有一个 doFilter 方法,当我们编写好 Filter,并配置对哪个 web 资源进行拦截后,WEB 服
务器每次在调用 web 资源的 service 方法之前,都会先调用一下 filter 的 doFilter 方法,因此,在该方法内编
写代码可达到如下目的:
1. 调用目标资源之前,让一段代码执行。
106
2. 是否调用目标资源(即是否让用户访问 web 资源)。
3. 调用目标资源之后,让一段代码执行。
web 服务器在调用 doFilter 方法时,会传递一个 filterChain 对象进来,filterChain 对象是 filter 接口中最
重要的一个对象,它也提供了一个 doFilter 方法,开发人员可以根据需求决定是否调用此方法,调用该方法,
则 web 服务器就会调用 web 资源的 service 方 法,即 web 资源就会被访问,否则 web 资源不会被访问。
3.5.1、Filter 开发步骤
Filter 开发分为二个步骤:
1. 编写 java 类实现 Filter 接口,并实现其 doFilter 方法。
2. 在 web.xml 文件中使用<filter>和<filter-mapping>元素对编写的 filter 类进行注册,并设置它所能拦
截的资源。
过滤器范例:
package com.mis.filter;
import java.io.IOException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
/**
* @ClassName: FilterDemo01
* @Description:filter 的三种典型应用:
* 1、可以在 filter 中根据条件决定是否调用 chain.doFilter(request, response)方法,
* 即是否让目标资源执行
* 2、在让目标资源执行之前,可以对 request\response 作预处理,再让目标资源执行
* 3、在目标资源执行之后,可以捕获目标资源的执行结果,从而实现一些特殊的功能
*/
public class FilterDemo01 implements Filter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("----过滤器初始化----");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//对 request 和 response 进行一些预处理
request.setCharacterEncoding("UTF-8");
response.setCharacterEncoding("UTF-8");
107
response.setContentType("text/html;charset=UTF-8");
System.out.println("FilterDemo01 执行前!!!");
chain.doFilter(request, response); //让目标资源执行,放行
System.out.println("FilterDemo01 执行后!!!");
}
@Override
public void destroy() {
System.out.println("----过滤器销毁----");
}
}
在 web. xml 中配置过滤器:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<display-name></display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!--配置过滤器-->
<filter>
<filter-name>FilterDemo01</filter-name>
<filter-class>com.mis.filter.FilterDemo01</filter-class>
</filter>
<!--映射过滤器-->
<filter-mapping>
<filter-name>FilterDemo01</filter-name>
<!--“/*”表示拦截所有的请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
</web-app>
3.5.2、Filter 链
在一个 web 应用中,可以开发编写多个 Filter,这些 Filter 组合起来称之为一个 Filter 链。
web 服务器根据 Filter 在 web.xml 文件中的注册顺序,决定先调用哪个 Filter,当第一个 Filter 的 doFilter
方法被调用时,web 服务器会创建一个代表 Filter 链的 FilterChain 对象传递给该方法。在 doFilter 方法中,
开发人员如果调用了FilterChain对象的doFilter方法,则web服务器会检查FilterChain对象中是否还有filter,
108
如果有,则调用第 2 个 filter,如果没有,则调用目标资源。
3.5.3、Filter 的生命周期
1、Filter 的创建
Filter 的创建和销毁由 WEB 服务器负责。web 应用程序启动时,web 服务器将创建 Filter 的实例对象,
并调用其 init 方法,完成对象的初始化功能,从而为后续的用户请求作好拦截的准备工作,filter 对象只会
创建一次,init 方法也只会执行一次。通过 init 方法的参数,可获得代表当前 filter 配置信息的 FilterConfig
对象。
2、Filter 的销毁
Web 容器调用 destroy 方法销毁 Filter。destroy 方法在 Filter 的生命周期中仅执行一次。在 destroy 方法
中,可以释放过滤器使用的资源。
3、FilterConfig 接口
用户在配置 filter 时,可以使用<init-param>为 filter 配置一些初始化参数,当 web 容器实例化 Filter 对
象,调用其 init 方法时,会把封装了 filter 初始化参数的 filterConfig 对象传递进来。因此开发人员在编写 filter
时,通过 filterConfig 对象的方法,就可获得:
String getFilterName():得到 filter 的名称。
String getInitParameter(String name): 返回在部署描述中指定名称的初始化参数的值。如果不存在返回
null.
Enumeration getInitParameterNames():返回过滤器的所有初始化参数的名字的枚举集合。
public ServletContext getServletContext():返回 Servlet 上下文对象的引用。
范例:利用 FilterConfig 得到 filter 配置信息
package com.mis.filter;
import java.io.IOException;
import java.util.Enumeration;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
public class FilterDemo02 implements Filter {
/* 过滤器初始化
* @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
*/
@Override
public void init(FilterConfig filterConfig) throws ServletException {
System.out.println("----过滤器初始化----");
/**
* <filter>
<filter-name>FilterDemo02</filter-name>
<filter-class>com.mis.filter.FilterDemo02</filter-class>
109
<!--配置 FilterDemo02 过滤器的初始化参数-->
<init-param>
<description>配置 FilterDemo02 过滤器的初始化参数</description>
<param-name>name</param-name>
<param-value>gacl</param-value>
</init-param>
<init-param>
<description>配置 FilterDemo02 过滤器的初始化参数</description>
<param-name>like</param-name>
<param-value>java</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>FilterDemo02</filter-name>
<!--“/*”表示拦截所有的请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
*/
//得到过滤器的名字
String filterName = filterConfig.getFilterName();
//得到在 web.xml 文件中配置的初始化参数
String initParam1 = filterConfig.getInitParameter("name");
String initParam2 = filterConfig.getInitParameter("like");
//返回过滤器的所有初始化参数的名字的枚举集合。
Enumeration<String> initParameterNames = filterConfig.getInitParameterNames();
System.out.println(filterName);
System.out.println(initParam1);
System.out.println(initParam2);
while (initParameterNames.hasMoreElements()) {
String paramName = (String) initParameterNames.nextElement();
System.out.println(paramName);
}
}
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
System.out.println("FilterDemo02 执行前!!!");
chain.doFilter(request, response); //让目标资源执行,放行
System.out.println("FilterDemo02 执行后!!!");
}
110
@Override
public void destroy() {
System.out.println("----过滤器销毁----");
}
}
3.5.4、Filter 的部署
Filter 的部署分为两个步骤:
1、注册 Filter
开发好 Filter 之后,需要在 web.xml 文件中进行注册,这样才能够被 web 服务器调用
在 web.xml 文件中注册 Filter 范例:
<filter>
<description>FilterDemo02 过滤器</description>
<filter-name>FilterDemo02</filter-name>
<filter-class>com.mis.filter.FilterDemo02</filter-class>
<!--配置 FilterDemo02 过滤器的初始化参数-->
<init-param>
<description>配置 FilterDemo02 过滤器的初始化参数</description>
<param-name>name</param-name>
<param-value>gacl</param-value>
</init-param>
<init-param>
<description>配置 FilterDemo02 过滤器的初始化参数</description>
<param-name>like</param-name>
<param-value>java</param-value>
</init-param>
</filter>
<description>用于添加描述信息,该元素的内容可为空,<description>可以不配置。
<filter-name>用于为过滤器指定一个名字,该元素的内容不能为空。
<filter-class>元素用于指定过滤器的完整的限定类名。
<init-param>元素用于为过滤器指定初始化参数,它的子元素<param-name>指定参数的名字,
<param-value>指定参数的值。在过滤器中,可以使用 FilterConfig 接口对象来访问初始化参数。如果过滤器
不需要指定初始化参数,那么<init-param>元素可以不配置。
2、映射 Filter
在 web.xml 文件中注册了 Filter 之后,还要在 web.xml 文件中映射 Filter
<!--映射过滤器-->
<filter-mapping>
<filter-name>FilterDemo02</filter-name>
<!--“/*”表示拦截所有的请求 -->
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>元素用于设置一个 Filter 所负责拦截的资源。一个 Filter 拦截的资源可通过两种方式来
111
指定:Servlet 名称和资源访问的请求路径
<filter-name>子元素用于设置 filter 的注册名称。该值必须是在<filter>元素中声明过的过滤器的名字
<url-pattern>设置 filter 所拦截的请求路径(过滤器关联的 URL 样式)
<servlet-name>指定过滤器所拦截的 Servlet 名称。
<dispatcher> 指 定 过 滤 器 所 拦 截 的 资 源 被 Servlet 容 器 调 用 的 方 式 , 可 以 是
REQUEST,INCLUDE,FORWARD 和 ERROR 之一,默认 REQUEST。用户可以设置多个<dispatcher> 子元素
用来指定 Filter 对资源的多种调用方式进行拦截。如下:
<filter-mapping>
<filter-name>testFilter</filter-name>
<url-pattern>/index.jsp</url-pattern>
<dispatcher>REQUEST</dispatcher>
<dispatcher>FORWARD</dispatcher>
</filter-mapping>
<dispatcher> 子元素可以设置的值及其意义:
REQUEST:当用户直接访问页面时,Web 容器将会调用过滤器。如果目标资源是通过 RequestDispatcher
的 include()或 forward()方法访问时,那么该过滤器就不会被调用。
INCLUDE:如果目标资源是通过 RequestDispatcher 的 include()方法访问时,那么该过滤器将被调用。
除此之外,该过滤器不会被调用。
FORWARD:如果目标资源是通过 RequestDispatcher 的 forward()方法访问时,那么该过滤器将被调用,
除此之外,该过滤器不会被调用。
ERROR:如果目标资源是通过声明式异常处理机制调用时,那么该过滤器将被调用。除此之外,过滤
器不会被调用。
3.6、Session 技术
在 WEB 开发中,服务器可以为每个用户浏览器创建一个会话对象(session 对象),注意:一个浏览器
独占一个 session 对象(默认情况下)。因此,在需要保存用户数据时,服务器程序可以把用户数据写到用户
浏览器独占的 session 中,当用户使用浏览器访问其它程序时,其它程序可以从用户的 session 中取出该用户
的数据,为用户服务。
Session 和 Cookie 的主要区别:
Cookie 是把用户的数据写给用户的浏览器。
Session 技术把用户的数据写到用户独占的 session 中。
Session 对象由服务器创建,开发人员可以调用 request 对象的 getSession 方法得到 session 对象。
3.6.1、session 实现原理
服务器是如何实现一个 session 为一个用户浏览器服务的呢?服务器创建 session 出来后,会把 session
的 id 号,以 cookie 的形式回写给客户机,这样,只要客户机的浏览器不关,再去访问服务器时,都会带着
session 的 id 号去,服务器发现客户机浏览器带 session id 过来了,就会使用内存中与之对应的 session 为之
服务。可以用如下的代码证明:。
package com.test.session;
import java.io.IOException;
112
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet(name = "SessionDemo1", urlPatterns = {"/SessionDemo1"})
public class SessionDemo1 extends HttpServlet {
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setCharacterEncoding("UTF=8");
response.setContentType("text/html;charset=UTF-8");
//使用 request 对象的 getSession()获取 session,如果 session 不存在则创建一个
HttpSession session = request.getSession();
//将数据存储到 session 中
session.setAttribute("data", "数据库系统实现");
//获取 session 的 Id
String sessionId = session.getId();
//判断 session 是不是新创建的
if (session.isNew()) {
response.getWriter().print("session 创建成功,session 的 id 是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在该 session 了,session 的 id 是:"+sessionId);
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
第一次访问时,服务器会创建一个新的 sesion,并且把 session 的 Id 以 cookie 的形式发送给客户端浏览
器,如下图所示:
113
点击刷新按钮,再次请求服务器,此时就可以看到浏览器再请求服务器时,会把存储到 cookie 中的
session 的 Id 一起传递到服务器端了,如下图所示:
Servlet API 中提供了一个 Filter 接口,开发 web 应用时,如果编写的 Java 类实现了这个接口,则把这
个 java 类称之为过滤器 Filter。通过 Filter 技术,开发人员可以实现用户在访问某个目标资源之前,对访问
的请求和响应进行拦截,如图 3-15 所示:
3.6.2、浏览器禁用 Cookie 后的 session 处理
1、IE8 禁用 cookie
工具->internet 选项->隐私->设置->将滑轴拉到最顶上(阻止所有 cookies)
114
2、解决方案:URL 重写
response.encodeRedirectURL(java.lang.String url) 用于对 sendRedirect 方法后的 url 地址进行重写。
response.encodeURL(java.lang.String url)用于对表单 action 和超链接的 url 地址进行重写
3、范例:禁用 Cookie 后 servlet 共享 Session 中的数据
IndexServlet
package com.test.session;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "IndexServlet", urlPatterns = {"/IndexServlet"})
//首页:列出所有书
public class IndexServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
115
PrintWriter out = response.getWriter();
//创建 Session
request.getSession();
out.write("本网站有如下书:<br/>");
Set<Map.Entry<String, Book>> set = DB.getAll().entrySet();
for (Map.Entry<String, Book> me : set) {
Book book = me.getValue();
String url = request.getContextPath() + "/BuyServlet?id=" + book.getId();
//response. encodeURL(java.lang.String url)用于对表单 action 和超链接的 url 地址进行重写
url = response.encodeURL(url);//将超链接的 url 地址进行重写
out.println(book.getName() + " <a href='" + url + "'>购买</a><br/>");
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
/**
* @author 模拟数据库
*/
class DB {
private static Map<String, Book> map = new LinkedHashMap<String, Book>();
static {
map.put("1", new Book("1", "javaweb 开发"));
map.put("2", new Book("2", "spring 开发"));
map.put("3", new Book("3", "hibernate 开发"));
map.put("4", new Book("4", "struts 开发"));
map.put("5", new Book("5", "ajax 开发"));
}
public static Map<String, Book> getAll() {
return map;
}
}
class Book {
private String id;
116
private String name;
public Book() {
super();
}
public Book(String id, String name) {
super();
this.id = id;
this.name = name;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
BuyServlet
package com.test.session;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet(name = "BuyServlet", urlPatterns = {"/BuyServlet"})
public class BuyServlet extends HttpServlet {
117
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String id = request.getParameter("id");
Book book = DB.getAll().get(id); //得到用户想买的书
HttpSession session = request.getSession();
List<Book> list = (List) session.getAttribute("list"); //得到用户用于保存所有书的容器
if(list==null){
list = new ArrayList<Book>();
session.setAttribute("list", list);
}
list.add(book);
//response. encodeRedirectURL(java.lang.String url)用于对 sendRedirect 方法后的 url 地址进
行重写
String url = response.encodeRedirectURL(request.getContextPath()+"/ListCartServlet");
System.out.println(url);
response.sendRedirect(url);
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
ListCartServlet
package com.test.session;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@WebServlet(name = "ListCartServlet", urlPatterns = {"/ListCartServlet"})
public class ListCartServlet extends HttpServlet {
@Override
118
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
HttpSession session = request.getSession();
List<Book> list = (List) session.getAttribute("list");
if (list == null || list.size() == 0) {
out.write("对不起,您还没有购买任何商品!!");
return;
}
//显示用户买过的商品
out.write("您买过如下商品:<br>");
for (Book book : list) {
out.write(book.getName() + "<br/>");
}
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
在禁用了 cookie 的 IE8 下的运行 IndexServlet,通过查看 IndexServlet 生成的 html 代码可以看到,每一
个超链接后面都带上了 session 的 Id,如下所示
本 网 站 有 如 下 书 : <br/>javaweb 开 发 <a
href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2
537CDE2DB2?id=1'>购买</a><br/>
spring 开 发 <a
href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2
537CDE2DB2?id=2'>购买</a><br/>
hibernate 开 发 <a
href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2
537CDE2DB2?id=3'>购买</a><br/>
struts 开 发 <a
href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2
537CDE2DB2?id=4'>购买</a><br/>
ajax 开 发 <a
href='/JavaWeb_Session_Study_20140720/servlet/BuyServlet;jsessionid=96BDFB9D87A08D5AB1EAA2
537CDE2DB2?id=5'>购买</a><br/>
119
当浏览器禁用了 cookie 后,就可以用 URL 重写这种解决方案解决 Session 数据共享问题。而且 response.
encodeRedirectURL(java.lang.String url) 和 response. encodeURL(java.lang.String url)是两个非常智能的方法,
当检测到浏览器没有禁用 cookie 时,那么就不进行 URL 重写了。
浏览器第一次访问时,服务器创建 Session,然后将 Session 的 Id 以 Cookie 的形式发送回给浏览器,
response. encodeURL(java.lang.String url)方法也将 URL 进行了重写,当点击刷新按钮第二次访问,由于没有
禁用 cookie,所以第二次访问时带上了 cookie,此时服务器就可以知道当前的客户端浏览器并没有禁用
cookie,那么就通知 response. encodeURL(java.lang.String url)方法不用将 URL 进行重写了。
3.6.3、session 对象的创建和销毁时机
1、session 对象的创建时机
在程序中第一次调用 request.getSession()方法时就会创建一个新的 Session,可以用 isNew()方法来判断
Session 是不是新创建的
范例:创建 session
//使用 request 对象的 getSession()获取 session,如果 session 不存在则创建一个
HttpSession session = request.getSession();
//获取 session 的 Id
String sessionId = session.getId();
//判断 session 是不是新创建的
if (session.isNew()) {
response.getWriter().print("session 创建成功,session 的 id 是:"+sessionId);
}else {
response.getWriter().print("服务器已经存在 session,session 的 id 是:"+sessionId);
}
2、session 对象的销毁时机
session对象默认 30 分钟没有使用,则服务器会自动销毁 session,在 web.xml 文件中可以手工配置 session
的失效时间,例如:
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5"
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee
http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<display-name></display-name>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<!-- 设置 Session 的有效时间:以分钟为单位-->
<session-config>
<session-timeout>15</session-timeout>
</session-config>
</web-app>
120
当需要在程序中手动设置 Session 失效时,可以手工调用 session.invalidate 方法,摧毁 session。
HttpSession session = request.getSession();
//手工调用 session.invalidate 方法,销毁 session
session.invalidate();
3.7、JSP 技术
JSP 技术是一种简化 Servlet 开发的动态网页技术。在传统的 HTML 文件里加入 Java 代码片段(Scriptlet)
或 JSP 标记,并以“.jsp”为扩展名进行保存,就构成了 JSP 页面。
当 Servlet/JSP 容器收到客户端发出的请求时,JSP 文件先被容器转换为 Servlet 类(这个过程只执行一
次),再以 Servlet 的方式运行。容器首先执行其中的程序片段,然后将执行结果插入到原 HTML 文件中,
最后以 HTML 格式响应给客户端。
其中程序片段可以使任何 Java 代码,如操作数据库,重新定向页面等。
JSP 页面中的静态部分构成页面的基本布局,俗称页面模版,主要是 Html 元素、控制 Html 元素显示样
式的 CSS 代码,以及相应浏览器端动作的 Javascript 代码。
JSP 页面中的动态部分实现动态内容的嵌入,主要是 JSP 指令、JSP 动作、JSP 表达式和 Java 片段,俗
称脚本。
JSP 运行原理如图 3-16 所示,它抛弃了 Servlet 用 out.println()输出页面内容的做法,实现了内容生成与
表示的分离,简化了页面的开发。
图 3-16 JSP 运行过程
3.7.1、JSP 指令
JSP 指令控制对整个页面的处理,如提供整个 JSP 网页相关的信息,并且用来设定 JSP 网页的相关属性,
例如:网页的编码方式、语法、信息等,还可以确定要导入的包及要实现的接口,可以引入其他的文件,
可以使用的 JSP 标签等。
JSP 指令主要有 page、include、和 taglib。JSP 指令的语法为:
<%@ 指令名称 属性 1=“属性值 1”┅ ┅ 属性 n=“属性值 n”%>
常用的 Page 指令如表 3-3:
表 3-3 常用的 Page 指令
121
属性 描述
language=“java”
主要指定 JSP Container 要用什么来编译 JSP 网页,目前只可以使用 Java 语
言
extends=“className” 主要定义此 JSP 网页产生的 Servlet 是继承哪个父类
import=“importList” 主要定义此 JSP 网页可以使用哪些 Java 包或类
session=“true|false” 决定此 JSP 网页是否可以使用 session 对象,默认值是 true。
buffer=“none|n kb” 决定输出流是否有缓冲区。默认为 8KB 的缓冲区。
autoFlush=“true|false” 决定输出流的缓冲去是否要自动清除,缓冲区面了会产生异常。默认值为true
isThreadSafe=“true|false”
主要告诉 JSP Container,此 JSP 网页能处理超过一个以上的请求。默认值为
true,并且,不建议设置为 false
info=“text” 主要表示此 JSP 的相关信息
errorPage=“error_url” 表示如果发生异常错误时,网页会被重新指向哪一个 URL
isErrorPage=“true|false” 表示此 JSP 页面是否为处理异常错误的网页
contentType=“ctinfo” 表示 MIME 类型和 JSP 网页的编码方式
pageEncoding=“ctinfo” 表示 JSP 网页的编码方式
isELIgnored=“true|false”
表示是否在此JSP网页中执行或忽略EL表达式。如果为true时,JSP Container
将忽略 EL 表达式;反之为 false 时,EL 表达式将会被执行
include 指令用于在 JSP 编译时插入一个包含文本或代码的文件,这个包含过程是静态的,而包含的文
件可以是 JSP 网页、HTML 网页、文本文件,或是一段 Java 代码。
include 指令的语法为:<%@ include file=”文件路径” %>,file 指向文件的路径。
注意:include 指令是指静态包含其他的文件。所谓的静态是指 file 不能为一个变量 URL;也不可以
在 file 所指定的文件路径后边接任何参数。同时,file 所指的路径必须是相对于此 JSP 网页的路径。
taglib 指令的作用是在 JSP 页面中,将标签库描述文件(TLD)引入到该页面中,并设置前缀,利用标
签的前缀去使用标签库描述符文件中的标签。标签描述符文件为 XML 格式,包含一系列标签的说明,它的
文件后缀名是.tld。
taglib 指令的语法如下:<%@ taglib uri=”标签库描述符文件” prefix=”别名” %>。
3.7.2、JSP 脚本
JSP 脚本由表达式、声明和代码片段三种类型。
表达式的语法为:<%= Java 表达式 %>,将在相应的位置输出 Java 表达式的结果。注意,Java 表达式
末尾没有分号。
声明的语法为:<%! Java 变量,方法等 %>,用来定义页面中的变量或方法。
代码片段的语法为:“<% java 代码 %>”。
3.7.3、JSP 内置对象
JSP 是 Servlet 的特殊形式。一般在 JSP 中使用 JAVA 类,需要先实例化相应的对象。为了方便使用
Servlet API,JSP 规范规定 JSP 容器提供如表 2 所示的内置对象。重要的 Request 对象方法如表 3-4 所示。
表 3-4 JSP 内置对象
与输入输出有关
request javax.servlet.http.HttpServletRequest 请求端信息
response javax.servlet.http.HttpServletResponse 响应端信息
out javax.servlet.jsp.JspWriter 数据流的标准输出
与作用域通信有关
122
pageContext javax.servlet.jsp.PageContext 表示此 JSP 的 PageContext
session javax.servlet.http.HttpSession 用户的会话状态
application javax.servlet.ServletContext 作用范围比 session 大
与 Servlet 有关
config javax.servlet.ServletConfig 表示此 JSP 的 ServletConfig
page java.lang.Object 如同 Java 的 this
与错误有关
exception java.lang.Throwable 异常处理
表 3 Request 对象方法
方法 说明
String getParameter(String name) 取得 name 的参数值
Enumeration getParameterNames() 取得所有的参数名称
String[] getParameterValues(String name) 取得所有 name 的参数值
Map getParameterMap() 取得一个要求参数的 Map
void setAttribute(String name, Object value) 以名/值的方式,将一个对象的值存放到 request 中
Object getAttribute(String name) 根据名称去获取 request 中存放对象的值
Response 对象主要生成对浏览器的响应。如用 setHeader(String name, String value)方法来设置表
头。如下三行代码可以控制 JSP 页面不被浏览器缓存:
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
用 setContentType(String name)设置作为响应生成的内容的类型和字符编码。
用 sendRedirect(String name)发送一个响应给浏览器,指示其请求另一个 URL。
Out 对象表示输出流,此输出流将作为请求的相应发送到客户端。常用的方法有 print()、println()
和 write()方法。
Session 对象存储有关用户会话的所有信息,用于在应用程序的网页之间跳转时,存储有关会话的信息。
Session 对象常用的方法如表 3-5 所示。
表 3-5 Session 对象常用方法
方法 说明
String getId() 返回 session 的 ID
void invalidate() 取消 session 对象,并将对象存放的内容完全抛弃
boolean isNew() 判断 session 是否为“新”的,所谓“新”的 session,
表示 session 已由服务器产生,但是 client 尚未使用
void setMaxInactiveInterval(int interval) 设定最大 session 不活动的时间,若超过这个时间,
session 将会失效,时间单位为秒
void setAttribute(String name, Object value) 以名称/值的方式,将一个对象的值存放到 session 中
Object getAttribute(String name) 根据名称去获取 session 中存放对象的值
Application 对象用于取得或更改 Servlet 的设定。Application 对象从服务器启动开始就存在,直到
服务器关闭为止。Application 对象的常用方法如表 3-6 所示。
表 3-6 Application 对象方法
方法 说明
String getServerInfo() 返回 Servlet 容器名称及版本号
123
URL getResource(String path) 返回指定资源(文件及目录)的 URL 路径
String getMimeType(String file) 返回指定文件的 MIME 类型
ServletContext getContext(String uri) 返回指定地址的 Application context
String getRealPath(String path) 返回本地端 path 的绝对路径
void log(String msg) 将信息写入 log 文件中
void log(String msg, Throwable err) 将 stack trace 所产生的异常信息写入 log
void setAttribute(String name,Object value)
以名称/值的方式,将一个对象的值存放到 applicaton
中
Object getAttribute(String name) 根据名称去获取 application 中存放对象的值
void removeAttribute(String name) 删除一个属性及其属性值
PageContext 对象用于访问页面作用域中定义的隐式对象。PageContext 对象的主要方法如表 3-7 所示。
表 3-7 PageContext 对象方法
方法 说明
JspWriter getOut() 返回当前页面的输出流,即 out 对象
ServletRequest getRequest() 返回当前页面的请求,即 request 对象
ServletResponse getResponse() 返回当前页面的响应,即 response 对象
ServletContext getServletContext() 返回当前页面的执行环境,即 application 对象
HttpSession getSession() 返回与当前页面关联的会话,即 session 对象
void setAttribute(String name, Object attr) page 范围内设置属性和属性值
void setAttribute(String name, Object attr,
int scope)
在制定范围内设定属性和属性值
Object getAttribute(String name) page 范围内根据名字获得属性值
Object getAttribute(String name, int scope) 在指定范围内获得属性值
void removeAttribute(String name) page 范围内删除属性
void removeAttribute(String name, int scope) 在制定范围内删除属性
Object findAttribute(String name) 所有范围中根据名字寻找属性
int getAttributeScope(String name) 返回某属性的作用范围
Enumeration getAttributeNamesInScope(int
scope)
返回指定范围内可用的属性名枚举
JSP 中有四种范围:Page、Request、Session 和 Application。
若要将数据存入 Page 范围时,可以用 pageContext 对象的 setAttribute()方法;若要取得 Page 范围
的数据时,可以使用 pageContext 对象的 getAttribute()方法。
Request 的范围是指在一个 JSP 网页发出请求到另一个 JSP 页面之间,随后,这个属性就失效了。设定
Request 范围的属性时可以利用 request 对象中的 setAttribute()和 getAttribute()方法。Servlet 中的
HttpServletRequest 的 getRequestDispatcher(请求的链接).forward()方法会带着 Request 范围的属性进
行传递;HttpServletResponse 的 sendRedirect()方法是重定向,只是迁移到某个页面,Request 范围的属
性会被丢弃而不跟随传递的。关于 Request 范围值的存取特点如表 3-8 所示。
表 3-8 Request 范围
方法 能否得到 Request 范围属性
<jsp:forward> true
<jsp:include> true
HttpServletRequest.getRequestDispatcher().forward() true
HttpServletResponst.sendRedirect() false
Session 作用范围比 Page 和 Request 范围要大,一般只有三种情况,Session 范围的属性才会失效。
第一种就是设定了 session 的最大不活动时间,在没有任何操作的情况下,多少秒后 session 对象失效;
第二种是我们在 web.xml 中设置 session 过期时间,即在没有任何操作的情况下,超过设定的时间,session
124
对象会失效,和第一种不同的是单位是分钟;第三种就是关闭浏览器了,这就意味着我们和服务器断线,
这样也会让 session 对象失效。存取 Session 范围同样用 setAttribute()和 getAttribute()方法。
需要特别注意,通过 setAttribute()方法设定属性后,在使用完此属性后,一定要通过
removeAttribute()方法删除,否则,session 内数据越来越多,占用的内存越来越大,服务器的负载就越
来越大。
Application 的作用范围是从服务器一开始执行服务到服务器关闭为止。Application 的范围最大,停
留的时间也最长,所以要使用时同 session 一样要特别注意甚至要限制使用,不然可能会造成服务器负载
越来越重而导致服务器崩溃的情况。
3.8、EL 表达式
EL 表达式简化了 JSP 表达式的使用方式,即不再使用<%=表达式%>,而用${表达式}来输出表达式或变
量的值。
${84.5E4}:输出 845000.0。
${scope.var}:输出指定范围的变量的值,如:
<%
// pageContext中设置一个属性color为red
pageContext.setAttribute("color", "red");
%>
<!-- 从pageContext中取出color属性 -->
<body bgcolor="${pageScope.color}"></body>
EL 表达式中的变量可以引用存储在 page、request、session 和 application 范围中的属性。
EL 表达式支持丰富的运算符:
算术运算符
运算符 说明 范例 结果
+ 加 ${17+5} 22
- 减 ${17-5} 12
* 乘 ${17*5} 85
/或 div 除 ${17/5}或${17div5} 3
%或 mod 余数 ${17+5}或${17mod5} 2
关系运算符
运算符 说明 范例 结果
==或 eq 等于 ${5==5}或${5 eq 5} true
!=或 ne 不等于 ${5!=5}或${5 ne 5} false
<或 lt 小于 ${3<5}或${3 lt 5} true
>或 gt 大于 ${3>5}或${3 gt 5} false
<=或 le 小于等于 ${3<=5}或${3 le 5} true
>=或 ge 大于等于 ${3>=5}或${3 ge 5} false
逻辑运算符
125
运算符 说明 范例 结果
&&或 and 与 ${A&&B}或${A and B} true/false
||或 or 或 ${A||B}或${A or B} true/false
!或 not 非 ${!A}或${not A } true/false
Empty 运算符:${empty param.name},当 param.name 为 null、空 String、空 Array、空 Map、空
Collection 时,返回 true;其他返回 false。
条件运算符:${A?B:C},当 A 为 true 时,执行 B;当 A 为 false 时,执行 C。
括号运算符:主要用来改变执行优先权,如${A*(B+C)}。
3.9、JSTL 核心标记库
使用 JSP 技术可以很方便地将页面元素和服务端代码混合在一起,构成各种丰富有趣的应用。然而,
在大的应用项目中,客户端脚本和服务器端脚本的*混合带来了后期维护的噩梦,不利于美工设计人员
和程序员之间的分工。一个非常有效的解决方法就是使用标签语言和标签库。标签库是一种可以将服务器
端代码格式转化成与客户端页面标签元素格式类似的一种技术。
JSTL 核心标签库标签共有 13 个,功能上分为 4 类:
1.表达式控制标签:out、set、remove、catch
2.流程控制标签:if、choose、when、otherwise
3.循环标签:forEach、forTokens
4.URL 操作标签:import、url、redirect
使用标签时,一定要在 jsp 文件头加入以下代码:
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
下面分别对这些标签进行说明:
1. <c:out> 用来显示数据对象(字符串、表达式)的内容或结果
使用 Java 脚本的方式为:<% out.println("hello") %> <% =表达式 %>
使用 JSTL 标签:<c:out value="字符串">,例如:
<body>
<c:out value="< 要显示的数据对象(未使用转义字符)>" escapeXml="true" default="默认值
"></c:out><br/>
<c:out value="< 要显示的数据对象(使用转义字符)>" escapeXml="false" default="默认值
"></c:out><br/>
<c:out value="${null}" escapeXml="false">使用的表达式结果为 null,则输出该默认值</c:out><br/>
</body>
那么网页显示效果为:
2. <c:set> 用于将变量存取于 JSP 范围中或 JavaBean 属性中。下面的例子中假设已经有
Person.java 这个类文件。
<%@ page language="java" import="java.util.*" pageEncoding="gb2312"%>
<%@page contentType="text/html; charset=utf-8" %>
<jsp:useBean id="person" class="lihui.Person"></jsp:useBean>
126
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>JSTL 测试</title>
</head>
<body>
<c:set value="张三" var="name1" scope="session"></c:set>
<c:set var="name2" scope="session">李四</c:set>
<c:set value="赵五" target="${person}" property="name"></c:set>
<c:set target="${person}" property="age">19</c:set>
<li>从 session 中得到的值:${sessionScope.name1}</li>
<li>从 session 中得到的值:${sessionScope.name2}</li>
<li>从 Bean 中获取对象 person 的 name 值:<c:out value="${person.name}"></c:out></li>
<li>从 Bean 中获取对象 person 的 age 值:<c:out value="${person.age}"></c:out></li>
</body>
</html>
一共有四种语法格式,前两种是给 jsp 的范围变量赋值,后两个是给 javabean 变量赋值
效果如下:
3.<c:remove> 主要用来从指定的 jsp 范围内移除指定的变量。使用类似,下面只给出语法:
<c:remove var="变量名" [scope="page|request|session|application"]></c:remove>
4.<c:catch> 用来处理 JSP 页面中产生的异常,并存储异常信息
<c:catch var="name1">
容易产生异常的代码
</c:catch>
如果抛异常,则异常信息保存在变量 name1 中。
5.<c:if>
<c:if test="条件 1" var="name" [scope="page|request|session|application"]></c:if>
例:
<body>
<c:set value="赵五" target="${person}" property="name"></c:set>
<c:set target="${person}" property="age">19</c:set>
<c:if test="${person.name == '赵武'}" var="name1"></c:if>
<c:out value="name1 的值:${name1}"></c:out><br/>
<c:if test="${person.name == '赵五'}" var="name2"></c:if>
<c:out value="name2 的值:${name2}"></c:out>
</body>
效果:
127
6. <c:choose> <c:when> <c:otherwise> 三个标签通常嵌套使用,第一个标签在最外层,最后一个标
签在嵌套中只能使用一次
例:
<c:set var="score">85</c:set>
<c:choose>
<c:when test="${score>=90}">
你的成绩为优秀!
</c:when>
<c:when test="${score>=70&&score<90}">
您的成绩为良好!
</c:when>
<c:when test="${score>60&&score<70}">
您的成绩为及格
</c:when>
<c:otherwise>
对不起,您没有通过考试!
</c:otherwise>
</c:choose>
7.<c:forEach>
语法:<c:forEach var="name" items="Collection" varStatus="statusName" begin="begin"
end="end" step="step"></c:forEach>
该标签根据循环条件遍历集合 Collection 中的元素。 var 用于存储从集合中取出的元素;items 指
定要遍历的集合;varStatus 用于存放集合中元素的信息。varStatus 一共有 4 种状态属性,下面例子中
说明:
<%@ page contentType="text/html;charset=GBK" %>
<%@page import="java.util.List"%>
<%@page import="java.util.ArrayList"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<html>
<head>
<title>JSTL: -- forEach 标签实例</title>
</head>
<body>
<h4><c:out value="forEach 实例"/></h4>
<hr>
<%
List a=new ArrayList();
a.add("贝贝");
a.add("晶晶");
a.add("欢欢");
a.add("莹莹");
128
a.add("妮妮");
request.setAttribute("a",a);
%>
<B><c:out value="不指定 begin 和 end 的迭代:" /></B><br>
<c:forEach var="fuwa" items="${a}">
<c:out value="${fuwa}"/><br>
</c:forEach>
<B><c:out value="指定 begin 和 end 的迭代:" /></B><br>
<c:forEach var="fuwa" items="${a}" begin="1" end="3" step="2">
<c:out value="${fuwa}" /><br>
</c:forEach>
<B><c:out value="输出整个迭代的信息:" /></B><br>
<c:forEach var="fuwa" items="${a}" begin="3" end="4" step="1" varStatus="s">
<c:out value="${fuwa}" />的四种属性:<br>
所在位置,即索引:<c:out value="${s.index}" /><br>
总共已迭代的次数:<c:out value="${s.count}" /><br>
是否为第一个位置:<c:out value="${s.first}" /><br>
是否为最后一个位置:<c:out value="${s.last}" /><br>
</c:forEach>
</body>
</html>
显示效果:
8.<c:forTokens> 用于浏览字符串,并根据指定的字符串截取字符串
语法:<c:forTokens items="stringOfTokens" delims="delimiters" [var="name" begin="begin"
end="end" step="len" varStatus="statusName"]></c:forTokens>
还是看个例子吧:
<%@ page contentType="text/html;charset=GBK"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
129
<title>JSTL: -- forTokens 标签实例</title>
</head>
<body>
<h4>
<c:out value="forToken 实例" />
</h4>
<hr>
<c:forTokens items="北、京、欢、迎、您" delims="、" var="c1">
<c:out value="${c1}"></c:out>
</c:forTokens>
<br>
<c:forTokens items="123-4567-8854" delims="-" var="t">
<c:out value="${t}"></c:out>
</c:forTokens>
<br>
<c:forTokens items="1*2*3*4*5*6*7" delims="*" begin="1" end="3"
var="n" varStatus="s">
<c:out value="${n}" />的四种属性:<br>
所在位置,即索引:<c:out value="${s.index}" />
<br>
总共已迭代的次数:<c:out value="${s.count}" />
<br>
是否为第一个位置:<c:out value="${s.first}" />
<br>
是否为最后一个位置:<c:out value="${s.last}" />
<br>
</c:forTokens>
</body>
</html>
显示结果:
9.URL 操作标签
130
(1)<c:import> 把其他静态或动态文件包含到 JSP 页面。与<jsp:include>的区别是后者只能包含
同一个 web 应用中的文件,前者可以包含其他 web 应用中的文件,甚至是网络上的资源。
语法:
<c:import url="url" [context="context"] [value="value"] [scope="..."]
[charEncoding="encoding"]></c:import>
<c:import url="url" varReader="name"
[context="context"][charEncoding="encoding"]></c:import>
看个例子:
<%@ page contentType="text/html;charset=GBK"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>JSTL: -- import 标签实例</title>
</head>
<body>
<h4>
<c:out value="import 实例" />
</h4>
<hr>
<h4>
<c:out value="绝对路径引用的实例" />
</h4>
<c:catch var="error1">
<c:import url="http://www.baidu.com" />
</c:catch>
<c:out value="${error1}"></c:out>
<hr>
<h4>
<c:out value="相对路径引用的实例,引用本应用中的文件" />
</h4>
<c:catch>
<c:import url="a1.txt" charEncoding="gbk" />
</c:catch>
<hr>
<h4>
<c:out value="使用字符串输出、相对路径引用的实例,并保存在 session 范围内" />
</h4>
<c:catch var="error3">
<c:import var="myurl" url="a1.txt" scope="session"
charEncoding="gbk"></c:import>
<c:out value="${myurl}"></c:out>
<c:out value="${myurl}" />
</c:catch>
<c:out value="${error3}"></c:out>
131
</body>
</html>
显示结果:
URL 路径有个绝对路径和相对路径。相对路径:<c:import url="a.txt"/>那么,a.txt 必须与当前文
件放在同一个文件目录下。如果以"/"开头,表示存放在应用程序的根目录下,如 Tomcat 应用程序的根目
录文件夹为 webapps。导入该文件夹下的 b.txt 的编写方式: <c:import url="/b.txt">。如果要访问
webapps 管理文件夹中的其他 Web 应用,就要用 context 属性。例如访问 demoProj 下的 index.jsp,则:
<c:import url="/index.jsp" context="/demoProj"/>.
(2)<c:redirect> 该标签用来实现请求的重定向。例如,对用户输入的用户名和密码进行验证,不
成功则重定向到登录页面。或者实现 Web 应用不同模块之间的衔接
语法:<c:redirect url="url" [context="context"]/>
或:<c:redirect url="url" [context="context"]>
<c:param name="name1" value="value1">
</c:redirect>
看个例子:
<%@ page contentType="text/html;charset=GBK"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:redirect url="http://127.0.0.1:8080">
<c:param name="uname">lihui</c:param>
<c:param name="password">11111</c:param>
</c:redirect>
则运行后,页面跳转为:http://127.0.0.1:8080/?uname=lihui&password=11111
(3)<c:url> 用于动态生成一个 String 类型的 URL,可以同上个标签共同使用,也可以使用 HTML 的
<a>标签实验超链接。
语法:<c:url value="value" [var="name"] [scope="..."] [context="context"]>
132
<c:param name="name1" value="value1">
</c:url>
或:<c:url value="value" [var="name"] [scope="..."] [context="context"]/>
看个例子:
<%@ page contentType="text/html;charset=GBK"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<c:out value="url 标签使用"></c:out>
<h4>
使用 url 标签生成一个动态的 url,并把值存入 session 中.
</h4>
<hr>
<c:url value="http://127.0.0.1:8080" var="url" scope="session">
</c:url>
<a href="${url}">Tomcat 首页</a>
显示效果:
3.10、中文乱码解决方案
当我们提交在表单中填入数据时,在 Servlet 中接收数据经常会出现乱码的现象。在将 Servlet 处理后的
中文页面内容输出到页面中显示时,经常会出现莫名其妙的中文乱码现象,这就要需要了解计算机当中的
字符编码概念,本节对此不作过多的介绍,只探讨在 Java 中出现的中文乱码现象的解决方案。
应用一:解决 tomcat 下中文乱码问题(先来个简单的)
在 tomcat 下,我们通常这样来解决中文乱码问题:
在 Java 中编写过滤器代码:
package filter;
import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;
import wrapper.GetHttpServletRequestWrapper;
public class ContentTypeFilter implements Filter {
private String charset = "UTF-8";
private FilterConfig config;
public void destroy() {
System.out.println(config.getFilterName()+"被销毁");
charset = null;
config = null;
133
}
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//设置请求响应字符编码
request.setCharacterEncoding(charset);
response.setCharacterEncoding(charset);
HttpServletRequest req = (HttpServletRequest)request;
System.out.println("----请求被"+config.getFilterName()+"过滤");
//执行下一个过滤器(如果有的话,否则执行目标 servlet)
chain.doFilter(req, response);
System.out.println("----响应被"+config.getFilterName()+"过滤");
}
public void init(FilterConfig config) throws ServletException {
this.config = config;
String charset = config.getServletContext().getInitParameter("charset");
if( charset != null && charset.trim().length() != 0)
{
this.charset = charset;
}
}
}
web.xml 中过滤器配置:
<!--将采用的字符编码配置成应用初始化参数而不是过滤器私有的初始化参数是因为在 JSP 和其他地方也可能需要使
用-->
<context-param>
<param-name>charset</param-name>
<param-value>UTF-8</param-value>
</context-param>
<filter>
<filter-name>ContentTypeFilter</filter-name>
<filter-class>filter.ContentTypeFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>ContentTypeFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
request.setCharacterEncoding(charset); 必须写在第一次使用 request.getParameter()之前,这样才能保证参
134
数是按照已经设置的字符编码来获取。
response.setCharacterEncoding(charset);必须写在 PrintWriter out = request.getWriter()之前,这样才能保证
out 按照已经设置的字符编码来进行字符输出。
通过过滤器,我们可以保证在 Servlet 或 JSP 执行之前就设置好了请求和响应的字符编码。
但是这样并不能完全解决中文乱码问题:
对于 post 请求,无论是“获取参数环节”还是“输出环节"都是没问题的;
对于 get 请求,"输出环节"没有问题,但是"获取参数环节"依然出现中文乱码,所以在输出时直接将乱
码输出了。
原因是 post 请求和 get 请求存放参数位置是不同的:
post 方式参数存放在请求数据包的消息体中。get 方式参数存放在请求数据包的请求行的 URI 字段中,
以 ? 开 始 以 param=value¶me2=value2 的 形 式 附 加 在 URI 字 段 之 后 。 而
request.setCharacterEncoding(charset); 只对消息体中的数据起作用,对于 URI 字段中的参数不起作用,我们
通常通过下面的代码来完成编码转换:
Java 代码:
String paramValue = request.getParameter("paramName");
paramValue = new String(paramValue.trim().getBytes("ISO-8859-1"), charset);
但是每次进行这样的转换实在是很麻烦,有没有统一的解决方案呢?
解决方案 1: 在 tomcat_home\conf\server.xml 中的 Connector 元素中设置 URIEncoding 属性为合适的字
符编码
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443"
URIEncoding="UTF-8"
/>
这样做的缺点是,同一个 tomcat 下的其他应用也将受到影响。而其每次部署时都需要类修改配置也很
麻烦。
解决方案 2:自定义请求包装器包装请求,将字符编码转换的工作添加到 getParameter()方法中
Java 代码:
package wrapper;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class GetHttpServletRequestWrapper extends HttpServletRequestWrapper {
private String charset = "UTF-8";
public GetHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
}
/**
135
* 获得被装饰对象的引用和采用的字符编码
* @param request
* @param charset
*/
public GetHttpServletRequestWrapper(HttpServletRequest request,
String charset) {
super(request);
this.charset = charset;
}
/**
* 实际上就是调用被包装的请求对象的 getParameter 方法获得参数,然后再进行编码转换
*/
public String getParameter(String name) {
String value = super.getParameter(name);
value = value == null ? null : convert(value);
return value;
}
public String convert(String target) {
System.out.println("编码转换之前:" + target);
try {
return new String(target.trim().getBytes("ISO-8859-1"), charset);
} catch (UnsupportedEncodingException e) {
return target;
}
}
}
修改过滤器的 doFilter 方法 代码如下:
Java 代码:
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
//设置请求响应字符编码
request.setCharacterEncoding(charset);
response.setCharacterEncoding(charset);
//新增加的代码
HttpServletRequest req = (HttpServletRequest)request;
if(req.getMethod().equalsIgnoreCase("get"))
{
req = new GetHttpServletRequestWrapper(req,charset);
}
System.out.println("----请求被"+config.getFilterName()+"过滤");
136
//传递给目标 servlet 或 jsp 的实际上时包装器对象的引用,而不是原始的 HttpServletRequest 对象
chain.doFilter(req, response);
System.out.println("----响应被"+config.getFilterName()+"过滤");
}
这样一来,在 servlet 中调用包装器的 getParameters 方法来获取参数,就已经完成了字符编码的转换过
程,我们就不需要在每次获取参数时来进行字符编码转换了。
附:源程序
d em o .rar
4、实验内容
1、Java Web 环境搭建和测试
⑴ 安装 Tomcat7 Web 应用程序服务器
登录 Apache Tomcat 官网 http://tomcat.apache.org/ 下载 Tomcat7,建议下载压缩包格式 32-bit Windows
zip,将压缩包解压至硬盘根目录,例如:D:\tomcat7。
⑵ 运行 Netbeans IDE 7.4,依次选择菜单“工具”>“服务器”,进入到添加服务器界面。点击
按钮,在弹出的选择服务器界面中选择“Apache Tomcat”,点击“下一步”按钮。界面如图 3-17 所示:
图 3-17 添加服务器
⑶ 在安装和登录详细信息界面中,浏览选择 tomcat7 安装根目录,在用户名口令处输入要添加的管理
用户,选择“完成”按钮。如图 3-18 所示:
137
图 3-18 配置信息
⑷ 到此为止,添加了 Tomcat7 web 服务器,在服务器设置界面中我们可以指定 Tomcat 服务器端口号,
这里指定了“8088”(为了避免和系统占用的 8080 端口冲突,建议更换一个)。
图 3-19 端口设置
⑸ 新建 Java Web 项目:进入到 Netbeans IDE7.4 环境,选择菜单“文件”>“新建项目”,在弹出的对
话框中选择“Java Web”类别,项目选择“Web 应用程序”,点击“下一步”按钮,输入项目名称和选择项
目要保存的路径。本例项目默认保存到 C:\Users\Administrator\Documents\NetBeansProjects 目录。
138
⑹ 点击“下一步”按钮,进入到服务器和设置界面,如果已经在第⑵步中添加了 Tomcat Web 服务器,
则在这里可以看到已经选择了 Apache Tomcat 服务器,上下文路径默认设置为/项目名称。点击“下一步”
按钮,进入到“框架”选择界面,如果 Web 应用程序要使用某些框架,则可以在这里选择,否则,选择“完
成”按钮。
⑺ 至此,新建项目工作完成。我们可以在 IDE 中看到新建的项目,默认添加了一个 index.jsp 文件。
⑻ 运行项目:选择工具栏上的 图标或者选择菜单“运行”>“运行项目”,IDE 开始在后台运行 tomcat
服务器,并打开默认浏览器显示运行结果。浏览器地址栏地址为 http://localhost:8088/JavaWebTest/,其中
http://localhost:8088/为 web 服务器的地址,JavaWebTest 为 web 应用程序上下文路径。
思考:
① 喜欢专研的同学可能会问到,Netbeans IDE 是如何和 Tomca7 服务器连接、管理和加载运行我们的
Web 项目呢?如果不知道,请去 D:\tomcat7\conf\Catalina\localhost 目录下一探究竟吧。
② 当我们运行 web 应用程序时,为什么能够自动加载项目中的 index.jsp 文件运行呢?
2、简单 Web 应用程序编写(此题在第 1 题的基础上编写)
⑴ 添加 Oracle 驱动程序 ojdbc6.jar 和 JSTL 核心标记库 standard.jar、jstl.jar 到库引用路径 中。
库文件下载地址:
lib.rar
在项目文件夹的 WEB-INF 目录下新建 lib 目录,然后将下载的三个库文件放到其中。
在项目资源管理器中展开库节点,然后在弹出的菜单中选择“添加 JAR/文件夹”,然后选择刚刚添加进
WEB-INF/lib 文件夹下的三个库文件,选择“打开”,此时会在库引用路径中添加三个库文件的引用。如下
图所示:
139
⑵ 将我们前面实验中已经编写好的数据库访问接口 IStudentDAO.java、接口实现类 StudentDAOImpl、
学生表模型类 Student.java、工厂类 DAOFactory.java 和数据库连接类 DatabaseBean.java 复制到项目文件中
(这里可以再次体会到代码重用的重要性)。包结构如下图所示:
⑶ 修改 index.jsp 页面的内容如下:
<%@page import="com.mis.model.Student"%>
<%@page import="java.util.*"%>
<%@page import="com.mis.util.*"%>
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>首页面</title>
</head>
<body>
<h1 align="center">显示所有学生信息内容</h1>
<table align="center" width="70%" border="1">
<thead>
<tr>
<td>学生学号</td>
140
<td>学生姓名</td>
<td>学生性别</td>
<td>学生年龄</td>
<td>学生系部</td>
</tr>
</thead>
<tbody>
<%
List<Student> students = DaoFactory.getStudentDao().getAllStudent();
for (Student stu : students) {
%>
<tr>
<td><%=stu.getSno()%></td>
<td><%=stu.getSname()%></td>
<td><%=stu.getSsex()%></td>
<td><%=stu.getSage()%></td>
<td><%=stu.getSdept()%></td>
</tr>
<%
}
%>
</tbody>
</table>
</body>
</html>
运行 index.jsp 页面,在浏览器当中将会显示出所有学生信息记录内容。我们将 JSP 脚本和网页元素内
容融合在一起输出页面内容,这种方式对小型应用程序来说简单可行,但对大型项目的代码管理和维护工
作量很大,不利于程序员和美工人员的分工。更好的方式是采用 MVC 思想,将项目进行分层组织管理,JSP
页面负责视图(V)的显示,Servlet 脚本负责控制(C)。下面我们将修改代码,还是上面显示所有学生记
录的效果。这回采用 MVC 的思想进行编写,新建一个 servlet 和一个 jsp 页面,当项目运行时,首先调用控
制层的 Servlet,由 Servlet 负责将要显示的所有数据查询出来,以集合对象的形式放到 request 对象当中,然
后转发 forward 到 display.jsp 页面显示出来。
⑷ 编写控制层的 servlet(DisplayServlet.java)
在项目资源管理器的源包节点,右键选择菜单“新建”>“Servlet…”。如果在“新建”菜单中没有“Servlet…”
选项,则选择“其他…”,然后在类别中选择“Web”,在文件类型中选择“Servlet”。
在弹出 New Sevlet 对话框中,我们输入要新建的 servlet 名称为 DisplayServlet,包名为 com.mis.servlet,
然后选择下一步,在配置 Servlet 部署中将“将信息添加到部署描述符(web.xml)”前的勾选上。这会将新建
的 servlet 配置信息注册到部署描述符 web.xml 中,同时会在项目的 WEB-INF 文件夹下新建 web.xml 文件。
141
⑸ 打开 web.xml 文件,我们发现 IDE 已经帮我们写好了 servlet 的配置信息。我们注意到,web.xml 文
件选项卡上有分类显示的按钮,点击不同的分类可以看到图形化的配置信息内容。如下图所示:
切换到“页面”视图,在欢迎文件中填入我们刚刚创建的 DisplayServlet 名称,这样,当项目运行后,首先
会自动调用 DisplayServlet。
⑹ 打开刚刚创建的 DisplayServlet.java 文件,IDE 已经帮我们写好了大部分代码结构。其中,
processRequest 方法代码为通过 PrintWriter 对象向客户端输出 HTML 页面内容。
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
try {
/* TODO output your page here. You may use following sample code. */
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
142
out.println("<title>Servlet DisplayServlet</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>Servlet DisplayServlet at " + request.getContextPath() + "</h1>");
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
}
运行项目,会在客户端默认浏览器中显示“Servlet DisplayServlet at /JavaWebTest”这样一段文字。
⑺ 修改 DisplayServlet.java 文件中的 processRequest 方法,查询出所有学生记录内容以集合对象的形式
存放到 request 的属性当中,然后转发到 display.jsp 页面去显示查询出的学生记录。代码如下所示:
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=UTF-8");
List<Student> students = DaoFactory.getStudentDao().getAllStudent();
request.setAttribute("students", students);
request.getRequestDispatcher("display.jsp").forward(request, response);
}
⑻ 在项目资源管理器的 web 页节点,右键选择菜单“新建”>“JSP…”。如果在“新建”菜单中没有
“JSP…”选项,则选择“其他…”,然后在类别中选择“Web”,在文件类型中选择“JSP”,新建一个名称
为 display.jsp 的页面。修改 display.jsp 页面代码如下所示:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>显示学生信息内容</title>
</head>
<body>
<h1 align="center">利用 EL 表达式和核心标记库显示所有学生信息内容</h1>
<table align="center" width="70%" border="1">
<thead>
<tr>
<td>学生学号</td>
<td>学生姓名</td>
<td>学生性别</td>
<td>学生年龄</td>
<td>学生系部</td>
143
</tr>
</thead>
<tbody>
<c:forEach items="${students}" var="stu">
<tr>
<td>${stu.sno}</td>
<td>${stu.sname}</td>
<td>${stu.ssex}</td>
<td>${stu.sage}</td>
<td>${stu.sdept}</td>
</tr>
</c:forEach>
</tbody>
</table>
<center><a href="insert.jsp">插入学生记录</a></center>
</body>
</html>
运行项目,同样将会在浏览器中显示出和 index.jsp 页面同样效果的数据。但从代码来看,display.jsp 页
面显得更为优雅,所有代码都是通过标签的形式展现出来,这对美工人员来说更易于展现视图层的内容。
3、中文乱码实验和解决方案(此题在第 2 题的基础上编写)
⑴ 在项目资源管理器的 web 页节点,右键选择菜单“新建”>“JSP…”。如果在“新建”菜单中没有
“JSP…”选项,则选择“其他…”,然后在类别中选择“Web”,在文件类型中选择“JSP”,新建一个名称
为 insert.jsp 的页面,我们将在这个页面中创建一个表单,用来提交学生表数据。修改 insert.jsp 页面代码如
下所示:
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>插入学生记录</title>
</head>
<body>
<h1>插入学生记录!</h1>
<p style="color:red;">${insertErr}</p>
<form id="insertForm" name="insertForm" method="post"
action="<%=request.getContextPath()%>/InsertServlet" >
<p>学生学号:<input id="sno" name="sno" type="text" /></p>
<p>学生姓名:<input id="sname" name="sname" type="text" /></p>
<p>学生性别:<input id="ssex" name="ssex" type="radio" checked="checked" value="男"
/>男 <input id="ssex" name="ssex" type="radio" value="女" />女</p>
<p>学生年龄:<input id="sage" name="sage" type="text" /></p>
<p>学生系部:<input id="sdept" name="sdept" type="text" /></p>
144
<p><input type="submit" value="提交" /></p>
</form>
</body>
</html>
⑵ 在 insert.jsp 页面代码中,我们注意到表单是以 post 的方式提交到 InsertServlet 中去处理。我们在
com.mis.servlet 包中新建一个 InsertServlet.java 文件,同样将 InsertServlet 配置信息注册到部署描述符文件
web.xml 中。修改 InsertServlet.java 文件中 processRequest 方法代码如下所示:
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 注意下面两行代码的关键作用
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
String sno = request.getParameter("sno");
String sname = request.getParameter("sname");
String ssex = request.getParameter("ssex");
String sage = request.getParameter("sage");
String sdept = request.getParameter("sdept");
PrintWriter out = response.getWriter();
try {
out.println("<!DOCTYPE html>");
out.println("<html>");
out.println("<head>");
out.println("<title>Servlet Insert</title>");
out.println("</head>");
out.println("<body>");
out.println("<h1>插入的学生信息内容</h1>");
out.println("学生学号:" + sno);
out.println("学生姓名:" + sname);
out.println("学生性别:" + ssex);
out.println("学生年龄:" + sage);
out.println("学生系部:" + sdept);
out.println("</body>");
out.println("</html>");
} finally {
out.close();
}
}
运行 insert.jsp 页面,随意填入一些信息(带中文字符的信息),然后提交页面,InsertServlet 接收到我
们提交的数据后,将信息在浏览器中输出。我们发现,浏览器能够正常显示出数据内容。
⑶ 我们将 insert.jsp 页面的表单提交方式改为 get 后,再次运行 insert.jsp 页面,同样填入一些信息(带
中文字符的信息),然后再次提交页面,这回我们发现,浏览器中显示的数据内容为乱码。
145
<form id="insertForm" name="insertForm" method="get"
action="<%=request.getContextPath()%>/InsertServlet" >
注意:有时浏览器还会显示出正确的数据,这是由于浏览器的缓存机制,页面还是显示原来的信息内
容,我们清空缓存后,会得到正确的结果。
⑷ 下面我们修改 InsertServlet.java 文件,将下面代码
String sno = request.getParameter("sno");
String sname = request.getParameter("sname");
String ssex = request.getParameter("ssex");
String sage = request.getParameter("sage");
String sdept = request.getParameter("sdept");
更改为:
String sno = new String(request.getParameter("sno").getBytes("iso-8859-1"), "utf-8");
String sname = new String(request.getParameter("sname").getBytes("iso-8859-1"), "utf-8");
String ssex = new String(request.getParameter("ssex").getBytes("iso-8859-1"), "utf-8");
String sage = new String(request.getParameter("sage").getBytes("iso-8859-1"), "utf-8");
String sdept = new String(request.getParameter("sdept").getBytes("iso-8859-1"), "utf-8");
再次运行 insert.jsp 页面,同样填入一些信息(带中文字符的信息),然后再次提交页面,这回我们发现,
浏览器中又能正确显示出我们想要的数据。虽然,这种方法能够解决表单 get 方式提交数据的中文乱码问题,
但如果有多个处理提交表单数据的 servlet,则我们在每个 servlet 都这样书写代码,将会显得过于繁琐。下
面,我们将要介绍一种通过过滤器(filter)来处理中文乱码的优雅方式,我们只需要编写一个过滤器,即
可在所有页面中处理中文乱码的问题。
⑸ 编写过滤器(CharEncodingFilter.java)
在项目资源管理器的源包节点,右键选择菜单“新建”>“过滤器…”。如果在“新建”菜单中没有“过
滤器…”选项,则选择“其他…”,然后在类别中选择“Web”,在文件类型中选择“过滤器”。
在弹出 New 过滤器对话框中,我们输入新建的过滤器名称为 CharEncodingFilter,包名为 com.mis.filter,
然后选择下一步,在配置过滤器部署中将“将信息添加到部署描述符(web.xml)”前的勾选上。这会将新建
的过滤器配置信息注册到部署描述符文件 web.xml 中。过滤器映射的应用于被设置为“/*”,表示过滤当前
web 应用程序下的所有文件。点击“下一步”按钮,进入到过滤器初始化参数设置界面,由于这里不需要
初始化参数,点击“完成”按钮,此时,IDE 帮我们生成了一个过滤器文件 CharEncodingFilter.java。
⑹ 我们可以阅读一下 IDE 帮我们生成的过滤器文件 CharEncodingFilter.java,了解一下过滤器的代码结
构。其中很多代码我们并不需要,将过滤器代码替换如下:
146
package com.mis.filter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletRequestWrapper;
public class CharEncodingFilter implements Filter {
private FilterConfig config = null;
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
if ("get".equalsIgnoreCase(httpRequest.getMethod())) {
System.out.println("get 请求");
GetHttpServletRequest wrapper = new GetHttpServletRequest(httpRequest, "UTF-8");
System.out.println("----请求被" + this.config.getFilterName() + "过滤----");
chain.doFilter(wrapper, response);
System.out.println("----响应被" + this.config.getFilterName() + "过滤----");
} else {
System.out.println("post 请求");
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
System.out.println("----请求被" + this.config.getFilterName() + "过滤----");
//执行下一个过滤器(如果有的话,否则执行目标 servlet)
chain.doFilter(request, response);
System.out.println("----响应被" + this.config.getFilterName() + "过滤----");
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
this.config = filterConfig;
}
147
@Override
public void destroy() {
System.out.println(this.config.getFilterName() + "被销毁");
this.config = null;
}
class GetHttpServletRequest extends HttpServletRequestWrapper {
private String encoding;
public GetHttpServletRequest(HttpServletRequest request) {
super(request);
}
public GetHttpServletRequest(HttpServletRequest request, String encoding) {
super(request);
this.encoding = encoding;
}
@Override
public String getParameter(String name) {
String value = super.getParameter(name);
if (null != value) {
try {
// tomcat 默认以 ISO8859-1 处理 GET 传来的参数。
// 把 tomcat 上的值用 ISO8859-1 获取字节流,再转换成 UTF-8 字符串
value = new String(value.getBytes("ISO8859-1"), encoding);
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
}
return value;
}
}
}
注意这段代码:
GetHttpServletRequest wrapper = new GetHttpServletRequest(httpRequest, "UTF-8");
GetHttpServletRequest 为我们自定义的 HttpServletRequest 对象包装器内部类。当表单提交的方式为“get”
时,将 HttpServletRequest 对象和字符编码传递给我们自定义的请求包装器 GetHttpServletRequest,通过使用
装饰模式来改变其状态,将字符编码转换的工作添加到 getParameter()方法中去处理。
⑺ 重新运行 insert.jsp 页面,同样填入一些信息(带中文字符的信息),然后再次提交页面,这回我们
发现,浏览器中又能正确显示出我们想要的数据。至此,我们已经完美解决了“get”和“post”方式提交表
单数据时遇到的中文乱码问题。
148
⑻ 虽然 InsertServlet 已经能够显示出正确的内容,但一般 Servlet 只负责做控制层,我们希望 InsertServlet
来处理插入学生数据的工作,将数据显示功能转发给其它页面处理。修改 InsertServlet.java 文件中的
processRequest 方法代码如下:
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/html;charset=UTF-8");
String sno = request.getParameter("sno");
String sname = request.getParameter("sname");
String ssex = request.getParameter("ssex");
String sage = request.getParameter("sage");
String sdept = request.getParameter("sdept");
Student stu = new Student();
stu.setSno(sno);
stu.setSname(sname);
stu.setSsex(ssex);
stu.setSage(Integer.parseInt(sage));
stu.setSdept(sdept);
if (DaoFactory.getStudentDao().insertStudent(stu)) {
response.sendRedirect(request.getContextPath() + "/DisplayServlet");
} else {
request.setAttribute("insertErr", "插入信息失败,请检查原因!");
request.getRequestDispatcher("insert.jsp").forward(request, response);
}
}
此时,当我们再次运行 insert.jsp 页面,提交数据后,InsertServlet 负责将数据插入到数据库。当插入成
功时,页面重定向到 DisplayServlet 显示所有学生数据信息;插入失败时,将错误提示信息封装到 request
属性当中,转发到原来的 insert.jsp 页面,在 insert.jsp 页面中显示出错信息。
出错信息由 insert.jsp 页面中这段代码负责显示:<p style="color:red;">${insertErr}</p>。
4、利用所学知识尝试编写删除学生数据、修改学生数据和查询指定学号学生数据的功能。(选做)
5、实验总结
请书写你对本次实验有哪些实质性的收获和体会,以及对本次实验有何良好的建议?
149
实验四、存储子程序(2 学时)
1、实验目的
(1)掌握存储子程序(主要是存储过程)的概念,学会编写简单的存储子程序及其使用。
(2)了解存储子程序调用。
2、实验性质
验证性实验
3、实验导读
3.1、PL/SQL 基础知识
3.1.1、数据类型
作为编程语言,PL/SQL 有标量、引用、复合和大对象等数据类型。
标量类型只能存储单值的数据,用于存储字符串、数字、布尔值、日期、二进制数据等。如 CHAR、
VARCHAR、VARCHAR2、NCHAR、NVARCHAR2、ROWID、UROWID 等数据类型支持字符/字符串,
NUMBER 支持数值,BOOLEAN 支持布尔值。DATE、TIMESTAMP、INTERVAL 等支持日期时间。
引用类型有 REF CURSOR 和 REF。大对象类型有 BLOB、CLOB。
复合类型由标量类型复合而成,使用前需要定义。PL/SQL 的复合类型有记录、集合和对象等。
记录类似结构体,其中的每一个成份称为域,域的数据类型可以是标量类型,也可以是记录类型。经
常使用记录类型来处理数据表中的记录,这时用“变量名.域名”引用记录的域。可以在存储子程序的参数
中使用记录类型,可以在函数的返回值中使用记录类型,可以把游标的当前行的数据用 FETCH …INTO…
取到一个记录型变量中,还可以使用 SQL 语句 INSERT INTO…VALUE 把一个记录型变量的值插入到数据
库中。
记录的定义语法为:
CREATE TYPE type_name IS RECORD(field1,field2,…,fieldn);
注:如果要定义一个局部使用的数据类型、可以不加 CREATE 关键字。
每个域 field 的定义为:
field_name filed_data_type [[NOT NULL] {DEFAULT|:=} default_value]
例 1:记录的定义与记录型变量的定义与使用。
DECLARE
TYPE DeptRecType IS RECORD(
deptid NUMBER(4) NOT NULL :=99,
dname department.department_name%type,
loc department.location_id%type,
reg region%ROWTYPE
150
);
DepartRecord DeptRecType ;
BEGIN
DepartRecord.DNAME=”计算机工程系”;
DepartRecord.deptid=”0101”;
Insert into department values DepartRecord;
DepartRecord.dname=”化学系”;
Update department set row = DepartRecord where deptid= DepartRecord.depatid;
END
注:表名%ROWTYPE 和游标名%ROWTYPE 返回的数据类型也为记录型。
如果应用需要在一个结构中存储一系列相同类型的数据,就用 PL/SQL 的集合类型(类似 Java 的
Collection)。PL/SQL 中集合类型主要有:
①变长数组 VARRAYS。能保存固定数量的元素(但可以在运行时改变它的大小),使用有序数字作为下
标,与 JAVA 语言中的数组类似。定义语法为
TYPE type_name IS {VARRAY | VARYING ARRAY} (size_limit)
OF element_type [NOT NULL];
②嵌套表。可容纳任意个数的元素,使用有序数字作下标,与 JAVA 语言中的 SET 类似。定义语法为
TYPE type_name IS TABLE OF element_type [NOT NULL];
③索引表,也叫关联数组。可容纳元素,但使用关键字存取,与 JAVA 语言中的 Hash 表类似。定义语
法为
TYPE type_name IS TABLE OF element_type [NOT NULL]
INDEX BY [BINARY_INTEGER | PLS_INTEGER | VARCHAR2(size_limit)];
集合类型提供一组通用的属性或方法,以支持对集合中元素的存取:
COUNT 属性: 返回集合中元素的个数。
DELETE 方法: 删除最后整个集合。
DELETE(X)方法: 删除集合中的第 X 个元素。
DELETE(X,Y)方法: 删除集合中从 X 到 Y 的元素。
EXISTS(x)方法: 判断位于位置 x 处的元素是否存在,如果存在则返回 TRUE,如果 x 大于集合的
最大范围、则返回 FALSE。
EXTEND 方法: 将一个 NULL 元素添加到集合的末端。
EXTEND(x)方法: 将 x 个 NULL 元素添加到集合的末端。
EXTEND(x,y)方法: 将 x 个位于 y 的元素添加到集合的末端。
FIRST 属性: 返回集合第一个元素的位置。
LAST 属性: 返回集合最后一个元素的位置。
NEXT(x)属性: 返回位置为 x 处的元素后面的那个元素。
PRIOR(x)属性: 返回 x 处的元素前面的那个元素。
TRIM()方法: 从集合末端删除一个元素。
TRIM(x)方法: 从集合末端删除 x 个元素,其中 x 要小于集合的 COUNT 数,否则系统会提示
出错。
注意:TRIM 和 EXTEND 只对嵌套表和可变数组有效、而对索引表无效、因为索引表是无序的。
151
集合类型中数据的遍历可用下面的代码段:
v_Count:=v_Pwd.FIRST;
WHILE v_Count<=v_Pwd.LAST LOOP
DBMS_OUTPUT.PUT_LINE(v_Pwd(v_Count));
v_Count:=v_Pwd.NEXT(v_Count);
END LOOP;
编程过程中可引用数据库基表的列的数据类型:表名.列%TYPE(注意、变量名%TYPE 也可用);也可
以引用数据库基表记录的数据类型:表名%ROWTYPE;还可引用游标返回的结果集的记录类型:游标名%
ROWTYPE。
3.1.2、运算符及其运算级别
幂运算(**)、符号运算(+、-)、乘除运算(*、/)、加减及连接运算(+、-、||)、比较运算(=, <, >,
<=, >=, <>, != , IS NULL, LIKE, BETWEEN, IN)、逻辑运算(NOT、AND、OR)
3.1.3、程序控制结构
三种控制结构如下:
(一)选择结构
IF…THEN 语句:
DECLARE
sales NUMBER(8,2) := 10100;
quota NUMBER(8,2) := 10000;
bonus NUMBER(6,2);
emp_id NUMBER(6) := 120;
BEGIN
IF sales > (quota + 200) THEN
bonus := (sales - quota)/4;
UPDATE employees SET salary = salary + bonus WHERE employee_id = emp_id;
END IF;
END;
IF…THEN….ELSE 语句:
DECLARE
sales NUMBER(8,2) := 12100;
quota NUMBER(8,2) := 10000;
bonus NUMBER(6,2);
emp_id NUMBER(6) := 120;
BEGIN
IF sales > (quota + 200) THEN
bonus := (sales - quota)/4;
ELSE
bonus := 50;
END IF;
UPDATE employees SET salary = salary + bonus WHERE employee_id = emp_id;
152
END;
IF 嵌套语句:
DECLARE
sales NUMBER(8,2) := 12100;
quota NUMBER(8,2) := 10000;
bonus NUMBER(6,2);
emp_id NUMBER(6) := 120;
BEGIN
IF sales > (quota + 200) THEN
bonus := (sales - quota)/4;
ELSE
IF sales > quota THEN
bonus := 50;
ELSE
bonus := 0;
END IF;
END IF;
UPDATE employees SET salary = salary + bonus WHERE employee_id = emp_id;
END;
IF…THEN…ELSIF 语句:
DECLARE
grade CHAR(1);
BEGIN
grade := 'B';
IF grade = 'A' THEN
DBMS_OUTPUT.PUT_LINE('Excellent');
ELSIF grade = 'B' THEN
DBMS_OUTPUT.PUT_LINE('Very Good');
ELSIF grade = 'C' THEN
DBMS_OUTPUT.PUT_LINE('Good');
ELSIF grade = 'D' THEN
DBMS_OUTPUT. PUT_LINE('Fair');
ELSIF grade = 'F' THEN
DBMS_OUTPUT.PUT_LINE('Poor');
ELSE
DBMS_OUTPUT.PUT_LINE('No such grade');
END IF;
END;
CASE…WHEN 语句:
DECLARE
grade CHAR(1);
153
BEGIN
grade := 'B';
CASE grade
WHEN 'A' THEN DBMS_OUTPUT.PUT_LINE('Excellent');
WHEN 'B' THEN DBMS_OUTPUT.PUT_LINE('Very Good');
WHEN 'C' THEN DBMS_OUTPUT.PUT_LINE('Good');
WHEN 'D' THEN DBMS_OUTPUT.PUT_LINE('Fair');
WHEN 'F' THEN DBMS_OUTPUT.PUT_LINE('Poor');
ELSE DBMS_OUTPUT.PUT_LINE('No such grade');
END CASE;
END;
(二)循环语句
LOOP 循环:
DECLARE
credit_rating NUMBER := 0;
BEGIN
LOOP
credit_rating := credit_rating + 1;
IF credit_rating > 3 THEN
EXIT; -- 立即退出循环
END IF;
END LOOP;
-- control resumes here
DBMS_OUTPUT.PUT_LINE ('Credit rating: ' || TO_CHAR(credit_rating));
IF credit_rating > 3 THEN
RETURN; -- use RETURN not EXIT when outside a LOOP
END IF;
DBMS_OUTPUT.PUT_LINE ('Credit rating: ' || TO_CHAR(credit_rating));
END;
WHILE 循环:
done := FALSE;
WHILE NOT done LOOP
sequence_of_statements
done := boolean_expression;
END LOOP;
FOR 循环:
DECLARE
p NUMBER := 0;
BEGIN
FOR k IN 1..500 LOOP -- calculate pi with 500 terms
p := p + ( ( (-1) ** (k + 1) ) / ((2 * k) - 1) );
EXIT WHEN credit_rating>3
154
END LOOP;
p := 4 * p;
DBMS_OUTPUT.PUT_LINE( 'pi is approximately : ' || p ); -- print result
END;
BEGIN
FOR i IN REVERSE 1..3 LOOP -- assign the values 1,2,3 to i
DBMS_OUTPUT.PUT_LINE (TO_CHAR(i));
END LOOP;
END;
PL/SQL 还提供了一些系统函数,如 SQLCODE、SQLERRM 可用于异常处理;TO_CHAR、TO_DATE、
TO_NUMBER、HEXTORAW、RAWTOHEX 可用于变量的数据类型转换,而 NVL、NULLIF 专门用于 NULL
处理。
PL/SQL 还提供几个伪列,如序列操作中会用到 CURRVAL 和 NEXTVAL,查询树形结构将用到 LEVEL,
追踪行物理地址可用 ROWID,追踪行号可用 ROWNUM。
3.2、游标
游标是 DBMS 为用户提供的一种特殊控制结构,由一个内存缓冲区和一个游标指针组成;其中缓冲区
存放 SQL 语句的执行结构;用户可通过游标指针获取缓冲区内的每一条记录。
游标一般要先声明,然后才能使用。如果游标声明时已经与 SQL 语句绑定,则称为静态游标,否则称
为动态游标。静态游标又分为隐式游标和显式游标。
ORACLE 用游标属性反映最近一次被执行的 DML、DDL 语句(INSERT、UPDATE、DELETE、SELECT
INTO、COMMIT、ROLLBACK)的结果信息,它们是%FOUND(标识 DML 操作是否改变了行记
录),%ISOPEN(标识游标是否打开),%NOTFOUND(标识 DML 操作是否没有改变行记录),%ROWCOUNT
(标识 DML 语句影响的行数)。
显式游标用“游标名%***”格式获取游标属性,隐式游标用“SQL%***”格式获取游标属性。
显式游标仅用于查询处理。使用显式游标需要经过声明、打开、推进并获取数据、关闭四个步骤。
例 1、分别处理两个游标
DECLARE
v_jobid employees.job_id%TYPE; -- 存取 job_id 的变量
v_lastname employees.last_name%TYPE; -- 存取 last_name 的变量
CURSOR c1 IS SELECT last_name, job_id FROM employees
WHERE REGEXP_LIKE (job_id, 'S[HT]_CLERK'); --声明游标
v_employees employees%ROWTYPE; -- 一个可以存储游标行的变量
CURSOR c2 is SELECT * FROM employees WHERE
REGEXP_LIKE (job_id, '[ACADFIMKSA]_M[ANGR]'); --声明游标
BEGIN
OPEN c1; -- 打开游标
LOOP
FETCH c1 INTO v_lastname, v_jobid; -- 从游标中取值、放入变量
EXIT WHEN c1%NOTFOUND; --游标推进到末尾的标识
DBMS_OUTPUT.PUT_LINE( RPAD(v_lastname, 25, ' ') || v_jobid );
END LOOP;
155
CLOSE c1; --关闭游标
DBMS_OUTPUT.PUT_LINE( '-------------------------------------' );
OPEN c2;
LOOP
FETCH c2 INTO v_employees;
-- 抓取整个行的数据到记录变量 v_employees
EXIT WHEN c2%NOTFOUND;
DBMS_OUTPUT.PUT_LINE( RPAD(v_employees.last_name, 25, ' ') ||
v_employees.job_id );
END LOOP;
CLOSE c2;
END;
例 2、使用 FOR 循环处理游标、可以省去 OPEN、CLOSE 等步骤。
DECLARE
CURSOR c1 IS SELECT last_name, job_id FROM employees
WHERE job_id LIKE '%CLERK%' AND manager_id > 120;
BEGIN
FOR item IN c1
LOOP
DBMS_OUTPUT.PUT_LINE('Name = ' || item.last_name || ', Job = ' ||item.job_id);
END LOOP;
END;
例 3、动态游标。
//声明部分。定义一个引用游标的数据类型,再使用该类型定义一个游标变量
DECLARE
TYPE empcurtyp IS REF CURSOR
RETURN employees%ROWTYPE;
emp_cv empcurtyp;
emp_rec employees%ROWTYPE;
BEGIN
//把游标变量与 SELECT 语句绑定。
OPEN emp_cv FOR SELECT * FROM employees WHERE employee_id < 120;
LOOP
FETCH emp_cv INTO emp_rec; -- 抓取数据
EXIT WHEN emp_cv%NOTFOUND; -- 退出测试
DBMS_OUTPUT.PUT_LINE('Name = ' || emp_rec.first_name || ' ' ||
emp_rec.last_name);
END LOOP;
CLOSE emp_cv;
END;
注:PL/SQL 查询方法小计
(1)查询结果至多一行:用 SELECT INTO 语句。
156
(2)查询多行:如果想一次把查询结果赋值给变量,用 BULK COLLECT 语句;如果想处理结果集的每一
行、用游标。
3.3、存储子程序
PL/SQL 程序的基本组成单元块。块分匿名块和命名块,匿名块不被存储,只能使用一次;命名块被存
储并可以多次使用,也称为子程序。子程序又分过程和函数两种类型。过程和函数都能接受参数,但过程
没有返回值,函数有返回值。一般过程强调实施某种操作,函数强调返回计算值。
命名块可以定义在包中,这时以包为单位存储和使用;也可以独立定义,这时称为存储过程或存储函
数,统称存储子程序。
在包中定义命名块时不能使用 CREATE 关键字,独立定义时需用 CREATE 关键字。
存储子程序是数据库服务器上一段预先编译好的用某种 SQL 扩展语言(如 PL/SQL)编写的代码块,
它以一个名称存储在数据库中,成为一个独立的数据库对象,作为一个单元使得用户可以在应用程序中调
用。
存储子程序首次运行时,数据库服务器会对其作相应的处理,如检查所引用数据库对象的存在性,编
译并优化生成的查询代码,并将其存放在 cache 中,以提高系统调用的性能。
3.3.1、存储子程序的优点
①便于代码复用。可用存储子程序来封装事务规则。一旦封装完成,这些规则就有一个一致的数据接
口,可用于多个应用。
②调用清晰。不像触发器,存储子程序必须被应用、脚本、批作业或任务调用。
③执行速度快。在创建时就已经通过语法检查和性能优化,因此在执行时无需进行再次编译。如此便
加快了语句的执行速度。
④规范程序设计。存储子程序独立于调用它的应用程序,因此用户可以根据功能模块的不同,对存储
子程序进行必要的修改,而不必修改应用程序本身。
⑤提高系统安全性。可以将存储子程序作为用户存取数据的管道。为提高数据的安全性,可以限制用
户对数据表的存取权限,然后建立特定的存储子程序供特定的用户使用,完成对表数据的访问。
3.3.2、创建存储子程序的规则
存储子程序引用的对象必须在创建前就存在,存储子程序中不能有 DDL 语句(如 CREATE
PROCEDURE、CREATE TRIGGER、CREATE VIEW),DML 中的 SELECT 只能使用 SELECT…INTO…形
式,或者出现游标定义中。
3.3.3、创建存储子程序的语法
(1)存储过程
CREATE [OR REPLACE] PROCEDURE 过程名
(VAR1 参数模式 数据类型 [:=缺省值],
…………,
VARn 参数模式 数据类型 [:=缺省值]
)
IS|AS
[
--说明部分。可选,不使用 DECLARE 关键字,主要定义局部变量。其中可用 PRAGMA
AUTONOMOUS _ TRANSACTION 声明自治事务
157
]
BEGIN
--执行部分
[
EXCEPTION
--异常处理部分
]
END;
(2)存储函数
CREATE [OR REPLACE] FUNCTION 函数名
(VAR1 参数模式 数据类型 [:=缺省值],
…………,
VARn [in|out|in out] 数据类型 [:=缺省值]
)
RETURN 返回值数据类型
IS|AS
[
--变量声明部分。同存储过程要求
]
BEGIN
--执行部分、一定有 RETURN 语句
[
EXCEPTION
--异常处理部分、一定有 RETURN 语句
]
END;
(3)参数模式
参数模式有 IN、OUT、IN OUT 三种。主程序使用 IN 模式向子程序传入值,使用 OUT 模式从子程序
返回值、使用 IN OUT 模式传入值并返回值。在子程序中一定要对 OUT、IN OUT 模式的参数赋值。IN 是
缺省的参数模式。
(4)参数定义中的数据类型不能明确精度,如不能使用 NUMBER(3,2)、而只能用 NUMBER。
(5)OUT、IN OUT 模式不能有默认值。
例 4、一个 IN 参数的存储过程
CREATE OR REPLACE PROCEDURE list_a_rating(in_rating IN NUMBER)
AS
matching_title VARCHAR2(50);
TYPE my_cursor IS REF CURSOR;
the_cursor my_cursor;
BEGIN
OPEN the_cursor FOR 'SELECT title FROM books WHERE rating = :in_rating'
USING in_rating;
DBMS_OUTPUT.PUT_LINE('All books with a rating of ' || in_rating || ':');
LOOP
158
FETCH the_cursor INTO matching_title;
EXIT WHEN the_cursor%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(matching_title);
END LOOP;
CLOSE the_cursor;
END list_a_rating;
例 2、一个 IN 参数、返回 NUMBER 型结果的函数
CREATE FUNCTION get_bal(acc_no IN NUMBER)
RETURN NUMBER
IS
acc_bal NUMBER(11,2);
BEGIN
SELECT order_total INTO acc_bal FROM orders WHERE customer_id = acc_no;
RETURN(acc_bal);
END;
3.3.4、存储子程序的调用
(1)PL/SQL 代码中:在 BEGIN…END 块中直接调用存储过程。
BEGIN
PROC_NAME(实参 1,实参 2,…,实参 N);
END;
(2)SQL*PLUS 中:用 EXECUTE 或 CALL 调用存储过程:
EXECUTE PROC_NAME(实参 1,实参 2,…,实参 N);
存储子程序如果有 OUT、IN OUT 参数,则需要先定义相应的变量,然后把这些变量传入到形参位置。
例 5、存储过程调用
CREATE OR REPLACE PROCEDURE testCall(
v1 in out number,
v2 in number:=100,
v3 out number)
IS
BEGIN
v3:=v1+v2;
v1:=100;
END testCall;
---------------------PL/SQL 调用
DECLARE
w1 number:=10;
w3 number;
BEGIN
testCall(w1,250,w3);--按照形参位置依次传入实参
dbms_output.put_line('w1='||w1||' w3='||w3);
testCall(v3=>w3,v1=>w1);--按照形参名称传入实参、没有传入的使用缺省值
159
dbms_output.put_line('w1='||w1||' w3='||w3);
END;
---------------------SQLP*LUS 调用
SQL>VAR w1 number;
SQL>VAR w3 number;
SQL>EXECUTE :w1 :=10;
SQL>EXECUTE testCall(:w1,250,:w3); ----用 CALL testCall(:w1,250,:w3);也可
SQL>PRINT :w1;
SQL> PRINT :w3;
SQL>EXECUTE testCall(v1=>:w1,v3=>:w3); --不能用 CALL testCall(v1=>:w1,v3=>:w3);
(3)PL/SQL 中不能直接调用存储函数、只能在 SQL 表达式或 PL/SQL 表达式中使用存储函数。
例 6、存储函数调用
CREATE OR REPLACE FUNCTION testFunc(
v1 in out number,
v2 in number:=100,
v3 out number)
RETURN number
AS
Result number;
BEGIN
v1:=v2+v1;
v3:=v2+55;
Result:=v1+v2+v3;
return(Result);
END TESTFUNC;
------------PL/SQL 调用
DECLARE
w1 number:=10;
w3 number;
t1 number;
t2 number;
BEGIN
t1:=testFunc(w1,250,w3);--依次传入
dbms_output.put_line('w1='||w1||' w3='||w3||' t1='||t1);
t2:=testFunc(v3=>w3,v1=>w1);
dbms_output.put_line('w1='||w1||' w3='||w3||' t1='||t2);
END;
---------------------SQLP*LUS 调用
SQL> var w1 number;
SQL> var w2 number;
160
SQL> var t1 number;
SQL> var t2 number;
SQL> execute :w1:=10;
SQL> execute :t1:=testFunc(:w1,250,:w3);//函数调用
SQL> execute :t1:=testFunc(v1=>:w1,v3=>:w3);
SQL> call testFunc(v1=>:w1,v3=>:w3) into :t2;//调用出错
SQL> call testFunc(:w1,250,:w3) into :t2;
注意:不能在一个存储过程中删除另一个存储过程、只能调用另一个存储过程
3.3.5、存储子程序的调试
(1)检索用户的子程序或触发器
SELECT object_name FROM user_objects
WHERE object_type=’PROCEDURE|FUNCTION’;
(2)当提交子程序时、系统会对代码进行编译、可用 SHOW ERRORS 查看出错信息。建议使用 PL/SQL
DEVELOPER 调试。
(3)查看子程序的代码
SQL>COLUMN LINE FORMAT 9999
SQL>COLUMN TEST FORMAT A70
SQL>SELECT line,text FROM user_source WHERE name=’子程序名’;
(4)使用 PL/SQL Developer 调试
3.3.6、删除子程序
SQL>DROP PROCEDURE|FUNCTION 子程序名
3.4、包
包是一个独立存储的数据库对象,由包规范和包体组成,用来分类组织和管理子程序。
用户可以使用包来组织自己的应用,系统也以包的形式提供 PL/SQL 编程的 API。
在包的规范部分,可以定义数据类型、声明变量、常量、异常、游标、子程序。包规范为 PL/SQL 程序
提供全局对象(包括全局变量)。
包体编写游标和子程序,即只有过程、函数、游标需要在包体中定义细节。包体中也可定义局部变量。
当某个连接第一次使用包中对象时,包被加载到与该连接对应的内存区域,所以不存在不同连接之间
的并发冲突。
3.4.1、包规范语法
CREATE OR REPLACE PACKAGE 包名
{IS | AS}
[公有数据类型定义[公有数据类型定义]…]
[公有游标声明[公有游标声明]…]
[公有常量声明[公有常量声明]…]
[公有变量声明[公有变量声明]…]
[公有异常声明[公有异常声]…]
[公有子程序声明[公有子程序声明]…]
161
END [包名]
3.4.2、包体语法
CREATE OR REPLACE PACKAGE BODY 包名
{IS | AS}
[私有数据类型定义[私有数据类型定义]…]
[私有变量、常量声明[私有变量、常量声明]…]
[私有子程序声明和定义[私有子程序声明和定义]…]
[公有游标定义[公有游标定义]…]
[公有子程序定义[公有子程序定义]…]
[BEGIN
包初始化代码
END]
END [包名]
说明:
①、用户可以在程序包中定义变量。这些变量既可以定义在程序包规范中、也可以定义在程序包的主
体中。定义在规范中的那些变量可以像规范中的过程和函数一样被引用、这些变量被称为公共变量。这些
变量可以被任何具有程序包的 execute 权限的用户进行读取和修改。在主体中定义的程序包级别的变量只
能由主体中的过程实现来访问、这些变量被称为私有变量。
同样、在包体中可以再定义内部的子程序、仅供包体使用。
②、程序包级别的变量与过程和函数中的局部变量不同、它们会在数据库会话期间保持其的状态。这
意味着、用户可以使用一次调用设置一个全局变量、以完成一些工作、然后返回、并且在过程执行时再次
调用它的值、然后离开。
③、包体中定义过程与函数不能使用 CREATE OR REPLACE。
3.4.3、包的开发步骤
① 编辑调试子程序。
② 把它们继承到包规范和包体中。
③ 调试包。
例 7、下面是一个包的例子,在包规范声明了一个引用游标的数据类型、两个变量、两个过程和一个函数。
CREATE OR REPLACE PACKAGE emp_pk
AS
TYPE EmpRecType IS RECORD(
Empno char(3),
Salary REAL;
);
P1 VARCHAR2(20);
CURSOR order_sal RETURN EmpRecType;
PROCEDURE hire_emp(eno char,ename varchar2,age number,sal number,mgr char,dno char);
PROCEDURE fire_emp(e_no char);
FUNCTION count_emp(d_no chat) return INTEGER;
END emp_pk;
162
CREATE OR REPLACE PACKAGE BODY emp_pk
AS
CURSOR order_sal RETURN EmpRecType IS SELECT eno,sal FROM emp ORDER BY sal;--定义游标
PROCEDURE hire_emp(eno char,ename varchar2,age number,sal number,mgr char,dno char)
IS
BEGIN
INSERT INTO emp VALUES(ENO,ENAME,AGE,SAL,MGR,DNO)
END;--定义过程
PROCEDURE fire_emp(e_no char)
AS
BEGIN
DELETE FROM emp WHERE eno=e_no;
END; --定义过程
FUNCTION count_emp(d_no chat)
RETURN INTEGER
AS
num INTEGER;
BEGIN
SELECT COUNT(*) INTO num FROM emp WHERE deptno=d_no;
RETURN num;
END; --定义函数
END emp_pk;
3.4.4、包的调用
①、包名.类型名
②、包名.数据项名
③、包名.子程序名
3.4.5、包的优点
①模块化
②简化应用程序设计
③信息隐藏
④附加功能。包的共享变量和光标持续存在于一个会话周期中、它们可以被所有在该环境中执行的子
程序共享。利用共享的变量、可以在一个会话周期的不同事务中维护数据。即共享变量跨事务。
⑤提高性能。当连接(用户)第一次调用包的子程序时、整个包会被加载到内存、以后的调用不需要
再读盘、也不必在编译子程序。
例 8:编写一个存储过程、输入部门号、返回该部门所有雇员信息。
分析:PL/SQL 中不能直接使用 SELECT 语句、只有游标中才能存储结果集。因此使用 OUT 模式的游
标型参数来返回部门信息。
方案 1、先用包定义一个引用游标数据类型、然后用该数据类型定义存储过程。
163
CREATE OR REPLACE PACKAGE TESTPACKAGE
AS
TYPE test_cursor IS REF CURSOR;
END TESTPACKAGE;
CREATE OR REPLACE PROCEDURE sp_pro9
(spNo in number,p_cursor out testpackage.test_cursor)
IS
BEGIN
open p_cursor for select * from employees where department_id = spNo;
END sp_pro9;
-----------------------------------测试一
SQL>VAR C1 REFCURSOR;
SQL>EXECUTE sp_pro9(90,:C1);
SQL>SELECT :CE FROM DUAL;
------------------------------------------测试二
DECLARE
c1 testpackage.test_cursor;
i employees%ROWTYPE;
BEGIN
sp_pro9(90,c1);
LOOP
FETCH c1 INTO i;
EXIT WHEN c1%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(i.first_name|| ' ' ||i.last_name);
END LOOP;
END;
方案 2、使用 Oracle 预定义的 REF CURSOR、这样不需要定义一个包;
CREATE OR REPLACE PROCEDURE getcur(p_rc out sys_refcursor)
AS
BEGIN
open p_rc for 'select * from sc';
END getcur;
-------------------------------测试一、不能用 FOR 循环--------------
DECLARE
c sys_refcursor;
i sc%rowtype;
BEGIN
getcur(c);
164
fetch c into i;
WHILE c%found loop
dbms_output.put_line(i.sno||' '||i.cno||' '||i.grade);
fetch c into i;
END LOOP;
END
-------------------------------------------------测试二-----------------------
SQL>VAR C1 REF CURSOR;
SQL>EXECUTE getcur(C1);
SQL>SELECT :CE FROM DUAL;
3.5、序列
序列是一个独立存储的可供多个用户共享的数据库对象,其能够为并发用户的应用程序提供一系列唯
一的整数数字,这些整数常为表主码值。
序列为每一次的请求提供唯一的、按一定规律递增或递减的整数。
3.5.1、序列的定义
CREATE SEQUENCE 序列名
INCREMENT BY <数字值> /* 步长*/
START WITH <数字值> /* 初始值*/
MAXVALUE <数字值> /* 最大值*/
3.5.2、序列的使用
序列有两个属性:NEXTVAL 返回下一个序列值、CURRVAL 返回当前的序列值。
例 1、先创建一个表、再创建一个序列、然后向该表追加数据。
CREATE TABLE cdpt(
id number(6),
name varchar2(30),
constraint pk_id primary key(id)
);
CREATE SEQUENCE seqForCdpt
INCREMENT BY 1
START WITH 1
MAXVALUE 999999
INSERT INTO cdpt VALUES(seqForCdpt.nextval, ‘ZHANGSAN’);
COMMIT;
SELECT seqForCdpt.CURRVAL FROM DUAL;
SELECT seqForCdpt.NEXTVAL FROM DUAL;
165
4、实验内容
(1)、阅读有关资料,理解存储过程概念。
(2)、在学生-课程数据库中,按要求编写一个存储子程序,并分别编写在 PL/SQL 块以及 PL/SQL
DEVELOPER 中测试该存储子程序的代码,上机验证你的代码。
①编写存储过程,通过输入参数接收学生学号信息,通过输出参数输出学生相应的平均成绩;
166
②编写存储函数,通过输入参数接收学生学号信息,返回学生的平均成绩;
③如果要求程序依据接收的学生学号信息,提供学生的平均成绩以及选课信息,又该如何实现(提示:
由于选课信息是多行多列的结果集,借助游标方可实现)?
167
(3)、在学生-课程数据库中,针对 student 表,定义包实现如下功能,并分别在 PL/SQL 块以及 PL/SQL
DEVELOPER 中测试该存储子程序的代码,上机验证你的代码。
① 查询所有学生信息记录
② 查询指定学号的学生信息记录
③ 插入学生信息记录(包括学号、姓名、性别、年龄、系部字段)
④ 删除指定学号学生信息记录
⑤ 更新学生信息记录(包括学号、姓名、性别、年龄、系部字段)
5、实验总结
请书写你对本次实验有哪些实质性的收获和体会,以及对本次实验有何良好的建议?
168
实验五、触发器(2 学时)
1、实验目的
(1)理解触发器的概念,掌握触发器的创建、修改、查看、删除等操作。
(2)理解触发器执行过程,了解触发器在保证数据完整性和实现商业规则上的使用。
2、实验性质
验证性实验
3、实验导读
3.1、触发器的概念
触发器是一种存储在 DBMS 上的能被 DBMS 自动执行的程序,须与一个表、视图或事件关联,当某个
事件发生时触发器程序被系统自动调用,以响应用户对数据库的某种操作。
3.2、触发器的分类
依据触发器关联对象的不同,分为以下三类:
DML 触发器。与一个表关联,能够对表上发生的 INSERT、DELETE、UPDATE 等事件(DML 操作)
产生响应。响应时机可以是事件发生前(用 BEFOR 关键字),也可以是事件(操作)发生后(用 AFTER
关键字)。
INSTEAD OF 触发器。与一个视图关联,能够对视图上发生的 INSERT、DELETE、UPDATE 事件(DML
操作)产生响应,以把对视图的操作转换为对基表的操作。
系统触发器。与一个数据库事件关联。数据库事件分为两类,系统事件包括系统启动 START、系统关
闭 SHUT DOWN、服务器错误 SERVERERROR。用户事件包括 CREATE、ALTER、DROP、ANALYZE、
ASSOCIATE STATISTICS、DISASSOCIATE STATISTICS、AUDIT、NOAUDIT、COMMENT、GRANT、
REVOKE、RENAME、TRUNCATE 以及 LOGON、LOGOFF。响应时机可以是事件发生前(BEFOR),
也可以是事件(操作)发生后(AFTER)。
依据触发时机,分为 AFTER、BEFORE、INSTEAD OF 三类。
依据触发器代码被执行的频率,有可以分为表级(或语句级)触发器(对某个事件运行一次触发器代码)
和行级触发器(对某个事件中受影响的每一行均运行一次触发器代码)。
3.3、触发器的作用
触发器可以使用 PL/SQL 语句进行复杂的逻辑处理,尤其是基于一个表创建的触发器可以对多个表进行
操作,以实现复杂的跨表业务约束。
级联修改数据库中相关的表。例如在数据库里面有两个相关联的表,那么我们可以创建一个触发
器,当修改一个表的数据时,同时会修改另一个表的相关数据。
169
执行比 Check 约束更复杂的约束操作(Check 可以夸字段,不能夸表;触发器可以跨表)。在触发
器中可以书写更加复杂的语句,例如可以引用多个表,并使用 IF 等语句做更复杂的操作。
回滚违反引用完整性的操作。
比较表修改前后数据间的差别,并根据差别采取相应的操作。
实现审计、日志以及其它的安全措施。
注:实现完整性约束是数据库应用开发的一个重要内容,可实现技术分为服务器端技术和客户端技术。
客户端技术指在提供给客户端的用户界面中,使用 Radio 控件、Check 控件、下拉菜单等控件限制客户只能
在特定数据项中选择。服务器端技术又分为两类,一类是使用 DBMS 自身提供的完整性约束机制,如主码约
束、唯一性约束、非空约束、Check 约束、外码约束。另一类是使用存储过程和触发器。由于触发器存在性
能上的不足,服务器端实现技术中应该优先选用 DBMS 自身的约束机制。
3.4、创建触发器的规则
在当前 SCHEMA 下创建触发器,需要用户有 CREATE TRIGGER 的系统权限。在其它 SCHEMA 下创建触
发器,需 CREATE ANY TRIGGER 权限。
如果触发器代码中包含有若干 SQL 语句,或是调用某些存储过程,当前用户必须具有执行这些操
作的权限,且这些权限必须是用直接授权(非通过角色授权)的方式得到。
每个触发器都是一个数据库对象,因此其名称必须按照标识符的命名规则来命名。
在实现应用的业务约束时,优先使用数据库约束而不是触发器。
DML 语句可出现在触发器体代码体内,但 SELECT 必须以 SELECT…INTO…的格式出现,或出现在游
标定义中。
缺省情况下,触发器和触发语句处于同一个事务上下文,拥有共同的锁资源,因此触发器内不能
出现 DDL 语句(DDL 语句会引起事务的立即提交)和事务处 理 语 句 ( 但 系 统 触 发 器 中
CREATE/ALTER/DROP TABLE 语句和 ALTER...COMPILE 语句允许使用)。这时,行级触发器中的代
码不能访问触发器所关联的基表(俗称变异表),但语句级触发器可以访问基表。
当然也可以让触发器与触发语句处于不同的事务上下文,即触发器另立门户,自己成为一个
独立的事务,俗称自治事务,这时触发器中的代码可以访问基表(但基表中的数据并没有主事务
要更改的结果,因为这时主事务还没有提交。),并需要自己显式的提交与回滚。
在触发器中实现自治事务,只需在触发器的声明部分加上如下语句:
PRAGMA AUTONOMOUS_TRANSACTION;
同理,过程和函数也有自治事务的概念,实现方法同上。
触发器不使用 AS 或 IS 关键字。如果要使用声明,则需要使用关键字 DECLARE 显式引入(存储子
程序在声明时不需要 DECLARE 显式引入)。
触发器的大小不能超过 32K。
3.5、触发器的语法
3.5.1、DML 触发器语法
CREATE OR REPLACE TRIGGER trigger_name
{BEFORE | AFTER}
{INSERT | DELETE | [UPDATE [OF(col,col2,col3……)]}
ON table_name
170
[FOR EACH ROW [WHEN (condition)]]
[DECLARE]
BEGIN
trigger_body
END trigger_name;
说明:
①、多个触发事件用 OR 连接:INSERT OR DELETE OR UPDATE。
②、对于 UPDATE 事件,可细化到特定的列。
③、如果有 FOR EACH ROW 子句则为行级触发器,对 DML 所影响的每一行都将调用触发器代码。在行级
触发器中可用 WHERE 条件,以限定仅对特定的行触发。ORACLE 为行级触发器提供两个关联名 NEW 和 OLD,
前者代表修改 之后的行,后者代表修改之前的行,在代码体中要引用这两行的数据,可用:NEW.列
名、:OLD.列名。不同事件环境下,两个关联名的定义表 5-1 所示。
表 5-1、行级触发器中关联名的意义
触发语句 :OLD :NEW
INSERT 未定义-所有字段均为 null 触发语句完成时,要插入的值
UPDATE 更新以前相应记录行的原始值 触发语句完成时,要更新的值
DELETE 更新以前相应记录行的原始值 未定义-所有字段均为 null
④、同一个事件可触发多个触发器。同一个事件触发的不同触发器执行顺序为:
语句(表)级 BEFORE 触发器
行级 BEFORE 触发器
触发语句
行级 AFTER 触发器
语句(表)级 AFTER 触发器
用户可以在 BEFORE 行级触发器把新、旧记录的值存储到一个全局变量(如一个临时表、一个 PL/SQL 表、
一个包变量),然后在 AFTER 表级触发器或 After 语句级触发器中再作进一步处理。
⑤、ORACLE 提供三个谓词 INSERTING、DELETING、UPDATING、UPDATING OF(列)来对触发事件进行鉴别。
例 1、行级触发器,带 WHEN 条件
CREATE OR REPLACE TRIGGER Print_salary_changes
BEFORE DELETE OR INSERT OR UPDATE ON Emp_tab
FOR EACH ROW
WHEN (new.Empno > 0)
DECLARE
sal_diff number;
BEGIN
sal_diff := :new.sal - :old.sal;
dbms_output.put(‘Old salary: ' || :old.sal);
dbms_output.put(‘New salary: ' || :new.sal);
dbms_output.put_line(‘Difference ' || sal_diff);
END;
3.5.2、INSTEAD OF 触发器语法:
171
CREATE OR REPLACE TRIGGER trigger_name
INSTEAD OF
{INSERT | DELETE | [UPDATE]}
ON view_name
[DECLARE]
BEGIN
Trigger_body;
END trigger_name;
说明:
①UPDATE 关键字不能细化到列。
②必须是行级触发器,可不用 FOR EACH ROW 关键字,但不能用 WHERE 子句。可访问基表。
例 2、一个 INSTEAD OF 触发器,带变量声明部分。
CREATE TABLE Project_tab (
Prj_level NUMBER,
Projno NUMBER,
Resp_dept NUMBER
);
CREATE TABLE Emp_tab (
Empno NUMBER NOT NULL,
Ename VARCHAR2(10),
Job VARCHAR2(9),
Mgr NUMBER(4),
Hiredate DATE,
Sal NUMBER(7,2),
Comm NUMBER(7,2),
Deptno NUMBER(2) NOT NULL
);
CREATE TABLE Dept_tab (
Deptno NUMBER(2) NOT NULL,
Dname VARCHAR2(14),
Loc VARCHAR2(13),
Mgr_no NUMBER,
Dept_type NUMBER
);
//创建视图
CREATE OR REPLACE VIEW manager_info AS
SELECT e.ename, e.empno, d.dept_type, d.deptno, p.prj_level,p.projno
FROM Emp_tab e, Dept_tab d, Project_tab p
WHERE e.empno = d.mgr_no
AND d.deptno = p.resp_dept;
//创建触发器
CREATE OR REPLACE TRIGGER manager_info_insert
INSTEAD OF INSERT ON manager_info
172
REFERENCING NEW AS n --给:new 换名
FOR EACH ROW
DECLARE
rowcnt number;//声明变量
BEGIN
SELECT COUNT(*) INTO rowcnt FROM Emp_tab WHERE empno = :n.empno;
IF rowcnt = 0 THEN
INSERT INTO Emp_tab (empno,ename) VALUES(:n.empno, :n.ename);
ELSE
UPDATE Emp_tab SET Emp_tab.ename = :n.ename
WHERE Emp_tab.empno = :n.empno;
END IF;
SELECT COUNT(*) INTO rowcnt FROM Dept_tab WHERE deptno = :n.deptno;
IF rowcnt = 0 THEN
INSERT INTO Dept_tab (deptno, dept_type) VALUES(:n.deptno, :n.dept_type);
ELSE
UPDATE Dept_tab SET Dept_tab.dept_type = :n.dept_type WHERE Dept_tab.deptno = :n.deptno;
END IF;
SELECT COUNT(*) INTO rowcnt FROM Project_tab WHERE Project_tab.projno = :n.projno;
IF rowcnt = 0 THEN
INSERT INTO Project_tab (projno, prj_level) VALUES(:n.projno, :n.prj_level);
ELSE
UPDATE Project_tab SET Project_tab.prj_level = :n.prj_level
WHERE Project_tab.projno = :n.projno;
END IF;
END;
3.5.3、数据库事件触发器
例 3、一个系统触发器,能够把登录用户登记到审计表中
CONNECT system/manager
GRANT ADMINISTER DATABASE TRIGGER TO scott;
CONNECT scott/tiger
CREATE TABLE audit_table (
seq number,
user_at VARCHAR2(10),
time_now DATE,
term VARCHAR2(10),
job VARCHAR2(10),
173
proc VARCHAR2(10),
enum NUMBER
);
CREATE OR REPLACE PROCEDURE foo (c VARCHAR2)
AS
BEGIN
INSERT INTO Audit_table (user_at) VALUES(c);
END;
CREATE OR REPLACE TRIGGER logontrig
AFTER LOGON ON DATABASE
-- 调用一个已经存在的存储过程 foo.ORA_LOGIN_USER 是一个函数,其返回触--发触发器的事件信息
CALL foo (ora_login_user)
例 4、用触发器实现外码约束。要求:当对子表作插入或修改时,确保外码的值在父表中;当对父表作
删除或更新主键时,如果有子表引用,可以限制父表的操作,或者级联删除/更新,或者设为 NULL;由多个
触发器实现。
--子表上的触发器
CREATE OR REPLACE TRIGGER Emp_dept_check
BEFORE INSERT OR UPDATE OF Deptno
ON Emp_tab
FOR EACH ROW WHEN (new.Deptno IS NOT NULL)
DECLARE
Dummy INTEGER; -- to be used for cursor fetch
Invalid_department EXCEPTION;
Valid_department EXCEPTION;
Mutating_table EXCEPTION;
PRAGMA EXCEPTION_INIT (Mutating_table, -4091);
CURSOR Dummy_cursor (Dn NUMBER) IS SELECT Deptno FROM Dept_tab
WHERE Deptno = Dn FOR UPDATE OF Deptno; --FOR UPDATE 非常重要
BEGIN
OPEN Dummy_cursor (:new.Deptno);
FETCH Dummy_cursor INTO Dummy;
IF Dummy_cursor%NOTFOUND THEN
RAISE Invalid_department;--抛出异常
ELSE
RAISE valid_department; --抛出异常
END IF;
CLOSE Dummy_cursor; --关闭游标
EXCEPTION
WHEN Invalid_department THEN
CLOSE Dummy_cursor; --如果抛出异常,前面的关闭游标语句将不被执行
174
Raise_application_error(-20000, 'Invalid Department'|| ' Number' ||
TO_CHAR(:new.deptno));
WHEN Valid_department THEN
CLOSE Dummy_cursor;
WHEN Mutating_table THEN
NULL;
END;
--以下是父表上的触发器,当子表上有引用时抛出异常
CREATE OR REPLACE TRIGGER Dept_restrict
BEFORE DELETE OR UPDATE OF Deptno
ON Dept_tab
FOR EACH ROW
--抛出异常
DECLARE
Dummy INTEGER; -- to be used for cursor fetch
Employees_present EXCEPTION;
employees_not_present EXCEPTION;
-- Cursor used to check for dependent foreign key values.
CURSOR Dummy_cursor (Dn NUMBER) IS
SELECT Deptno FROM Emp_tab WHERE Deptno = Dn;
BEGIN
OPEN Dummy_cursor (:old.Deptno);
FETCH Dummy_cursor INTO Dummy;
IF Dummy_cursor%FOUND THEN
RAISE Employees_present; -- dependent rows exist
ELSE
RAISE Employees_not_present; -- no dependent rows
END IF;
CLOSE Dummy_cursor;
EXCEPTION
WHEN Employees_present THEN
CLOSE Dummy_cursor;
Raise_application_error(-20001, 'Employees Present in'
|| ' Department ' || TO_CHAR(:old.DEPTNO));
WHEN Employees_not_present THEN
CLOSE Dummy_cursor;
END;
--以下是父表上的触发器,把子表上的外码设置为 NULL
CREATE OR REPLACE TRIGGER Dept_set_null
AFTER DELETE OR UPDATE OF Deptno
ON Dept_tab
175
FOR EACH ROW
BEGIN
IF UPDATING AND :OLD.Deptno != :NEW.Deptno OR DELETING THEN
UPDATE Emp_tab SET Emp_tab.Deptno = NULL
WHERE Emp_tab.Deptno = :old.Deptno;
END IF;
END;
--级联删除
CREATE OR REPLACE TRIGGER Dept_del_cascade
AFTER DELETE ON Dept_tab
FOR EACH ROW
BEGIN
DELETE FROM Emp_tab
WHERE Emp_tab.Deptno = :old.Deptno;
END;
--以下实现级联更新
--先创建一个序列,作为更新的标识
CREATE SEQUENCE Update_sequence INCREMENT BY 1 MAXVALUE 5000 CYCLE;
--包中存放修改标识
CREATE OR REPLACE PACKAGE Integritypackage AS
Updateseq NUMBER;
END Integritypackage;
CREATE OR REPLACE PACKAGE BODY Integritypackage AS
END Integritypackage;
-- 给子表追加一个标识列:
ALTER TABLE Emp_tab ADD Update_id NUMBER;
--表级触发器,更新主表 Deptno 列时从序列中取一值放到包的全局变量中
CREATE OR REPLACE TRIGGER Dept_cascade1
BEFORE UPDATE OF Deptno
ON Dept_tab
DECLARE
Dummy NUMBER;
BEGIN
SELECT Update_sequence.NEXTVAL INTO Dummy FROM dual;
Integritypackage.Updateseq := Dummy;
END;
176
--行级触发器,更新主表 Deptno 列时,同步更新子表相应的值,下面的代码使用了
--乐观锁的处理方法。
CREATE OR REPLACE TRIGGER Dept_cascade2
AFTER DELETE OR UPDATE OF Deptno
ON Dept_tab
FOR EACH ROW
BEGIN
IF UPDATING THEN
UPDATE Emp_tab SET Deptno = :new.Deptno,Update_id = Integritypackage.Updateseq
WHERE Emp_tab.Deptno = :old.Deptno AND Update_id IS NULL;
END IF;
IF DELETING THEN
DELETE FROM Emp_tab WHERE Emp_tab.Deptno = :old.Deptno;
END IF;
END;
--表级触发器
CREATE OR REPLACE TRIGGER Dept_cascade3
AFTER UPDATE OF Deptno
ON Dept_tab
BEGIN
UPDATE Emp_tab SET Update_id = NULL WHERE Update_id = Integritypackage.Updateseq;
END;
3.6、查询触发器信息
系统提供 user_triggers 视图,结构如下,用户可通过该视图获取某个触发器的详细信息。
名称 类型
-------------------------------------------------------------------
TRIGGER_NAME VARCHAR2(30)
TRIGGER_TYPE VARCHAR2(16)
TRIGGERING_EVENT VARCHAR2(227)
TABLE_OWNER VARCHAR2(30)
BASE_OBJECT_TYPE VARCHAR2(16)
TABLE_NAME VARCHAR2(30)
COLUMN_NAME VARCHAR2(4000)
REFERENCING_NAMES VARCHAR2(128)
WHEN_CLAUSE VARCHAR2(4000)
STATUS VARCHAR2(8)
DESCRIPTION VARCHAR2(4000)
ACTION_TYPE VARCHAR2(11)
TRIGGER_BODY LONG
177
上述结构中,TRIGGER_TYPE 的值如“AFTER EACH ROW”,TRIGGERING_EVENT 的值如“INSERT”,
BASE_OBJECT_TYPE 的值如“TABLE”,STATUS 的值如“NO”或“OFF”(一般在做数据恢复时,可以让触发器
处于 OFF 状态)。
检索用户的触发器可用如下 SQL 语句:
SELECT object_name FROM user_objects WHERE object_type=’ TRIGGER’;
在 PL/SQL 中查看触发器代码体,可用如下的语句:
SQL> set LONG 200
SQL>SELECT TRIGGER_BODY FROM USER_TRIGGERS
WHERE TRIGGER_NAME='SCORE_ROW';
3.7、其它触发器相关语法
删除:DROP TRIGGER 触发器名
重新编译|使触发器失效|:ALTER TRIGGER 触发器名 COMPILE|DISABLE|ENABLE;
使表上的触发器全部失效:ALTER TABLE 表名 DISABLE ALL TRIGGERS;
说明:当删除了一个表时,和这个表相关联的所有触发器也会被删除;当删除触发器时,系统也会将
所有保存在系统表里的关于该触发器的信息删除。
3.8、实验方案讨论
本题目显然要在 sc 表上建立触发器,当然最好是行级触发器:在行级触发器中访问 SC 表,用聚集函
数获取学生的平均值,然后更新 student 表的字段,但这会带来“访问变异表”问题。
方案一:写两个触发器,行级触发器把学号保存到一个中间结果,语句级触发器再据学号访问 SC 表,
获取平均值。
第一步:创建一个包,其中定义一个可以存放学生学号的索引表。
CREATE OR REPLACE PACKAGE STUDENT_SCORE
AS
--定义数据类型
TYPE INDEX_TABLE_TYPE_FOR_SCORE IS TABLE OF SC.SNO%TYPE INDEX BY VARCHAR2(9);
STUDENT_SNOS INDEX_TABLE_TYPE_FOR_SCORE;--定义变量
END STUDENT_SCORE;
第二步:定义行级触发器
CREATE OR REPLACE TRIGGER SCORE_ROW
AFTER INSERT OR DELETE OR UPDATE
ON SC
FOR EACH ROW
BEGIN
IF INSERTING THEN
178
--SCORE IS NOT NULL
IF (:NEW.GRADE IS NOT NULL) THEN
STUDENT_SCORE.STUDENT_SNOS(:NEW.SNO):=:NEW.SNO;
END IF;
END IF;
IF DELETING THEN
--SCORE IS NOT NULL
IF (:OLD.GRADE IS NOT NULL) THEN
STUDENT_SCORE.STUDENT_SNOS(:OLD.SNO):=:OLD.SNO;
END IF;
END IF;
IF UPDATING THEN
IF :OLD.SNO!=:NEW.SNO THEN
STUDENT_SCORE.STUDENT_SNOS(:NEW.SNO):=:NEW.SNO;
STUDENT_SCORE.STUDENT_SNOS(:OLD.SNO):=:OLD.SNO;
ELSE
STUDENT_SCORE.STUDENT_SNOS(:NEW.SNO):=:NEW.SNO;
END IF;
END IF;
END SCORE_ROW;
第三步:定义语句级触发器
CREATE OR REPLACE TRIGGER SCORE_STATEMENT
AFTER INSERT OR DELETE OR UPDATE
ON SC
DECLARE
POS_STR VARCHAR2(9);
SNO_TEMP SC.SNO%TYPE;
BEGIN
POS_STR:=STUDENT_SCORE.STUDENT_SNOS.FIRST;
WHILE POS_STR IS NOT NULL
LOOP
SNO_TEMP:=STUDENT_SCORE.STUDENT_SNOS(POS_STR);
UPDATE STUDENT SET SAVGGRADE=(SELECT AVG(GRADE) FROM SC
WHERE SNO=SNO_TEMP) WHERE SNO=SNO_TEMP;
POS_STR:=STUDENT_SCORE.STUDENT_SNOS.NEXT(POS_STR);
END LOOP;
END SCORE_STATEMENT;
评价:上述方案只考虑向包变量中添加学号,没有考虑触发器退出时把包变量中学号删除;当语句级
触发器下一次运行时,检索到的学号可能是上一次处理的结果。建议在行级触发器开始时清空包变量中的
数据。
179
方案(二)使用 Oracle 临时表。行级触发器把学号存放到临时表中。表级触发器再作处理。
第一步:创建临时表
CREATE GLOBAL TEMPORARY TABLE STUDENT_T_TRANSACTION
ON COMMIT DELETE ROWS
AS
select sno from sc where 1=2;
或者
CREATE GLOBAL TEMPORARY TABLE STUDENT_T_TRANSACTION
(
SNO CHAR(9)
)
ON COMMIT DELETE ROWS;
第二步:创建行级触发器
CREATE OR REPLACE TRIGGER t1_row
AFTER DELETE or INSERT or UPDATE
ON SC
FOR EACH ROW
DECLARE
-- local variables here
BEGIN
if inserting then
insert into student_t_Transaction values(:new.sno);
elsif deleting then
insert into student_t_Transaction values(:old.sno);
else
insert into student_t_Transaction values(:new.sno);
insert into student_t_Transaction values(:old.sno);
end if;
END t1_row;
第三步:创建语句级触发器
CREATE OR REPLACE TRIGGER t2_statement
AFTER delete or insert or update
ON SC
DECLARE
-- local variables here
CURSOR c1 IS SELECT DISTINCT sno FROM student_t_Transaction;
avg1 NUMBER;
BEGIN
FOR i IN c1 LOOP
SELECT AVG(grade) INTO avg1 FROM sc WHERE sno=i.sno;
dbms_output.put_line(i.sno);
180
UPDATE student SET savggrade = avg1 WHERE sno = i.sno;
END LOOP;
END t2_statement;
181
4、实验内容
(1)、阅读有关资料,理解触发器概念。
(2)、编写触发器,实现 student 表执行插入操作后给出相应提示。
(3)、在学生-课程数据库中,学生表中有“平均成绩”列 SAvgGrade,要求当对选课表 SC 做 insert(注
意特殊情况:插入的元组中成绩字段可能为 null)、delete、update 修改时,系统自动修改学生表中平均
成绩。请编写触发器实现该业务,并设计案例加以测试。
182
(4)、在数据库中创建视图 info_view,包含学生学号、姓名、课程号、课程名、成绩。该视图依赖于表
student、course 和 sc,是不可更新视图。请编写替代触发器,当向视图中插入数据时分别向表 student、
course 和 sc 中插入数据,从而实现向视图插入数据的功能。
5、实验总结
请书写你对本次实验有哪些实质性的收获和体会,以及对本次实验有何良好的建议?
183
实验六、C/S 架构应用程序(3 学时)
1、实验目的
(1)理解 C/S 架构相关概念。
(2)掌握 J2EE 平台下 C/S 架构应用的实现。
(3)掌握图形用户界面中 BLOB 数据类型的处理
2、实验性质
综合性实验
3、实验导读
3.1、数据库应用系统架构
数据库应用系统伴随计算机硬软件技术的发展,从集中式架构发展到分层式架构,分层式架构又分为
两层 C/S 架构、三层 C/S 架构和多层 C/S 架构。三层 C/S 架构中以 B/S 架构最为流行。
3.1.1、集中式结构
集中式结构将系统的所有程序,包括 DBMS、应用程序、与用户终端进行通信的软件等集中到一台或
几台称为主机的计算机中,所有的数据及数据处理工作都在主机中运行,客户通过终端使用系统。这种结
构的应用系统升级方便(不用考虑客户机的系统升级)。但对硬件的一次性投资较高,数据集中处理会随着
用户的增多形成瓶颈;数据传输的开销较大。
现在已经很少使用这种结构。
3.1.2、分层架构
分层架构一般指的是将一个系统分为多个部分,系统功能由各部分协同完成。分层的目的,早期主要
是提高资源的利用率和系统的性能,现在主要是降低系统开发的难度。
(1)两层 C/S 架构
将数据库应用系统的计算机分为客户机和服务器两类,系统的功能在客户机和服务器之间划分,形成
一种客户机请求服务,服务器提供服务的应用系统结构。其中客户机负责应用逻辑、用户界面表达逻辑的
处理和显示,通过网络与服务器交互。服务器负责向客户机提供数据服务,实现数据管理和事务逻辑,有
时也完成有限的应用逻辑。
(2)三层 C/S 架构
三层 C/S 结构将应用协同的功能分成表示层、功能层和数据层三部分。其中表示层负责显示并与用户
交互-客户机完成的功能,功能层实现应用逻辑(应用服务),数据层负责数据管理和数据库服务。
三层 C/S 架构中,表示层用浏览器实现,就得到一个 B/S/S 结构,俗称 B/S 架构。
(3)多层 C/S 架构
为了应对大规模复杂系统的开发,把功能层进一步细化分层,就得到多层 C/S 架构。
3.1.3、开发架构设计与实现
184
在类的组织上,一般按表示层(界面类)、业务逻辑层(领域对象或实体对象)、数据存取层(DAO 接
口、DAO 实现)、辅助工具层(JDBC 调用)来组织系统的包架构。
实现上可以使用单列模式、工厂模式、装饰模式等设计模式,以提高系统的性能和复用率。
3.2、两层 C/S 架构的实现
两层 C/S 架构逻辑上把整个系统分为服务器和客户机两部分,服务器提供数据源,客户机提供系统与
用户的交互、从服务器存取数据和实现系统的业务逻辑处理。物理上可以把服务器与客户机集中在一台计
算机上。客户机通常为用户提供基于 Windows 的交互界面,在 J2EE 平台上,一般基于 JSwing 实现 Windows
窗口。
两层架构的开发一般分分析、设计、实现、测试等主要步骤。
分析阶段主要分析系统的业务需求、功能需求、数据需求(数据存储和数据处理两个方面),形成系统
开发的数据字典。
设计阶段主要设计系统的拓扑结构、功能模块、用户界面、数据库结构及开发架构。其中数据库结构
设计又可分为概念结构设计、逻辑结构设计和物理结构设计三个阶段。开发架构包括代码的类、包;数据
库端的存储子程序、包、序列等。
实现阶段依据设计文档进行编码,最后是加载数据,进行调试。
3.3、SDI 界面的设计与实现
SDI 称为单文档应用程序界面,它是一个单独的窗体,不能多文档共存。界面用于系统与用户的交互,
一般有菜单区、快捷按钮区和内容区。通过事件处理程序,实现内容区与菜单选项的同步。内容区常用一
个 JPanel 来实现。实验二中实验内容设计的就是一个单文档应用程序界面。
图 6-1 是一个简单的用户窗口,它由以下几个部分组成:
图 6-1 一个简单的用户窗口
1、菜单
菜单区由一个菜单栏 (JMenuBar)实现,其中可以放置多个一级菜单(JMenu 实现),而每个一级菜单又
可以由多个菜单项(JMenuItem 实现)组成。通过 JMenuItem 的 setAccelerator()方法可设置快捷方式,通过
JMenuItem 的 addActionListener()方法可注册事件监听程序。最后把菜单项加到主菜单,把主菜单加到菜单
栏,把菜单栏加到主窗口。
2、子窗口
185
每个子窗口用 Jpanel 或 JDialog 或 JOptionPane 实现。子窗口中的组件布局可以使用布局管理器,也可
以自行对组件绝对定位。子组件的实现包括组件实例化、设置组件属性(字体、颜色、Border)、注册监听
器等。
SDI 单窗体应用程序设计方法详见实验二内容,此处不再敖述。
3.4、MDI 界面的设计与实现
MDI 称为多文档应用程序界面,它使用一个外部窗体包含多个内部窗体,内部窗体支持拖动、关闭、
图标化、调整大小、标题显示等功能。当主窗体最小化时,内部窗体会被隐藏;当拖动内部窗体时,不能
拖出主窗体。
3.4.1、创建桌面面板
javax.swing. JDesktopPane 类是多窗体界面或虚拟桌面的容器,该类创建的对象是一个桌面面板,
JDesktopPane 类只有一个无入口参数的构造方法 JDesktopPane (),用于构造一个桌面面板容器。
例如:
JDesktopPane desktopPane = new JDesktopPane() ; //创建一个桌面面板
JDesktopPane 类创建的对象是一个桌面面板,该类提供的方法可以方便地操作桌面面板中的内部窗体
和组件。该类的常用方法如表 6-1 所示。
表 6-1 JDesktopPane 类的常用方法说明
方法 说明
JInternalFrame[] getAllFrames() 返回桌面中当前显示的所有内部窗体
JinternalFrame getSelectedFrame()
返回此桌面面板中当前活动的内部窗体,如果当前没有活动的
内部窗体,则返回 null
void remove(int index) 从此桌面面板中移除指定索引的组件
void removeAll() 从此桌面面板中移除所有组件
JinternalFrame selectFrame(boolean b) 选择此桌面面板中的下一个内部窗体
void setDragMode(int dragMode) 设置桌面面板使用的“拖动样式”
void setSelectedFrame(JInternalFrame f) 设置此桌面面板中当前活动的内部窗体
JDesktopPane 类创建的对象是一个桌面面板,使用该类的 setDragMode 方法可以设置内部窗体的拖动模
式。该类设置拖动模式的字段如表 6-2 所示。
表 6-2 JDesktopPane 类设置拖动模式的字段说明
字段 说明
JDesktopPane. LIVE_DRAG_MODE
表示正在被拖动项的所有内容是都出现在桌面面板内部,是默
认设置
JDesktopPane. OUTLINE_DRAG_MODE 表示正在被拖动项的轮廓出现在桌面面板内部
例如:
JDesktopPane desktopPane = new JDesktopPane() ; //创建一个桌面面板
desktopPane. setDragMode(JDesktopPane. OUTLINE_DRAG_MODE) ; //拖动时只显示轮廓
3.4.2、创建内部窗体
186
内部窗体通常与桌面面板一起使用,这样可以方便内部窗体的操作。javax.swing.JInternalFrame 类创建
的对象是一个内部窗体。该类的构造方法如表 6-3 所示。
表 6-3 JInternalFrame 类的构造方法的说明
构造方法 说明
JInternalFrame()
创建不可调整大小的、不可关闭的、不可最大化的、不可
图标化的、没有标题的内部窗体
JInternalFrame(String title)
创建不可调整大小的、不可关闭的、不可最大化的、不可
图标化的、具有指定标题的内部窗体
JInternalFrame(String title, boolean resizable)
创建不可关闭的、不可最大化的、不可图标化的,以及具
有指定标题和可调整大小的内部窗体
JInternalFrame(String title, boolean resizable,
boolean closable)
创建不可最大化的、不可图标化的,以及具有指定标题、
可调整大小和可关闭的内部窗体
JInternalFrame(String title, boolean resizable,
boolean closable, boolean maximizable)
创建不可图标化的,但具有指定标题、可调整大小、可关
闭和可最大化的内部窗体
JInternalFrame(String title, boolean resizable,
boolean closable, boolean maximizable, boolean
iconifiable)
创建具有指定标题、可调整、可关闭、可最大化和可图标
化的内部窗体
JInternalFrame 类创建的对象是一个内部窗体,该类提供的方法可以实现对内部窗体的常用设置。该类
的常用方法如表 6-4 所示。
表 6-4 JInternalFrame 类的常用方法的说明
方法 说明
void dispose() 使次内部窗体不可见、取消选定并关闭它
String getTitle() 返回内部窗体的标题
boolean isClosed() 返回内部窗体当前是否已关闭
boolean isIcon() 返回内部窗体当前是否已图标化
boolean isSelected() 返回内部窗体当前是否为选定的或处于激活状态
void setFrameIcon(Icon icon) 设置要在此内部窗体的标题栏中显示的图像
void setMaximizable(boolean b) 设置内部窗体是否提供最大化按钮
void setJMenuBar(JMenuBar m) 设置内部窗体的菜单栏
void setResizable(boolean b) 设置是否可以调整内部窗体的大小
void setSelected(boolean selected) 设置是否激活此内部窗体
void setTitle(String title) 设置内部窗体的标题
void show()
如果内部窗体不可见,则将该内部窗体置于前端,
使其可见并尝试选定它
void toBack() 将此内部窗体发送至后台
void toFrond 将此内部窗体置于前端
void addInternalFrameListener(InternalFrameListener l)
添加指定的侦听器,以从此内部窗体接收内部窗体
事件
我们可以为 JInternalFrame 添加指定的侦听器,以便从内部窗体接收内部窗体事件。通常我们只需继承
InternalFrameAdapter 适配器类即可。
例如:addInternalFrameListener(new MyFrameListener());
187
// 窗体监听器,当窗体可见时,更新学生下拉选择框
private final class MyFrameListener extends InternalFrameAdapter {
@Override
public void internalFrameActivated(InternalFrameEvent e) {
initComboBox();
}
}
3.4.3、MDI 窗体设计案例
多窗体界面的创建方法是首先创建桌面面板,然后把桌面面板放到 JFrame 窗体内容面板默认布局的中
央,再创建内部窗体,并把内部窗体放到桌面面板容器中。这些内部窗体通常由主菜单或工具栏上的快捷
按钮来调用,这些按钮需要添加事件监听器,在单击该按钮时,由事件监听器创建并初始化相应的内部窗
体,然后显示该内部窗体。例如,下图就是一个 MDI 窗体设计界面。
通常菜单和按钮监听器所实现的业务逻辑基本相同,都是创建并初始化内部窗体,然后显示它们。假
如我们需要创建 20 个内部窗体,那么至少需要 20 个事件监听器类来实现相同的功能,这些繁琐的工作会
占用大量的程序开发时间,影响工作进度。如果它们能够使用同一个事件监听器类就可以实现代码重用,
同时也节省了代码工作量,提高程序开发速度。
这样的开发思路存在很多优点,但是实现起来并不容易,内部窗体的名称、类名都可以获取,但是如
何根据指定的类名去创建内部窗体对象呢?
Java 的反射功能为这个思路提供了可行性。在 java.lang.reflect 包中有 Field 类、Method 类和 Constructor
类,这 3 个类分别描述类的字段、方法和构造方法。这里需要的就是类的构造方法,只有调用类的构造方
法才能创建该类的实例对象。可以通过 Class 类的 getConstructor()方法获取 Constructor 类的实例对象,然后
调用该对象的 newInstance()方法创建类的实例对象。关键代码如下:
// 用来保存内部窗体的 Map 对象
private Map<String, JInternalFrame> ifs = new HashMap<String, JInternalFrame>();
188
......
try {
Class<?> fClass = Class.forName("com.mis.internalframe." + frameName);
Constructor<?> constructor = fClass.getConstructor(null);
jf = (JInternalFrame) constructor.newInstance(null);
ifs.put(frameName, jf);
} catch (Exception e) {
e.printStackTrace();
}
代码贴士:
调用 Class 类的 forName()方法加载指定的 Java 类,该方法将返回该类的 Class 实例对象。
调用指定类的 getConstructor()方法获取指定的构造器,方法中使用 null 作参数,是调用该类的默
认构造器,因为类的默认构造器没有任何参数。
调用构造器的 newInstance()方法,同样传递参数 null,这样就可以调用默认的构造方法创建内部窗
体对象。
通常激活同一个命令有多种方式,用户可以通过工具栏中的按钮、菜单选择特定的功能。在学生信息
管理系统界面中,最常用的命令就是弹出内部窗体,将系统中需要弹出的内部窗体命令统一放入
OpenFrameAction 类中,这样触发任何一种组件事件时,都会按照统一的方式处理。
java.swing 包提供了一个非常有用的机制,用来封装命令,并将其连接到多个事件源,这种机制就是
Action 接口。Action 接口提供 ActionListener 接口的一个有用扩展,以便若干控件访问相同的功能。
Action 接口原型定义如下:
public interface Actionextends ActionListener
Action 接口提供了如下方法:
public void actionPerformed(ActionEvent e)
public Object getValue(String key)
public void putValue(String key, Object value)
public boolean isEnabled()
public void setEnabled(boolean b)
public void addPropertyChangeListener(PropertyChangeListener listener)
public void removePropertyChangeListener(PropertyChangeListener listener)
其中,第一个方法在实现 ActionListener 接口的程序中经常会看到。getValue()与 putValue()方法用来存
储与提取动作对象的预定义名称与值。例如:
action. putValue(Action.SMALL_ICON, new ImageIcon(imgUrl)); //将图标存储到动作对象中
表 6-5 动作对象的预定义名称
名称 值
NAME 用来存储动作的 String 名称的键,用于菜单或按钮
SMALL_ICON 用来存储小型 Icon(比如 ImageIcon)的键
SHORT_DESCRIPTION 用来存储动作的简短 String 描述的键,用于工具提示文本
LONG_DESCRIPTION 用来存储动作的较长 String 描述的键,用于上下文相关的帮助文件
setEnabled()方法用于开启或禁用动作对象,isEnabled()方法用于检查动作是否启用。
189
实现 Action 接口需要将接口中的所有方法都实现,所以在通常情况下都使用实现该接口的
AbstractAction 类。本系统中的 OpenFrameAction 类正是继承了 AbstractAction 类,在 OpenFrameAction 类中
只要重写 AbstractAction 类中的 actionPerformed()方法即可。
注意,Action 实现在存储方面的开销比典型的 ActionListener 要高,但后者不具有集中控制功能和广
播属性更改的优点。因此,应该注意只在需要这些优点的地方使用 Action,在别处使用 ActionListener 即
可。
例:MDI 主窗体设计代码范例
package com.mis.ui;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.beans.PropertyVetoException;
import java.lang.reflect.Constructor;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Timer;
import javax.swing.*;
import javax.swing.event.*;
public class MDIFrame extends javax.swing.JFrame {
public MDIFrame() {
initComponents();
setStatusBarTime();
}
private void initComponents() {
desktopPane = new JDesktopPane();
desktopPane.setBackground(Color.DARK_GRAY);
statusToolBar = new JToolBar();
statusLabel = new JLabel();
userLabel = new JLabel();
userLabel.setText("作者:齐心 湖北汽车工业学院 电气与信息工程学院 2013-2014 版权所有");
// 状态工具栏设置
statusToolBar.setFloatable(false);
statusLabel.setHorizontalTextPosition(SwingConstants.RIGHT);
statusToolBar.setLayout(new BorderLayout());
statusToolBar.add(userLabel, BorderLayout.WEST);
statusToolBar.add(statusLabel, BorderLayout.EAST);
statusToolBar.setBorder(BorderFactory.createLoweredBevelBorder());
// 菜单工具栏设置
toolbar = new JToolBar();
190
toolbar.setFloatable(false);
toolbar.setRollover(true);
studentManageButton = createFrameButton("学生信息管理", "StudentManageInternalFrame");
studentTableManageButton = createFrameButton(" 学 生 信 息 表 格 管 理 ",
"StudentTableManageInternalFrame");
studentQueryButton = createFrameButton("学生信息查询", "StudentQueryInternalFrame");
courseManageButton = createFrameButton("课程信息管理", "CourseManageInternalFrame");
courseQueryButton = createFrameButton("课程信息查询", "CourseQueryInternalFrame");
scManageButton = createFrameButton("选课信息管理", "SCManageInternalFrame");
scQueryButton = createFrameButton("选课信息查询", "SCQueryInternalFrame");
toolbar.add(studentManageButton);
toolbar.add(studentTableManageButton);
toolbar.add(studentQueryButton);
toolbar.addSeparator();
toolbar.add(courseManageButton);
toolbar.add(courseQueryButton);
toolbar.addSeparator();
toolbar.add(scManageButton);
toolbar.add(scQueryButton);
// 菜单设置
menuBar = new JMenuBar();
studentMenu = new JMenu();
courseMenu = new JMenu();
scMenu = new JMenu();
userMenu = new JMenu();
helpMenu = new JMenu();
aboutMenuItem = new JMenuItem();
exitMenuItem = new JMenuItem();
exitMenuItem.addActionListener(new java.awt.event.ActionListener() {
@Override
public void actionPerformed(java.awt.event.ActionEvent evt) {
System.exit(0);
}
});
studentMenu.setMnemonic('S');
studentMenu.setText("学生信息管理(S)");
courseMenu.setMnemonic('C');
courseMenu.setText("课程信息管理(C)");
scMenu.setMnemonic('I');
scMenu.setText("选课信息管理(I)");
191
userMenu.setMnemonic('M');
userMenu.setText("用户管理(M)");
helpMenu.setMnemonic('H');
helpMenu.setText("帮助(H)");
exitMenuItem.setMnemonic('X');
exitMenuItem.setText("退出(X)");
aboutMenuItem.setMnemonic('A');
aboutMenuItem.setText("关于(A)");
StudentManageMenuItem = new JMenuItem(new OpenFrameAction(" 学 生 信 息 管 理 ",
"StudentManageInternalFrame"));
StudentTableManageMenuItem = new JMenuItem(new OpenFrameAction(" 学 生 信息 表 格 管 理",
"StudentTableManageInternalFrame"));
StudentQueryMenuItem = new JMenuItem(new OpenFrameAction(" 学 生 信 息 查 询 ",
"StudentQueryInternalFrame"));
courseManageMenuItem = new JMenuItem(new OpenFrameAction(" 课 程 信 息 管 理 ",
"CourseManageInternalFrame"));
courseQueryMenuItem = new JMenuItem(new OpenFrameAction(" 课 程 信 息 查 询 ",
"CourseQueryInternalFrame"));
SCManageMenuItem = new JMenuItem(new OpenFrameAction(" 选 课 信 息 管 理 ",
"SCManageInternalFrame"));
SCQueryMenuItem = new JMenuItem(new OpenFrameAction(" 选 课 信 息 查 询 ",
"SCQueryInternalFrame"));
studentMenu.add(StudentManageMenuItem);
studentMenu.add(StudentTableManageMenuItem);
studentMenu.add(StudentQueryMenuItem);
courseMenu.add(courseManageMenuItem);
courseMenu.add(courseQueryMenuItem);
scMenu.add(SCManageMenuItem);
scMenu.add(SCQueryMenuItem);
helpMenu.add(aboutMenuItem);
helpMenu.add(exitMenuItem);
menuBar.add(studentMenu);
menuBar.add(courseMenu);
menuBar.add(scMenu);
menuBar.add(userMenu);
menuBar.add(helpMenu);
setJMenuBar(menuBar);
getContentPane().add(toolbar, java.awt.BorderLayout.PAGE_START);
getContentPane().add(desktopPane, java.awt.BorderLayout.CENTER);
192
getContentPane().add(statusToolBar, java.awt.BorderLayout.SOUTH);
setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
setTitle("学生成绩管理系统");
pack();
}
// 设置状态栏时间动态显示
private void setStatusBarTime() {
Timer timer = new Timer();
final SimpleDateFormat s = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
TimerTask tt = new TimerTask() {
@Override
public void run() {
Date date = new Date();
statusLabel.setText(" 当前时间:" + s.format(date));// 显示系统日期时间
statusLabel.setHorizontalAlignment(SwingConstants.CENTER);// 设置文字在状态中居
中显示
}
};
timer.schedule(tt, 0, 1000);
}
private JButton createFrameButton(String fname, String cname) {
Action action = new OpenFrameAction(fname, cname);
JButton button = new JButton(action);
button.setFocusPainted(false);
button.setHideActionText(false);
return button;
}
private class OpenFrameAction extends AbstractAction {
private String codeName = null;
private Icon icon = null;
public OpenFrameAction(String fname, String cname) {
String imgUrl = "ActionIcon/" + fname + ".png";
icon = new ImageIcon(imgUrl);
this.codeName = cname;
putValue(Action.NAME, fname);
putValue(Action.SHORT_DESCRIPTION, fname);
putValue(Action.SMALL_ICON, icon);
193
}
@Override
public void actionPerformed(ActionEvent e) {
JInternalFrame jf = getIFrame(codeName);
// 设置内部窗体的标题栏图标
jf.setFrameIcon(icon);
// 在内部窗体关闭时,从内部窗体容器 ifs 对象中清除该窗体
jf.addInternalFrameListener(new InternalFrameAdapter() {
@Override
public void internalFrameClosed(InternalFrameEvent e) {
ifs.remove(codeName);
}
});
if (jf.getDesktopPane() == null) {
desktopPane.add(jf);
jf.setSize(500, 500);
jf.setVisible(true);
}
try {
jf.setSelected(true);
} catch (PropertyVetoException e1) {
e1.printStackTrace();
}
}
private JInternalFrame getIFrame(String frameName) {
JInternalFrame jf = null;
if (!ifs.containsKey(frameName)) {
try {
Class<?> fClass = Class.forName("com.mis.internalframe." + frameName);
Constructor<?> constructor = fClass.getConstructor(null);
jf = (JInternalFrame) constructor.newInstance(null);
ifs.put(frameName, jf);
} catch (Exception e) {
e.printStackTrace();
}
} else {
jf = ifs.get(frameName);
}
return jf;
}
}
194
public static void main(String args[]) {
try {
javax.swing.UIManager.setLookAndFeel(javax.swing.UIManager
.getSystemLookAndFeelClassName());
} catch (Exception e) {
e.printStackTrace();
}
java.awt.EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
JFrame frame = new MDIFrame();
Dimension d = Toolkit.getDefaultToolkit().getScreenSize();
frame.setSize(3 * d.width / 4, 3 * d.height / 4);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
});
}
private JMenuBar menuBar;
private JMenu studentMenu;
private JMenu courseMenu;
private JMenu scMenu;
private JMenu userMenu;
private JMenu helpMenu;
private JMenuItem StudentManageMenuItem;
private JMenuItem StudentTableManageMenuItem;
private JMenuItem StudentQueryMenuItem;
private JMenuItem courseManageMenuItem;
private JMenuItem courseQueryMenuItem;
private JMenuItem SCManageMenuItem;
private JMenuItem SCQueryMenuItem;
private JMenuItem aboutMenuItem;
private JMenuItem exitMenuItem;
private JLabel statusLabel;
private JLabel userLabel;
private JDesktopPane desktopPane;
private JButton studentManageButton;
private JButton studentTableManageButton;
195
private JButton studentQueryButton;
private JButton courseManageButton;
private JButton courseQueryButton;
private JButton scManageButton;
private JButton scQueryButton;
private JToolBar toolbar;
private JToolBar statusToolBar;
private Map<String, JInternalFrame> ifs = new HashMap<String, JInternalFrame>();
}
其中,大部分代码的含义我们已经在前面做了介绍。
主窗体 MDIFrame 采用默认边框布局方式,将工具栏、桌面面板和状态栏依次放到主窗体的北部、中
部和南部。setStatusBarTime()方法采用定时器动态更新状态栏显示时间。
在 OpenFrameAction 内部类的构造方法中,我们通过上图所示文件结构,在项目根节点的 ActionIcon
文件夹下放置了若干中文名称.png 图片,构造 OpenFrameAction 对象时我们分别传递了 fname 和 cname 两
个变量,用来生成 Action 对象 Action.NAME、Action.SHORT_DESCRIPTION、Action.SMALL_ICON 等属
性。
private Map<String, JInternalFrame> ifs = new HashMap<String, JInternalFrame>();
我们在主窗体 MDIFrame 中采用键/值对的方式定义了一个用来保存所有内部窗体的 Map 对象 ifs。当我
们选择菜单或点击工具栏上的快捷按钮时,将会触发 OpenFrameAction 对象的 actionPerformed 事件,它首
先检查当前要打开的内部窗体(JInternalFrame)是否已经在 ifs 中存在,如果不存在,则创建内部窗体实例,
并将它放置到 ifs 中;如果存在,则直接从 ifs 中取出并返回。同时,为内部窗体添加关闭事件监视器,当
内部窗体关闭时,从 ifs 对象中移除。最后,将内部窗体放置到桌面面板 desktopPane 中显示出来。
剩下的工作,我们只需在项目中根据业务逻辑功能设置不同的内部窗体(JInternalFrame)。内部窗体
(JInternalFrame)的设计方法和 SDI(单文档应用程序)的设计方法基本相同,此处不再敖述。
3.5、BLOB 数据类型的处理
BLOB (binary large object)即二进制大对象,是一个可以存储二进制文件的容器。在图形用户界面下,
我们如何来操作 BLOB 数据类型呢?假设有以下案例需求:在添加学生记录对话框中(如图 6-2 所示),通
196
过点击“浏览”按钮选择一张照片,填写学生信息后,将学生记录添加到 student 表中。数据库 student 表中
SPicture 字段为 BLOB 类型,保存了学生的照片内容。我们可以通过下面介绍的方式来处理基于图形用户界
面的 BLOB 数据类型。
图 6-2 添加学生记录界面
3.5.1、Student 表模型设计
在 com.stu.model 包中新建 Student.java 类,用于构建学生表模型。
public class Student implements Serializable {
private String sno;
private String sname;
private String ssex;
private int sage;
private String sdept;
private double savgGrade;
private String photoPath;
private ImageIcon photo;
public Student() {
}
//get 和 set 方法封装数据成员
[此处省略......]
}
在学生表模型类中,photoPath 成员变量用来保存文件选择对话框选择图片时的图片绝对路径,photoPath
成员变量用来保存学生照片的图像。
3.5.2、“浏览”按钮的事件处理
在图形用户界面中,我们要在窗体中显示一副图像,可以在窗体中放置 JLabel 控件,然后构造一个
ImageIcon 对象,调用 JLabel 控件的 setIcon(Icon icon)方法将图像显示在 JLabel 控件中。
首先,构造一个针对应用程序根目录的文件选择器,其中只显示 .jpg 和 .png 图像。当文件选择器选
择一张图像文件后,通过 ImageIO.read()方法将图像文件保存到 ImageIcon 当中,适当调整图像的大小以适
197
应 photoLabel 标签显示图像的大小,然后将图像文件的绝对路径保存到学生表模型的 photoPath 成员变量
中。示例代码如下:
private void explorerButtonActionPerformed(java.awt.event.ActionEvent evt) {
JFileChooser chooser = new JFileChooser();
// 设置文件类型过滤器
FileFilter filter = new FileNameExtensionFilter("图像文件(jpg,png)", "jpg", "png");
chooser.setAcceptAllFileFilterUsed(false);
chooser.addChoosableFileFilter(filter);
// 设置文件对话框指向当前应用程序的根目录
chooser.setCurrentDirectory(new File("."));
int returnVal = chooser.showOpenDialog(this);
if (returnVal == JFileChooser.APPROVE_OPTION) {
File file = chooser.getSelectedFile();
try {
BufferedImage image = ImageIO.read(file);
int width = photoLabel.getWidth();
ImageIcon icon = new ImageIcon(image);
if (icon.getIconWidth() > width) {
icon = new ImageIcon(icon.getImage().getScaledInstance(
width, -1, Image.SCALE_DEFAULT));
}
photoLabel.setText(null);
photoLabel.setIcon(icon);
stu.setPhotoPath(file.getAbsolutePath());
} catch (IOException ex) {
ex.printStackTrace();
}
}
}
3.5.3、数据库访问类中添加数据功能的实现
在数据库访问类 StudentDaoImpl.java 中,我们定义了操作数据库的相应方法。其中 insertStudent 方法为
添加学生记录功能的实现,当我们传递一个已存储了要添加的学生数据的对象时,需要先判断 stu 变量是否
已经保存了图像的地址。如果 stu 变量没有保存图像的地址,说明用户没有选择图像,构造一个空的 Blob
对象,将学生数据保存到数据库中;如果 stu 变量保存了图像的地址,则通过文件输入流的方式读取要保存
的图像,通过 psmt.setBinaryStream 方法将图像数据保存到数据库当中。示例代码如下:
public boolean insertStudent(Student stu) throws SQLException {
FileInputStream in = null;
try {
conn = DatabaseBean.getConnection();
psmt = conn.prepareStatement("insert into
student(sno,sname,ssex,sage,sdept,spicture) values(?,?,?,?,?,?)");
198
psmt.setString(1, stu.getSno());
psmt.setString(2, stu.getSname());
psmt.setString(3, stu.getSsex());
psmt.setInt(4, stu.getSage());
psmt.setString(5, stu.getSdept());
if (stu.getPhotoPath() != null && !"".equals(stu.getPhotoPath())) {
in = new FileInputStream(stu.getPhotoPath());
psmt.setBinaryStream(6, in, in.available());
} else {
Blob blob = null;
psmt.setBlob(6, blob);
}
psmt.executeUpdate();
return true;
} catch (SQLException | FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} finally {
if (in != null) {
try {
in.close();
} catch (IOException ex) {
ex.printStackTrace();
}
}
DatabaseBean.close(rs, psmt, conn);
}
return false;
}
3.5.4、数据库访问类中查询所有学生数据功能的实现
在数据库访问类 StudentDaoImpl.java 中,我们定义了操作数据库的相应方法。其中 getAllStudents 方法
为查询所有学生记录功能的实现,返回所有学生记录信息的集合对象。我们首先查询所有学生记录信息,
遍历所有学生结果集,rs.getBlob("spicture")方法返回一个 Blob 对象。如果 picBlob 不为空,说明学生记录中
包含图像信息,则通过 picBlob.getBinaryStream()方法以流的形式获取此 Blob 实例指定的 BLOB 值,然后
调用 ImageIO.read()方法读入 BLOB 值,通过 BLOB 的值构造一个保存了学生图像的 ImageIcon 对象 icon,
调用 stu.setPhoto(icon)方法将图像保存到学生记录中。
如果学生记录中包含示例代码如下:
public List<Student> getAllStudents() throws SQLException {
List<Student> students = new ArrayList<>();
try {
199
conn = DatabaseBean.getConnection();
psmt = conn.prepareStatement("select * from student");
rs = psmt.executeQuery();
while (rs.next()) {
Student stu = new Student();
stu.setSno(rs.getString("sno"));
stu.setSname(rs.getString("sname"));
stu.setSsex(rs.getString("ssex"));
stu.setSage(rs.getInt("sage"));
stu.setSdept(rs.getString("sdept"));
stu.setSavgGrade(rs.getDouble("savgGrade"));
Blob picBlob = rs.getBlob("spicture");
if (picBlob != null) {
ImageIcon icon = new ImageIcon(ImageIO.read(picBlob
.getBinaryStream()));
//限定图片单元格尺寸为 50*50px
icon = new ImageIcon(icon.getImage().getScaledInstance(50,
50, Image.SCALE_DEFAULT));
stu.setPhoto(icon);
}
students.add(stu);
}
} catch (SQLException | IOException e) {
e.printStackTrace();
} finally {
DatabaseBean.close(rs, psmt, conn);
}
return students;
}
200
4、实验内容
(1)、阅读有关资料,理解 C/S 架构的概念。
201
(2)、编写一个存储过程,该存储过程向 SC 追加一条选课记录,如果该记录的成绩 grade 字段非空,则更
新 Student 表的 savggrade 字段。要求两个操作组成一个事务,然后编写 JAVA 程序,用 JDBC 调用该存储过
程。
如果上述两个操作直接封装在 java 的 JDBC 调用中,并用 JDBC 实现事务处理,相应 JAVA 代码又应该
如何编写?
202
(3)在学生-课程数据库上实现一个简单的基于 C/S 架构的“学生成绩管理系统”,该系统包括课程信息管
理、学生信息管理和选课信息管理三个功能模块,每个功能模块提供对相应信息的浏览、查询、增、删、
改等操作。要求如下:
① 用户交互界面为图形窗口。
② 学生信息管理模块必做,其它两个模块选做。
③ 数据库的访问通过存储过程调用实现。
④ 关类分层组织成合适的包的形式。
5、实验总结
请书写你对本次实验有哪些实质性的收获和体会,以及对本次实验有何良好的建议?
203
实验七、B/S 架构应用程序(3 学时)
1、实验目的
(1)理解 B/S 架构相关概念。
(2)掌握 J2EE 平台下简单 B/S 架构应用的实现。
2、实验性质
综合性实验
3、实验导读
3.1、B/S 架构开发步骤
作为特殊的三层架构,B/S 架构的开发同样需要经过分析、设计、实现、测试等主要步骤。
分析阶段主要分析系统的业务需求、功能需求、数据需求(数据存储和数据处理两个方面),形成系统
开发的数据字典。
设计阶段主要设计系统的拓扑结构、功能模块、用户界面、数据库结构及开发架构。其中数据库结构
设计又可分为概念结构设计、逻辑结构设计和物理结构设计三个阶段。开发架构包括代码的类、包;数据
库端的存储子程序、包、序列等。
实现阶段依据设计文档进行编码,最后是加载数据,进行调试。
在 J2EE 平台,用户界面用 Html 和 JSP 实现。通过样式表实现版面元素显示风格的统一;通过 JavaScript
代码实现页面数据提交前的验证。
用 Servlet 来接受页面提交的数据,用领域对象或者实体类来封装接收到的数据,用 JavaBean 处理业务
逻辑,用 DAO 类实现对数据库的访问。
在类的组织上,一般按表示层(界面类,主要是 Servlet)、业务逻辑层(领域对象或实体对象)、数据
存取层(DAO 接口、DAO 实现)、辅助工具层(JDBC 调用)来组织系统的包架构。
实现上可以使用应用单列模式、工厂模式、装饰模式等设计模式,以提高系统的性能和复用率。
3.2、Servlet 3.0 新特性详解
Servlet 3.0 作为 Java EE 6 规范体系中一员,随着 Java EE 6 规范一起发布。该版本在前一版本(Servlet
2.5)的基础上提供了若干新特性用于简化 Web 应用的开发和部署。注意:Tomcat 需要使用 7.0 版本以上
的 Web 服务器才能够支持 Servlet 3.0 新技术。
异步处理支持:有了该特性,Servlet 线程不再需要一直阻塞,直到业务处理完毕才能再输出响应,
最后才结束该 Servlet 线程。在接收到请求之后,Servlet 线程可以将耗时的操作委派给另一个线程来完成,
自己在不生成响应的情况下返回至容器。针对业务处理较耗时的情况,这将大大减少服务器资源的占用,
并且提高并发处理速度。
新增的注解支持:该版本新增了若干注解,用于简化 Servlet、过滤器(Filter)和监听器(Listener)
的声明,这使得 web.xml 部署描述文件从该版本开始不再是必选的了。
204
可插性支持:熟悉 Struts2 的开发者一定会对其通过插件的方式与包括 Spring 在内的各种常用框
架的整合特性记忆犹新。将相应的插件封装成 JAR 包并放在类路径下,Struts2 运行时便能自动加载这些
插件。现在 Servlet 3.0 提供了类似的特性,开发者可以通过插件的方式很方便的扩充已有 Web 应用的功
能,而不需要修改原有的应用。
3.2.1、异步处理支持
Servlet 3.0 之前,一个普通 Servlet 的主要工作流程大致如下:首先,Servlet 接收到请求之后,可能
需要对请求携带的数据进行一些预处理;接着,调用业务接口的某些方法,以完成业务处理;最后,根据
处理的结果提交响应,Servlet 线程结束。其中第二步的业务处理通常是最耗时的,这主要体现在数据库操
作,以及其它的跨网络调用等,在此过程中,Servlet 线程一直处于阻塞状态,直到业务方法执行完毕。在
处理业务的过程中,Servlet 资源一直被占用而得不到释放,对于并发较大的应用,这有可能造成性能的瓶
颈。对此,在以前通常是采用私有解决方案来提前结束 Servlet 线程,并及时释放资源。
Servlet 3.0 针对这个问题做了开创性的工作,现在通过使用 Servlet 3.0 的异步处理支持,之前的
Servlet 处理流程可以调整为如下的过程:首先,Servlet 接收到请求之后,可能首先需要对请求携带的数据
进行一些预处理;接着,Servlet 线程将请求转交给一个异步线程来执行业务处理,线程本身返回至容器,
此时 Servlet 还没有生成响应数据,异步线程处理完业务以后,可以直接生成响应数据(异步线程拥有
ServletRequest 和 ServletResponse 对象的引用),或者将请求继续转发给其它 Servlet。如此一来,Servlet 线
程不再是一直处于阻塞状态以等待业务逻辑的处理,而是启动异步线程之后可以立即返回。
异步处理特性可以应用于 Servlet 和过滤器两种组件,由于异步处理的工作模式和普通工作模式在实现
上有着本质的区别,因此默认情况下,Servlet 和过滤器并没有开启异步处理特性,如果希望使用该特性,
则必须按照如下的方式启用:
1、对于使用传统的部署描述文件 (web.xml) 配置 Servlet 和过滤器的情况,Servlet 3.0 为 <servlet> 和
<filter> 标签增加了 <async-supported> 子标签,该标签的默认取值为 false,要启用异步处理支持,则将其
设为 true 即可。以 Servlet 为例,其配置方式如下所示:
<servlet>
<servlet-name>DemoServlet</servlet-name>
<servlet-class>footmark.servlet.Demo Servlet</servlet-class>
<async-supported>true</async-supported>
</servlet>
2、对于使用 Servlet 3.0 提供的 @WebServlet 和 @WebFilter 进行 Servlet 或过滤器配置的情况,这
两个注解都提供了 asyncSupported 属性,默认该属性的取值为 false,要启用异步处理支持,只需将该属性
设置为 true 即可。以 @WebFilter 为例,其配置方式如下所示:
@WebFilter(urlPatterns = "/demo",asyncSupported = true)
public class DemoFilter implements Filter{...}
一个简单的模拟异步处理的 Servlet 示例如下:
@WebServlet(urlPatterns = "/demo", asyncSupported = true)
public class AsyncDemoServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest req, HttpServletResponse resp)
throws IOException, ServletException {
resp.setContentType("text/html;charset=UTF-8");
PrintWriter out = resp.getWriter();
205
out.println("进入 Servlet 的时间:" + new Date() + ".");
out.flush();
//在子线程中执行业务调用,并由其负责输出响应,主线程退出
AsyncContext ctx = req.startAsync();
new Thread(new Executor(ctx)).start();
out.println("结束 Servlet 的时间:" + new Date() + ".");
out.flush();
}
}
public class Executor implements Runnable {
private AsyncContext ctx = null;
public Executor(AsyncContext ctx){
this.ctx = ctx;
}
public void run(){
try {
//等待十秒钟,以模拟业务方法的执行
Thread.sleep(10000);
PrintWriter out = ctx.getResponse().getWriter();
out.println("业务处理完毕的时间:" + new Date() + ".");
out.flush();
ctx.complete();
} catch (Exception e) {
e.printStackTrace();
}
}
}
除此之外,Servlet 3.0 还为异步处理提供了一个监听器,使用 AsyncListener 接口表示。它可以监控如
下四种事件:
(1)异步线程开始时,调用 AsyncListener 的 onStartAsync(AsyncEvent event) 方法;
(2)异步线程出错时,调用 AsyncListener 的 onError(AsyncEvent event) 方法;
(3)异步线程执行超时,则调用 AsyncListener 的 onTimeout(AsyncEvent event) 方法;
(4)异步执行完毕时,调用 AsyncListener 的 onComplete(AsyncEvent event) 方法;
要注册一个 AsyncListener,只需将准备好的 AsyncListener 对象传递给 AsyncContext 对象的
addListener() 方法即可,如下所示:
AsyncContext ctx = req.startAsync();
ctx.addListener(new AsyncListener() {
public void onComplete(AsyncEvent asyncEvent) throws IOException {
// 做一些清理工作或者其他
206
}
...
});
3.2.2、新增的注解支持
Servlet 3.0 的部署描述文件 web.xml 的顶层标签 <web-app> 有一个 metadata-complete 属性,该属性
指定当前的部署描述文件是否是完全的。如果设置为 true,则容器在部署时将只依赖部署描述文件,忽略
所有的注解(同时也会跳过 web-fragment.xml 的扫描,亦即禁用可插性支持,具体请看后文关于 可插性
支持的讲解);如果不配置该属性,或者将其设置为 false,则表示启用注解支持(和可插性支持)。
1. @WebServlet
@WebServlet 用于将一个类声明为 Servlet,该注解将会在部署时被容器处理,容器将根据具体的属性
配置将相应的类部署为 Servlet。该注解具有下表给出的一些常用属性(以下所有属性均为可选属性,但是
vlaue 或者 urlPatterns 通常是必需的,且二者不能共存,如果同时指定,通常是忽略 value 的取值):
表 7-1 @WebServlet 主要属性列表
属性名 类型 描述
name String
指定 Servlet 的 name 属性,等价于 <servlet-name>。如果没
有显式指定,则该 Servlet 的取值即为类的全限定名。
value String[] 该属性等价于 urlPatterns 属性。两个属性不能同时使用。
urlPatterns String[]
指定一组 Servlet 的 URL 匹配模式。等价于 <url-pattern> 标
签。
loadOnStartup int 指定 Servlet 的加载顺序,等价于 <load-on-startup> 标签。
initParams WebInitParam[] 指定一组 Servlet 初始化参数,等价于 <init-param> 标签。
asyncSupported boolean
声 明 Servlet 是 否 支 持 异 步 操 作 模 式 , 等 价 于
<async-supported> 标签。
description String 该 Servlet 的描述信息,等价于 <description> 标签。
displayName String
该 Servlet 的 显 示 名 , 通 常 配 合 工 具 使 用 , 等 价 于
<display-name> 标签。
下面是一个简单的示例:
@WebServlet(urlPatterns = {"/simple"}, asyncSupported = true,
loadOnStartup = -1, name = "SimpleServlet", displayName = "ss",
initParams = {@WebInitParam(name = "username", value = "tom")}
)
public class SimpleServlet extends HttpServlet{ … }
如此配置之后,就可以不必在 web.xml 中配置相应的 <servlet> 和 <servlet-mapping> 元素了,容器会
在部署时根据指定的属性将该类发布为 Servlet。它的等价的 web.xml 配置形式如下:
<servlet>
<display-name>ss</display-name>
<servlet-name>SimpleServlet</servlet-name>
<servlet-class>footmark.servlet.SimpleServlet</servlet-class>
<load-on-startup>-1</load-on-startup>
<async-supported>true</async-supported>
207
<init-param>
<param-name>username</param-name>
<param-value>tom</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>SimpleServlet</servlet-name>
<url-pattern>/simple</url-pattern>
</servlet-mapping>
2. @WebInitParam
该注解通常不单独使用,而是配合 @WebServlet 或者 @WebFilter 使用。它的作用是为 Servlet 或者
过滤器指定初始化参数,这等价于 web.xml 中 <servlet> 和 <filter> 的 <init-param> 子标签 。
@WebInitParam 具有下表给出的一些常用属性:
表 7-2 @WebInitParam 的常用属性
属性名 类型 是否可选 描述
name String 否 指定参数的名字,等价于 <param-name>。
value String 否 指定参数的值,等价于 <param-value>。
description String 是 关于参数的描述,等价于 <description>。
3. @WebFilter
@WebFilter 用于将一个类声明为过滤器,该注解将会在部署时被容器处理,容器将根据具体的属性配
置将相应的类部署为过滤器。该注解具有下表给出的一些常用属性 ( 以下所有属性均为可选属性,但是
value、urlPatterns、servletNames 三者必需至少包含一个,且 value 和 urlPatterns 不能共存,如果同时指
定,通常忽略 value 的取值 ):
表 7-3 @WebFilter 的常用属性
属性名 类型 描述
filterName String 指定过滤器的 name 属性,等价于 <filter-name>
value String[] 该属性等价于 urlPatterns 属性。但是两者不应该同时使用
urlPatterns String[] 指定一组过滤器的 URL 匹配模式。等价于 <url-pattern> 标签
servletNames String[]
指定过滤器将应用于哪些 Servlet。取值是 @WebServlet 中的
name 属性的取值,或者是 web.xml 中 <servlet-name> 的取值
dispatcherTypes DispatcherType
指定过滤器的转发模式。具体取值包括:
ASYNC、ERROR、FORWARD、INCLUDE、REQUEST
initParams WebInitParam[] 指定一组过滤器初始化参数,等价于 <init-param> 标签
asyncSupported boolean
声明过滤器是否支持异步操作模式,等价于 <async-supported>
标签
description String 该过滤器的描述信息,等价于 <description> 标签
displayName String
该过滤器的显示名,通常配合工具使用,等价于 <display-name>
标签
下面是一个简单的示例:
@WebFilter(servletNames = {"SimpleServlet"},filterName="SimpleFilter")
public class LessThanSixFilter implements Filter{...}
208
如此配置之后,就可以不必在 web.xml 中配置相应的 <filter> 和 <filter-mapping> 元素了,容器会在
部署时根据指定的属性将该类发布为过滤器。它等价的 web.xml 中的配置形式为:
<filter>
<filter-name>SimpleFilter</filter-name>
<filter-class>xxx</filter-class>
</filter>
<filter-mapping>
<filter-name>SimpleFilter</filter-name>
<servlet-name>SimpleServlet</servlet-name>
</filter-mapping>
4. @WebListener
该注解用于将类声明为监听器,被 @WebListener 标注的类必须实现以下至少一个接口:
ServletContextListener
ServletContextAttributeListener
ServletRequestListener
ServletRequestAttributeListener
HttpSessionListener
HttpSessionAttributeListener
该注解使用非常简单,其属性如下:
表 7-4 @WebListener 的常用属性
属性名 类型 是否可选 描述
value String 是 该监听器的描述信息。
一个简单示例如下:
@WebListener("This is only a demo listener")
public class SimpleListener implements ServletContextListener{...}
如此,则不需要在 web.xml 中配置 <listener> 标签了。它等价的 web.xml 中的配置形式如下:
<listener>
<listener-class>footmark.servlet.SimpleListener</listener-class>
</listener>
5. @MultipartConfig
该注解主要是为了辅助 Servlet 3.0 中 HttpServletRequest 提供的对上传文件的支持。该注解标注在
Servlet 上面,以表示该 Servlet 希望处理的请求的 MIME 类型是 multipart/form-data。另外,它还提供了
若干属性用于简化对上传文件的处理。具体如下:
表 7-5 @MultipartConfig 的常用属性
属性名 类型 是否可选 描述
fileSizeThreshold int 是 当数据量大于该值时,内容将被写入文件。
location String 是 存放生成的文件地址。
maxFileSize long 是 允许上传的文件最大值。默认值为 -1,表示没有限制。
maxRequestSize long 是 针对该 multipart/form-data 请求的最大数量,默认值为
209
-1,表示没有限制。
3.2.3、HttpServletRequest 对文件上传的支持
此前,对于处理上传文件的操作一直是让开发者头疼的问题,因为 Servlet 本身没有对此提供直接的支
持,需要使用第三方框架来实现,而且使用起来也不够简单。如今这都成为了历史,Servlet 3.0 已经提供了
这个功能,而且使用也非常简单。为此,HttpServletRequest 提供了两个方法用于从请求中解析出上传的文
件:
Part getPart(String name)
Collection<Part> getParts()
前 者用于 获取请 求中给 定 name 的 文件 ,后者 用于获 取所有 的文件 。每一 个文件 用一个
javax.servlet.http.Part 对象来表示。该接口提供了处理文件的简易方法,比如 write()、delete() 等。至此,
结合 HttpServletRequest 和 Part 来保存上传的文件变得非常简单,如下所示:
Part photo = request.getPart("photo");
photo.write("/tmp/photo.jpg");
// 可以将两行代码简化为 request.getPart("photo").write("/tmp/photo.jpg") 一行。
另外,开发者可以配合前面提到的 @MultipartConfig 注解来对上传操作进行一些自定义的配置,比如
限制上传文件的大小,以及保存文件的路径等。其用法非常简单,故不在此赘述了。
需要注意的是,如果请求的 MIME 类型不是 multipart/form-data,则不能使用上面的两个方法,否则
将抛异常。
例 1:Servlet 3.0 实现文件上传
1、新建 index.jsp 文件,可支持单文件和多文件上传
<%@ page language="java" pageEncoding="UTF-8"%>
<!DOCTYPE HTML>
<html>
<head>
<title>Servlet3.0 实现文件上传</title>
</head>
<body>
<fieldset>
<legend>
上传单个文件
</legend>
<!-- 文件上传时必须要设置表单的 enctype="multipart/form-data"-->
<form action="${pageContext.request.contextPath}/UploadServlet"
method="post" enctype="multipart/form-data">
上传文件:
<input type="file" name="file">
<br>
<input type="submit" value="上传">
</form>
</fieldset>
<hr />
210
<fieldset>
<legend>
上传多个文件
</legend>
<!-- 文件上传时必须要设置表单的 enctype="multipart/form-data"-->
<form action="${pageContext.request.contextPath}/UploadServlet"
method="post" enctype="multipart/form-data">
上传文件:
<input type="file" name="file1">
<br>
上传文件:
<input type="file" name="file2">
<br>
<input type="submit" value="上传">
</form>
</fieldset>
</body>
</html>
2、新建UploadServlet.java,采用注解的方式进行配置。在项目根目录/WEB-INF下新建文件夹uploadFile,
用于保存上传的图片。
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;
//使用@WebServlet 配置 UploadServlet 的访问路径
@WebServlet(name = "UploadServlet", urlPatterns = "/UploadServlet")
//使用注解@MultipartConfig 将一个 Servlet 标识为支持文件上传
@MultipartConfig//标识 Servlet 支持文件上传
public class UploadServlet extends HttpServlet {
@Override
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
211
response.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");
PrintWriter out = response.getWriter();
//存储路径
String savePath = request.getServletContext().getRealPath("/WEB-INF/uploadFile");
//获取上传的文件集合
Collection<Part> parts = request.getParts();
//上传单个文件
if (parts.size() == 1) {
//Servlet3.0 将 multipart/form-data 的 POST 请求封装成 Part,通过 Part 对上传的文件进行操作。
//Part part = parts[0];//从上传的文件集合中获取 Part 对象
Part part = request.getPart("file");// 通 过 表 单 file 控 件 (<input type="file"
name="file">)的名字直接获取 Part 对象
//Servlet3 没有提供直接获取文件名的方法,需要从请求头中解析出来
//获取请求头,请求头的格式:form-data; name="file"; filename="snmp4j--api.zip"
String header = part.getHeader("content-disposition");
//获取文件名
String fileName = getFileName(header);
//把文件写到指定路径
part.write(savePath + File.separator + fileName);
} else {
//一次性上传多个文件
for (Part part : parts) {//循环处理上传的文件
//获取请求头,请求头的格式:form-data; name="file"; filename="snmp4j--api.zip"
String header = part.getHeader("content-disposition");
//获取文件名
String fileName = getFileName(header);
//把文件写到指定路径
part.write(savePath + File.separator + fileName);
}
}
out.println("上传成功");
out.flush();
out.close();
}
/**
* 根据请求头解析出文件名 请求头的格式:火狐和 google 浏览器下:form-data; name="file";
* filename="snmp4j--api.zip" IE 浏览器下:form-data; name="file";
* filename="E:\snmp4j--api.zip"
*
* @param header 请求头
* @return 文件名
212
*/
public String getFileName(String header) {
/**
* String[] tempArr1 =
* header.split(";");代码执行完之后,在不同的浏览器下,tempArr1 数组里面的内容稍有区别
* 火狐或者 google 浏览器下:tempArr1={form-data,name="file",filename="snmp4j--api.zip"}
* IE 浏览器下:tempArr1={form-data,name="file",filename="E:\snmp4j--api.zip"}
*/
String[] tempArr1 = header.split(";");
/**
* 火狐或者 google 浏览器下:tempArr2={filename,"snmp4j--api.zip"}
* IE 浏览器下:tempArr2={filename,"E:\snmp4j--api.zip"}
*/
String[] tempArr2 = tempArr1[2].split("=");
//获取文件名,兼容各种浏览器的写法
String fileName = tempArr2[1].substring(tempArr2[1].lastIndexOf("\\") +
1).replaceAll("\"", "");
return fileName;
}
@Override
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
this.doGet(request, response);
}
}
3.3、BLOB 数据类型的处理
3.2.1、将图片保存到数据库
我们在上节中介绍了 Servlet 3.0 新技术中有关文件上传的内容,上述上传的图片的功能只是将图片保
存到指定的文件夹下,那么如何将上传的图片保存到数据库的 BLOB 数据类型的字段中呢?方法很简单,
我们只需要通过 Part 对象 part.getInputStream()方法获取到上传文件的输入流,getInputStream方法原型如下:
public InputStream getInputStream() throws IOException
// 以输入流的方式获取上传对象 part 中的内容
然后通过java.sql.PreparedStatement 的setBinaryStream方法即可将文件以二进制流的方式保存到数据库
中。setBinaryStream 方法原型如下:
public void setBinaryStream(int parameterIndex, InputStream x, int length) throws SQLException
//将指定参数设置为给定输入流,该输入流将具有给定字节数。
示例代码如下:
213
// stu.getPhoto()方法返回的是图像的输入流
if (stu.getPhoto().available() > 0) {
String sql = "insert into student(sno,sname,ssex,sage,sdept,spicture) values(?,?,?,?,?,?)";
psmt = conn.prepareStatement(sql);
psmt.setString(1, stu.getSno());
psmt.setString(2, stu.getSname());
psmt.setString(3, stu.getSsex());
psmt.setInt(4, stu.getSage());
psmt.setString(5, stu.getSdept());
psmt.setBinaryStream(6, stu.getPhoto(), stu.getPhoto().available());
psmt.executeUpdate();
}
3.2.2、将图片从数据库中读取并显示
Servlet 的功能非常强大,它不光可以返回文本类型的数据,还可以生成非文本类型的数据。它们在原
理上是一样的,只是 Content-Type 的设置、输出流的获取和写入的数据略有不同。本节来看一个根据指定
学号从数据库中读取 BLOB 数据并显示为一副 jpeg 图像的 Servlet。
@WebServlet(name = "ShowPic", urlPatterns = {"/Student/ShowPic"})
public class ShowPic extends HttpServlet {
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("image/jpeg");
String sno = request.getParameter("sno");
Connection conn = null;
PreparedStatement psmt = null;
ResultSet rs = null;
try {
conn = DatabaseBean.getConnection();
String sql = "select spicture from student where sno=?";
psmt = conn.prepareStatement(sql);
psmt.setString(1, sno);
rs = psmt.executeQuery();
if (rs.next()) {
Blob b = rs.getBlob("spicture");
if (b != null) {
byte[] bytes = b.getBytes(1, (int) b.length());
OutputStream outs = response.getOutputStream();
outs.write(bytes);
outs.flush();
} else {
String imageFile = "img/nopic.png";
int len = 0;
214
byte[] data = new byte[1024];
imageFile = getServletContext().getRealPath(imageFile);
InputStream imageIn = new FileInputStream(new File(imageFile));
OutputStream outs = response.getOutputStream();
while ((len = imageIn.read(data)) != -1) {
outs.write(data, 0, len);
}
outs.flush();
}
}
} catch (IOException e) {
Logger.getLogger(ShowPic.class.getName()).log(Level.SEVERE, null, e);
} catch (SQLException e) {
Logger.getLogger(ShowPic.class.getName()).log(Level.SEVERE, null, e);
} finally {
DatabaseBean.release(rs, psmt, conn);
}
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
processRequest 方法中的 response.setContentType("image/jpeg")用来设置 Servlet 输出内容为 Jpeg 图像,
首先,根据指定学号从数据中查询 BLOB 数据类型字段 spicture 的内容,如果图像内容不为空,则通过
response.getOutputStream()获取一个输出流,将图像内容通过输出流在浏览器中显示;如果图像内容为空,
则通过文件输入流将 img 文件夹中的 nopic.png 图片读入,利用输出流将图片显示在浏览器中。
3.4、分页显示技术的实现
分页显示功能是在 Web 应用中经常要使用到的功能,当检索到的数据量比较大时,如果全部显示在同
一个页面里会使页面的可读性变差,同时也加大系统的负担。在这种情况下,一般都要用到分页显示技术,
将数据分几页显示出来。在 Java Web 上实现分页技术,方式实际上有很多,也各有个的特点。分页技术无
外乎两种,一种是直接将所有数据取出来,放到一个集合里,通过传递 start 和 end 参数控制分页,还有
一种就是把分页工作交给数据库来处理,让数据库读取需要的 start~end 之间的数据。本节内容将采用后一
种方式来进行设计。
1、 设计分页类 Pagination.java,用来封装分页信息,其中保存了分页显示要用到的相关属性。
215
public class Pagination {
private int currPage = 1; //当前页码
private int countPage = 0; //总页数
private int pageSize = 10; //每页显示记录数
private int countSize = 0; //总记录条数
public int getCurrPage() {
return currPage;
}
public void setCurrPage(int currPage) {
this.currPage = currPage;
}
public int getCountPage() {
if(countSize != 0){
if(countSize%pageSize != 0){
countPage = countSize/pageSize +1;
}else{
countPage = countSize/pageSize;
}
}
return countPage;
}
public void setCountPage(int countPage) {
this.countPage = countPage;
}
public int getPageSize() {
return pageSize;
}
public void setPageSize(int pageSize) {
this.pageSize = pageSize;
}
public int getCountSize() {
return countSize;
}
public void setCountSize(int countSize) {
216
this.countSize = countSize;
}
}
2、 在数据库访问类 StudentDaoImpl.java 中,我们修改了获取所有学生记录 getAllStudents(Pagination
pagination)方法和获取指定学号的学生记录 getAllStudents(String sno, Pagination pagination)方法,将分页信息
对象 Pagination 传递过去。首先,方法查询满足条件的学生记录总数,根据记录总数设置 pagination 对象相
关属性,计算出要显示页面的开始和结束记录位置,然后从数据库中查询出开始和结束位置区间的所有记
录内容。在 getAllStudents(String sno, Pagination pagination)方法中,我们使用了模糊查询,其中 sql 语句用到
了like操作符,like操作符用于在WHERE子句中搜索列中的指定模式,使用psmt.setString(1, "%" + sno + "%")
方式来注册关键字。
public List<Student> getAllStudents(Pagination pagination) {
List<Student> students = new ArrayList<Student>();
Connection conn = null;
PreparedStatement psmt = null;
ResultSet rs = null;
try {
//统计总记录数
conn = DatabaseBean.getConnection();
String sql = "select count(*) as counts from student";
psmt = conn.prepareStatement(sql);
rs = psmt.executeQuery();
rs.next();
pagination.setCountSize(rs.getInt("counts"));
//求指定显示的记录数
int start = (pagination.getCurrPage() - 1) * pagination.getPageSize() + 1;
int end = pagination.getCurrPage() * pagination.getPageSize();
//小于等于该页最大条数,大于等于该页最小条数
sql = "SELECT * FROM(SELECT ROWNUM NO,s.* FROM "
+ "(SELECT * FROM student ORDER BY sno ASC) s "
+ "WHERE ROWNUM<=?) WHERE NO >=?";
psmt = conn.prepareStatement(sql);
psmt.setInt(1, end);
psmt.setInt(2, start);
rs = psmt.executeQuery();
while (rs.next()) {
Student stu = new Student();
stu.setSno(rs.getString("sno"));
stu.setSname(rs.getString("sname"));
stu.setSsex(rs.getString("ssex"));
stu.setSage(rs.getInt("sage"));
stu.setSdept(rs.getString("sdept"));
stu.setSavgGrade(rs.getDouble("savgGrade"));
217
students.add(stu);
}
return students;
} catch (SQLException e) {
Logger.getLogger(StudentDaoImpl.class.getName()).log(Level.SEVERE, null, e);
return null;
} finally {
DatabaseBean.release(rs, psmt, conn);
}
}
@Override
public List<Student> getAllStudents(String sno, Pagination pagination) {
List<Student> students = new ArrayList<Student>();
Connection conn = null;
PreparedStatement psmt = null;
ResultSet rs = null;
try {
//统计总记录数
conn = DatabaseBean.getConnection();
String sql = "select count(*) as counts from student where sno like ?";
psmt = conn.prepareStatement(sql);
psmt.setString(1, "%" + sno + "%");
rs = psmt.executeQuery();
rs.next();
pagination.setCountSize(rs.getInt("counts"));
//求指定显示的记录数
int start = (pagination.getCurrPage() - 1) * pagination.getPageSize() + 1;
int end = pagination.getCurrPage() * pagination.getPageSize();
sql = "SELECT s2.* FROM (SELECT ROWNUM r, s1.* FROM "
+ "(SELECT * FROM student WHERE sno LIKE ? ) s1 "
+ "WHERE ROWNUM <= ?) s2 WHERE s2.r >= ?";
psmt = conn.prepareStatement(sql);
psmt.setString(1, "%" + sno + "%");
psmt.setInt(2, end);
psmt.setInt(3, start);
rs = psmt.executeQuery();
while (rs.next()) {
Student stu = new Student();
stu.setSno(rs.getString("sno"));
stu.setSname(rs.getString("sname"));
stu.setSsex(rs.getString("ssex"));
stu.setSage(rs.getInt("sage"));
218
stu.setSdept(rs.getString("sdept"));
stu.setSavgGrade(rs.getDouble("savgGrade"));
students.add(stu);
}
return students;
} catch (SQLException e) {
Logger.getLogger(StudentDaoImpl.class.getName()).log(Level.SEVERE, null, e);
return null;
} finally {
DatabaseBean.release(rs, psmt, conn);
}
}
3、当我们在显示页面 index_table.jsp 中点击类似下面代码的超级链接时,将会调用 StudentIndex.java 这个
Servlet 来处理要显示的第 2 页内容。
示例超级链接:<a href="/SimpleStudentWebMis/StudentIndex?page=2">2</a>
StudentIndex.java 代码如下所示:
import com.qixin.domain.Student;
import com.qixin.util.DaoFactory;
import com.qixin.util.Pagination;
import java.io.IOException;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@WebServlet(name = "StudentIndex", urlPatterns = {"/StudentIndex"})
public class StudentIndex extends HttpServlet {
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String p = request.getParameter("page");
String sno = request.getParameter("sno");
Pagination pagination = new Pagination();
List<Student> students;
int page = 1;
if (p != null) {
page = Integer.parseInt(p);
}
pagination.setCurrPage(page);
if (sno != null && !"".equals(sno)) {
219
students = DaoFactory.getStudentDao().getAllStudents(sno, pagination);
} else {
students = DaoFactory.getStudentDao().getAllStudents(pagination);
}
//将学生记录信息转发到下一页
request.setAttribute("students", students);
//将分页信息转发到下一页
request.setAttribute("pagination", pagination);
//将模糊查询的学号信息转发到下一页,便于分页查询
request.setAttribute("sno", sno);
request.getRequestDispatcher("index_table.jsp").forward(request, response);
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
4、上述 Servlet 处理完要显示的数据内容后,将查询到的所有学生记录信息集合对象、分页信息对象和模糊
查询中要用到的学号信息注册到 request 对象的属性当中,然后将页面转发到 index_table.jsp 来显示查询到
的结果。index_table.jsp 页面中分页导航功能核心代码如下:
代码使用核心标记库时需引入声明<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>,并且
添加 jstl.jar 和 standard.jar 库文件。
......
<table id="display-table" width="100%" border="0" cellspacing="0" cellpadding="0">
<thead>
<tr>
<th>学号</th>
<th>姓名</th>
<th>性别</th>
<th>年龄</th>
<th>系部</th>
<th>平均成绩</th>
</tr>
</thead>
220
<tbody>
<c:forEach var="stu" items="${students}" varStatus="status">
<c:if test="${status.count%2==0}"><tr class="odd"></c:if>
<c:if test="${status.count%2!=0}"><tr></c:if>
<td><div class="student_sno">${stu.sno}</div></td>
<td>${stu.sname}</td>
<td>${stu.ssex}</td>
<td>${stu.sage}</td>
<td>${stu.sdept}</td>
<td>${stu.savgGrade}</td>
</tr>
</c:forEach>
</tbody>
</table>
......
<!—分页开始 -->
<div class="pagging">
<div class="left">总记录数:${pagination.countSize} - 总页数:${pagination.countPage} </div>
<div class="right">
<c:if test="${pagination.currPage > 1}">
<a href="<%= request.getContextPath()%>/StudentIndex?page=1&sno=${sno}">首页 </a>
<a href="<%= request.getContextPath()%>/StudentIndex?page=${pagination.currPage -
1}&sno=${sno}">上一页</a>
</c:if>
<c:forEach var="i" begin="${pagination.currPage - 3<=0?1:pagination.currPage - 3}"
end="${pagination.currPage - 1}">
<a href="<%=
request.getContextPath()%>/StudentIndex?page=${i}&sno=${sno}">${i}</a>
</c:forEach>
<a class="select">${pagination.currPage}</a>
<c:forEach var="i" begin="${pagination.currPage + 1}" end="${pagination.countPage -
pagination.currPage>3?pagination.currPage + 3:pagination.countPage}">
<a href="<%=
request.getContextPath()%>/StudentIndex?page=${i}&sno=${sno}">${i}</a>
</c:forEach>
<c:if test="${pagination.currPage < pagination.countPage}">
<a href="<%= request.getContextPath()%>/StudentIndex?page=${pagination.currPage +
1}&sno=${sno}">下一页 </a>
<a href="<%=
request.getContextPath()%>/StudentIndex?page=${pagination.countPage}&sno=${sno}">尾页</a>
</c:if>
</div>
221
</div>
<!—分页结束 -->
index_table.jsp 页面中显示的导航效果如下:
3.5、JQuery.Validate 客户端验证
当我们提交数据时,需要对提交的数据进行有效性验证,这不仅仅是保证提交的数据合法有效,更重
要的是保证数据库的安全性。
SQL 注入攻击是黑客对数据库进行攻击的常用手段之一。随着 B/S 模式应用开发的发展,使用这种模
式编写应用程序的程序员也越来越多。但是由于程序员的水平及经验也参差不齐,相当大一部分程序员在
编写代码的时候,没有对用户输入数据的合法性进行判断,使应用程序存在安全隐患。
表单数据验证分为服务器端验证和客户端验证两种:
服务器端验证是将提交的数据直接发送给服务器,由服务器端程序对数据的有效性进行合法验证,如
果发现错误,返回错误信息给用户,这种方式需要客户端频繁和服务器产生数据通信,这样势必造成服务
器的负担,严重影响服务器的性能;
客户端验证是在表单提交之前,对要提交的数据进行合法性验证,如果发现错误,不允许提交表单;
验证通过才可以提交表单。客户端验证的常用方法有 javascript 验证和 jquery 验证两种方法。目前,主流
的方式是采用 jquery 验证,它可以采用异步的方式进行数据合法性验证,不用刷新页面即可以显示验证结
果,可以给用户更好的用户体验。
JQuery Validate 为表单提供了强大的验证功能,让客户端表单验证变得更简单,同时提供了大量的定
制选项,满足应用程序各种需求。该插件捆绑了一套有用的验证方法,包括 URL 和电子邮件验证,同时
提供了用来编写用户自定义方法的 API。
下面我们简单介绍通过 JQuery Validate 插件来实现表单验证的方法。
1、要使用 JQuery Validate 插件来验证表单,我们需插入以下两个 JS 库文件。
下载地址:
js.rar
<script type="text/javascript" src="jquery.js" ></script>
<script type="text/javascript" src="jquery.validate.js" ></script>
2、我们可以使用表 7-6 所示校验规则来验证表单内容。
表 7-6 默认校验规则
序号 规则 描述
1 required:true 必须输入的字段。
2 remote:"check.php" 使用 ajax 方法调用 check.php 验证输入值。
3 email:true 必须输入正确格式的电子邮件。
4 url:true 必须输入正确格式的网址。
5 date:true 必须输入正确格式的日期。日期校验 ie6 出错,慎用。
6
dateISO:true 必须输入正确格式的日期(ISO),例如:2009-06-23,1998/01/22。只
验证格式,不验证有效性。
7 number:true 必须输入合法的数字(负数,小数)。
222
8 digits:true 必须输入整数。
9 creditcard: 必须输入合法的信用卡号。
10 equalTo:"#field" 输入值必须和 #field 相同。
11 accept: 输入拥有合法后缀名的字符串(上传文件的后缀)。
12 maxlength:5 输入长度最多是 5 的字符串(汉字算一个字符)。
13 minlength:10 输入长度最小是 10 的字符串(汉字算一个字符)。
14 rangelength:[5,10] 输入长度必须介于 5 和 10 之间的字符串(汉字算一个字符)。
15 range:[5,10] 输入值必须介于 5 和 10 之间。
16 max:5 输入值不能大于 5。
17 min:10 输入值不能小于 10。
3、创建插入表单页面 insert.jsp(注意红色代码引用的资源地址,在实际使用时应根据项目相对路径加
以修改)
<%@page contentType="text/html" pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<link rel="stylesheet" href="css/style.css" type="text/css" media="all" />
<title>JSP Page</title>
<script type="text/javascript" src="js/jquery.js" ></script>
<script type="text/javascript" src="js/jquery.validate.js" ></script>
<script type="text/javascript" src="js/myValidate.js" ></script>
</head>
<body>
<h1>JQuery 插入表单验证!</h1>
<form id="insertForm" name="insertForm" action="Display" method="post"
enctype="multipart/form-data">
<table border="0">
<tr>
<td>学号:</td>
<td><input type="text" id="sno" name="sno"/></td>
<td></td>
</tr>
<tr>
<td>姓名:</td>
<td><input type="text" id="sname" name="sname"/></td>
<td></td>
</tr>
<tr>
<td>年龄:</td>
<td><input type="text" id="sage" name="sage"/></td>
<td></td>
</tr>
<tr>
223
<td>图片:</td>
<td><input type="file" id="spicture" name="spicture" /></td>
<td></td>
</tr>
<tr>
<td></td>
<td align="right"><input type="submit" value="提交" /></td>
<td></td>
</tr>
</table>
</form>
</body>
</html>
4、创建自定义的校验规则文件 myValidate.js。
$(document).ready(function() {
//页面插入表单验证
$("#insertForm").validate({
success: "checked",
errorClass: "error",
rules: {
sno: {required: true, digits: true, maxlength: 7, minlength: 7,
remote: {
url: "CheckSno",
type: "post",
dataType: "json",
data: {
sno: function() {
return $("#sno").val();
}
}
}
},
sname: {required: true, maxlength: 10},
sage: {required: true, digits: true, min: 15, max: 45},
spicture: {accept: 'jpg|png|gif'}
},
messages: {
sno: {required: '学号不能为空', digits: '学号必须为数字', maxlength: '请输入 7 位数字学号
', minlength: '请输入 7 位数字学号', remote: '学号已占用'},
sname: {required: '姓名不能为空', maxlength: '最多输入 10 个字符'},
sage: {required: '年龄不能为空', digits: '请输入数字', min: '年龄最小 15 岁', max: '年龄
最大 45 岁'},
spicture: {accept: '上传图片格式必须为:gif|png|jpg'}
224
},
errorPlacement: function(error, element) {
error.appendTo(element.parent().next());
}
});
});
这里的代码采用了 JQuery 语法规则,如果想深入了解 JQuery 的知识,可以去找一些相关的资料学习,
这里不再深入讲解。我们在实际应用中,只需按照上面模板编写即可。
$("#insertForm").validate 表示我们要验证的表单名称为 insertForm,这里要和 insert.jsp 文件中表单属性
id 的内容要一致。<form id="insertForm" name="insertForm" ......>
success: "checked"和 errorClass: "error"表示我们验证成功和验证失败时要加载的 css 样式风格。
我们在 insert.jsp 文件中引用的 style.css 样式表文件中定义下面两个样式风格,将两个图片文件拷贝到
相应的 images 文件夹下,注意图片相对路径的引用。
label.error {
background:url("images/unchecked.gif") no-repeat 5px 0px;
padding-left: 20px;
padding-bottom: 2px;
font-weight: bold;
color: #EA5200;
}
label.checked {
background:url("images/checked.gif") no-repeat 5px 0px;
}
rules: {......}部分定义了各个表单域(sno,sname,sage,spicture)所对应的验证规则,其中 sno、sname、sage、
spicture 的名称也要和各个表单域中定义的 id 属性的内容要一致,验证规则请对应表 1 所示内容进行查看。
messages: {......}部分定义了各个表单域验证出错时所对应和验证规则相匹配的提示信息文本。
errorPlacement 部分表示错误放置的位置,默认情况是:error.appendTo(element.parent());即把错误信息
放在验证的元素后面。
在要验证的表单域 sno 字段中有下面这段代码,它的作用是使用 ajax 的方式进行异步验证,默认会提
交当前验证的值到远程地址,如果需要提交其他的值,可以使用 data 选项。远程地址只能输出 "true" 或
"false",不能有其他输出。这里表示将 sno 字段的值提交到远程 CheckSno 这个 Servlet 去处理。
remote: {
url: "CheckSno", //后台处理程序
type: "post", // 数据发送方式
dataType: "json", //接受数据格式
data: { //要传递的数据
sno: function() {
return $("#sno").val();
}
}
}
5、编写异步验证 Servlet。
225
@WebServlet(name = "CheckSno", urlPatterns = {"/CheckSno"})
public class CheckSno extends HttpServlet {
protected void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("UTF-8");
response.setContentType("text/json;charset=UTF-8");
String sno = request.getParameter("sno");
PrintWriter out = response.getWriter();
boolean flag = DaoFactory.getStudentDao().checkSno(sno);
out.print(flag == false ? "false" : "true");
out.close();
}
@Override
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
processRequest(request, response);
}
}
注意下面这两段代码:
response.setContentType("text/json;charset=UTF-8"); 表示输出响应内容为 json 格式,
out.print(flag == false ? "false" : "true"); 表示输出内容只能是 true 和 false 两个值。
最终,JQuery Validate 插件验证显示效果如下图所示:
4、实验内容
(1)、阅读有关资料,理解 B/S 架构的概念。
226
(2)设计三个 JSP 页面,每个页面内嵌一个表单,三个表单分别收集学生信息(不包含平均成绩和图片字段)、
课程信息、选课信息。并编写相应的 Servlet,用其接收页面信息并显示。
227
(3)、在学生-课程数据库上实现一个简单的基于 B/S 架构的“学生成绩管理系统”,该系统包括课程信息管
理、学生信息管理和选课信息管理三个功能模块,每个功能模块提供对相应信息的浏览、查询、增、删、
改等操作。通过实验,以加深对 B/S 架构的认识,初步掌握 B/S 架构应用系统的开发过程。要求如下:
① 用户交互界面用页面(JSP 或 HTML)实现。
② 课程信息管理模块必做,其它两个模块选做。
③ 对数据库的访问通过存储过程调用实现。
④ 相关类分层组织成合适的包的形式。
5、实验总结
请书写你对本次实验有哪些实质性的收获和体会,以及对本次实验有何良好的建议?