第2章预备知识 2.1 Java程序设计基础 J a v a是J S P的基础,要学习J S P技术,J a v a基础是必不可少的。本节将简要介绍J a v a的基本语 法和概念。已经是J a v a编程人员的读者就不用阅读了,这里是为没有多少J a v a经验的读者提供一 个快速入门的方法。这里对J a v a语言的介绍仅仅是一个基本的概况,要想深入学习J S P,必须对 J a v a语言有深刻的理解,笔者推荐机械工业出版社翻译出版的《J a v a编程思想》一书,本书限于 篇幅,就不多讲了。 2.1.1 Java语言规则 J a v a语言的基本结构像C / C + +, 任何用面向过程语言编写过程序的人都可以了解J a v a语言的 大部分结构。 1. 程序结构 J a v a语言的源程序代码由一个或多个编译单元( c o m p i l a t i o n u n i t )组成,每个编译单元只能包 含下列内容(空格和注释除外): • 程序包语句(package statement )。 • 入口语句(import statements) 。 • 类的声明(class declarations) 。 • 界面声明(interface declarations)。 每个J a v a的编译单元可包含多个类或界面,但是每个编译单元最多只能有一个类或者界面是 公共的。Java 的源程序代码被编译后,便产生了J a v a字节代码。J a v a的字节代码由一系列不依 赖于机器的指令组成,这些指令能被J a v a的运行系统(runtime system)有效地解释。J a v a的运行系 统工作起来如同一台虚拟机。在当前的J a v a实现中,每个编译单元就是一个以. j a v a为后缀的文件。 每个编译单元有若干个类,编译后,每个类生成一个. c l a s s文件。. c l a s s文件是J a v a虚拟机能够识 别的代码。在引入了J A R这个概念以后,现在可以把许多J a v a的c l a s s文件压缩进入一个J A R文件 中。新版本的J a v a已经可以直接读取J A R文件加以执行。 2. 注释 注释有三种类型: / / 注释一行 / * 一行或多行注释 */ / * * 文档注释 **/ 文档注释一般放在一个变量或函数定义之前,表示在任何自动生成文档系统中调入,提取 注释生成文档的工具叫做j a v a d o c,其中还包括一些以@开头的变量,如: @ s e e、@ v e r s i o n、 @ p a r a m等等,具体用法参见J D K自带的工具文档。 3. 标识符 变量、函数、类和对象的名称都是标识符,程序员需要标识和使用的东西都需要标识符。 在J a v a语言里,标识符以字符_或$开头,后面可以包含数字,标识符是大小写有区别的,没有长 度限制。 有效的标识符如: gogogo brood_war Hello _and_you $bill。 声明如: int a_number; char _onechar; float $bill。 以下为J a v a的关键字: a b s t r a c t c o n t i n u e f o r new switch b o o l e a n d e f a u l t g o t o n u l l synchronized b r e a k d o i f p a c k a g e this b y t e d o u b l e i m p l e m e n t s p r i v a t e threadsafe b y v a l u e e l s e i m p o r t p r o t e c t e d throw c a s e e x t e n d s i n s t a n c e o f p u b l i c transient c a t c h f a l s e i n t r e t u r n true char f i n a l i n t e r f a c e short try c l a s s f i n a l l y l o n g s t a t i c void c o n s t f l o a t n a t i v e s u p e r while 以下单词被保留使用: c a s t、f u t u r e、g e n e r i c、i n n e r、o p e r a t o r、o u t e r、r e s t、v a r。 4. 数据类型 J a v a使用五种基本类型: i n t e g e r (整数),f l o a t i n g (浮点数),p o i n t (指针),B o o l e a n (布尔变量), Character or String(字符或字符串)。此外,还有一些复合的数据类型,如数组等。 Integer 包含下面几种类型: 整数长度( B i t s ) 数据类型表示 8 b y t e 1 6 short 3 2 int 6 4 l o n g floating 下边给出的数据表示都是浮点数的例子: 3 . 1 4 1 5 9,3 . 1 2 3 E 1 5,4 e 5 浮点数长度(Bits) 数据类型表示 3 2 f l o a t 6 4 double Boolean 下边是布尔变量的两种可能取值: t r u e false Character 下边给出的都是字符的例子: 第二章预备知识17
a s d f String 下边给出的都是字符串的例子: "gogogo,rock and roll" " J S P高级编程" 数组可以定义任意类型的数组,如char s[],这是字符型数组; int array [],这是整型数组; 还可以定义数组的数组. intblock[][]=new int [2][3];数组边界在运行时被检测,以避免堆栈溢出。 在J a v a里,数组实际上是一个对象,数组有一个成员变量: l e n g t h。可以用这个成员函数来 查看任意数组的长度。 在J a v a里创建数组,可使用两种基本方法: 1) 创建一个空数组。 int list[]=new int[50]; 2) 用初始数值填充数组. String names[] = { "Chenji","Yuan","Chun","Yang" }; 它相当于下面功能: String names[]; names = new String[4]; names[0]=new String("Chenji"); names[1]=new String("Yuan"); names[2]=new String("Chun"); names[3]=new String("Yang"); 在编译时不能这样创建静态数组: int name[50]; / /将产生一个编译错误 也不能用n e w操作去填充一个没定义大小的数组。如: int name[]; for (int i=0;i<9; i++) { name[i] = i; } 5. 表达式 J a v a语言的发展中有许多是从C语言借鉴而来的,所以J a v a的表达式和C语言非常类似。 运算符 运算符( o p e r a t o r )优先级从高到低排列如下: . [ ] () ++ -- ! ~ instanceof * / % + - << >> >>> < > <= >\ == ! = & ^ && || ? : = op = , (2) 整数运算符 在整数运算时,如果操作数是l o n g类型,则运算结果是l o n g类型,否则为i n t类型,绝不会是 b y t e,s h o r t或c h a r型。这样,如果变量i被声明为s h o r t或b y t e,i + 1的结果会是i n t。如果结果超过 该类型的取值范围,则按该类型的最大值取模。单目整数运算符是: 运算符操作 - 非 ~ 位补码 + + 加1 - - 减1 18第一部分JSP 入门
+ +运算符用于表示直接加1操作。增量操作也可以用加运算符和赋值操作间接完成。+ + lvalue (左值表示l v a l u e + = 1, ++lvalue 也表示lvalue =lvalue +1 (只要l v a l u e没有副作用)。- -运 算符用于表示减1操作。+ +和- -运算符既可以作为前缀运算符,也可以作为后缀运算符。双目 整数运算符是: 运算符操作 + 加 - 减 * 乘 / 除 % 取模 & 位与 | 位或 ^ 位异或 < < 左移 > > 右移(带符号) > > > 添零右移 整数除法按零舍入。除法和取模遵守以下等式: ( a/b ) * b + ( a%b ) == a。整数算术运算的 异常是由于除零或按零取模造成的。它将引发一个算术异常,下溢产生零,上溢导致越界。例 如:加1超过整数最大值,取模后,变成最小值。一个o p =赋值运算符,和上表中的各双目整数 运算符联用,构成一个表达式。整数关系运算符<,>,< =,> =,= =和! =产生b o o l e a n类型的数 据。 ( 3 ) 布尔运算符 布尔( b o o l e a n )变量或表达式的组合运算可以产生新的b o o l e a n值。单目运算符!是布尔非。 双目运算符&、|和^是逻辑A N D、O R和X O R运算符,它们强制两个操作数求布尔值。为避免 右侧操作数冗余求值,用户可以使用短路求值运算符&& 和||。用户可以使用= =和! =,赋值 运算符也可以用& =、| =、^ =。三元条件操作符? : 和C语言中的一样。 (4) 浮点运算符 浮点运算符可以使用常规运算符的组合,如单目运算符+ +、- -,双目运算符+、-、* 和/, 以及赋值运算符+ =,- =,* =,和/ =。此外,还有取模运算:%和% =也可以用于浮点数,例如: a % b和a-((int) (a/b)*b)的语义相同。这表示a % b的结果是除完后剩下的浮点数部分。只有单精度 操作数的浮点表达式按照单精度运算求值,产生单精度结果。如果浮点表达式中含有一个或一 个以上的双精度操作数,则按双精度运算,结果是双精度浮点数。 (5) 数组运算符 数组运算符形式如下: <expression> [ <expression>] 可给出数组中某个元素的值。合法的取值范围是从0到数组的长度减1。取值范围的检查只 在运行时刻实施。 ( 6 ) 对象运算符 双目运算符instanceof 测试某个对象是否是指定类或其子类的实例。例如: if (myObject instanceof MyClass) { 第二章预备知识19
MyClass anothermyObject=( MyClass) myObject; ... } 是判定m y O b j e c t是否是M y C l a s s的实例或是其子类的实例。 (7) 强制和转换 J a v a语言和解释器限制使用强制和转换,以防止出错导致系统崩溃。整数和浮点数间可以来 回强制转换,但整数不能强制转换成数组或对象。对象不能被强制为基本类型。 6. Java流控制 下面几个控制结构是从C语言借鉴的。 ( 1 ) 分支结构 i f / e l s e分支结构: if (Boolean) { statemanets; } else { statements; } s w i t c h分支结构: switch(expr1) { case expr2: statements; break; case expr3: statements; break; default: statements; break; } (2) 循环结构 f o r循环结构: for (init expr1;test expr2;increment expr3) { statements; } W h i l e循环结构: While(Boolean) { statements; } D o循环结构: do statements; } while (Boolean); 20第一部分JSP 入门
2.1.2 Java变量和函数 J a v a的类包含变量和函数。数据变量可以是原始的类型,如i n t、c h a r等。成员函数是可执行 的过程。例如,下面的程序: public class TestClass public TestClass () { i=10; } public void addI(int j) { i=i+j; } } Te s t C l a s s包含一个变量i和两个成员函数,TestClass(int first)和addI(int j)。 成员函数是一个可被其他类或自己类调用的处理子程序。一个特殊的成员函数叫构造函数, 这个函数名称一般与本类名称相同。它没有返回值。 在J a v a里定义一个类时,可定义一个或多个可选的构造函数,当创建本类的一个对象时,用 某一个构造函数来初始化本对象。用前面程序例子来说明,当Te s t C l a s s类创建一个新实例时, 所有成员函数和变量被创建(创建实例)。构造函数被调用。 TestClass testObject; testObject = new TestClass(); 关键词n e w用来创建一个类的实例,一个类用n e w初始化前并不占用内存,它只是一个类型 定义,当t e s t O b j e c t对象初始化后,t e s t O b j e c t对象里的i变量等于1 0。可以通过对象名来引用变量 i。(有时称为实例变量) testObject.i++;// testObject实例变量加1,因为t e s t O b j e c t有Te s t C l a s s类的 所有变量和成员函数,可以使用同样的语法来调用成员函数a d d I:addI(10); 现在t e s t O b j e c t . i变 量等于2 1。 J a v a并不支持析构函数( C + +里的定义),因为j a v a对象无用时,有自动清除的功能,同时它 也提供了一个自动垃圾箱的成员函数,在清除对象时被调用: Protected void finalize() { close(); } 2.1.3 子类 子类是利用存在的对象创建一个新对象的机制,比如,如果有一个H o r s e类,你可以创建一 个Z e b r a子类,Z e b r a是H o r s e的一种。 class Zebra extends Horse { int number_OF_stripes: } 关键词e x t e n d s来定义对象有的子类. Z e b r a是H o r s e的子类。H o r s e类里的所有特征都将拷贝到 Z e b r a类里,而Z e b r a类里可以定义自己的成员函数和实例变量。Z e b r a称为H o r s e的派生类或继承。 另外,你也许还想覆盖基类的成员函数,可用Te s t C l a s s说明,下面是一派生类覆盖A d d I功能的 例子。 import TestClass; public class NewClass extends TestClass { 第二章预备知识21
public void AddI(int j) { i=i+(j/2); } } 当N e w C l a s s类的实例创建时,变量i初始化值为1 0,但调用A d d I产生不同的结果。 NewClass newObject; newObject=new NewClass(); newObject.AddI(10); 当创建一个新类时,可以标明变量和成员函数的访问层次。 public public void AnyOneCanAccess(){} public实例变量和成员函数可以由任意其他类调 用。 protected protected void OnlySubClasses(){} protected实例变量和成员函数只能被其子类调 用。 private private String CreditCardNumber; private实例变量和成员函数只能在本类里调用。 friendly void MyPackageMethod(){}是缺省的,如果没有定义任何访问控制,实例变量或函 数缺省定义成f r i e n d l y,这意味着可以被本包里的任意对象访问,但其他包里的对象不可访问。 对于静态成员函数和变量,有时候,你创建一个类,希望这个类的所有实例都公用一个变 量。就是说,所有这个类的对象都只有实例变量的同一个拷贝。这种方法的关键词为s t a t i c, 例 如: class Block { static int number=50; } 所有从B l o c k类创建的对象的n u m b e r变量值都是相同的。无任在哪个对象里改变了n u m b e r的 值, 所有对象的n u m b e r都跟着改变。同样,可以定义s t a t i c成员函数,但这个成员函数不能访问 非s t a t i c函数和变量。 class Block { static int number = 50; int localvalue; static void add_local(){ localvalue++; file://没有运行 } static void add_static() { number++;//运行 } } 2.1.4 this和s u p e r 访问一个类的实例变量时, t h i s关键词是指向这个类本身的指针,在前面的Te s t C l a s s例子中, 可以增加构造函数如下: public class TestClass { 22第一部分JSP 入门
int i; public TestClass() { i = 10; } public TestClass (int value) { this.i = value; } public void AddI(int j) { i = i + j; } } 这里,t h i s指向Te s t C l a s s类的指针。如果在一个子类里覆盖了父类的某个成员函数,但又想 调用父类的成员函数,可以用super 关键词指向父类的成员函数。 import TestClass; public class NewClass extends TestClass { public void addI (int j) { i = i+(j/2); super.addI (j); } } 下面程序里,i变量被构造函数设为1 0,然后为1 5,最后被父类( Te s t C l a s s )设为2 5。 NewClass newObject; newObject = new NewClass(); newObject.addI(10); 2.1.5 类的类型 迄今为止,在类前面只用了一个p u b l i c关键词,其实它有下面4种选择: a b s t r a c t。一个a b s t r a c t类必须至少有一个虚拟函数,一个a b s t r a c t类不能直接创建对象,必须 继承子类后才能创建对象。 f i n a l一个f i n a l类声明了子类链的结尾,用f i n a l声明的类不能再派生子类。 P u b l i c。p u b l i c类能被其他的类访问。在其他包里,如果想使用这个类,必须先i m p o r t,否 则它只能在它定义的p a c k a g e里使用。 s y n c h r o n i c a b l e。这个类标识表示所有类的成员函数都是同步的。 2.1.6 抽象类 面向对象的一个最大优点就是能够定义怎样使用这个类而不必真正定义好成员函数。当程 序由不同的用户实现时,这是很有用的,这不需用户使用相同的成员函数名。 在j a v a里,G r a p h i c s类中一个a b s t r a c t类的例子如下: public abstract class Graphics { public abstract void drawLine(int x1,int y1,int x2, int y2); public abstract void drawOval(int x,int y,int width, int height); 第二章预备知识23
public abstract void drawRect(int x,int y,int width, int height); } 在G r a p h i c s类里声明了几个成员函数,但成员函数的实际代码是在另外一个地方实现的。 public class MyClass extends Graphics { public void drawLine (int x1,int y1,int x2, int y2) { <画线程序代码> } } 当一个类包含一个a b s t r a c t成员函数时,这个类必须定义为a b s t r a c t类。然而并不是a b s t r a c t类 的所有成员函数都是a b s t r a c t的。A b s t r a c t类不能有私有成员函数(它们不能被实现),也不能有静 态成员函数。 2.1.7 接口 当确定多个类的操作方式都很相像时, a b s t r a c t成员函数是很有用的。但如果需要使用这个 a b s t r a c t成员函数,必须创建一个新类,这样有时很繁琐。接口提供了一种抽象成员函数的有利 方法。一个接口包含了在另一个地方实现的成员函数的收集。成员函数在接口里定义为p u b l i c和 a b s t r a c t。接口里的实例变量是p u b l i c、s t a t i c和f i n a l。接口和抽象的主要区别是,一个接口提供 了封装成员函数协议的方法而不必强迫用户继承类。 例如: public interface AudiClip { file://Start playing the clip. void play(); file://Play the clip in a loop. void loop(); file://Stop playing the clip void stop(); } 想使用Audio Clip接口的类使用i m p l e n e n t s关键词来提供成员函数的程序代码。 class MyClass implements AudioClip { void play(){ <实现代码> } void loop <实现代码> } void stop <实现代码> } } 接口的优点是:一个接口类可以被任意多的类实现,每个类可以共享程序接口而不必关心 其他类是怎样实现的。 2.1.8 包 包( P a c k a g e )由一组类( c l a s s )和界面( i n t e r f a c e )组成。它是管理大型名字空间,避免名字冲突 的工具。每一个类和界面的名字都包含在某个包中。按照一般的习惯,它的名字由 . 号分隔 的单词构成,第一个单词通常是开发这个包的组织的名称。 定义一个编译单元的包由p a c k a g e语句定义。如果使用p a c k a g e语句,编译单元的第一行必 须无空格,也无注释。其格式如下: 24第一部分JSP 入门
package packageName; 若编译单元无p a c k a g e语句,则该单元被置于一个缺省的无名的包中。 在J a v a语言里,提供了一个包可以使用另一个包中类和界面的定义和实现的机制。用i m p o r t 关键词来标明来自其他包中的类。一个编译单元可以自动把指定的类和界面输入到它自己的包 中。在一个包中的代码可以有两种方式定义自其他包中的类和界面:在每个引用的类和界面前 面给出它们所在的包的名字: file://前缀包名法acme. project.FooBar obj=new acme. project. FooBar( ); 使用i m p o r t语句引入一个类或一个界面,或包含它们的包。引入的类和界面的名字在当前的 名字空间可用。引入一个包时,则该包所有的公有类和界面均可用。其形式如下: // 从acme.project 引入所有类 import acme.project.*; 这个语句表示a c m e . p r o j e c t中所有的公有类被引入当前包。以下语句从acme. project包中进 入一个类E m p l o y e c _ L i s t。 file://从acme. project而引入Employee_List import acme.project.Employee_list; Employee_List obj = new Employee_List( ); 在使用一个外部类或界面时,必须要声明该类或界面所在的包,否则会产生编译错误。 i m p o r t (引入)类包(class package)用i m p o r t关键词调入指定p a c k a g e名字如路径和类名,用*匹 配符可以调入多于一个类名。 import java.Date; import java.awt.*; 如果j a v a源文件不包含p a c k a g e,它放在缺省的无名p a c k a g e。这与源文件同目,类可以这样 引入: import MyClass; J a v a系统包: J a v a语言提供了一个包含窗口工具箱、实用程序、一般I / O、工具和网络功能 的包。 各个包的用法和类详解在J D K自带的文档中都有详细的说明,希望读者能够好好的看一看, 对大部分的常用包的类都熟悉了,才能更好地掌握J a v a这门技术。 在了解J a v a语言的概况以后,接下来看一看与J S P技术密切相关的三种J a v a技术:J a v a E e a n s, J D B C,Java Servlet。 2.2 JavaBeans J a v a B e a n s是什么?J a v a B e a s n s是一个特殊的类,这个类必须符合J a v a B e a n s规范。J a v a B e a n s 原来是为了能够在一个可视化的集成开发环境中可视化、模块化地利用组件技术开发应用程序 而设计的。不过,在J S P中,不需要使用任何可视化的方面,但仍然需要利用J a v a B e a n s的属性、 事件、持久化和用户化来实现模块化的功能。下面分别介绍J a v a B e a n s的属性、事件、持久化和 用户化。 第二章预备知识25
2.2.1 JavaBeans的属性 J a v a B e a n s的属性与一般J a v a程序中所指的属性,或者说与所有面向对象的程序设计语言中 对象的属性是一个概念,在程序中的具体体现就是类中的变量。在J a v a B e a n s设计中,按照属性 的不同作用又细分为四类: Simple, Index, Bound与C o n s t r a i n e d属性。 1. Simple属性 Simple 属性表示伴随有一对g e t / s e t方法(C语言的过程或函数在J a v a程序中称为方法)的 变量。属性名与和该属性相关的g e t / s e t方法名对应。例如:如果有s e t X和g e t X方法,则暗指有一 个名为X的属性。如果有一个方法名为i s X,则通常暗指X是一个布尔属性(即X的值为 t r u e或f a l s e)。例如,在下面这个程序中: public class alden1 extends Canvas { string ourString= "Hello"; file://属性名为ourString,类型为字符串 public alden1(){ file://alden1()是alden1的构造函数,与C++中构造函数的意义相同 setBackground(Color.red); setForeground(Color.blue); } /* "set"属性*/ public void setString(String newString) { ourString=newString; } /* "get"属性*/ public String getString() { return ourString; } } 2. Indexed属性 I n d e x e d属性表示一个数组值。使用与该属性对应的s e t / g e t方法可取得数组中的数值。该属 性也可一次设置或取得整个数组的值。例如: public class alden2 extends Canvas { int[] dataSet={1,2,3,4,5,6}; // dataSet是一个indexed属性 public alden2() { setBackground(Color.red); setForeground(Color.blue); } /* 设置整个数组 */ public void setDataSet(int[] x){ dataSet=x; } /* 设置数组中的单个元素值*/ public void setDataSet(int index, int x){ dataSet[index]=x; 26第一部分JSP 入门
} /* 取得整个数组值*/ public int[] getDataSet(){ return dataSet; } /* 取得数组中的指定元素值*/ public int getDataSet(int x){ return dataSet[x]; } } 3. Bound属性 B o u n d属性是指当该种属性的值发生变化时,要通知其他的对象。每次属性值改变时,这种 属性就触发一个P r o p e r t y C h a n g e事件(在J a v a程序中,事件也是一个对象)。事件中封装了属性名、 属性的原值、属性变化后的新值。这种事件传递到其他的B e a n s,至于接收事件的B e a n s应做什 么动作,由其自己定义。 当P u s h B u t t o n的b a c k g r o u n d属性与D i a l o g的b a c k g r o u n d属性绑定时,若P u s h B u t t o n的 b a c k g r o u n d属性发生变化,D i a l o g的b a c k g r o u n d属性也发生同样的变化。例如: public class alden3 extends Canvas{ String ourString= "Hello"; file://ourString是一个bound属性 private PropertyChangeSupport changes = new PropertyChangeSupport(this); /*Java是纯面向对象的语言,如果要使用某种方法则必须指明是要使用哪个对象的方法,在下面的程序中要进行点 火事件的操作,这种操作所使用的方法是在PropertyChangeSupport类中的。所以上面声明并实例化了一个 changes对象,在下面将使用changes的firePropertyChange方法来点火ourString的属性改变事件。*/ public void setString(string newString){ String oldString = ourString; ourString = newString; /* ourString的属性值已发生变化,于是接着点火属性改变事件*/ changes.firePropertyChange("ourString",oldString,newString); } public String getString(){ return ourString; } /** 以下代码是为开发工具所使用的。我们不能预知alden3将与哪些其他的Beans组合成为一个应用,无法预知若 alden3的ourString属性发生变化时有哪些其他的组件与此变化有关,因而alden3这个Beans要预留出一些接口 给开发工具,开发工具使用这些接口,把其他的JavaBeans对象与alden3挂接。*/ public void addPropertyChangeListener(PropertyChangeLisener l){ changes.addPropertyChangeListener(l); } public void removePropertyChangeListener(PropertyChangeListener l){ changes.removePropertyChangeListener(l); } 第二章预备知识27
通过上面的代码,开发工具调用c h a n g e s的a d d P r o p e r t y C h a n g e L i s t e n e r方法把其他J a v a B e a n注 册入o u r S t r i n g属性的监听者队列l中,l是一个Ve c t o r数组,可存储任何J a v a对象。开发工具也可 使用c h a n g e s的r e m o v e P r o p e r t y C h a n g e L i s t e n e r方法,从l中注销指定的对象,使a l d e n 3的o u r S t r i n g 属性的改变不再与这个对象有关。当然,当程序员手写代码编制程序时,也可直接调用这两个 方法,把其他J a v a对象与a l d e n 3挂接。 4. Constrained属性 J a v a B e a n s的Co n s t r a i n e d属性是指当这个属性的值要发生变化时,与这个属性已建立了某种连 接的其他J a v a对象可否决属性值的改变。Co n s t r a i n e d属性的监听者通过抛出P r o p e r t y Ve t o E x c e p t i o n 来阻止该属性值的改变。 例如:下面程序中的Co n s t r a i n e d属性是P r i c e I n C e n t s。 public class JellyBean extends Canvas{ private PropertyChangeSupport changes=new PropertyChangeSupport(this); private VetoableChangeSupport Vetos=new VetoableChangeSupport(this); /*与前述changes相同,可使用VetoableChangeSupport对象的实例Vetos中的方法,在特定条件下来阻 止PriceInCents值的改变。*/ ...... public void setPriceInCents(int newPriceInCents) throws PropertyVetoException { /* 方法名中throws PropertyVetoException的作用是当有其他Java对象否决PriceInCent s的改变时,要抛出例外。*/ /* 先保存原来的属性值*/ int oldPriceInCents=ourPriceInCents; /**点火属性改变否决事件*/ vetos.fireVetoableChange ("priceInCents",new Integer(OldPriceInCents), new Integer(newPriceInCents)); /**若有其他对象否决priceInCents的改变,则程序抛出例外,不再继续执行下面的两条语句,方法结束。若无其 他对象否决priceInCents的改变,则在下面的代码中把ourPriceIncents赋予新值,并点火属性改变事件*/ ourPriceInCents=newPriceInCents; changes.firePropertyChange ("priceInCents", new Integer(oldPriceInCents),new Integer(newPriceInCents)); } /**与前述changes相同,也要为PriceInCents属性预留接口,使其他对象可注册入PriceInCents否决改变监 听者队列中,或把该对象从中注销 public void addVetoableChangeListener(VetoableChangeListener l){ vetos.addVetoableChangeListener(l); } public void removeVetoableChangeListener(VetoableChangeListener l){ vetos.removeVetoableChangeListener(l); } ...... } 从上面的例子中可看到,一个Co n s t r a i n e d属性有两种监听者:属性变化监听者和否决属性 改变的监听者。否决属性改变的监听者在自己的对象代码中有相应的控制语句,在监听到有 Co n s t r a i n e d属性要发生变化时,在控制语句中判断是否应否决这个属性值的改变。 28第一部分JSP 入门
总之,某个B e a n s的Co n s t r a i n e d属性值可否改变取决于其他的B e a n s或者是J a v a对象是否允许 这种改变。允许与否的条件由其他的B e a n s或J a v a对象在自己的类中进行定义。 2.2.2 JavaBeans的事件 事件处理是J a v a B e a n s体系结构的核心之一。通过事件处理机制,可让一些组件作为事件源, 发出可被描述环境或其他组件接收的事件。这样,不同的组件就可在构造工具内组合在一起, 组件之间通过事件的传递进行通信,构成一个应用。从概念上讲,事件是一种在源对象和 监听者对象之间某种状态发生变化的传递机制。事件有许多不同的用途,例如在Wi n d o w s系 统中常要处理的鼠标事件、窗口边界改变事件、键盘事件等。在J a v a和J a v a B e a n s中则定义了一 个一般的、可扩充的事件机制,这种机制能够: • 对事件类型和传递模型的定义和扩充提供一个公共框架,并适合于广泛的应用。 • 与J a v a语言和环境有较高的集成度。 • 事件能被描述环境捕获和触发。 • 能使其他构造工具采取某种技术在设计时直接控制事件、事件源和事件监听者之间的联 系。 • 事件机制本身不依赖于复杂的开发工具。 特别地,还应当: • 能够发现指定的对象类可以生成的事件。 • 能够发现指定的对象类可以观察(监听)到的事件。 • 提供一个常规的注册机制,允许动态操纵事件源与事件监听者之间的关系。 • 不需要其他的虚拟机和语言即可实现。 • 事件源与监听者之间可进行高效的事件传递。 • 能完成J a v a B e a n事件模型与相关的其他组件体系结构事件模型的中立映射。 1. 概述 J a v a B e a n s事件模型总体结构的主要构成: 事件从事件源到监听者的传递是通过对目标监听 者对象的J a v a方法调用进行的。对每个明确的事件发生,都相应地定义一个明确的J a v a方法。这 些方法都集中定义在事件监听者(E v e n t L i s t e n e r)接口中,这个接口要继承j a v a . u t i l . E v e n t L i s t e n e r。 实现了事件监听者接口中一些或全部方法的类就是事件监听者。伴随着事件的发生,相应的状态 通常都封装在事件状态对象中,该对象必须继承自j a v a . u t i l . E v e n t O b j e c t。事件状态对象作为单参 传递给应响应该事件的监听者方法中。发出某种特定事件的事件源的标识是:遵从规定的设计格 式为事件监听者定义注册方法,并接受对指定事件监听者接口实例的引用。有时,事件监听者不 能直接实现事件监听者接口,或者还有其他的额外动作时,就要在一个源与其他一个或多个监听 者之间插入一个事件适配器类的实例,以建立它们之间的联系。 2. 事件状态对象 与事件发生有关的状态信息一般都封装在一个事件状态对象中,这种对象是j a v a . u t i l . E v e n t O b j e c t 的子类。按设计习惯,这种事件状态对象类名应以E v e n t结尾。例如: public class MouseMovedExampleEvent extends java.util.EventObject{ 第二章预备知识29
protected int x, y; /* 创建一个鼠标移动事件MouseMovedExampleEvent */ MouseMovedExampleEvent(java.awt.Component source, Point location) { super(source); x = location.x; y = location.y; } /* 获取鼠标位置*/ public Point getLocation() { return new Point(x, y); } } 3. 事件监听者接口与事件监听者 由于J a v a事件模型是基于方法调用的,因而需要一个定义并组织事件操纵方法的方式。 J a v a B e a n s中,事件操纵方法都被定义在继承了j a v a . u t i l . E v e n t L i s t e n e r类的事件监听者(E v e n t L i s t e n e r) 接口中,按规定,E v e n t L i s t e n e r接口的命名要以L i s t e n e r结尾。任何一个类如果想操纵在E v e n t L i s t e n e r 接口中,定义的方法都必须以实现这个接口方式进行。这个类就是事件监听者。 例如: /*先定义了一个鼠标移动事件对象*/ public class MouseMovedExampleEvent extends java.util.EventObject { // 在此类中包含了与鼠标移动事件有关的状态信息 ... } /*定义了鼠标移动事件的监听者接口*/ interface MouseMovedExampleListener extends java.util.EventListener { /*在这个接口中定义了鼠标移动事件监听者所应支持的方法*/ void mouseMoved(MouseMovedExampleEvent mme); } 在接口中只定义方法名,方法的参数和返回值类型。如上面接口中的m o u s e M o v e d方法的具 体实现是在下面的A r b i t r a r y O b j e c t类中定义的: class ArbitraryObject implements MouseMovedExampleListener { public void mouseMoved(MouseMovedExampleEvent mme) { ... } } A r b i t r a r y O b j e c t就是M o u s e M o v e d E x a m p l e E v e n t事件的监听者。 4. 事件监听者的注册与注销 为了让各种可能的事件监听者把自己注册入合适的事件源中,就建立源与事件监听者间的 事件流,事件源必须为事件监听者提供注册和注销的方法。在前面的b o u n d属性介绍中,已看到 了这种使用过程,在实际中,事件监听者的注册和注销要使用标准的设计格式: public void add< ListenerType>(< ListenerType> listener); public void remove< ListenerType>(< ListenerType> listener); 例如: 30第一部分JSP 入门
首先定义了一个事件监听者接口: public interface ModelChangedListener extends java.util.EventListener { void modelChanged(EventObject e); } 接着定义事件源类: public abstract class Model { private Vector listeners = new Vector(); // 定义了一个存储事件监听者的数组 /*上面设计格式中的< ListenerType>在此处即是下面的ModelChangedListener*/ public synchronized void addModelChangedListener(ModelChangedListener mcl){ listeners.addElement(mcl); } file://把监听者注册入listeners数组中 public synchronized void removeModelChangedListener(ModelChangedListener mcl){ listeners.removeElement(mcl); file://把监听者从listeners中注销 } /*以上两个方法的前面均冠以synchronized,是因为运行在多线程环境时,可能同时有几个对象同时要进行注册 和注销操作,使用synchronized来确保它们之间的同步。开发工具或程序员使用这两个方法建立源与监听者之间的事件 流*/ protected void notifyModelChanged() { /**事件源使用本方法通知监听者发生了modelChanged事件*/ Vector l; EventObject e = new EventObject(this); /* 首先要把监听者拷贝到l数组中,冻结EventListeners的状态以传递事件。这样来确保在事件传递到所有监 听者之前,已接收了事件的目标监听者的对应方法暂不生效。*/ synchronized(this) { l = (Vector)listeners.clone(); } for (int i = 0; i < l.size(); i++) { /* 依次通知注册在监听者队列中的每个监听者发生了modelChanged事件,并把事件状态对象e作为参数传递 给监听者队列中的每个监听者*/ ((ModelChangedListener)l.elementAt(i)).modelChanged(e); } } } 在程序中可见,事件源M o d e l类显式地调用了接口中的m o d e l C h a n g e d方法,实际是把事件状 态对象e作为参数,传递给了监听者类中的m o d e l C h a n g e d方法。 5. 适配类 适配类是J a v a事件模型中极其重要的一部分。在一些应用场合,事件从源到监听者之间的传 递要通过适配类来转发。例如:当事件源发出一个事件,而有几个事件监听者对象都可接收 该事件,但只有指定对象做出反应时,就要在事件源与事件监听者之间插入一个事件适配器类, 由适配器类来指定事件应该是由哪些监听者来响应。 适配类成为了事件监听者,事件源实际是把适配类作为监听者注册入监听者队列中,而真 第二章预备知识31
正的事件响应者并未在监听者队列中,事件响应者应做的动作由适配类决定。目前绝大多数的 开发工具在生成代码时,事件处理都是通过适配类来进行的。 2.2.3 持久化 当J a v a B e a n s在构造工具内被用户化,并与其他B e a n s建立连接之后,它的所有状态都应当可 被保存,下一次被装载进构造工具内或在运行时,就应当是上一次修改完的信息。为了能做到 这一点,要把B e a n s的某些字段的信息保存下来,在定义B e a n s时要使它实现j a v a . i o . S e r i a l i z a b l e 接口。例如: public class Button implements java.io.Serializable { } 实现了序列化接口的B e a n s中字段的信息将被自动保存。若不想保存某些字段的信息则可在 这些字段前冠以t r a n s i e n t或s t a t i c关键字,t r a n s i e n t和s t a t i c变量的信息是不可被保存的。通常,一 个B e a n s所有公开出来的属性都应当是被保存的,也可有选择地保存内部状态。B e a n s开发者在 修改软件时,可以添加字段,移走对其他类的引用,改变一个字段的p r i v a t e / p r o t e c t e d / p u b l i c状 态,这些都不影响类的存储结构关系。然而,当从类中删除一个字段,改变一个变量在类体系 中的位置,把某个字段改成t r a n s i e n t / s t a t i c,或原来是t r a n s i e n t / s t a t i c,现改为别的特性时,都将 引起存储关系的变化。 J a v a B e a n s的存储格式 J a v a B e a n s组件被设计出来后,一般是以扩展名为j a r的Z i p格式文件存储,在j a r中包含与 J a v a B e a n s有关的信息,并以M A N I F E S T文件指定其中的哪些类是J a v a B e a n s。以j a r文件存储的 J a v a B e a n s在网络中传送时极大地减少了数据的传输数量,并把J a v a B e a n s运行时所需要的一些资 源捆绑在一起。 这里主要论述了J a v a B e a n s s的一些内部特性及其常规设计方法,参考的是J a v a B e a n s s规范书。 随着世界各大I S V对J a v a B e a n s s越来越多的支持,规范在一些细节上还在不断演化,但基本框架 不会再有大的变动。 2.2.4 用户化 J a v a B e a n s开发者可以给一个B e a n s添加定制器(C u s t o m i z e r)、属性编辑器(P r o p e r t y E d i t o r) 和B e a n I n f o接口来描述一个B e a n s的内容, B e a n s的使用者可在构造环境中通过与B e a n s附带在一 起的这些信息来用户化B e a n s的外观和应做的动作。一个B e a n s不必都有B e a n C u s t o m i z e r、 P r p e r t y E d i t o r和B e a n I n f o,根据实际情况,这些是可选的,当有些B e a n s较复杂时,就要提供这 些信息,以Wi z a r d的方式使B e a n的使用者能够定制一个B e a n s。有些简单的B e a n s可能没有这些 信息,则构造工具可使用自带的透视装置,透视出B e a n s的内容,并把信息显示到标准的属性表 或事件表中供使用者定制B e a n s,前几节提到的B e a n s的属性、方法和事件名要以一定的格式命 名,主要的作用就是供开发工具对B e a n s进行透视。当然也是给程序员在手写程序中使用B e a n s 提供方便,使其能观其名,知其意。 1. 定制器接口 32第一部分JSP 入门
当一个B e a n有了自己的定制器时,在构造工具内就可展现出自己的属性表。在定义定制器 时必须要实现j a v a . b e a n s . C u s t o m i z e r接口。例如,下面是一个按钮 B e a n s的定制器: public class OurButtonCustomizer extends Panel implements Customizer { ... ... /*当实现像OurButtonCustomizer这样的常规属性表时,一定要在其中实现addProperChangeListener和 removePropertyChangeListener,这样,构造工具可用这些功能代码为属性事件添加监听者。*/ ... ... private PropertyChangeSupport changes=new PropertyChangeSupport(this); public void addPropertyChangeListener(PropertyChangeListener l) { changes.addPropertyChangeListener(l); } public void removePropertyChangeListener(PropertyChangeListener l) { changes.removePropertyChangeListener(l); } ... ... } 2. 属性编辑器接口 一个J a v a B e a n s可提供P r o p e r t y E d i t o r类,为指定的属性创建一个编辑器。这个类必须继承自 j a v a . b e a n s . P r o p e r t y E d i t o r S u p p o r t类。构造工具与手写代码的程序员不直接使用这个类,而是在 下一小节的B e a n I n f o中实例化并调用这个类。例如: public class MoleculeNameEditor extends java.beans.PropertyEditorSupport { public String[] getTags() { String resule[]={ "HyaluronicAcid","Benzene","buckmisterfullerine", "cyclohexane","ethane","water"}; return resule; } } 上例中是为Ta g s属性创建了属性编辑器,在构造工具内,从下拉表格中选择M o l e c u l e N a m e 的属性应是H y a l u r o n i c A i d或w a t e r。 3. BeanInfo接口 每个B e a n类也可能有与之相关的B e a n I n f o类,在其中描述了这个B e a n在构造工具内出现时 的外观。B e a n I n f o中可定义属性、方法、事件,显示它们的名称,提供简单的帮助说明。 例如: public class MoleculeBeanInfo extends SimpleBeanInfo { public PropertyDescriptor[] getPropertyDescriptors() { try { PropertyDescriptor pd=new PropertyDescriptor("moleculeName",Molecule.class); /*通过pd引用了上一节的MoleculeNameEditor类,取得并返回 moleculeName属性*/ pd.setPropertyEditorClass(MoleculeNameEditor.class); PropertyDescriptor result[]={pd}; return result; 第二章预备知识33
} catch(Exception ex) { System.err.println("MoleculeBeanInfo: unexpected exeption: "+ex); return null; } } } 2.3 Java Servlet 鉴于J S P和Java Servlet的紧密联系,笔者认为学习J S P一定要有Java Servlet的基础,当然, 不需要成为Java Servlet的专家,但一定要有概念上的认识。 下面将介绍Java Servlet的基础知识,对于具体的程序开发,现在不需要读者立即掌握,本 节的目的在于让读者能够对S e r v l e t有所了解。 2.3.1 HTTP Servlet API Java Servlet 开发工具( J S D K)提供了多个软件包,在编写Servlet 时需要用到这些软件包。 其中包括两个用于所有Servlet 的基本软件包:javax.servlet 和j a v a x . s e r v l e t . h t t p。可从s u n公司的 We b站点Java Servlet 开发工具,现在一般使用J S D K 2 . 0。这里主要介绍j a v a x . s e r v l e t . h t t p提 供的HTTP Servlet应用编程接口。 1. 简述 创建一个HTTP Servlet,需要扩展HttpServlet 类,该类是用专门的方法来处理H T M L表格 数据的GenericServlet 的一个子类。HttpServlet 类包含i n i t ( )、d e s t r o y ( )、service() 等方法。其中 init() 和destroy() 方法是继承的。 (1) init() 方法 在Servlet 的生命期中,仅执行一次init() 方法。它是在服务器装入Servlet 时执行的。可以 配置服务器,以在启动服务器或客户机首次访问Servlet 时装入S e r v l e t。无论有多少客户机访问 S e r v l e t,都不会重复执行init() 。 (2)service() 方法 service() 方法是Servlet 的核心。每当一个客户请求一个HttpServlet 对象时,该对象的 service() 方法就要被调用,而且传递给这个方法一个请求(S e r v l e t R e q u e s t)对象和一个响 应(S e r v l e t R e s p o n s e)对象作为参数。在HttpServlet 中已存在service() 方法。缺省的服务功 能是调用与HTTP 请求的方法相应的do 功能。例如, 如果HTTP 请求方法为G E T,则缺省情 况下就调用doGet() 。Servlet 应该为Servlet 支持的HTTP 方法覆盖do 功能。因为 HttpServlet.service() 方法会检查请求方法是否调用了适当的处理方法,不必覆盖service() 方法。 只需覆盖相应的do 方法就可以了。 (3)destroy() 方法 destroy() 方法仅执行一次,即在服务器停止且卸装Servlet 时执行该方法。典型的,将 Servlet 作为服务器进程的一部分来关闭。缺省的destroy() 方法通常是符合要求的,但也可以覆 盖它,典型的是管理服务器端资源。例如,如果Servlet 在运行时会累计统计数据,则可以编写 34第一部分JSP 入门
一个destroy() 方法,该方法用于在未装入Servlet 时将统计数字保存在文件中。另一个示例是关 闭数据库连接。 (4)G e t S e r v l e t C o n f i g()方法 G e t S e r v l e t C o n f i g()方法返回一个ServletConfig 对象,该对象用来返回初始化参数和 S e r v l e t C o n t e x t。ServletContext 接口提供有关servlet 的环境信息。 (5)G e t S e r v l e t I n f o()方法 G e t S e r v l e t I n f o()方法是一个可选的方法,它提供有关servlet 的信息,如作者、版本、版 权。 当服务器调用sevlet 的S e r v i c e()、d o G e t()和d o P o s t()这三个方法时,均需要请求和 响应对象作为参数。请求对象提供有关请求的信息,而响应对象提供了一个将响应信 息返回给浏览器的通信途径。javax.servlet 软件包中的相关类为S e r v l e t R e s p o n s e和S e r v l e t R e q u e s t, 而javax.servlet.http 软件包中的相关类为HttpServletRequest 和H t t p S e r v l e t R e s p o n s e。 Servlet 通过这些对象与服务器通信并最终与客户机通信。Servlet 能通过调用请求 (R e q u e s t)对象的方法获知客户机环境、服务器环境的信息和所有由客户机提供的信息。S e r v l e t 可以调用响应对象的方法发送响应,该响应是准备发回客户机的。 2. 常用HTTP Servlet API概览 支持H T T P协议的s e r v l e t可以使用j a v a x . s e r v l e t . h t t p包进行开发,而j a v a x . s e r v l e t包中的核心功 能提供了We b开发的许多类和函数,为进行J S P 的开发带来了极大的方便。比如,抽象 H t t p S e r v l e t 类包含对不同H T T P 请求方法和头信息的支持, H t t p S e r v l e t R e q u e s t 和 H t t p S e r v l e t R e s p o n s e接口允许直接与We b服务器通信,而H t t p S e s s i o n提供内置会话跟踪功能; C o o k i e类可以很快地设置和处理HTTP Cookie,H t t p U t i l s类用于处理请求字串。 (1) Cookie C o o k i e类提供了读取、创建和操纵HTTP Cookie的便捷途径,允许s e r v l e t在客户端存储少量 的数据。C o o k i e主要用于会话跟踪和存储少量用户配置信息数据。 S e r v l e t用H t t p S e r v l e t R e q u e s t的g e t C o o k i e()方法获取C o o k i e信息; H t t p S e r v l e t R e s p o n s e的 a d d C o o k i e()方法向客户端发送新的C o o k i e,因为是使用HTTP 头设置的,所以a d d C o o k i e() 必须在任何输出发送到客户端之前调用。 虽然Java Web Server有一个s u n . s e r v l e t . u t i l . C o o k i e类能完成大致相同的工作,但最初的 Servlet API 1.0 却没有C o o k i e类。s u n . s e r v l e t . u t i l . C o o k i e类和当前C o o k i e类唯一不同的是获取和 创建方法是C o o k i e类的静态组成部分,而不是H t t p S e r v l e t R e q u e s t和H t t p S e r v l e t R e s p o n s e的接口。 (2) HttpServlet H t t p S e r v l e t是开发HTTP servlet框架的抽象类,,其中的s e r v i c e ( )方法将请求分配给H T T P的 protected service()方法。 (3) HttpServletRequest H t t p S e r v l e t R e q u e s t通过扩展S e r v l e t R e q u e s t基类,为HTTP servlets提供附加的功能。它支持 C o o k i e s和s e s s i o n跟踪及获取H T T P头信息的功能; H t t p S e r v l e t R e q u e s t还能解析H T T P的表单数 据,并将其存为s e r v l e t参数。 第二章预备知识35
服务器将H t t p S e r v l e t R e q u e s t对象传给H t t p S e r v l e t的s e r v i c e ( )方法。 (4) HttpServletResponse H t t p S e r v l e t R e s p o n s e扩展S e r v l e t R e s p o n s e类,允许操纵H T T P协议相关数据,包括响应头和 状态码。它定义了一系列常量,用于描述各种H T T P状态码,还包含用于s e s s i o n跟踪操作的帮助 函数。 (5) HttpSession H t t p S e s s i o n接口提供了对We b访问者的认证机制。H t t p S e s s i o n接口允许s e r v l e t查看和操纵会 话相关信息,比如创建访问时间和会话身份识别。它还包含一些方法,用于绑定会话到特定的 对象,允许购物车和其他的应用程序保存数据用于各连接间共享,而不必存到数据库或其 他的e x t r a - s e r v l e t资源中。 S e r v l e t调用H t t p S e r v l e t R e q u e s t的g e t S e s s i o n ( )方法来获得H t t p S e s s i o n对象,定制S E S S I O N的 行为,比如在销毁S E S S I O N之前等待的时间,依服务器而定。 虽然任何对象都可以绑定到S E S S I O N,然而对一些事务繁忙的Se r v l e t,绑定大的对象到 S E S S I O N中将会加重服务器的负担。减轻服务器负担最常用的解决办法是,仅仅绑定用于实现 j a v a . i o . S e r i a l i z a b l e接口的对象(它包含Java API核心中的所有数据类型对象)。有些服务器能将 S e r i a l i z a b l e对象写入磁盘中,U n s e r i a l i z a b l e对象比如j a v a . s q l . C o n n e c t i o n ,必须保留在内存中。 (6) HttpSessionBindingEvent H t t p S e s s i o n B i n d i n g L i s t e n e r监听对象绑定或断开绑定于会话时, H t t p S e s s i o n B i n d i n g E v e n t被 传递到H t t p S e s s i o n B i n d i n g L i s t e n e r。 (7) HttpSessionBindingListener 当对象绑定于H t t p S e s s i o n或从H t t p S e s s i o n松开绑定时,通过调用valueBound () 和 valueUnbound ()来通知用于实现H t t p S e s s i o n B i n d i n g L i s t e n e r的接口。其他情况下,这个接口可以 顺序清除与s e s s i o n相关的资源,例如数据库连接等。 (8) HttpSessionContext H t t p S e s s i o n C o n t e x t提供了访问服务器上所有活动s e s s i o n的方法,这对s e r v l e t清除不活动的 s e s s i o n,显示统计信息和其他共享信息是很有用的。S e r v l e t 通过调用H t t p S e s s i o n 的 getSessionContext () 方法获得H t t p S e s s i o n C o n t e x t对象。 (9) HttpUtils 这是一个容纳许多有用的基于H T T P方法的容器对象,使用这些方法,可以使S e r v l e t开发更 方便。 2.3.2 系统信息 要成功建立We b应用,必须了解它所需的运行环境,还要了解执行s e r v l e t的服务器和发送请 求的客户端的具体情况。不管应用程序运行于哪种环境,都应该确切知道应用程序所应处理的 请求信息。 S e r v l e t有许多方法可以获取这些信息。大多数情况下,每个方法都会返回不同的结果。比 较C G I用于传递信息的环境变量,读者会发现s e r v l e t方法具有以下几个优点: 36第一部分JSP 入门
• 强有力的类型检查。 • 延迟计算。 • 与服务器的更多交互。 1. 初始化参数 每个注册的s e r v l e t名称都有与之相关的特定参数, s e r v l e t程序在任何时候都可获得这些参 数;它们经常使用i n i t()方法来为s e r v l e t设定初始或缺省值以在一定程度上定制s e r v l e t的行为。 (1) 获得初始参数 s e r v l e t用g e t I n i t P a r a m e t e r()方法来获取初始参数: public String ServletConfig.getInitParameter(String name) 这个方法返回初始参数的名称或空值(如果不存在)。返回值总是S t r i n g类型,由s e r v l e t对它 进行解释。 G e n e r i c S e v l e t类实现S e r v l e t C o n f i g接口,直接访问g e t I n i t P a r a m e t e r()方法。 (2) 获取初始参数名 s e r v l e t调用g e t I n i t P a r a m e t e r N a m e s()方法可检验它的所有参数: public Enumeration ServletConfig.getInitParameterNames() 这个方法返回值是字符串对象的枚举类型或空值(如果没参数),经常用于程序的调试。 G e n e r i c S e r v l e t类也可以使得s e r v l e t s直接访问这个方法。 2. 服务器 s e r v l e t能了解服务器的大多数信息。它能知道主机名称,端口号,服务器软件及其他信息。 S e r v l e t还能将这些信息显示给客户端,以定制客户机基于特定服务器数据包的行为,甚至可以 明确要运行s e r v l e t的机器的行为。 ( 1 ) 服务器相关信息 s e r v l e t通过四个方法得到服务器的信息:两个由传递给s e r v l e t的S e r v l e t R e q u e s t对象调用,两 个从S e r v l e t C o n t e x t对象调用,S e r v l e t C o n t e x t包含s e r v l e t的运行环境。通过使用方法g e t S e r v e r N a m e() 和g e t S e r v e r P o r t(),S e r v l e t能为具体请求分别获取服务器的名称和端口号: public String ServletRequest.getServerName() public int ServletRequest.getServerPort() S e r v l e t C o n t e x t的G e t S e r v e r I n f o()和g e t A t t r i b u t e()方法提供服务器软件和属性的信息: public String ServletContext.getServerInfo() public Object ServletContext.getAttribute(String name) (2) 锁定s e r v l e t到服务器 利用服务器信息可以完成许多特殊的功能,例如:写了一个s e r v l e t,而且不想让它随处运行, 或许你想出售,限制那些未授权的拷贝,或者想通过一个软件证书锁定s e r v l e t到客户机上。另一 种情况可能是,开发人员为s e r v l e t编写了一个证书并且想确保它运行于防火墙后。这都不难做到, 因为s e r v l e t能及时地访问到服务器的相关信息。 3 客户端 对每个请求,s e r v l e t能获取客户机、要求认证的页面及实际用户的有关信息。这些信息可用 第二章预备知识37
于登录访问,收集用户个人资料或限制某些客户端的访问。 (1) 获取客户机信息 s e r v l e t可用g e t R e m o t e A d d r ( )和g e t R e m o t e H o s t ( )分别获取客户机的I P地址和主机名称: public String ServletRequest.getRemoteAddr() public String ServletRequest.getRemoteHost() 以上两种方法都返回字符串对象类型。这些信息来自于连接服务器和客户端的s o c k e t端口,所以 远程地址和远程主机名有可能是代理服务器的地址和主机名称。远程地址可能是1 9 2 . 2 6 . 8 0 . 11 8, 而远程主机名可能是d i s t . e n g r. s g i . c o m。 I n e t A d d r e s s . g e t B y N a m e ( )方法可以将I P地址和远程主机名称转化为j a v a . n e t . I n e t A d d r e s s对象: InetAddress remoteInetAddress = InetAddress.getByName(req.getRemoteAddr()); (2) 限制为只允许某些地区的机器访问 由于美国政府对好的加密技术出口的限制政策,在某些We b站点上,允许的软件就得特 别谨慎。利用s e r v l e t的获取客户机信息的功能,可以很好地加强这种限制。这些s e r v l e t能检查客 户机,并对那些来自美国和加拿大的机器提供链接。 对于商业应用,可以确定让一个S e r v l e t只对来自企业内部网的客户机服务,从而保证安全。 (3) 获取用户相关信息 如果要对Web pages作更进一步的限制,而不是根据地域限制访问,该怎么做呢?比如,发 布的在线杂志,只想让已定购的用户可以访问。读者也许会说,这好办,我早都能做,不一定 非得用s e r v l e t。 是的,几乎每种H T T P服务器都内嵌了这种功能,可以限制特定用户访问所有或部分网页。 怎样设定限制依你所使用的服务器不同而有差异,这里我们给出它们的共同工作机理。浏览器 第一次试图访问某个页面时,服务器会给浏览器一个要求身份验证的响应,浏览器收到这个响 应后,会弹出一个要求输入用户名和密码的对话框。 用户输入信息后,浏览器会试图再一次访问该页,但这次请求信息中附加了用户名称和密 码信息。服务器接受用户名/密码对后,就会处理此请求;如果相反,服务器不接受此用户名/密 码对,浏览器的访问再一次被拒绝,用户必须重新输入。 那么,s e r v l e t是怎样处理的呢?当访问受限制的s e r v l e t时,s e r v l e t可以调用g e t R e m o t e U s e r ( ) 方法,获取服务器认可的用户名称: public String HttpServletRequest.getRemoteUser() S e r v l e t也可以用g e t A u t h Ty p e()方法获取认证类型: public String HttpServletRequest.getAuthType() 这个方法返回所用的认证类型或可空值(如果未加限制),最常使用的认证类型是 B A S I C 和D I G E S T。 (4) 个性化的欢迎信息 一个简单的调用g e t R e m o t e U s e r ( )方法的s e r v l e t,可以通过称呼用户名称向他问好,并记住他 上次登录的时间。但是需要注意的是,这种方法仅仅适用于授权用户。对于未授权用户,可以 使用S e s s i o n。 38第一部分JSP 入门
4. 请求 前面讲述了s e r v l e t如何获取服务器和客户机的相关信息,现在要学习真正重要的内容: s e r v l e t如何知道客户请求什么。 (1) 请求参数 每个对s e r v l e t的访问都可以有许多与之相关的参数,这些参数都是典型的名称/值对,用于 告诉s e r v l e t在处理请求时所需的额外信息。千万注意别把这里的参数与前面提到的与s e r v l e t自身 相关的参数搞混。 幸运的是,即使s e r v l e t要获取许多参数,获取每个参数的方法也都一样,即g e t P a r a m e t e r ( )和 g e t P a r a m e t e r Va l u e s ( )方法: public String ServletRequest.getParameter(String name) public String[] ServletRequest.getParameterValues(String name) g e t P a r a m e t e r ( )以字符串形式返回命名的参数,如果没指定参数则返回空值,返回值必须保 证是正常的编码形式。如果此参数有多个值,那么这个值是与服务器相关的,这种情况下应该 用g e t P a r a m e t e r Va l u e s ( )方法,这个方法以字符对象数组的形式返回相应参数的所有值,如果没 指定,当然为空值。返回的每个值在数组中占一个单位长度。 除了能获取参数值之外, s e r v l e t还能用g e t P a r a m e t e r N a m e s ( )获取参数名称: public Enumeration ServletRequest.getParameterNames() 这个方法以字符串枚举类型返回参数名称,或者在没有参数时返回空值。这个方法经常用 于程序的调试。 最后,s e r v l e t还能用g e t Q u e r y S t r i n g ( )方法获取请求的二进制字串: public String ServletRequest.getQueryString() 这个方法返回请求的二进制字串(已编码的G E T参数信息),如果没有请求字串,则返回空值。 这些底层数据很少用来处理表单数据。 (2) 发布许可证密钥 如果现在准备编写一个给特定主机和端口号发布K e y e d S e r v e r L o c k许可证密钥的s e r v l e t,从 s e r v l e t获取的密钥可用于解锁K e y e d S e r v e r L o c k的s e r v l e t。那么,怎么知道s e r v l e t所要解锁的主机 名和端口号呢?当然是请求参数。 (3) 路径信息 除参数信息外, H T T P请求还能包括附加路径信息或虚拟路径等。通常,附加路径 信息是用于指明s e r v l e t要用到的文件在服务器上的路径,一般用H T T P请求的U R L形式表示,可 能像这样: h t t p:/ / s e r v e r:p o r t / s e r v l e t / Vi e w F i l e / i n d e x . h t m l 这将激活ViewFile servlet,同时传递 i n d e x . h t m l作为附加路径信息。S e r v l e t可以访问这 个路径信息,还能将字串i n d e x . h t m l转化为文件i n d e x . h t m l的真实路径。什么是/ i n d e x . h t m l 的真实路径呢?它是指当客户直接请求 / i n d e x . h t m l文件时,服务器返回的完整文件系统路径。 可能是d o c u m e n t _ r o o t / i n d e x . h t m l,当然服务器也可能用别名将它改变了。 除了用明确的U R L形式指定外,附加信息也可写成H T M L表单中A C T I O N参数的形式: 第二章预备知识39
<FORM METHOD=GET ACTION="/servlet/Dictionary/dict/definitions.txt"> word to look up: <INPUT TYPE=TEXT NAME="word"><P> <INPUT TYPE=SUBMIT><P> </FORM> 表单激活Dictionary servlet处理请求任务,同时传递附加路径信息 d i c t / d e f i n i t i o n s . t x t。 s e r v l e t会用单词d e f i n i t i o n s查找d e f i n i t i o n s . t x t文件,如果客户请求 / d i c t / d e f i n i t i o n s . t x t文件, 同时在s e r v e r _ r o o t / p u b l l i c _ h t m l / d i c t / d e f i n i t i o n s . t x t也存在,客户将会看到相同的文件。 1 ) 获取路径信息。 s e r v l e t可用g e t P a t h I n f o ( )方法获取附加路径信息: public String HttpServletRequest.getPathInfo() 这个方法返回与请求相关的附加路径信息,或者在没给定时,返回空值。S e r v l e t通常需要 知道给定文件的真实文件系统路径,这样就有了g e t P a t h Tr a n s l a t e d ( )方法: public String HttpServletRequest.getPathTranslated() 这个方法返回已经转化为真实文件路径的附加路径信息,或者在没有附加路径信息时返回空 值。返回的路径未必要指向已存在的文件和目录,已转化的路径可能是:C:\ J a v a We b S e r v e r 1 . 1 . 1 \public_html\dict\definitions.txt。 2 ) 特别的路径转换。 有时,s e r v l e t需要在附加路径信息中没有的路径,就得用g e t R e a l P a t h ( )方法来完成此项任务: public String ServletRequest.getRealPath(String path) 这个方法返回任何给定虚拟路径的真实路径,或者返回空值。如果给定路径是/,这个 方法返回服务器文档的根目录;如果给定路径是g e t P a t h I n f o ( ),返回的路径与g e t P a t h Tr a n s l a t e d ( )方 法的返回值相同。Generic servlets和HTTP servlets都可使用这个方法,CGI中没有与之相关的函数。 3 ) 获取M I M E类型。 s e r v l e t知道文件路径后,还往往需要知道文件类型,可使用g e t M i m e Ty p e ( )方法来做这项工作: public String ServletContext.getMimeType(String file) 这个方法返回指定文件的M I M E类型,或者在不知道的情况下返回空值。有时,在文件不存 在时也返回 t e x t / p l a i n。常见的文件类型有:t e x t / h t m l,t e x t / p l a i n,i m a g e / g i f和 i m a g e / j p e g。 下面这个语句代码可获取附加路径信息的M I M E类型: String type = getServletContext().getMimeType(req.getPathTranslated()) (4) 服务文件 许多应用服务器,例如We b L o g i,利用s e r v l e t来处理每个请求,这不仅是s e r v l e t处理能力上 的优势,而且,这为服务器的模块化设计带来了极大的便利。比如,所有的文件都由c o m . s u n . s e r v e r.http.FileServlet servlet提供服务,此s e r v l e t在f i l e下注册,并负责处理 /别名(是请求的 缺省文件)。 (5) 决定被请求的内容 40第一部分JSP 入门
s e r v l e t能用几种方法获取客户请求的确切文件。毕竟,最根部的s e r v l e t总假定为直接请求的 目标,在一个很长的s e r v l e t链中,每个s e r v l e t只能有一个连接。 没有方法用于直接返回客户用于请求的原始U R L, j a v a x . s e r v l e t . h t t p . H t t p U t i l s类的 g e t R e q u e s t U R L ( )方法能完成类似的工作: public static StringBuffer HttpUtils.getRequestURL(HttpServletRequest req) 这个方法基于H t t p S e r v l e t R e q u e s t对象的变量信息,重构请求的U R L,它返回S t r i n g B u ff e r类 型,包含构架(像H T T P)、服务器名、端口号和额外路径信息。重构后的URL 应该与客户请求 的U R L非常相像,它们之间的差别非常细微(比如空格形式在客户端用% 2 0表示,而在服务器 端是"+")。由于这个方法返回S t r i n g B u ff e r类型,所以U R L能被有效地修改(比如,附加些查 询参数)。这个方法经常用于创建重定向消息和报告错误。 大多数情况下,s e r v l e t并不真正需要请求的U R L,而需要的是U R I,它是由方法g e t R e q u e s t U R I ( ) 返回的: public String HttpServletRequest.getRequestURI() 这个方法返回统一资源标示符( U R I),对正常的HTTP servlet来说,一个U R I可以看成是 U R L减去构架、主机名、端口号和请求字串,但包含一些额外路径信息。 (6) 请求机理 除了知道请求的内容外, s e r v l e t还有一种方法用于获取如何请求的信息。G e t S c h e m e ( )方法 用于返回请求的构架: public String ServletRequest.getScheme() 例如h t t p, h t t p s,和f t p,还有J a v a特有的 j d b c和r m i。虽然C G I也有包含构 架的变量S E RV E R _ U R L,但没有与g e t S c h e m e ( )相应的函数。对HTTP servlet来说,这个方法表 明请求是通过使用S S L安全连接(用h t t t p s表示),还是非安全连接(用 h t t p表示)。 g e t P r o t o c o l ( )方法返回用于请求的协议和版本号: public String ServletRequest.getProtocol() 协议和版本号用斜线隔开。如果不能决定协议类型,则返回空值。对HTTP servlet来说,协 议通常是v H T T P / 1 . 0 v或v H T T P / 1 . 1, HTTP servlet能用协议版本决定客户端是否可使用 HTTP 1.1的新特性。 要获取请求所用的方法, s e r v l e t调用g e t M e t h o d ( ): public String HttpServletRequest.getMethod() 这个函数返回用于请求的H T T P方法,包括 G E T,P O S T和 H E A D,H t t p S e r v l e t的 实现函数s e r v i c e ( )用这个方法分派请求任务。 (7) 请求头 H T T P请求和响应有许多与HTTP 头相关的内容。这些头提供了一些与请求(或响应)有关 的额外信息。HTTP 1.0协议提供了十几种头, HTTP 1.1则更多。对头的完整讨论超出了本书的 范围,这里描述s e r v l e t最常访问的头。 s e r v l e t在执行请求时很少需要读HTTP 头,许多与请求相关的头信息都由服务器处理了。这 第二章预备知识41
里举一个服务器如何限制对某些文档的访问的例子,服务器使用H T T P头,而s e r v l e t并不了解其 中细节。当服务器接到访问受限页面的请求时,它会检查带有适当认证信息头的请求,这个头 应该包含合法的用户名和密码,如果没有,服务器发出包含W W W- A u t h e n t i c a t e的头,告诉浏览 器对资源的访问被拒绝,如果客户发送的请求包含正确的认证头,服务器会授权访问并允许激 活的s e r v l e t通过g e t R e m o t e U s e r ( )方法获取用户名称。 1) 访问头值。 H T T P头值可以通过H t t p S e r v l e t R e q u e s t对象访问。使用g e t H e a d e r ( )、g e t D a t a H e a d e r ( )或 g e t I n t H e a d e r ( )方法, 它们分别返回S t r i n g类型、long 类型和i n t类型数据: public String HttpServletRequest.getHeader(String name) public long HttpServletRequest.getDateHeader(String name) public int HttpServletRequest.getIntHeader(String name) g e t H e a d e r ( )方法以S t r i n g类型返回指定头的值,如果头未作为请求的组成部分,则返回空值, 所有类型的头都可以用这种方法获取。 g e t D a t e H e a d e r ( )返回l o n g类型的值,表示日期,如果头未作为请求的组成部分,则返回- 1。 当被调用的头值不能转化为D a t e时,此方法会抛出一个I l l e g a l A rg u m e n t E x c e p t i o n。这个方法在 处理l a s t - M o d i f e d和I f - M o d i f i e d - S i n c e的头时非常有用。 G e t I n t H e a d e r ( )返回头的整型值或- 1(如果未作为请求的组成部分被发送),当被调用的头不 能转化为整型时,这个方法抛出N u m b e r F o r m a t E x c e p t i o n异常。 如果能使用g e t H e a d e r N a m e s ( )访问,s e r v l e t还能获得所有的头名称: public Enumeration HttpServletRequest.getHeaderNames() 这个方法以S t r i n g对象的枚举类型返回所有头的名称。如果没有头,则返回空的枚举类型。 2) servlet链中的头信息。 s e r v l e t链加了一个有趣的处理s e r v l e t头信息的环,不像其他的s e r v l e t s,处于链中或链末的 s e r v l e t不是从客户请求中读取头信息值,而是从前一个s e r v l e t的响应信息中读取头信息值。 这种处理方法的强有力和灵活性来自于这样一个事实: s e r v l e t能智能地处理前一个s e r v l e t的 输出,不仅在内容方面,而且在头信息值方面。比如,它能向响应信息中加些额外的头信息, 或改变已有的头信息。它甚至还能禁止头信息。 但是这种强大的功能是要负责任的:除非s e r v l e t明确地读取前一个s e r v l e t的响应头信息,并 作为它自己的响应信息的一部分加以发送,否则此头信息将不被发送,客户端就看不到此信息。 正常的链中s e r v l e t总是会传递前一个s e r v l e t的头,除非有特殊原因而需要处理别的什么。 (8) 输入流 每个由s e r v l e t处理的请求都有一个与之相关的输入流,就像s e r v l e t将相关的r e s p o n s e对象写 到P r i n t Wr i t e r或O u t p u t S t r e a m中一样, s e r v l e t也能从R e a d e r或I n p u t S t r e a m中读取与请求相关的信 息。从输入流中读取的数据可以为任何数据类型和任意长度,输入流有三个作用: 1) 在s e r v l e t链中,把前一个s e r v l e t的响应信息向下传递; 2 ) 将与P O S T请求相关的内容传递给HTTP servlet; 3) 把客户端发来的二进制信息传递给non-HTTP servlet。 42第一部分JSP 入门
要读取输入流中的字符数据,应该使用g e t R e a d e r ( )方法,并返回B u ff e r d R e a d e r的类对象: public BufferdReader ServletRequest.getReader() throws IOException 使用B u ff e r e d R e a d e r作为返回的数据类型的优点是,它能在各种字符集间正确地转换。如果 g e t I n p u t S t r e a m ( )在同样的请求之前被调用,这个方法会抛出I l l e g a l S t a t e E x c e p t i o n异常,如果输 入字符不被支持或是未知字符,则抛出U n s u p p o r t e d E n c o d i n g E x c e p t i o n异常。 要从输入流中读取二进制数据,就得使用g e t I n p u t S t r e a m ( )方法,并返回S e r v l e t I n p u t S t r e a m 类型: public ServletInputStream ServletRequest.getInputStream() throws IOException S e r v l e t I n p u t S t r e a m是I n p u t S t r e a m的直接子类,可以当作正常的I n p u t S t r e a m来处理,具有有 效的一次一行地读取数据的能力。如果在同一个请求之前调用g e t R e a d e r ( ),这个方法会抛出 I l l e g a l S t a t e E x c e p t i o n异常。一旦有了S e r v l e t I n p u t S t r e a m,就可以用r e a d L i n e ( )进行行读取: public int ServletInputStream.readLine(byte b[],int off,int len) throws IOException 这个方法从输入流中将b y t e s读入字节数组b中,开始处由o ff给出,遇到‘ \ n’时或读够l e n 字节时结束读取。结束符 \ n也读入缓存。这个方法返回已读取的字节数,或到达输入流的末 尾时返回- 1。 S e r v l e t使用g e t C o n t e n t Ty p e ( )和g e t C o n t e n t L e n g t h ( )分别获取经过输入流发送的内容类型和数 据长度: public String ServletRequest.getContentType() public int ServletRequest.getContentLength() g e t C o n t e n t Ty p e ( )方法返回经过输入流的发送内容类型,或返回空值(如果类型不明,比如 没有数据);g e t C o n t e n t L e n g t h ( )返回按字节计算的长度,如果类型不明则返回- 1。 1 ) 用输入流构建s e r v l e t链。 链中的s e r v l e t从前一个s e r v l e t中通过输入流获得响应信息。 2 ) 输入流处理P O S T请求。 当s e r v l e t处理P O S T请求时,使用输入流来访问P O S T数据的情况是极少见的。典型地, P O S T数据只不过是参数信息, s e r v l e t可以很方便地用g e t P a r a m e t e r ( )方法获取它。S e r v l e t可以检 查输入流的类型来识别P O S T请求的类型,如果是a p p l i c a t i o n / x - w w w - f o r m - u r l e n c o d e d类型,数据 可由g e t P a r a m e t e r ( )方法或相似的方法获取。 S e r v l e t应在调用g e t P a r a m e t e r ( )方法之前调用g e t C o n t e n t L e n g t h ( ),以免被拒绝访问。恶意客 户可能在发送P O S T请求时,发送不合理的巨大的数据,企图在s e r v l e t调用g e t P a r a m e t e r ( )方法时, 把服务器的速度降到最慢。S e r v l e t可以用g e t C o n t e n t L e n g t h ( )来验证数据长度的合理性,或限定 小于4 k作为预防措施。 3 ) 用输入流接收文件。 s e r v l e t可以使用输入流来接收上传的文件,在讲解之前,需提醒很重要的一点是:上传文件 是试验性的,并且不是所有的浏览器都支持。Netscape Navigator 从3 . 0,微软从Internet Exploer 4 . 0才开始支持文件上传。 第二章预备知识43
简单地说,任何数量的文件和参数都可以在单个P O S T请求中作为表单数据被传送, P O S T 请求格式与标准a p p l i c a t i o n / x - w w w - f o r m - u r l e n c o d e d的表单格式不同,所以有必要将类型设为 m u l t i p a r t / f o r m - d a t a。 文件上载的客户端程序的编写相当简单,下面这个H T M L将发送一个表单,询问用户名称和 要上载的文件。注意E N C T Y P E属性和F I L E输入类型的使用。 <FORM ACTION = "/servlet/UploadTest" ENCTYPE="multipart/form-data" METHOD=POST> What is your name? <INPUT TYPE=TEXT NAME=submitter> <BR> Which file do you want to upload? <INPUT TYPE=FILE NAME=file> <BR> <INPUT TYPE=SUBMIT> </FORM> 具体如何实现文件上传,请见本书第11章。 (9) 额外属性 有时s e r v l e t需要了解请求的其他信息,并且这些信息用前面提到的方法无法获取,这时就得 使出最后一招,即g e t A t t r i b u t e()方法。还记得S e r v l e t C o n t e n t有个g e t A t t r i b u t e()方法是如何 返回特定服务器的属性的吗? S e r v l e t R e q u e s t也有一个g e t A t t r i b u t e()方法: public object ServletRequest.getAttribute(String name) 这个方法返回指定请求的服务器属性,如果服务器不支持指定请求的属性则返回空值。这 个方法允许服务器为s e r v l e t提供有关请求的定制信息。例如, Java Web Server有三个可获得的属 性:j a v a x . n e t . s s l . c i p h e r s u i t e、j a v a . n e t . s s l . p e e r _ c e r t i f i c a t e s和j a v a x . n e t . s s l . s e s s i o n。运行在J a v a Web Server上的s e r v l e t可以窥探客户S S L连接的这些属性。 2.3.3 传送H T M L信息 本节首先讲解从s e r v l e t如何返回一个标准的HTML response,然后讲解如何建立客户端的持 久连接以降低返回r e s p o n s e的开销。最后还要探讨一下在处理H T M L和H T T P时的一些额外的东 西,包括用支持类来对象化H T M L输出,返回错误和其他状态码,发送定制的头信息,重定向请 求,使用客户牵引,客户掉线检测和向服务器日志中写数据等。 1. response的结构 HTTP servlet能为客户返回三种信息:一个状态码、任意数量的H T T P头和应答信息。状态 码是个整数,就像你想像的那样,它描述了应答状态。状态码能表明成功和失败,或告诉客户 机采取下一步动作完成请求过程。数字状态码通常伴有原因词,以人们容易看懂的方式来描 述状态。通常状态码在后台工作并由浏览器软件解释。有时,尤其是当出错时,浏览器向用户 显示状态码。最常见的状态码可能要数 404 Not Found,当服务器不能定位U R L时,它就会发 出这个状态码。 响应体是响应信息的主要内容,对H T M L页来说,响应体就是H T M L本身。对图片来说,响 应体是构成图片的字节。响应体可以是任何类型和任意长度;客户端通过解释响应信息中的 H T T P头知道该接收什么。 普通的s e r v l e t比HTTP servlet简单--它只向用户返回应响体。然而,对G e n e r i c S e r v l e t子类 44第一部分JSP 入门
来说,用A P I将一个响应体分成许多精细的部分是可能的,好像返回多条目的感觉。事实上,这 就是HTTP servlet要做的工作。在底层,服务器将响应以字节流的形式发送给客户端,任何设置 状态码或头的方法都是在这个基础上的抽象。 明白这点很重要,因为即使s e r v l e t程序员不必了解H T T P协议的细节,协议确实会影像 s e r v l e t调用方法的顺序。特别是, H T T P协议规定状态码和头必须在响应体之前发送。因此, s e r v l e t要注意发送任何响应体之前总是先设置状态码和头。有些服务器,包括Java Web Server, 内部缓存了一些s e r v l e t响应体(通常大约是4 K)--这允许在即使s e r v l e t写了一个较短的响应体 之后,可以有一定自由度地设置状态码和头。然而,这些行为都是服务器相关的,一名明智的 s e r v l e t编程人员,应该忘掉这一切。 2. 发送标准的响应信息 S e r v l e t R e s p o n s e的s e t C o n t e n t Ty p e ( )方法可以将响应的内容设置为指定的M I M E类型。 public void ServletResponse.setContentType(String type) 在HTTP servlet中,这个方法用于设置C o n t e n t - Type HTTP头。 G e t Wr i t e r ( )方法返回一个P r i n t Wr i t e r对象,用于写基于字符的响应数据: public PrintWriter ServletResponse.getWriter() throws IOException 这个w r i t e r根据内容类型中给定的字符集进行字符编码,如果没指定字符集(这是常事), w r i t e r将用I S O - 8 8 5 9 - 1进行编码, I S O - 8 8 5 9 8 - 1主要是用于西欧文字。字符集在后面的章节中会有 所提及,所以现在你只要记住在得到P r i n t Wr i t e r之前,总是先设置内容的类型。如果g e t O u t p u t S t r e a m ( )已被这个响应调用,这个方法将抛出I l l e g a l S t a t e E x c e p t i o n异常;如果输出流的编码形式 不被支持或不明,则抛出U n s u p p o r t e d E n c o d i n g E x c e p t i o n异常。 除了用P r i n t Wr i t e r返回响应外, s e r v l e t还能用j a v a . i o . O u t p u t S t r e a m的特殊子类来写二进制数 据,这就是S e r v l e t O u t p u t S t r e a m类,它在j a v a . s e r v l e t中定义。可以用g e t O u t p u t S t r e a m ( )方法得到 一个S e r v l e t O u t p u t S t r e a m对象: public ServletOutputStream ServletResponse.getOutputStream() throws IOException 这个方法返回一个S e r v l e t O u t p u t S t r e a m类对象,用于写二进制(一次一个字节)响应数据。不进 行任何编码。如果已为这个响应调用了g e t Wr i t e r ( ),这个方法将返回一个I l l e g a l S t a t e E x c e p t i o n异常。 S e r v l e t O u t p u t S t r e a m很像标准的Java PrintStream类,在Servlet API v1.0中,这个类用于所有 的s e r v l e t的输出,既可是文本类型,又可是二进制类型。在Servlet API v2.0中,它仅用于处理二 进制的输出。由于是O u t p u t S t r e a m的直接子类,它拥有O u t p u t S t r e a m的w r i t e ( )、f l u s h ( )和c l o s e ( ) 方法。为解决这个问题,它加了自己的p r i n t ( )、p r i n t l n ( )方法,用于写大多数的J a v a原始类型的 数据。S e r v l e t O u t p u t S t r e a m接口与P r i n t S t r e a m接口的不同是: S e r v l e t O u t p u t S t r e a m的p r i n t()和 p r i n t l n()方法不能明确地直接显示O b j e c t或c h a r [ ]类型的数据。 3 使用持续连接 持续连接(有时又称作" k e e p - a l i v e"连接)能优化s e r v l e t向客户返回内容的方式。要明白 它的优化机理,必须首先弄懂H T T P连接是如何工作的。下面将对这里面的基本概念做一些浅显 的解释。 当客户,比如浏览器,想从服务器请求一个We b文档时,首先会与服务器建立一个s o c k e t的 第二章预备知识45
连接。通过这个连接,客户端可以发出请求和接收服务器响应。客户端发送空行表示完成请求, 反过来,服务器通过关闭s o c k e t连接表明响应已完成。 就这么简单。但是如果收到的网页中含有< I M G >标签或< A P P L E T >标签,要求客户机从服 务器接收更多的内容,该怎么办呢?那么又得另起一个s o c k e t,如果一个网页中包含1 0个图片和 一个由2 5个类组成的a p p l e t,就需要3 6个连接来传送此页(当然,现在可以使用J A R文件打包这 些类而减少连接数目)。难怪有人戏称W W W为Word Wide Wa i t!。 一个较好的方法是同一个s o c k e t连接接收更多的网页内容,有时也把这种技术称作持续连接。 持续连接的关键是客户端与服务器必须在服务器的响应结束处与客户端开始下一个连接的地方 达成一致。它们可能用一个空行作为记号,但如果响应信息本身包含一个空行又怎么办呢?持 续连接的工作方式是,服务器在响应体中设置C o n t e n t - L e n g t h头,告诉客户端这个响应体有多大。 客户端就知道接收完这个响应体之后,才控制下一个s o c k e t连接。 大多数服务器都能处理它所服务文件的C o n t e n t - L e n g t h头,但对不同的s e r v l e t,处理方式不 一样。S e r v l e t可以使用s e t C o n t e n t L e n g t h ( )方法来处理动态内容的持续连接: public void ServletResponse.setContentLength(int len) 这个方法返回服务器响应内容的长度,在HTTP servlet 中,这个方法设定HTTP Content- L e n g t h头。注意这个方法的使用是可选的,然而,如果使用它, s e r v l e t将会充分利用持续连接的 优点,客户端也能在时精确显示过程控制。 调用s e t C o n t e n t L e n g t h()方法时,有两点要注意: s e r v l e t必须在发送响应信息之前调用此 方法,给定的长度必须精确。哪怕只有一个字节的误差,都可能出问题。这听起来似乎很难做 到,实际上,s e r v l e t用B y t e A r r a y O u t p u t S t r e a m类型来缓存输出。 s e r v l e t 不是将响应信息写到由g e t Wr i t e r ( ) 返回的P r i n t Wr i t e r ,而是写到建立在 B y t e A r r a y O u t p u t S t r e a m基础上的P r i n t Wr i t e r中,这个数组会随s e r v l e t的输出而增长。当s e r v l e t准备 退出时,它能设置内容的长度为缓存尺寸,并将内容发送到客户端缓存。注意,字节是基于 S e r v l e t O u t p u t S t r e a m字节对象发送的。做这么点简单的修改, s e r v l e t就可能利用持续连接的优点。 持续连接是要付出代价的,这一点很重要。缓存所有的输出和成批发送数据需要更多的内 存,还可能延迟客户端开始接收数据的时间点。对响应较短的s e r v l e t来说,可以接受;但对响应 较长的s e r v l e t,考虑内存开销和延迟的代价,可能还不如建立几个连接。 还要注意一点,并不是所有的s e r v l e t都支持持续连接。就是说,设定s e r v l e t响应的内容长度 是好的选择,这个长度信息被支持持续连接的服务器使用,而被其他的服务器忽略。 4. 生成H T M L H T M L生成包为s e r v l e t提供了一系列的类,这些类抽象了H T M L的许多细节,尤其是H T M L 的标签。抽象的程度取决于所使用的包:有些只有极少的H T M L标签,留下一些基本的细节(比 如打开和关闭H T M L标签)给编程人员。利用这类包类似于手工写H T M L,在这里就不讨论了。 另一类包是很好地抽象了H T M L的具体内容,同时把H T M L作为J a v a对象的集合。一个网页可以 看作是一个对象,它可以包含其他H T M L对象(比如列表和表格),还能包含更多的H T M L对象 (比如列表项和表格单元)。这种面向对象的方法可以极大地简化H T M L的生成工作,并且使得 s e r v l e t更容易编写、维护,有时则更高效。 46第一部分JSP 入门
生成Hello Wo r l d 下例是一个普通的Hello World Servlet,作为一个最简单的S e r v l e t,相信读者通过前面的学 习可以理解它。 import java.io.*; import javax.servlet.*; import javax.servlet.http.*; /**Hello World! Servlet Class */ public class HelloWorld extends HttpServlet { /**doGet方法的重载,用于处理客户端的GET请求*/ public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { response.setContentType("text/html"); PrintWriter out = response.getWriter(); out.println("<html>"); out.println("<body>"); out.println("<head>"); out.println("<title>Hello World!</title>"); out.println("</head>"); out.println("<body>"); out.println("<h1>Hello World!</h1>"); out.println("</body>"); out.println("</html>"); } } 5. 状态码 目前我们的例子还没有设置H T T P响应的状态码。如果s e r v l e t没有特别指定状态码,服务器 会将其设为缺省值200 O K状态码。这在成功返回标准响应时非常便利。然而,利用状态码, s e r v l e t可以对响应信息做更多的工作,比如,它可以重定向请求和报告错误等。 最常使用的状态码在H t t p S e r v l e t R e s p o n s e类中被定义为助记常量( public final static int)。表 2 - 1列出了其中的很少几个,全部的状态码可以在第6章中查到。 表2-1 HTTP状态码 助记常量码值默认消息意义 S C _ O K 2 0 0 O K 客客户请求成功,服务器的响应信 息包含请求的数据。这是缺省的状 态码 S C _ N O _ C O N T E N T 2 0 4 No Content 客请求成功,但没有新的响应体返 回。接收到这个状态码的浏览器应 该保留它们当前r的文档视图。当 s e r v l e t从表单中收集数据,希望浏览 器保留表单,同时避免"D o c u m e n t contains no data"错误信息时,这个 状态码很有用 第二章预备知识47
(续) 助记常量码值默认消息意义 S C _ M O V E D _ P E R M A N E N T LY 3 0 1 Moved Perma-nently 客被访问的资源被永久的到一个新 的位置,在将来的请求中应使用新 的U R L。新的U R L由L o c a t i o n头给 出,大多数的浏览器会自动访问新 的路径 SC_MOVED _TEMPORARILY 3 0 2 Moved Tempo- rarily 客被访问的资源被暂时移到一个新 的位置,在将来的请求中仍使用新 的U R L。新的U R L由L o c a t i o n头给 出,大多数的浏览器会自动访问新 的路径 SC_ UNAUTHORIZED 401 U n a u t h o r i e d 客请求没有正确的地认证。用于 W W W- A u t h e n t i c a t e和A u t h e n t i c a t i o n 连接 S C _ N O T _ F O U N D 4 0 4 Not Found 客被请求的资源没找到或不可访问 SC_INTERNAL_ SERV E R _ E R R O R 500 客Internal Server Error 客服务器内部错误,妨碍了请求过 程的正常进行 SC_NOT_ IMPLEMENTED 5 0 1 Not Implemented 客服务器不支持用于完成请求的函 数 S C _ S E RV I C E _ U N AVA I L A B L E 5 0 3 Service Unavail-able 客服务器暂时不可访问,将来可恢 复。如果服务器知道何时可被访问, 它会提供R e t r y - A f t e r头 设定状态码 s e r v l e t可用S e t S t a t u s ( )方法设定响应状态码: public void HttpServletResponse.setStatus(int sc) public void HttpServletResponse.setStatus(int sc, String sm) 这两个方法都可将H T T P的状态码设为指定的值,这个状态码可为一个数字,或为在 H t t p S e r v l e t R e s p o n s e中定义的S C _ X X X码。带一个参数的方法,原因部分被设置为缺省的消息; 带两个参数的方法,原因部分可指定不同的消息。记住, s e t S t a t u s()方法应在s e r v l e t返回任何 响应体之前调用。 如果s e r v l e t在处理请求时将状态码设置报错,则它调用s e n d E r r o r ()方法,而不是 s e n d S t a t u s()方法: public void HttpServletResponse.setError(int sc) public void HttpServletResponse.setError(int sc, String sm) s e r v l e t对s e n d E r r o r()和s e n d S t a t u s的处理过程不同。当带两个参数的方法被调用时,状态 码的消息参数可被替代的原因取代,或者它被直接用于响应体中,这取决于服务器的执行。 6. HTTP头 S e r v l e t能设定H T T P头,提供与响应相关的额外信息。表2 - 2列出了一些s e r v l e t最常使用的 H T P P头。 |