0%

JavaWeb初探

JavaWeb初探

Day 01

25/06/2020

JAVA反射

获取Class实例:

  • 通过静态变量class获取,即直接通过类名获取

  • 通过调用Class.forName(name)方法获取,传递类完整名称为参数

  • 通过实例变量的getClass()方法获取

Class实例在JVM中是唯一的

1
2
3
4
5
6
7
8
// example
Class c1 = String.class;
Class c2 = Class.forName("java.lang.String");
String s = "hello";
Class c3 = s.getClass();

// 可直接通过==符号判断两Class实例是否相同
boolean b = (c1 == c2); // true

访问字段

  • Field getField(name):根据字段名获取某个public的field(包括父类)
  • Field getDeclaredField(name):根据字段名获取当前类的某个field(不包括父类)
  • Field[] getFields():获取所有public的field(包括父类)
  • Field[] getDeclaredFields():获取当前类的所有field(不包括父类)
1
2
3
4
5
6
class Student{
private String num;
public int age;
}
Field f1 = Student.class.getField("age");
Field f2 = Student.class.getDeclaredField("num"); // 访问私有变量不能直接使用getField()方法

Field对象的一些方法

  • getName():返回字段名称,例如,"name"
  • getType():返回字段类型,也是一个Class实例,例如,String.class
  • getModifiers():返回字段的修饰符,它是一个int,不同的bit表示不同的含义。

通过Field实例获取或修改字段的值

1
2
3
4
5
6
7
8
9
// 还是上面Student的例子
Student s = new Student();
f1 = s.getclass().getField("age");
f2 = s.getclass().getDeclaredField("age");
Object value = f1.get(s); // 获取age字段的值 此处为0
f2.setAccessible(true);
Object value = f2.get(s); // 要想获得private修饰字段的值,需要使用setAccessible方法 不过这也不是万能的 有些是可以限制的
f1.set(s, 15); // 设置字段age的值 设置为15
f2.set(s, "hello"); // 同理

获取方法

  • Method getMethod(name, Class...):获取某个publicMethod(包括父类)
  • Method getDeclaredMethod(name, Class...):获取当前类的某个Method(不包括父类)
  • Method[] getMethods():获取所有publicMethod(包括父类)
  • Method[] getDeclaredMethods():获取当前类的所有Method(不包括父类)
1
2
3
4
5
6
class Student{
public int get(){}
private void set(int a, String s){}
}
Method m1 = Student.class.getMethod("get");
Method m2 = Student.class.getDeclaredMethod("set", int.class, String.class); // 后面的可变参数代表该方法的参数列表

Method对象的一些方法:

  • getName():返回方法名称,例如:"getScore"
  • getReturnType():返回方法返回值类型,也是一个Class实例,例如:String.class
  • getParameterTypes():返回方法的参数类型,是一个Class数组,例如:{String.class, int.class}
  • getModifiers():返回方法的修饰符,它是一个int,不同的bit表示不同的含义。

invoke()方法,对Mehtod实例调用该方法,相当于调用Method对应的实例的方法,第一个参数为对象实例,后面跟可变参数,即该方法的参数列表

1
2
3
m1.invoke(); // 相当于student.get()
m2.setAccessible(true); // 和Field同理
m2.invoke(1, "jack");

关于多态:

即在子类重写了父类方法时,反射机制又是如何呢?也同样遵循多态的原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// example
class P{
public void prin() {
System.out.println("p");
}
}
class C extends P{
@Override
public void prin() {
System.out.println("c");
}
}
Method m = P.class.getMethod("prin");
m.invoke(new P()); // p
m.invoke(new C()); // c

调用构造方法

通过反射创建实例:

  • newInstance()方法,要求该构造方法为public无参数构造方法
  • 通过Constructor对象来实现能够调用任意的构造方法

对于构造Constructor对象的方法:

  • getConstructor(Class...):获取某个publicConstructor
  • getDeclaredConstructor(Class...):获取某个Constructor
  • getConstructors():获取所有publicConstructor
  • getDeclaredConstructors():获取所有Constructor
1
2
3
4
5
Student s = Student.class.newInstance();
// Constructor
Constructor c = Student.class.getConstructor(Student.class);
// 同样对于私有构造方法 需要调用setAccessible(true)方法
Student s2 = (Student) c.newInstance(); // 这里的newInstance方法可以传递参数 具体根据构造方法来对应

获取继承关系

获取父类Class实例:

getSuperClass()方法

1
2
3
Class i = int.class;
Class n = i.getSuperClass(); // 获取父类 也就是Number
Class s = n.getSuperClass().getSuperClass(); // 如果已经不存在父类 则返回null

获取接口:

getInterfaces()方法

1
2
Class i[] = int.class.getInterfaces(); // 因为存在多个接口 所以返回Class数组 
// 如果想获得父接口 也需要使用getInterfaces()方法 接口使用getSuperClass()方法会返回null

两个Class对象判断是否为继承关系:

使用isAssignableFrom()方法

1
2
Number.class.isAssignableFrom(Integer.class); // true,因为Integer可以赋值给Number
// 可以类比instanceof

动态代理

可以在运行期动态创建某个interface的实例。

JDK提供的动态创建接口对象的方式,就叫动态代理。

即可以实现在不编写实现类的情况下,创建接口的实例。利用Proxy.newProxyInstance()方法

在运行期动态创建一个interface实例的方法如下:

  1. 定义一个InvocationHandler实例,它负责实现接口的方法调用;
  2. 通过Proxy.newProxyInstance()创建interface实例,它需要3个参数:
    1. 使用的ClassLoader,通常就是接口类的ClassLoader
    2. 需要实现的接口数组,至少需要传入一个接口进去;
    3. 用来处理接口方法调用的InvocationHandler实例。
  3. 将返回的Object强制转型为接口。

但实质其实就是JDK帮我们自动编写了一个上述类(不需要源码,可以直接生成字节码),并不存在可以直接实例化接口的黑魔法。

参考:廖雪峰博客-Java


Day 02

26/06/2020

XML

定义:XML全称为Extensible Markup Language,意思是可扩展的标记语言。XML语法上和HTML比较相似,但HTML中的元素是固定的,而XML的标签是可以由用户自定义的。

1
2
3
4
5
6
7
8
9
<!--部分xml 例子-->
<student number="1001">
<name>zhangSan</name>
<age>23</age>
<sex>male</sex>
<teacher name="liSi">
<wife id="xxx"><name>xxx</name></wife>
</teacher>
</student>

XML语法

文档声明:

  • 文档声明必须为<?xml开头,以?>结束;

  • 文档声明必须从文档的0行0列位置开始;

  • 文档声明只有三个属性:

    • versioin:指定XML文档版本。必须属性,因为我们不会选择1.1,只会选择1.0;

    • encoding:指定当前文档的编码。可选属性,默认值是utf-8;

    • standalone:指定文档独立性。可选属性,默认值为yes,表示当前文档是独立文档。如果为no表示当前文档不是独立的文档,会依赖外部文件。

1
<?xml version="1.0" encoding="utf-8" standalone="no"?>

元素

元素是XML文档中最重要的组成部分:

  • 普通元素的结构:开始标签、元素体、结束标签,例如:<hello>大家好</hello>

  • 元素体:元素体可以是元素,也可以是文本,例如:<b><a>你好</a></b>,其中<b>元素的元素体是<a>元素,而<a>元素的元素体是文本;

  • 空元素:空元素只有开始标签,而没有结束标签,例如:<c/>,但元素必须自己闭合。

元素属性:

  • 属性是元素的一部分,它必须出现在元素的开始标签中;

  • 属性的定义格式:属性名=属性值,其中属性值必须使用单引或双引;

  • 一个元素可以有0~N个属性,但一个元素中不能出现同名属性;

1
2
3
<student number="1001">
<!--.......-->
</student>

转义

转义字符:例如<,>等等这类符号,需要使用相应的转义字符来代替。例如:<&lt;代替。

CDATA段:由于使用转义字符会大大降低XML文档可读性,使用CDATA段就不会有这种问题,格式如下:

1
<a><![CDATA[<a>]]></a>

需要注意的是,在CDATA段中不能包含”]]>”,即CDATA段的结束定界符。

处理指令

处理指令,简称PI(Processing instruction)。处理指令用来指挥解析器如何解析XML文档内容。

例如,在XML文档中可以使用xml-stylesheet指令,通知XML解析器,应用css文件显示xml文档内容。

放于文档声明下方

1
2
<?xml version="1.0" encoding="gbk"?>
<?xml-stylesheet type="text/css" href="a.css"?>

格式

格式良好的XML就是格式正确的XML文档,只有XML的格式是良好的,XML解释器才能解释它。下面是对格式良好XML文档的要求:

  • 必须要有XML文档声明;

  • 必须且仅能有一个根元素;

  • 元素和属性的命名必须遵循XML要求:

    • XML命名区分大小写,例如<a><A>是两上不同的元素;

    • 名称中可以包含:字母、数字、下划线、减号,但不能以数字、减号开头;

    • 不能以xml开头,无论是大写还是小写都不可以,例如<xml><Xml><XML>都是错误的;

    • 不能包含空格,例如<ab cd>是错误的。

  • 元素之间必须合理包含,例如:<a><b>xxx</b></a>是合理的,而<a><b>xxx</a></b>就是错误的包含。

DTD

定义:DTD(Document Type Definition),文档类型定义,用来约束XML文档。或者可以把DTD理解为创建XML文档的结构!例如可以用DTD要求XML文档的根元素名为<students><students>中可以有1~N个<student><student>子元素为<name><age><sex><student>元素还有number属性。

1
2
3
4
5
6
<!--例子-->
<!ELEMENT students (student+)> <!--students元素包含1-n个student元素-->
<!ELEMENT student (name,age,sex)> <!--内容依次为name age sex三个元素-->
<!ELEMENT name (#PCDATA)> <!--定义name元素内容为文本-->
<!ELEMENT age (#PCDATA)> <!--定义age元素内容为文本-->
<!ELEMENT sex (#PCDATA)> <!--定义sex元素内容为文本-->

分类

  • 内部DTD:在XML文档内部嵌入DTD,只对当前XML文档有效;

  • 外部DTD:独立的DTD文件,扩展名为.dtd;

    • 本地DTD:DTD文件在本地,不在网络上。自己项目,或本公司内部使用的;

    • 公共DTD:DTD文件在网络上,不在本地。都是大公司或组织发布的,共大家使用!

内部DTD

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="utf-8" standalone="yes" ?>
<!--内部DTD例子-->
<!DOCTYPE students [
<!ELEMENT students (student+)>
<!ELEMENT student (name, age, sex)>
<!ELEMENT name (#PCDATA)>
<!ELEMENT age (#PCDATA)>
<!ELEMENT sex (#PCDATA)>
]>
<students>
<student>
<name>zhangSan</name>
<age>23</age>
<sex>male</sex>
</student>
</students>
  • 位置:内部DTD在文档声明下面,在根元素上面;

  • 语法格式:放到<!DOCTYPE 根元素名称[ ]>之间;

  • 只对当前XML文档有效;

本地DTD

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
<!DOCTYPE students SYSTEM "students.dtd"> <!--此处为dtd文件路径-->
<students>
<student>
<name>zhangSan</name>
<age>23</age>
<sex>male</sex>
</student>
</students>
  • 位置:本地硬盘上;

  • 语法格式:直接定义元素或属性即可;

  • 本地所有XML文档都可以引用这个dtd文件;

公共DTD

1
2
3
4
5
6
7
8
9
<?xml version="1.0" encoding="utf-8" standalone="no" ?>
<!DOCTYPE students PUBLIC "-//qdmmy6//DTD ST 1.0//ZH" "http://www.qdmmy6.com/xml/dtds/st.dtd">
<students>
<student>
<name>zhangSan</name>
<age>23</age>
<sex>male</sex>
</student>
</students>

格式为:<!DOCTYPE 根元素 PUBLIC "DTD名称" "DTD网址">

DTD语法

定义元素

定义元素语法:<!ELEMENT 元素名 元素描述>

  • <!ELEMENT name (#PCDATA)>,定义名为name的元素,内容为文本类型。

  • <!ELEMENT student (name,age,sex)>,定义名为student元素,内容依次为name、age、sex元素;

  • <!ELEMENT student ANY>,定义名为student元素,内容任意;

  • <!ELEMENT student EMPTY>,定义名为student元素,不能有内容,即空元素,注意空元素是可以有属性的。

子元素出现次数

可以使用*+?来指定子元素出现的次数

*:可以出现0~N次;+:可以出现1~N次;?:可以出现0~1次。

例如:<!ELEMENT student(name,age?,hobby*,grade+) >,定义student元素,第一子元素为name,必须且仅能出现一次,age是可有可无的,hobby可以出现0N次,grade可以出现1N次。

枚举类型子元素

<!ELEMENT student (name | age | sex)>,表示student子元素为name、age、sex其中之一,必须仅且能选择其一。

定义属性

定义属性的语法:

<!ATTLIST 元素名 属性名 属性类型 设置说明>

例如:<!ATTLIST student number CDATA #REQUIRED>,给student元素定义属性number,类型为文本,这个默认是必须的。

属性设置说明:

#REQUIRED:说明属性是必须的;#IMPLIED:说明属性是可选的;默认值:在不给出属性值时,使用默认值。

属性的类型:

  • CDATA:文本类型;

  • Enumerated:枚举类型;

  • ID:ID类型,ID类型的属性用来标识元素的唯一性,即元素的ID属性值不能与其他元素的ID属性值相同;

  • IDREF:ID引用类型,用来指定另一个元素,与另一个元素建立关联关系,IDREF类型的属性值必须是另一个元素的ID。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<!--例子-->
<!ELEMENT students (student+) >
<!ELEMENT student EMPTY>
<!ATTLIST student number ID #REQUIRED>
<!ATTLIST student name CDATA #REQUIRED>
<!ATTLIST student sex (male | female) "male" >
<!ATTLIST student friend IDREF #IMPLIED>
<!--上面为students.dtd文件内容-->
<?xml version="1.0" ?>
<!DOCTYPE students SYSTEM "students.dtd">

<students>
<student number="itcast_001" name="zhangSan"/>
<student number="itcast_002" name="liSi" sex="male"/>
<student number="itcast_003" name="wangWu" sex="female" friend="itcast_002"/>
</students>
定义实体

将多而长的字符串定义为用简单符号表示的实体,从而在使用时只用使用该符号就可以了,方便且简洁。

实体分为两种:一般实体和参数实体。

  • 一般实体:在XML文档中使用;

    • 定义一般实体:<!ENTITY 实体名 "实体值">,例如:<!ENTITY 大美女 "白冰">
    • 一般实体引用:&实体名;,例如<xxx>&大美女;</xxx>
  • 参数实体:在DTD使用。

    • 定义参数实体:<!ENTITY % 实体名 "实体值">,”%”与实体名之间的空格是必须的;例如:<!ENTITY % friend "student friend IDREF #IMPLIED">
    • 参数实体引用:%实体名;;例如:<!ATTLIST %friend;>

参数实体是在DTD内部使用,而不是在XML中使用。在内部DTD中使用参数实体会有诸多限制

1
2
3
4
5
6
7
8
9
<!--参数实体例子-->
<!ELEMENT students (student+) >
<!ELEMENT student EMPTY>
<!ATTLIST student number ID #REQUIRED>
<!ATTLIST student name CDATA #REQUIRED>
<!ATTLIST student sex (male | female) "male">
<!ENTITY % friend "<!ATTLIST student friend IDREF #IMPLIED>"> <!--定义-->
%friend; <!--使用-->
<!ENTITY itcast "北京传智播客教育科技有限公司">

Schema

特点:

  • Schema是新的XML文档约束;

  • Schema要比DTD强大很多;

  • Schema本身也是XML文档,但Schema文档的扩展名为xsd,而不是xml。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
<!--students.xsd-->
<?xml version="1.0"?>
<xsd:schema xmlns="http://www.itcast.cn/xml"
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
targetNamespace="http://www.itcast.cn/xml"
elementFormDefault="qualified"> <!--指定名称空间-->
<xsd:element name="students" type="studentsType"/> <!--定义studentsType类型-->
<xsd:complexType name="studentsType">
<xsd:sequence>
<xsd:element name="student" type="studentType" minOccurs="0" maxOccurs="unbounded"/> <!--指定其包含的student元素数目-->
</xsd:sequence>
</xsd:complexType>
<xsd:complexType name="studentType"> <!--定义studentType类型-->
<xsd:sequence> <!--指定其元素按顺序出现-->
<xsd:element name="name" type="xsd:string"/>
<xsd:element name="age" type="ageType" />
<xsd:element name="sex" type="sexType" />
</xsd:sequence>
<xsd:attribute name="number" type="numberType" use="required"/> <!--指定其包含的属性-->
</xsd:complexType>
<xsd:simpleType name="sexType">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="male"/> <!--指定枚举选项-->
<xsd:enumeration value="female"/>
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="ageType"> <!--定义ageType类型-->
<xsd:restriction base="xsd:integer">
<xsd:minInclusive value="0"/>
<xsd:maxInclusive value="100"/> <!--指定范围-->
</xsd:restriction>
</xsd:simpleType>
<xsd:simpleType name="numberType"> <!--定义numberType类型-->
<xsd:restriction base="xsd:string">
<xsd:pattern value="ITCAST_\d{4}"/> <!--正则表达式-->
</xsd:restriction>
</xsd:simpleType>
</xsd:schema>

<!--students.xml-->
<?xml version="1.0"?>

<students xmlns="http://www.itcast.cn/xml"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.itcast.cn/xml students.xsd" > <!--指定约束文件-->
<student number="ITCAST_1001">
<name>zhangSan</name>
<age>20</age>
<sex>male</sex>
</student>
<student number="ITCAST_1002">
<name>liSi</name>
<age>25</age>
<sex>female</sex>
</student>
</students>

schema名称空间

名称空间定义:如果一个XML文档中使用多个Schema文件,而这些Schema文件中定义了相同名称的元素时就会出现名字冲突。这就像一个Java文件中使用了import java.util.*和import java.sql.*时,在使用Date类时,那么就不明确Date是哪个包下的Date了。

总之名称空间就是用来处理元素和属性的名称冲突问题,与Java中的包是同一用途。如果每个元素和属性都有自己的名称空间,那么就不会出现名字冲突问题,就像是每个类都有自己所在的包一样,那么类名就不会出现冲突。

目标名称空间

在XSD文件中为定义的元素指定名称,即指定目标名称空间。这需要给<xsd:schema>元素添加targetNamespace属性。

例如:<xsd:schema targetNamespace="http://www.itcast.cn/xml">

名称空间可以是任意字符串,但通常我们会使用公司的域名作为名称空间,这与Java中的包名使用域名的倒序是一样的!千万不要以为这个域名是真实的,它可以是不存在的域名。

如果每个公司发布的Schema都随意指定名称空间,如a、b之类的,那么很可能会出现名称空间的名字冲突,所以还是使用域名比较安全,因为域名是唯一的。

当使用了targetNamespace指定目标名称空间后,那么当前XSD文件中定义的元素和属性就在这个名称空间之中了。

XML指定XSD文件

在XML文件中需要指定XSD约束文件,这需要使用在根元素中使用schemaLocation属性来指定XSD文件的路径,以及目标名称空间。格式为:schemaLocation="目标名称空间 XSD文件路径"

例如:<students schemaLocation="http://www.itcast.cn/xml students.xsd">

schemaLocation是用来指定XSD文件的路径,也就是说为当前XML文档指定约束文件。但它不只要指定XSD文件的位置,还要指定XSD文件的目标名称空间。

其中http://www.itcast.cn/xml为目标名称空间,students.xsd为XSD文件的位置,它们中间使用空白符(空格或换行)分隔。

也可以指定多个XSD文件,格式为:

schemaLocation="目标名称空间1 XSD文件路径1 目标名称空间2 XSD文件路径2"

下面是spring配置文件的例子,它一共指定两个XSD文件

1
2
3
4
5
6
7
<?xml version="1.0" encoding="UTF-8"?>
<beans
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
</beans>
定义名称空间

现在我们已经知道一个XML中可以指定多个XSD文件,例如上面Spring的配置文件中就指定了多个XSD文件,那么如果我在<beans>元素中给出一个子元素<bean>,你知道它是哪个名称空间中的么?显然是无法知道的。

所以只是使用schemaLocation指定XSD是不够的,它只是导入了这个XSD及XSD的名称空间而已。schemaLocation的作用就相当于Java中导入Jar包的作用!最终还是在Java文件中使用import来指定包名的。

xmlns是用来指定名称空间前缀的,所谓前缀就是”简称”,例如中华人民共和国简称中国一样,然后我们在每个元素前面加上前缀,就可以处理名字冲突了。

格式为:xmln:前缀="名称空间"

注意,使用xmlns指定的名称空间必须是在schemaLocation中存在的名称空间。

1
2
3
4
5
6
7
8
9
10
<beans
xmlns:b="http://www.springframework.org/schema/beans"
xmlns:aop="http://www.springframework.org/schema/aop" <!--这里指定了两个前缀-->
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<b:bean></b:bean>
<aop:scoped-proxy/>
</beans>
默认名称空间

在一个XML文件中,可以指定一个名称空间没有前缀,那么在当前XML文档中没有前缀的元素就来自默认名称空间。

1
2
3
4
5
6
7
8
9
10
<beans
xmlns="http://www.springframework.org/schema/beans" <!--默认名称空间-->
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<bean></bean> <!--使用默认名称空间-->
<aop:scoped-proxy/>
</beans>
W3C的元素和属性

如果我们的XML文件中需要使用W3C提供的元素和属性,那么可以不在schemaLocation属性中指定XSD文件的位置,但一定要定义名称空间,例如:

1
2
3
4
5
6
7
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"	<!--指定名称空间-->
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">
<!--这里无需指定-->
</beans>

上面定义了一个名称空间,前缀为xsi,名称空间为http://www.w3.org/2001/XMLSchema-instance。这个名称空间无需在schemaLocation中存在。

你可能已经发现了,schemaLocation这个属性其实是w3c定义的属性,与元素一定,属性也需要指定”出处”,xsi:schemaLocation中的xsi就是名称空间前缀。也就是说,上面我们在没有指定xsi名称空间时,就直接使用schemaLocation是错误的。也就是在上面其他的例子中,我们是没有指明xsi代表什么的,所以就是错误的,因为不存在xsi这个前缀。

XML解析器

DOM and SAX 简介

定义:XML是保存数据的文件,XML中保存的数据也需要被程序读取然后使用。那么程序使用什么来读取XML文件中的数据呢?XML解析器!例如.properties文件的解析器是Properties类一样!

XML不只被Java语言使用,还被C++、C#、Javascript等等语言使用,所以解析XML不是一门语言的工作!

主流的XML解析有两种标准:DOM和SAX。它们是标准,是思想,不是真正的解析器,它们是跨语言的!!!

  • DOM(Document Object Model):W3C组织提供的解析XML文档的标准接口;

  • SAX(Simple API for XML):社区讨论的产物,是一种事实上的标准。

Apache的xerces组件实现了DOM和SAX,所以在我们在Java中解析XML需要使用xerces。所以我们称xerces是DOM、SAX解析器。

DOM解析原理

DOM要求解析器把整个XML文档装载到一个Document对象中。即使用DOM解析器解析XML文档的结果就是一个Document对象。

一个XML文档解析后对应一个Document对象,可以通过Document对象获取根元素,然后在通过根元素获取根元素的子元素…,这说明DOM解析方式保留了元素之间的结构关系!

  • 优点:元素与元素之间的结构关系保留了下来;

  • 缺点:如果XML文档过大,那么把整个XML文档装载进内存,可能会出现内存溢出的现象!

SAX解析原理

DOM解析后的结果是一个Document对象,而SAX解析没有结果!SAX要求在开始解析之前用户提供一个接口的实现对象,然后把接口实现对象传递给SAX解析器,然后在SAX解析器的过程中不断调用实现对象的方法。

  • DOM是解析时把数据放到了Document对象中,然后用户从Document中获取需要的数据;

  • SAX要求用户参与到解析过程中来,把想要做的事情写到接口实现对象中,然后SAX在解析过程中来调用接口实现对象的方法。

SAX解析器会在解析XML文档的过程中,在发生特定事件时,调用接口中特定的方法。例如在SAX解析到某个元素的开始标签时,输出元素名称!其中解析到开始标签就是特定的事件,而输出元素名称,就是接口中方法的实现。

1
2
3
4
5
6
7
8
// 接口中方法
public void startDocument(); // 开始解析时调用
public void endDocument(); // 结束解析时调用
public void startElement(String uri, String localName, String qName, Attibutes atts); // 遇到元素开始标签调用
public void endElement(String uri, String localName, String qName) // 遇到元素结束标签调用
public void characters(char[] ch, int start, int length); // 遇到文本内容调用
public void ignorableWhitespace(char[] ch, int start, int length); // 遇到元素和元素之间空白调用
public void processingInstruction(String target, String data); // 遇到处理指令时调用

接口的实现由我们来完成,然后我们需要把接口实现类对象”交给”SAX解析器,然后让SAX开始解析。SAX会在特定事件发生时,调用接口中的方法,完成我们交给它的任务。

  • 优点:适合解析大XML文件(内存空间占用小),因为是解析一行处理一行,处理完了就不需要在保留数据了;

  • 缺点:因为是解析一行处理一行,解析之后数据就丢失了,所以元素与元素之间的结构关系没有保留下来。

JAXP

JAXP(Java API for XML Processing)是由Java提供的,JAXP是对所有像xerces一样的解析的提供统一接口的API。

当我们使用JAXP完成解析工作时,还需要为JAXP指定xerces或其他解析器,当需要更换解析器时,无需修改代码,只需要修改配置即可。

JAXP不是解析器,但使用它可以方便的切换解析器。所以在我们的程序中只会使用JAXP,而不会直接使用Xeces。

JDOM和DOM4j

DOM和SAX是跨语言的XML解析准备,在Java中使用并不方便。而JDOM和DOM4j是专门为Java语言提供的解析工具!使用起来很方便,所以真实开发中使用JDOM或DOM4J比较多。

又因为DOM4J与JDOM比较结果为DOM4j完胜,所以这里只会对DOM4j介绍,而不会介绍JDOM。

DOM 和 SAX 解析

DOM中的核心概念就是节点,在XML文档中的元素、属性、文本、处理指令,在DOM中都是节点!

JAXP-DOM

使用DOM解析XML的目标就是获取到Document对象,然后在从Document中获取到需要的数据。Document对象就是XML文档在内存中的样子。

1
2
3
4
// 获取Document对象 三步
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); // 获取工厂
DocumentBuilder builder = factory.newDocumentBuilder(); // 获取解析器
Document document = builder.parse(new File("students.xml")); // 解析xml获得Document
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 遍历Document
@Test
public void fun1() throws SAXException, IOException, ParserConfigurationException {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();
Document document = builder.parse(new File("src/students.xml"));

// 获取根元素
Element rootEle = document.getDocumentElement();
// 获取元素的所有子节点
NodeList nodeList = rootEle.getChildNodes();
// 循环遍历所有节点
for(int i = 0; i < nodeList.getLength(); i++) {
// 获取其中每个节点
Node node = nodeList.item(i);
// 判断节点的类型是否为元素
if(node.getNodeType() == Node.ELEMENT_NODE) {
// 强转成元素类型
Element stuEle = (Element) node;
// 获取元素的名称
String eleName = stuEle.getNodeName();
// 获取元素的number属性值
String number = stuEle.getAttribute("number");
// 获取名为name的子元素,因为返回值为NodeList,
// 所以需要使用item(0)方法获取第一个name子元素
// getTextContent()是获取节点的文本内容
String name = stuEle.getElementsByTagName("name").item(0).getTextContent();
String age = stuEle.getElementsByTagName("age").item(0).getTextContent();
String sex = stuEle.getElementsByTagName("sex").item(0).getTextContent();

System.out.println(eleName + ":[number=" + number + ", name=" + name + ", age=" + age + ", sex=" + sex + "]");
}
}
}
JAXP-SAX

使用SAX解析XML文档需要先给出DefaultHandler的子类,重写其中的方法。然后在使用SAX开始解析时把DefaultHandler子类对象传递给SAX解析器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class MyContentHandler extends DefaultHandler {
public void startDocument() throws SAXException {
System.out.println("开始解析...");
}

public void endDocument() throws SAXException {
System.out.println("解析结束...");
}

public void startElement(String uri, String localName, String qName,
Attributes atts) throws SAXException {
System.out.println(qName + "元素解析开始");
}

public void endElement(String uri, String localName, String qName)
throws SAXException {
System.out.println(qName + "元素解析结束");
}

public void characters(char[] ch, int start, int length)
throws SAXException {
System.out.print(new String(ch, start, length).trim());
}
}

使用SAX解析首先需要获取工厂,再通过工厂获取解析器对象,然后使用解析对象完成解析工作:

1
2
3
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
parser.parse(new File("src/students.xml"), new MyContentHandler());

DOM4j 解析

DOM4J是针对Java开发人员专门提供的XML文档解析规范,它不同与DOM,但与DOM相似。DOM4J针对Java开发人员而设计,所以对于Java开发人员来说,使用DOM4J要比使用DOM更加方便。

在DOM4J中,也有Node、Document、Element等接口,结构上与DOM中的接口比较相似。但它们是不同的类:

img

Node

  • Attribute:表示属性节点;

  • Branch:表示可以包含子元素的节点:

    • Document:表示整个文档;
    • Element:表示元素节点;
  • CharacterData:表示文本节点:

    • Text:表示文本内容;
    • CDATA:表示CDATA段内容;
    • Comment:表示注释内容。

再次强调,DOM和DOM4J是不同的,DOM中的Document是org.w3c.Document,而DOM4J中的Document是org.dom4j.Document,它们是不同的类,其他Node、Element也是一样。

读取、保存、创建Document

使用dom4j需要导入:

  • dom4j.jar

  • jaxen.jar

1
2
3
// 读取
SAXReader reader = new SAXReader();
Document doc = reader.read("src/students.xml");
1
2
3
4
5
6
7
8
9
10
11
12
// 保存xml文档

// 创建格式化器,使用\t缩进,添加换行
OutputFormat format = new OutputFormat("\t", true);
// 清空数据中原有的换行
format.setTrimText(true);
// 创建XML输出流对象
XMLWriter writer = new XMLWriter(new FileWriter("src/a.xml"), format);
// 输出Document
writer.write(doc);
// 关闭流
writer.close();
1
2
// 创建Document
Document doc = DocumentHelper.createDocument();

遍历Document

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public void fun1() throws DocumentException {
SAXReader reader = new SAXReader();
Document doc = reader.read("src/students.xml");

// 获取根元素
Element rootEle = doc.getRootElement();
// 获取根元素的所有子元素
List<Element> eleList = rootEle.elements();
// 遍历元素集合
for (Element stuEle : eleList) {
// 获取元素名称
String eleName = stuEle.getName();

// 获取元素的number属性值
String number = stuEle.attributeValue("number");
// 获取元素的name子元素内容
String name = stuEle.elementText("name");
// 获取元素的age子元素内容
String age = stuEle.elementText("age");
// 获取元素的sex子元素内容
String sex = stuEle.elementText("sex");
System.out.println(eleName + ": [number=" + number + ", name="
+ name + ", age=" + age + ", sex=" + sex + "]");
}
}

添加student元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
SAXReader reader = new SAXReader();
Document doc = reader.read("src/students.xml");

// 获取根元素<students>
Element root = doc.getRootElement();
// 为root添加名为student的子元素,并返回这个新添加的子元素
Element stuEle = root.addElement("student");
// 给元素添加属性number,值为123
stuEle.addAttribute("number", "123");
// 添加子元素name,并设置name子元素的文本内容为wangWu
stuEle.addElement("name").setText("wangWu");
stuEle.addElement("age").setText("30");
stuEle.addElement("sex").setText("male");

//////////////////

// 创建格式化器,使用\t缩进,添加换行
OutputFormat format = new OutputFormat("\t", true);
// 清空数据中原有的换行
format.setTrimText(true);
// 创建XML输出流对象
XMLWriter writer = new XMLWriter(new FileWriter("src/a.xml"), format);
// 输出Document
writer.write(doc);
// 关闭流
writer.close();

查询元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
SAXReader reader = new SAXReader();
Document doc = reader.read("src/students.xml");
/*
* selectSingNode()方法的参数是XPath
* XPath是在XML文档中查找的一门表达式语言
* "//"表示查找整个XML文档
* student表示查找名为student的元素
* []表示条件
* @number表示number属性
* @number='ITCAST_1001'表示条件为number属性等于ITCAST_1001
* selectSingNode()方法在查找到多个满足XPath的元素时,只返回第一个。
*/
Element stuEle1 = (Element)doc.selectSingleNode("//student[@number='ITCAST_1001']");
// 把元素转换成字符串
System.out.println(stuEle1.asXML());

// 查找name子元素内容为liSi的student元素
Element stuEle2 = (Element) doc.selectSingleNode("//student[name='liSi']");
System.out.println(stuEle2.asXML());

修改元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SAXReader reader = new SAXReader();
Document doc = reader.read("src/students.xml");

// 查找元素
Element stuEle = (Element)doc.selectSingleNode("//student[@number='ITCAST_1001']");
// 修改student元素的name子元素内容为"张三"
stuEle.element("name").setText("张三");

//////////////////

// 创建格式化器,使用\t缩进,添加换行
OutputFormat format = new OutputFormat("\t", true);
// 清空数据中原有的换行
format.setTrimText(true);
// 创建XML输出流对象
XMLWriter writer = new XMLWriter(new FileWriter("src/a.xml"), format);
// 输出Document
writer.write(doc);
// 关闭流
writer.close();

删除学生元素

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
SAXReader reader = new SAXReader();
Document doc = reader.read("src/students.xml");

// 查找元素
Element stuEle = (Element)doc.selectSingleNode("//student[@number='ITCAST_1001']");
// 获取父元素来删除元素
stuEle.getParent().remove(stuEle);

//////////////////

// 创建格式化器,使用\t缩进,添加换行
OutputFormat format = new OutputFormat("\t", true);
// 清空数据中原有的换行
format.setTrimText(true);
// 创建XML输出流对象
XMLWriter writer = new XMLWriter(new FileWriter("src/a.xml"), format);
// 输出Document
writer.write(doc);
// 关闭流
writer.close();
DOM4j API 介绍

Node方法:

  • String asXML():把当前节点转换成字符串,如果当前Node是Document,那么就会把整个XML文档返回;

  • String getName():获取当前节点名字;Document的名字就是绑定的XML文档的路径;Element的名字就是元素名称;Attribute的名字就是属性名;

  • Document getDocument():返回当前节点所在的Document对象;

  • short getNodeType():获取当前节点的类型;

  • String getNodeTypeName():获取当前节点的类型名称,例如当前节点是Document的话,那么该方法返回Document;

  • String getStringValue():获取当前节点的子孙节点中所有文本内容连接成的字符串;

  • String getText():获取当前节点的文本内容。如果当前节点是Text等文本节点,那么本方法返回文本内容;例如当前节点是Element,那么当前节点的内容不是子元素,而是纯文本内容,那么返回文本内容,否则返回空字符串;

  • void setDocument(Document doc):给当前节点设置文档元素;

  • void setParent(Element parent):给当前节点设置父元素;

  • void setText(String text):给当前节点设置文本内容;

Branch方法:

  • void add(Element e):添加子元素;
  • void add(Node node):添加子节点;
  • void add(Comment comment):添加注释;
  • Element addElement(String eleName):通过名字添加子元素,返回值为子元素对象;
  • void clearContent():清空所有子内容;
  • List content():获取所有子内容,与获取所有子元素的区别是,liSi元素没有子元素,但有子内容;
  • Element elementById(String id):如果元素有名为”ID”的属性,那么可以使用这个方法来查找;
  • int indexOf(Node node):查找子节点在子节点列表中的下标位置;
  • Node node(int index):通过下标获取子节点;
  • int nodeCount():获取子节点的个数;
  • Iterator nodeIterator():获取子节点列表的迭代器对象;
  • boolean remove(Node node):移除指定子节点;
  • boolean remove(Commont commont):移除指定注释;
  • boolean remove(Element e):移除指定子元素;
  • void setContent(List content) :设置子节点内容;

Document方法:

  • Element getRootElement():获取根元素;
  • void setRootElement():设置根元素;
  • String getXmlEncoding():获取XML文档的编码;
  • void setXmlEncoding():设置XML文档的编码;

Element方法:

  • void add(Attribute attr):添加属性节点;
  • void add(CDATA cdata):添加CDATA段节点;
  • void add(Text Text):添加Text节点;
  • Element addAttribute(String name, String value):添加属性,返回值为当前元素本身;
  • Element addCDATA(String cdata):添加CDATA段节点;
  • Element addComment(String comment):添加属性节点;
  • Element addText(String text):添加Text节点;
  • void appendAttributes(Element e):把参数元素e的所有属性添加到当前元素中;
  • Attribute attribute(int index):获取指定下标位置上的属性对象;
  • Attribute attribute(String name):通过指定属性名称获取属性对象;
  • int attributeCount():获取属性个数;
  • Iterator attributeIterator():获取当前元素属性集合的迭代器;
  • List attributes():获取当前元素的属性集合;
  • String attributeValue(String name):获取当前元素指定名称的属性值;
  • Element createCopy():clone当前元素对象,但不会copy父元素。也就是说新元素没有父元素,但有子元素;
  • Element element(String name):获取当前元素第一个名称为name的子元素;
  • Iterator elementIterator():获取当前元素的子元素集合的迭代器;
  • Iterator elementIterator(String name):获取当前元素中指定名称的子元素集合的迭代器;
  • List elements():获取当前元素子元素集合;
  • List elements(String name):获取当前元素指定名称的子元素集合;
  • String elementText(String name):获取当前元素指定名称的第一个元素文件内容;
  • String elementTextTrime(String name):同上,只是去除了无用空白;
  • boolean isTextOnly():当前元素是否为纯文本内容元素;
  • boolean remove(Attribute attr):移除属性;
  • boolean remove(CDATA cdata):移除CDATA;
  • boolean remove(Text text):移除Text。

DocumentHelper静态方法介绍:

  • static Document createDocument():创建Dcoument对象;
  • static Element createElement(String name):创建指定名称的元素对象;
  • static Attribute createAttrbute(Element owner, String name, String value):创建属性对象;
  • static Text createText(String text):创建属性对象;
  • static Document parseText(String text):通过给定的字符串生成Document对象;

Day 03

27/06/2020

C/S

  • C/S结构即客户端/服务器(Client/Server),例如QQ;

  • 需要编写服务器端程序,以及客户端程序,例如我们安装的就是QQ的客户端程序;

  • 缺点:软件更新时需要同时更新客户端和服务器端两端,比较麻烦;

  • 优点:安全性比较好。

B/S

  • B/S结构即浏览器/服务器(Browser/Server);
  • 优点:只需要编写服务器端程序;
  • 缺点:安全性较差。

Tomcat

Tomcat的目录结构

  • bin:该目录下存放的是二进制可执行文件,如果是安装版,那么这个目录下会有两个exe文件:tomcat6.exe、tomcat6w.exe,前者是在控制台下启动Tomcat,后者是弹出UGI窗口启动Tomcat;如果是解压版,那么会有startup.bat和shutdown.bat文件,startup.bat用来启动Tomcat,但需要先配置JAVA_HOME环境变量才能启动,shutdawn.bat用来停止Tomcat;

  • conf:这是一个非常非常重要的目录,这个目录下有四个最为重要的文件:

    • server.xml:配置整个服务器信息。例如修改端口号,添加虚拟主机等;下面会详细介绍这个文件;
    • tomcatusers.xml:存储tomcat用户的文件,这里保存的是tomcat的用户名及密码,以及用户的角色信息。可以按着该文件中的注释信息添加tomcat用户,然后就可以在Tomcat主页中进入Tomcat Manager页面了;
    • web.xml:部署描述符文件,这个文件中注册了很多MIME类型,即文档类型。这些MIME类型是客户端与服务器之间说明文档类型的,如用户请求一个html网页,那么服务器还会告诉客户端浏览器响应的文档是text/html类型的,这就是一个MIME类型。客户端浏览器通过这个MIME类型就知道如何处理它了。当然是在浏览器中显示这个html文件了。但如果服务器响应的是一个exe文件,那么浏览器就不可能显示它,而是应该弹出下载窗口才对。MIME就是用来说明文档的内容是什么类型的!
    • context.xml:对所有应用的统一配置,通常我们不会去配置它。
  • lib:Tomcat的类库,里面是一大堆jar文件。如果需要添加Tomcat依赖的jar文件,可以把它放到这个目录中,当然也可以把应用依赖的jar文件放到这个目录中,这个目录中的jar所有项目都可以共享之,但这样你的应用放到其他Tomcat下时就不能再共享这个目录下的Jar包了,所以建议只把Tomcat需要的Jar包放到这个目录下;

  • logs:这个目录中都是日志文件,记录了Tomcat启动和关闭的信息,如果启动Tomcat时有错误,那么异常也会记录在日志文件中。

  • temp:存放Tomcat的临时文件,这个目录下的东西可以在停止Tomcat后删除!

  • webapps:存放web项目的目录,其中每个文件夹都是一个项目;如果这个目录下已经存在了目录,那么都是tomcat自带的。项目。其中ROOT是一个特殊的项目,在地址栏中没有给出项目目录时,对应的就是ROOT项目。http://localhost:8080/examples,进入示例项目。其中examples就是项目名,即文件夹的名字。

  • work:运行时生成的文件,最终运行的文件都在这里。通过webapps中的项目生成的!可以把这个目录下的内容删除,再次运行时会生再次生成work目录。当客户端用户访问一个JSP文件时,Tomcat会通过JSP生成Java文件,然后再编译Java文件生成class文件,生成的java和class文件都会存放到这个目录下。

  • LICENSE:许可证。

  • NOTICE:说明文件。

Web 应用

静态应用
  • 在webapps下创建一个hello目录;
  • 在webapps\hello\下创建index.html;
  • 启动tomcat;
  • 打开浏览器访问http://localhost:8080/hello/index.html
1
2
3
4
5
6
7
8
9
<!--index.html-->
<html>
<head>
<title>hello</title>
</head>
<body>
<h1>Hello World!</h1>
</body>
</html>
动态应用
  • 在webapps下创建hello1目录;
  • 在webapps\hello1\下创建WEB-INF目录;
  • 在webapps\hello1\WEB-INF\下创建web.xml;
  • 在webapps\hello1\下创建index.html。
  • 打开浏览器访问http://localhost:8080/hello/index.html
1
2
3
4
5
6
7
<?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">
</web-app>

完整的Web应用还需要在WEB-INF目录下创建:

  • classes;
  • lib目录;

webapps

|- hello

​ |-index.html

​ |-WEB-INF

​ |-web.xml

​ |-classes

​ |-lib

  • hello:应用目录,hello就是应用的名称;
  • index.html:应用资源。应用下可以有多个资源,例如css、js、html、jsp等,也可以把资源放到文件夹中,例如:hello\html\index.html,这时访问URL为:http://localhost:8080/hello/html/index.html;
  • WEB-INF:这个目录名称必须是大写,这个目录下的东西是无法通过浏览器直接访问的,也就是说放到这里的东西是安全的;
  • web.xml:应用程序的部署描述符文件,可以在该文件中对应用进行配置,例如配置应用的首页
1
2
3
<welcome-file-list>
<welcome-file>index.html</welcome-file>
</welcome-file-list>
  • classes:存放class文件的目录;

  • lib:存放jar包的目录;

配置外部应用

也可以把应用放到Tomcat之外,这就是外部应用了。例如我们把上面写的hello应用从webapps目录中剪切到C盘下,即C:/hello。现在hello这个Web应用已经不在Tomcat中了,这时我们需要在tomcat中配置外部应用的位置,配置的方式一共有两种:

  • conf/server.xml:打开server.xml文件,找到<Host>元素,在其中添加<Context>元素,代码如下:
1
2
3
4
<Host name="localhost" appBse="webapps"
unpackWARs="true" autoDeploy="true">
<Context path="itcast_hello" docBse="C:/hello/"/>
</Host>

1)path:指定当前应用的名称

2)docBase:指定应用的物理位置

3)浏览器访问路径:http://localhost:8080/itcast_hello/index.html

  • conf/catalana/localhost:在该目录下创建itcast_hello.xml文件,在该文件中编写<Context>元素,代码如下:
1
<Context docBase="C:/hello/"/>

1)文件名:指定当前应用的名称;

2)docBase:指定应用的物理位置;

3)浏览器访问路径:http://localhost:8080/itcast_hello/index.html

server.xml
1
2
3
4
5
6
<Server>
<Servier>
<Connector>
<Engine>
<Host>
<Context>
  • <Server>:根元素,表示整个服务器的配置信息;
  • <Service><Server>的子元素,在<Server>中只能有一个<Service>元素,它表示服务;
  • <Connector><Service>的子元素,在<Service>中可以有N个<Connector>元素,它表示连接。
  • <Engine><Service>的子元素,在<Service>中只能有一<Engine>元素,该元素表示引擎,它是<Service>组件的核心。
  • <Host><Engine>的子元素,在<Engine>中可以有N个<Host>元素,每个<Host>元素表示一个虚拟主机。所谓虚拟主机就像是真的主机一样,每个主机都有自己的主机名和项目目录。例如<Host name="localhost" appBase="webapps">表示主机名为localhost,这个主机的项目存放在webapps目录中。访问这个项目下的主机时,需要使用localhost主机名,项目都存放在webapps目录下。
  • <Context><Host>元素的子元素,在<Host>中可以有N个<Context>元素,每个<Context>元素表示一个应用。如果应用在<Host>的appBase指定的目录下,那么可以不配置<Context>元素,如果是外部应用,那么就必须配置<Context>。如果要为应用指定资源,也需要配置<Context>元素。

我们可以把<Server>看作是一个大酒店:

  • <Service>:酒店的服务部门;

  • <Connector>:服务员;

  • <Engine>:后厨;

  • <Host>:后厨中的一个区,例如川菜区是一个<Host>、粤菜区是一个<Host>

  • <Context>:后厨的一个厨师。

用户发出一个请求:http://localhost:8080/hello/index.jsp。发现是http/1.1协议,而且还是8080端口,所以就交给了处理这一请求的”服务员(处理HTTP请求的<Connector>)”,”服务员”再把请求交给了”后厨(<Engine>)”,因为请求是要一盘水煮鱼,所以由”川菜区(<Host>)”负责,因为”大老王师傅<Context>“做水煮鱼最地道,所以由它完成。

  • <Connector>:关心请求中的http、和8080;
  • <Host>:关心localhost;
  • <Context>:关心hello
映射虚拟主机

我们的目标是,在浏览器中输出:http://www.itcast.cn就可以访问我们的项目。

完成这一目标,我们需要做三件事:

  • 修改端口号为80,这一点应该没有问题吧;

  • 在本机上可以解析域名为127.0.0.1,这需要修改C:\WINDOWS\system32\drivers\etc\hosts文件,添加对http://www.itcast.cn和127.0.01的绑定关系;

  • 在server.xml文件中添加一个<Host>(主机)。

1
2
<Host name="www.itcast.cn" appBase="F:/itcastapps" unpackWARs="true" autoDeploy="true">
</Host>
  • name="www.itcast.cn":指定虚拟主机名为www.itcast.cn
  • appBase=”F:/itcastapps”:指定当前虚拟主机的应用程序存放目录为F:/itcastapps。
  • 在itcastapps目录下创建名为ROOT的应用,因为一个主机只可以有一个名为ROOT的应用,名为ROOT的应用在浏览器中访问是可以不给出应用名称。

HTTP 协议

HTTP(hypertext transport protocol),即超文本传输协议。这个协议详细规定了浏览器和万维网服务器之间互相通信的规则。

HTTP就是一个通信规则,通信规则规定了客户端发送给服务器的内容格式,也规定了服务器发送给客户端的内容格式。其实我们要学习的就是这个两个格式!客户端发送给服务器的格式叫”请求协议”;服务器发送给客户端的格式叫”响应协议”。

URL 和 URI

URL:统一资源定位符,就是一个网址,例如:http://www.itcast.cn就是一个URL。/hello/index.html也是一个URL,URL必须是一个真实存在的网址。

URI:统一资源标识符:比URI包含了URL,URI的范围更加宽泛,URI可以是一个不存在的网址。在网络上用来标签资源的都是URI,例如zhangSan@163.com也是URI。

请求协议

请求协议的格式如下:

1
2
3
4
请求首行;  
请求头信息;
空行;
请求体。

浏览器发送给服务器的内容就这个格式的,如果不是这个格式服务器将无法解读!在HTTP协议中,请求有很多请求方法,其中最为常用的就是GET和POST。不同的请求方法之间的区别,后面会一点一点的介绍。

响应协议

响应协议的格式如下:

1
2
3
4
响应首行;
响应头信息;
空行;
响应体。

响应内容是由服务器发送给浏览器的内容,浏览器会根据响应内容来显示。


Day 04

28/06/2020

Servlet

Servlet是JavaWeb的三大组件之一,它属于动态资源。Servlet的作用是处理请求,服务器会把接收到的请求交给Servlet来处理,在Servlet中通常需要:

  • 接收请求数据;
  • 处理请求;
  • 完成响应。

  例如客户端发出登录请求,或者输出注册请求,这些请求都应该由Servlet来完成处理!Servlet需要我们自己来编写,每个Servlet必须实现javax.servlet.Servlet接口。

实现Servlet的方式

实现Servlet有三种方式:

  • 实现javax.servlet.Servlet接口;
  • 继承javax.servlet.GenericServlet类;
  • 继承javax.servlet.http.HttpServlet类;

通常我们会去继承HttpServlet类来完成我们的Servlet,但学习Servlet还要从javax.servlet.Servlet接口开始学习。

1
2
3
4
5
6
7
8
9
// Servlet.java
public interface Servlet {
public void init(ServletConfig config) throws ServletException;
public ServletConfig getServletConfig();
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
public String getServletInfo();
public void destroy();
}

创建应用

开始第一个Servlet应用!首先在webapps目录下创建helloservlet目录,它就是我们的应用目录了,然后在helloservlet目录中创建准备JavaWeb应用所需内容:

  • 创建/helloservlet/WEB-INF目录;

  • 创建/helloservlet/WEB-INF/classes目录;

  • 创建/helloservlet/WEB-INF/lib目录;

  • 创建/helloservlet/WEB-INF/web.xml文件;

接下来我们开始准备完成Servlet,完成Servlet需要分为两步:

  • 编写Servlet类;

  • 在web.xml文件中配置Servlet;

1
2
3
4
5
6
7
8
9
10
11
12
// HelloServlet.java
public class HelloServlet implements Servlet {
public void init(ServletConfig config) throws ServletException {}
public ServletConfig getServletConfig() {return null;}
public void destroy() {}
public String getServletInfo() {return null;}

public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {
System.out.println("hello servlet!");
}
}

我们暂时忽略Servlet中其他四个方法,只关心service()方法,因为它是用来处理请求的方法。我们在该方法内给出一条输出语句!

1
2
3
4
5
6
7
8
9
<!--web.xml 重要 背-->
<servlet>
<servlet-name>hello</servlet-name>
<servlet-class>cn.itcast.servlet.HelloServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello</servlet-name>
<url-pattern>/helloworld</url-pattern>
</servlet-mapping>

在web.xml中配置Servlet的目的其实只有一个,就是把访问路径与一个Servlet绑定到一起,上面配置是把访问路径:”/helloworld”与”cn.itcast.servlet.HelloServlet”绑定到一起。

  • <servlet>:指定HelloServlet这个Servlet的名称为hello;

  • <servlet-mapping>:指定/helloworld访问路径所以访问的Servlet名为hello。

  • <servlet><servlet-mapping>通过<servlet-name>这个元素关联在一起了!

接下来,我们编译HelloServlet,注意,编译HelloServlet时需要导入servlet-api.jar,因为Servlet.class等类都在servlet-api.jar中。

javac -classpath F:/tomcat6/lib/servlet-api.jar -d . HelloServlet.java

然后把HelloServlet.class放到/helloworld/WEB-INF/classes/目录下,然后启动Tomcat,在浏览器中访问:http://localhost:8080/helloservlet/helloworld即可在控制台上看到输出!

  • /helloservlet/WEB-INF/classes/cn/itcast/servlet/HelloServlet.class;

Servlet接口

Servlet 的生命周期

所谓xxx的生命周期,就是说xxx的出生、服务,以及死亡。Servlet生命周期也是如此!与Servlet的生命周期相关的方法有:

  • void init(ServletConfig);

  • void service(ServletRequest,ServletResponse);

  • void destroy();

    init

服务器会在Servlet第一次被访问时创建Servlet,或者是在服务器启动时创建Servlet。如果服务器启动时就创建Servlet,那么还需要在web.xml文件中配置。也就是说默认情况下,Servlet是在第一次被访问时由服务器创建的。

而且一个Servlet类型,服务器只创建一个实例对象,例如在我们首次访问http://localhost:8080/helloservlet/helloworld时,服务器通过”/helloworld”找到了绑定的Servlet名称为cn.itcast.servlet.HelloServlet,然后服务器查看这个类型的Servlet是否已经创建过,如果没有创建过,那么服务器才会通过反射来创建HelloServlet的实例。当我们再次访问http://localhost:8080/helloservlet/helloworld时,服务器就不会再次创建HelloServlet实例了,而是直接使用上次创建的实例。

在Servlet被创建后,服务器会马上调用Servlet的void init(ServletConfig)方法。请记住, Servlet出生后马上就会调用init()方法,而且一个Servlet的一生。这个方法只会被调用一次。这好比小孩子出生后马上就要去剪脐带一样,而且剪脐带一生只有一次。

我们可以把一些对Servlet的初始化工作放到init方法中!

service

当服务器每次接收到请求时,都会去调用Servlet的service()方法来处理请求。服务器接收到一次请求,就会调用service()方法一次,所以service()方法是会被调用多次的。正因为如此,所以我们才需要把处理请求的代码写到service()方法中!

destroy

Servlet是不会轻易离去的,通常都是在服务器关闭时Servlet才会离去!在服务器被关闭时,服务器会去销毁Servlet,在销毁Servlet之前服务器会先去调用Servlet的destroy()方法,我们可以把Servlet的临终遗言放到destroy()方法中,例如对某些资源的释放等代码放到destroy()方法中。

Servlet 接口相关类型

在Servlet接口中还存在三个我们不熟悉的类型:

  • ServletRequest:service() 方法的参数,它表示请求对象,它封装了所有与请求相关的数据,它是由服务器创建的;

  • ServletResponse:service()方法的参数,它表示响应对象,在service()方法中完成对客户端的响应需要使用这个对象;

  • ServletConfig:init()方法的参数,它表示Servlet配置对象,它对应Servlet的配置信息,那对应web.xml文件中的<servlet>元素。

ServletRequest和ServletResponse

ServletRequest和ServletResponse是Servlet#service() 方法的两个参数,一个是请求对象,一个是响应对象,可以从ServletRequest对象中获取请求数据,可以使用ServletResponse对象完成响应。你以后会发现,这两个对象就像是一对恩爱的夫妻,永远不分离,总是成对出现。

ServletRequest和ServletResponse的实例由服务器创建,然后传递给service()方法。如果在service() 方法中希望使用HTTP相关的功能,那么可以把ServletRequest和ServletResponse强转成HttpServletRequest和HttpServletResponse。这也说明我们经常需要在service()方法中对ServletRequest和ServletResponse进行强转,这是很心烦的事情。不过后面会有一个类来帮我们解决这一问题的。

HttpServletRequest方法:

  • String getParameter(String paramName):获取指定请求参数的值;

  • String getMethod():获取请求方法,例如GET或POST;

  • String getHeader(String name):获取指定请求头的值;

  • void setCharacterEncoding(String encoding):设置请求体的编码!因为GET请求没有请求体,所以这个方法只只对POST请求有效。当调用request.setCharacterEncoding("utf-8")之后,再通过getParameter()方法获取参数值时,那么参数值都已经通过了转码,即转换成了UTF-8编码。所以,这个方法必须在调用getParameter()方法之前调用!

HttpServletResponse方法:

  • PrintWriter getWriter():获取字符响应流,使用该流可以向客户端输出响应信息。例如response.getWriter().print("<h1>Hello JavaWeb!</h1>")

  • ServletOutputStream getOutputStream():获取字节响应流,当需要向客户端响应字节数据时,需要使用这个流,例如要向客户端响应图片;

  • void setCharacterEncoding(String encoding):用来设置字符响应流的编码,例如在调用setCharacterEncoding("utf-8");之后,再response.getWriter()获取字符响应流对象,这时的响应流的编码为utf-8,使用response.getWriter()输出的中文都会转换成utf-8编码后发送给客户端;

  • void setHeader(String name, String value):向客户端添加响应头信息,例如setHeader("Refresh", "3;url=http://www.itcast.cn"),表示3秒后自动刷新到http://www.itcast.cn

  • void setContentType(String contentType):该方法是setHeader("content-type", "xxx")的简便方法,即用来添加名为content-type响应头的方法。content-type响应头用来设置响应数据的MIME类型,例如要向客户端响应jpg的图片,那么可以setContentType("image/jepg"),如果响应数据为文本类型,那么还要台同时设置编码,例如setContentType("text/html;chartset=utf-8")表示响应数据类型为文本类型中的html类型,并且该方法会调用setCharacterEncoding("utf-8")方法;

  • void sendError(int code, String errorMsg):向客户端发送状态码,以及错误消息。例如给客户端发送404:response(404, "您要查找的资源不存在!")

ServletConfig

ServletConfig对象对应web.xml文件中的<servlet>元素。例如你想获取当前Servlet在web.xml文件中的配置名,那么可以使用servletConfig.getServletName()方法获取!

1
2
3
4
5
6
<servlet>
<servlet-name>One</servlet-name>
<servlet-class>
cn.itcast.servlet.OneServlet
</servlet-class>
</servlet>

ServletConfig对象是由服务器创建的,然后传递给Servlet的init()方法,你可以在init()方法中使用它!

  • String getServletName():获取Servlet在web.xml文件中的配置名称,即<servlet-name>指定的名称;

  • ServletContext getServletContext():用来获取ServletContext对象,ServletContext会在后面讲解;

  • String getInitParameter(String name):用来获取在web.xml中配置的初始化参数,通过参数名来获取参数值;

  • Enumeration getInitParameterNames():用来获取在web.xml中配置的所有初始化参数名称;

<servlet>元素中还可以配置初始化参数:

1
2
3
4
5
6
7
8
9
10
11
12
<servlet>
<servlet-name>One</servlet-name>
<servlet-class>cn.itcast.servlet.OneServlet</servlet-class>
<init-param>
<param-name>paramName1</param-name>
<param-value>paramValue1</param-value>
</init-param>
<init-param>
<param-name>paramName2</param-name>
<param-value>paramValue2</param-value>
</init-param>
</servlet>

在OneServlet中,可以使用ServletConfig对象的getInitParameter()方法来获取初始化参数,例如:

String value1 = servletConfig.getInitParameter("paramName1");//获取到paramValue1

GenericServlet

GenericServlet是Servlet接口的实现类,我们可以通过继承GenericServlet来编写自己的Servlet。下面是GenericServlet类的源代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
// GenericServlet.java
public abstract class GenericServlet implements Servlet, ServletConfig,
java.io.Serializable {
private static final long serialVersionUID = 1L;
private transient ServletConfig config;
public GenericServlet() {}
@Override
public void destroy() {}
@Override
public String getInitParameter(String name) {
return getServletConfig().getInitParameter(name);
}
@Override
public Enumeration<String> getInitParameterNames() {
return getServletConfig().getInitParameterNames();
}
@Override
public ServletConfig getServletConfig() {
return config;
}
@Override
public ServletContext getServletContext() {
return getServletConfig().getServletContext();
}
@Override
public String getServletInfo() {
return "";
}
@Override
public void init(ServletConfig config) throws ServletException {
this.config = config;
this.init();
}
public void init() throws ServletException {}
public void log(String msg) {
getServletContext().log(getServletName() + ": " + msg);
}
public void log(String message, Throwable t) {
getServletContext().log(getServletName() + ": " + message, t);
}
@Override
public abstract void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException;
@Override
public String getServletName() {
return config.getServletName();
}
}

在GenericServlet中,定义了一个ServletConfig config实例变量,并在init(ServletConfig)方法中把参数ServletConfig赋给了实例变量。然后在该类的很多方法中使用了实例变量config。

如果子类覆盖了GenericServlet的init(StringConfig)方法,那么this.config=config这一条语句就会被覆盖了,也就是说GenericServlet的实例变量config的值为null,那么所有依赖config的方法都不能使用了。如果真的希望完成一些初始化操作,那么去覆盖GenericServlet提供的init()方法,它是没有参数的init()方法,它会在init(ServletConfig)方法中被调用。

GenericServlet还实现了ServletConfig接口,所以可以直接调用getInitParameter()、getServletContext()等ServletConfig的方法。

HttpServlet

HttpServlet类是GenericServlet的子类,它提供了对HTTP请求的特殊支持,所以通常我们都会通过继承HttpServlet来完成自定义的Servlet。

HttpServlet类中提供了service(HttpServletRequest,HttpServletResponse)方法,这个方法是HttpServlet自己的方法,不是从Servlet继承来的。在HttpServlet的service(ServletRequest,ServletResponse)方法中会把ServletRequest和ServletResponse强转成HttpServletRequest和HttpServletResponse,然后调用service(HttpServletRequest,HttpServletResponse)方法,这说明子类可以去覆盖service(HttpServletRequest,HttpServletResponse)方法即可,这就不用自己去强转请求和响应对象了。

其实子类也不用去覆盖service(HttpServletRequest,HttpServletResponse)方法,因为HttpServlet还要做另一步简化操作,下面会介绍。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// HttpServlet.java
public abstract class HttpServlet extends GenericServlet {
protected void service(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
……
}
@Override
public void service(ServletRequest req, ServletResponse res)
throws ServletException, IOException {

HttpServletRequest request;
HttpServletResponse response;

try {
request = (HttpServletRequest) req;
response = (HttpServletResponse) res;
} catch (ClassCastException e) {
throw new ServletException("non-HTTP request or response");
}
service(request, response);
}
……
}

doGet()和doPost()

在HttpServlet的service(HttpServletRequest,HttpServletResponse)方法会去判断当前请求是GET还是POST,如果是GET请求,那么会去调用本类的doGet()方法,如果是POST请求会去调用doPost()方法,这说明我们在子类中去覆盖doGet()或doPost()方法即可。

Servlet 细节

Servlet与线程安全

因为一个类型的Servlet只有一个实例对象,那么就有可能会现时出一个Servlet同时处理多个请求,那么Servlet是否为线程安全的呢?答案是:”不是线程安全的”。这说明Servlet的工作效率很高,但也存在线程安全问题!

所以我们不应该在Servlet中随意创建成员变量,因为可能会存在一个线程对这个成员变量进行写操作,另一个线程对这个成员变量进行读操作。

让服务器在启动时就创建Servlet

默认情况下,服务器会在某个Servlet第一次收到请求时创建它。也可以在web.xml中对Servlet进行配置,使服务器启动时就创建Servlet。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<servlet>
<servlet-name>hello1</servlet-name>
<servlet-class>cn.itcast.servlet.Hello1Servlet</servlet-class>
<load-on-startup>0</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello1</servlet-name>
<url-pattern>/hello1</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>hello2</servlet-name>
<servlet-class>cn.itcast.servlet.Hello2Servlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello2</servlet-name>
<url-pattern>/hello2</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>hello3</servlet-name>
<servlet-class>cn.itcast.servlet.Hello3Servlet</servlet-class>
<load-on-startup>2</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>hello3</servlet-name>
<url-pattern>/hello3</url-pattern>
</servlet-mapping>

<servlet>元素中配置<load-on-startup>元素可以让服务器在启动时就创建该Servlet,其中<load-on-startup>元素的值必须是大于等于0的整数,它的使用是服务器启动时创建Servlet的顺序。上例中,根据<load-on-startup>的值可以得知服务器创建Servlet的顺序为Hello1Servlet、Hello2Servlet、Hello3Servlet。

<url-pattern>

<url-pattern><servlet-mapping>的子元素,用来指定Servlet的访问路径,即URL。它必须是以”/“开头!

1)可以在<servlet-mapping>中给出多个<url-pattern>,例如:

1
2
3
4
5
<servlet-mapping>
<servlet-name>AServlet</servlet-name>
<url-pattern>/AServlet</url-pattern>
<url-pattern>/BServlet</url-pattern>
</servlet-mapping>

那么这说明一个Servlet绑定了两个URL,无论访问/AServlet还是/BServlet,访问的都是AServlet。

2)还可以在<url-pattern>中使用通配符,所谓通配符就是星号”*“,星号可以匹配任何URL前缀或后缀,使用通配符可以命名一个Servlet绑定一组URL,例如:

  • <url-pattern>/servlet/*<url-patter>:/servlet/a、/servlet/b,都匹配/servlet/*;

  • <url-pattern>*.do</url-pattern>:/abc/def/ghi.do、/a.do,都匹配*.do;

  • <url-pattern>/*<url-pattern>:匹配所有URL;

请注意,通配符要么为前缀,要么为后缀,不能出现在URL中间位置,也不能只有通配符。例如:/*.do就是错误的,因为星号出现在URL的中间位置上了。.也是不对的,因为一个URL中最多只能出现一个通配符。

注意,通配符是一种模糊匹配URL的方式,如果存在更具体的<url-pattern>,那么访问路径会去匹配具体的<url-pattern>。例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<servlet>
<servlet-name>hello1</servlet-name>
<servlet-class>cn.itcast.servlet.Hello1Servlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello1</servlet-name>
<url-pattern>/servlet/hello1</url-pattern>
</servlet-mapping>
<servlet>
<servlet-name>hello2</servlet-name>
<servlet-class>cn.itcast.servlet.Hello2Servlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>hello2</servlet-name>
<url-pattern>/servlet/*</url-pattern>
</servlet-mapping>

当访问路径为http://localhost:8080/hello/servlet/hello1时,因为访问路径即匹配hello1的<url-pattern>,又匹配hello2的<url-pattern>,但因为hello1的<url-pattern>中没有通配符,所以优先匹配,即设置hello1。

web.xml文件的继承(了解)

每个完整的JavaWeb应用中都需要有web.xml,但我们不知道所有的web.xml文件都有一个共同的父文件,它在Tomcat的conf/web.xml路径。

ServletContext

服务器会为每个应用创建一个ServletContext对象:

  • ServletContext对象的创建是在服务器启动时完成的;

  • ServletContext对象的销毁是在服务器关闭时完成的。

ServletContext对象的作用是在整个Web应用的动态资源之间共享数据!例如在AServlet中向ServletContext对象中保存一个值,然后在BServlet中就可以获取这个值,这就是共享数据了。

获取ServletContext

在Servlet中获取ServletContext对象:

  • 在void init(ServletConfig config)中:ServletContext context = config.getServletContext();,ServletConfig类的getServletContext()方法可以用来获取ServletContext对象;

  • 在GenericeServlet或HttpServlet中获取ServletContext对象:

  • GenericServlet类有getServletContext()方法,所以可以直接使用this.getServletContext()来获取;

1
2
3
4
5
6
7
8
9
10
11
public class MyServlet implements Servlet {
public void init(ServletConfig config) {
ServletContext context = config.getServletContext();
}

}
public class MyServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) {
ServletContext context = this.getServletContext();
}
}

域对象的功能

ServletContext是JavaWeb四大域对象之一:

  • PageContext;

  • ServletRequest;

  • HttpSession;

  • ServletContext;

所有域对象都有存取数据的功能,因为域对象内部有一个Map,用来存储数据,下面是ServletContext对象用来操作数据的方法:

  • void setAttribute(String name, Object value):用来存储一个对象,也可以称之为存储一个域属性,例如:servletContext.setAttribute(“xxx”, “XXX”),在ServletContext中保存了一个域属性,域属性名称为xxx,域属性的值为XXX。请注意,如果多次调用该方法,并且使用相同的name,那么会覆盖上一次的值,这一特性与Map相同;

  • Object getAttribute(String name):用来获取ServletContext中的数据,当前在获取之前需要先去存储才行,例如:String value = (String)servletContext.getAttribute(“xxx”);,获取名为xxx的域属性;

  • void removeAttribute(String name):用来移除ServletContext中的域属性,如果参数name指定的域属性不存在,那么本方法什么都不做;

  • Enumeration getAttributeNames():获取所有域属性的名称;

获取应用初始化参数

还可以使用ServletContext来获取在web.xml文件中配置的应用初始化参数!注意,应用初始化参数与Servlet初始化参数不同:

1
2
3
4
5
6
7
8
9
10
11
<web-app ...>
...
<context-param>
<param-name>paramName1</param-name>
<param-value>paramValue1</param-value>
</context-param>
<context-param>
<param-name>paramName2</param-name>
<param-value>paramValue2</param-value>
</context-param>
</web-app>
1
2
3
4
5
6
7
8
9
ServletContext context = this.getServletContext();
String value1 = context.getInitParameter("paramName1");
String value2 = context.getInitParameter("paramName2");
System.out.println(value1 + ", " + value2);

Enumeration names = context.getInitParameterNames();
while(names.hasMoreElements()) {
System.out.println(names.nextElement());
}

获取资源相关方法

获取真实路径

还可以使用ServletContext对象来获取Web应用下的资源,例如在hello应用的根目录下创建a.txt文件,现在想在Servlet中获取这个资源,就可以使用ServletContext来获取。

  • 获取a.txt的真实路径:String realPath = servletContext.getRealPath(“/a.txt”),realPath的值为a.txt文件的绝对路径:F:\tomcat6\webapps\hello\a.txt;

  • 获取b.txt的真实路径:String realPath = servletContext.getRealPath(“/WEB-INF/b.txt”);

获取资源流

不只可以获取资源的路径,还可以通过ServletContext获取资源流,即把资源以输入流的方式获取:

  • 获取a.txt资源流:InputStream in = servletContext.getResourceAsStream(“/a.txt”);

  • 获取b.txt资源流:InputStream in = servletContext.getResourceAsStream(“/WEB-INF/b.txt”);

获取指定目录下所有资源路径

还可以使用ServletContext获取指定目录下所有资源路径,例如获取/WEB-INF下所有资源的路径:

1
2
Set set = context.getResourcePaths("/WEB-INF");
System.out.println(set);

访问量统计

1
2
3
4
5
6
7
8
9
10
ServletContext application  = this.getServletContext();	// 获取ServletContext对象
Integer count = (Integer)application.getAttribute("count"); // 获取对象的count属性
if(count == null) { // 如果不存在 则创建访问量 并置1
count = 1;
} else {
count++;
}
response.setContentType("text/html;charset=utf-8");
response.getWriter().print("<h1>本页面一共被访问" + count + "次!</h1>"); // 向客户端返访问次数
application.setAttribute("count", count); // 保存

获取类路径资源

这里要讲的是获取类路径下的资源,对于JavaWeb应用而言,就是获取classes目录下的资源。

1
2
3
4
5
InputStream in = this.getClass().getResourceAsStream("/xxx.txt");
System.out.println(IOUtils.toString(in));
// or
InputStream in = this.getClass().getClassLoader().getResourceAsStream("xxx.txt");
System.out.println(IOUtils.toString(in));
  • Class类的getResourceAsStream(String path):

    • 路径以”/“开头,相对classes路径;
    • 路径不以”/“开头,相对当前class文件所有路径,例如在cn.itcast.servlet.MyServlet中执行,那么相对/classes/cn/itcast/servlet/路径;
  • ClassLoader类的getResourceAsStream(String path):

    • 相对classes路径;

Day 05

29/06/2020

2.png

response

response是Servlet.service方法的一个参数,类型为javax.servlet.http.HttpServletResponse。在客户端发出每个请求时,服务器都会创建一个response对象,并传入给Servlet.service()方法。response对象是用来对客户端进行响应的,这说明在service()方法中使用response对象可以完成对客户端的响应工作。

response对象的功能分为以下四种:

  • 设置响应头信息;
  • 发送状态码;
  • 设置响应正文;
  • 重定向;

响应正文

response是响应对象,向客户端输出响应正文(响应体)可以使用response的响应流,repsonse一共提供了两个响应流对象:

  • PrintWriter out = response.getWriter():获取字符流;

  • ServletOutputStream out = response.getOutputStream():获取字节流;

当然,如果响应正文内容为字符,那么使用response.getWriter(),如果响应内容是字节,例如下载时,那么可以使用response.getOutputStream()。

注意,在一个请求中,不能同时使用这两个流!也就是说,要么你使用repsonse.getWriter(),要么使用response.getOutputStream(),但不能同时使用这两个流。不然会抛出IllegalStateException异常。

字符流
  • 字符编码

在使用response.getWriter()时需要注意默认字符编码为ISO-8859-1,如果希望设置字符流的字符编码为utf-8,可以使用response.setCharaceterEncoding(“utf-8”)来设置。这样可以保证输出给客户端的字符都是使用UTF-8编码的!

但客户端浏览器并不知道响应数据是什么编码的!如果希望通知客户端使用UTF-8来解读响应数据,那么还是使用response.setContentType(“text/html;charset=utf-8”)方法比较好,因为这个方法不只会调用response.setCharaceterEncoding(“utf-8”),还会设置content-type响应头,客户端浏览器会使用content-type头来解读响应数据。

  • 缓冲区

response.getWriter()是PrintWriter类型,所以它有缓冲区,缓冲区的默认大小为8KB。也就是说,在响应数据没有输出8KB之前,数据都是存放在缓冲区中,而不会立刻发送到客户端。当Servlet执行结束后,服务器才会去刷新流,使缓冲区中的数据发送到客户端。

如果希望响应数据马上发送给客户端:

  • 向流中写入大于8KB的数据;
  • 调用response.flushBuffer()方法来手动刷新缓冲区;

设置响应头

可以使用response对象的setHeader()方法来设置响应头!使用该方法设置的响应头最终会发送给客户端浏览器!

  • response.setHeader(“content-type”, “text/html;charset=utf-8”):设置content-type响应头,该头的作用是告诉浏览器响应内容为html类型,编码为utf-8。而且同时会设置response的字符流编码为utf-8,即response.setCharaceterEncoding(“utf-8”);

  • response.setHeader("Refresh","5; URL=http://www.itcast.cn"):5秒后自动跳转到传智主页。

设置状态码及其他方法

  • response.setContentType(“text/html;charset=utf-8”):等同与调用response.setHeader(“content-type”, “text/html;charset=utf-8”);

  • response.setCharacterEncoding(“utf-8”):设置字符响应流的字符编码为utf-8;

  • response.setStatus(200):设置状态码;

  • response.sendError(404, “您要查找的资源不存在”):当发送错误状态码时,Tomcat会跳转到固定的错误页面去,但可以显示错误信息。

重定向

当你访问http://www.sun.com时,你会发现浏览器地址栏中的URL会变成http://www.oracle.com/us/sun/index.html,这就是重定向了。

重定向是服务器通知浏览器去访问另一个地址,即再发出另一个请求。

响应码为200表示响应成功,而响应码为302表示重定向。所以完成重定向的第一步就是设置响应码为302。

因为重定向是通知浏览器再第二个请求,所以浏览器需要知道第二个请求的URL,所以完成重定向的第二步是设置Location头,指定第二个请求的URL地址。

1
2
3
4
5
6
7
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setStatus(302);
response.setHeader("Location", "http://www.itcast.cn");
}
}

上面代码的作用是:当访问AServlet后,会通知浏览器重定向到传智主页。客户端浏览器解析到响应码为302后,就知道服务器让它重定向,所以它会马上获取响应头Location,然后发出第二个请求。

更便捷的重定向
1
2
3
4
5
6
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect("http://www.itcast.cn");
}
}

response.sendRedirect()方法会设置响应头为302,以设置Location响应头。

如果要重定向的URL是在同一个服务器内,那么可以使用相对路径,例如:

1
2
3
4
5
6
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.sendRedirect("/hello/BServlet");
}
}

重定向的URL地址为:http://localhost:8080/hello/BServlet

小结
  • 重定向是两次请求;
  • 重定向的URL可以是其他应用,不局限于当前应用;
  • 重定向的响应头为302,并且必须要有Location响应头;
  • 重定向就不要再使用response.getWriter()或response.getOutputStream()输出数据,不然可能会出现异常;

request

request是Servlet.service()方法的一个参数,类型为javax.servlet.http.HttpServletRequest。在客户端发出每个请求时,服务器都会创建一个request对象,并把请求数据封装到request中,然后在调用Servlet.service()方法时传递给service()方法,这说明在service()方法中可以通过request对象来获取请求数据。

request的功能可以分为以下几种:

  • 封装了请求头数据;

  • 封装了请求正文数据,如果是GET请求,那么就没有正文;

  • request是一个域对象,可以把它当成Map来添加获取数据;

  • request提供了请求转发和请求包含功能。

域方法

request是域对象!在JavaWeb中一共四个域对象,其中ServletContext就是域对象,它在整个应用中只创建一个ServletContext对象。request其中一个,request可以在一个请求中共享数据。

一个请求会创建一个request对象,如果在一个请求中经历了多个Servlet,那么多个Servlet就可以使用request来共享数据。现在我们还不知道如何在一个请求中经历多个Servlet,后面在学习请求转发和请求包含后就知道了。

下面是request的域方法:

  • void setAttribute(String name, Object value):用来存储一个对象,也可以称之为存储一个域属性,例如:servletContext.setAttribute(“xxx”, “XXX”),在request中保存了一个域属性,域属性名称为xxx,域属性的值为XXX。请注意,如果多次调用该方法,并且使用相同的name,那么会覆盖上一次的值,这一特性与Map相同;

  • Object getAttribute(String name):用来获取request中的数据,当前在获取之前需要先去存储才行,例如:String value = (String)request.getAttribute(“xxx”);,获取名为xxx的域属性;

  • void removeAttribute(String name):用来移除request中的域属性,如果参数name指定的域属性不存在,那么本方法什么都不做;

  • Enumeration getAttributeNames():获取所有域属性的名称;

request获取请求头数据

request与请求头相关的方法有:

  • String getHeader(String name):获取指定名称的请求头;

  • Enumeration getHeaderNames():获取所有请求头名称;

  • int getIntHeader(String name):获取值为int类型的请求头。

request其他方法

request中还提供了与请求相关的其他方法,有些方法是为了我们更加便捷的方法请求头数据而设计,有些是与请求URL相关的方法。

  • int getContentLength():获取请求体的字节数,GET请求没有请求体,没有请求体返回-1;

  • String getContentType():获取请求类型,如果请求是GET,那么这个方法返回null;如果是POST请求,那么默认为application/x-www-form-urlencoded,表示请求体内容使用了URL编码;

  • String getMethod():返回请求方法,例如:GET

  • Locale getLocale():返回当前客户端浏览器的Locale。java.util.Locale表示国家和言语,这个东西在国际化中很有用;

  • String getCharacterEncoding():获取请求体编码,如果没有setCharacterEncoding(),那么返回null,表示使用ISO-8859-1编码;

  • void setCharacterEncoding(String code):设置请求编码,只对请求体有效!注意,对于GET而言,没有请求体!!!所以此方法只能对POST请求中的参数有效!

  • String getContextPath():返回上下文路径,例如:/hello

  • String getQueryString():返回请求URL中的参数,例如:name=zhangSan

  • String getRequestURI():返回请求URI路径,例如:/hello/oneServlet

  • StringBuffer getRequestURL():返回请求URL路径,例如:http://localhost/hello/oneServlet,即返回除了参数以外的路径信息;

  • String getServletPath():返回Servlet路径,例如:/oneServlet

  • String getRemoteAddr():返回当前客户端的IP地址;

  • String getRemoteHost():返回当前客户端的主机名,但这个方法的实现还是获取IP地址;

  • String getScheme():返回请求协议,例如:http;

  • String getServerName():返回主机名,例如:localhost

  • int getServerPort():返回服务器端口号,例如:8080

2.png

eg. 可以使用request.getRemoteAddr()方法获取客户端的IP地址,然后判断IP是否为禁用IP。

1
2
3
4
5
6
7
String ip = request.getRemoteAddr();
System.out.println(ip);
if(ip.equals("127.0.0.1")) {
response. getWriter().print("您的IP已被禁止!");
} else {
response.getWriter().print("Hello!");
}

request获取请求参数

最为常见的客户端传递参数方式有两种:

  • 浏览器地址栏直接输入:一定是GET请求;

  • 超链接:一定是GET请求;

  • 表单:可以是GET,也可以是POST,这取决与<form>的method属性值;

GET请求和POST请求的区别:

  • GET请求:

    • 请求参数会在浏览器的地址栏中显示,所以不安全;

    • 请求参数长度限制长度在1K之内;

    • GET请求没有请求体,无法通过request.setCharacterEncoding()来设置参数的编码;

  • POST请求:

    • 请求参数不会显示浏览器的地址栏,相对安全;

    • 请求参数长度没有限制;

1
2
3
4
5
6
7
<a href="/hello/ParamServlet?p1=v1&p2=v2">超链接</a>
<hr/>
<form action="/hello/ParamServlet" method="post">
参数1:<input type="text" name="p1"/><br/>
参数2:<input type="text" name="p2"/><br/>
<input type="submit" value="提交"/>
</form>

下面是使用request获取请求参数的API:

  • String getParameter(String name):通过指定名称获取参数值;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String v1 = request.getParameter("p1");
String v2 = request.getParameter("p2");
System.out.println("p1=" + v1);
System.out.println("p2=" + v2);
}

public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String v1 = request.getParameter("p1");
String v2 = request.getParameter("p2");
System.out.println("p1=" + v1);
System.out.println("p2=" + v2);
}
  • String[] getParameterValues(String name):当多个参数名称相同时,可以使用方法来获取;

  • Enumeration getParameterNames():获取所有参数的名字;

  • Map getParameterMap():获取所有参数封装到Map中,其中key为参数名,value为参数值,因为一个参数名称可能有多个值,所以参数值是String[],而不是String。

请求转发与请求包含

无论是请求转发还是请求包含,都表示由多个Servlet共同来处理一个请求。例如Servlet1来处理请求,然后Servlet1又转发给Servlet2来继续处理这个请求。

请求转发

在AServlet中,把请求转发到BServlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("AServlet");
RequestDispatcher rd = request.getRequestDispatcher("/BServlet");
rd.forward(request, response);
}
}
// --------------------------------------------------------------------------------
public class BServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("BServlet");
}
}
请求包含

在AServlet中,把请求包含到BServlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("AServlet");
RequestDispatcher rd = request.getRequestDispatcher("/BServlet");
rd.include (request, response);
}
}
// ----------------------------------------------------------------------------------
public class BServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
System.out.println("BServlet");
}
}
请求转发与请求包含比较
  • 如果在AServlet中请求转发到BServlet,那么在AServlet中就不允许再输出响应体,即不能再使用response.getWriter()和response.getOutputStream()向客户端输出,这一工作应该由BServlet来完成;如果是使用请求包含,那么没有这个限制;

  • 请求转发虽然不能输出响应体,但还是可以设置响应头的,例如:response.setContentType(“text/html;charset=utf-8”);

  • 请求包含大多是应用在JSP页面中,完成多页面的合并;

  • 请求转发大多是应用在Servlet中,转发目标大多是JSP页面;

2.png

请求转发与重定向比较
  • 请求转发是一个请求,而重定向是两个请求;

  • 请求转发后浏览器地址栏不会有变化,而重定向会有变化,因为重定向是两个请求;

  • 请求转发的目标只能是本应用中的资源,重定向的目标可以是其他应用;

  • 请求转发对AServlet和BServlet的请求方法是相同的,即要么都是GET,要么都是POST,因为请求转发是一个请求;

  • 重定向的第二个请求一定是GET;

路径

与路径相关的操作

  • 超链接

  • 表单

  • 转发

  • 包含

  • 重定向

  • <url-pattern>

  • ServletContext获取资源

  • Class获取资源

  • ClassLoader获取资源

客户端路径

超链接、表单、重定向都是客户端路径,客户端路径可以分为三种方式:

  • 绝对路径;

  • 以”/“开头的相对路径;

  • 不以”/“开头的相对路径;

例如:http://localhost:8080/hello1/pages/a.html中的超链接和表单如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
绝对路径:<a href="http://localhost:8080/hello2/index.html">链接1</a>
客户端路径:<a href="/hello3/pages/index.html">链接2</a>
相对路径:<a href="index.html">链接3</a>
<hr/>
绝对路径:
<form action="http://localhost:8080/hello2/index.html">
<input type="submit" value="表单1"/>
</form>
客户端路径:
<form action="/hello2/index.html">
<input type="submit" value="表单2"/>
</form>
相对路径:
<form action="index.html">
<input type="submit" value="表单3"/>
</form>
  • 链接1和表单1:没什么可说的,它使用绝对路径;

  • 链接2和表单2:以”/“开头,相对主机,与当前a.html的主机相同,即最终访问的页面为http://localhost:8080/hello2/index.html

  • 链接3和表单3:不以”/“开头,相对当前页面的路径,即a.html所有路径,即最终访问的路径为:http://localhost:8080/hello1/pages/index.html

建议使用”/“

强烈建议使用”/“开头的路径,这说明在页面中的超链接和表单都要以”/“开头,后面是当前应用的名称,再是访问路径:

<form action="/hello/servlet/AServlet">

</form>

<a href="/hello/b.html">链接</a>

其中/hello是当前应用名称,这也说明如果将来修改了应用名称,那么页面中的所有路径也要修改,这一点确实是个问题。这一问题的处理方案会在学习了JSP之后讲解!

在Servlet中的重定向也建议使用”/“开头。同理,也要给出应用的名称!例如:

1
response.sendRedirect("/hello/BServlet");

其中/hello是当前应用名,如果将来修改了应用名称,那么也要修改所有重定向的路径,这一问题的处理方案是使用request.getContextPath()来获取应用名称。

1
response.sendRedirect(request.getContextPath() + "/BServlet");

服务器端路径

服务器端路径必须是相对路径,不能是绝对路径。但相对路径有两种形式:

  • 以”/“开头;

  • 不以”/“开头;

其中请求转发、请求包含都是服务器端路径,服务器端路径与客户端路径的区别是:

  • 客户端路径以”/“开头:相对当前主机;

  • 服务器端路径以”/“开头:相对当前应用;

1
2
3
4
5
6
7
// eg1
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.getRequestDispatcher("/BServlet").forward(request, response);
}
}

假设访问AServlet的路径为:http://localhost:8080/hello/servlet/AServlet

因为路径以”/“开头,所以相对当前应用,即http://localhost:8080/hello/BServlet

1
2
3
4
5
6
7
// eg2
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.getRequestDispatcher("BServlet").forward(request, response);
}
}

假设访问AServlet的路径为:http://localhost:8080/hello/servlet/AServlet

因为路径不以”/“开头,所以相对当前应用,即http://localhost:8080/hello/servlet/BServlet

<url-pattern>路径

<url-pattern>必须使用”/“开头,并且相对的是当前应用。

ServletContext获取资源

必须是相对路径,可以”/“开头,也可以不使用”/“开头,但无论是否使用”/“开头都是相对当前应用路径。

例如在AServlet中获取资源,AServlet的路径路径为:http://localhost:8080/hello/servlet/AServlet

1
2
3
4
5
6
7
8
9
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String path1 = this.getServletContext().getRealPath("a.txt");
String path2 = this.getServletContext().getRealPath("/a.txt");
System.out.println(path1);
System.out.println(path2);
}
}

path1和path2是相同的结果:http://localhost:8080/hello/a.txt

Class获取资源

Class获取资源也必须是相对路径,可以”/“开头,也可以不使用”/“开头。

1
2
3
4
5
6
7
8
9
10
11
12
13
package cn.itcast;

import java.io.InputStream;

public class Demo {
public void fun1() {
InputStream in = Demo.class.getResourceAsStream("/a.txt");
}

public void fun2() {
InputStream in = Demo.class.getResourceAsStream("a.txt");
}
}

其中fun1()方法获取资源时以”/“开头,那么相对的是当前类路径,即/hello/WEB-INF/classes/a.txt文件;

其中fun2()方法获取资源时没有以”/“开头,那么相对当前Demo.class所在路径,因为Demo类在cn.itcast包下,所以资源路径为:/hello/WEB-INF/classes/cn/itcast/a.txt。

ClassLoader获取资源

ClassLoader获取资源也必须是相对路径,可以”/“开头,也可以不使用”/“开头。但无论是否以”/“开头,资源都是相对当前类路径。

1
2
3
4
5
6
7
8
9
public class Demo {
public void fun1() {
InputStream in = Demo.class.getClassLoader().getResourceAsStream("/a.txt");
}

public void fun2() {
InputStream in = Demo.class.getClassLoader().getResourceAsStream("a.txt");
}
}

fun1()和fun2()方法的资源都是相对类路径,即classes目录,即/hello/WEB-INF/classes/a.txt

编码

请求编码

直接在地址栏中给出中文

请求数据是由客户端浏览器发送服务器的,请求数据的编码是由浏览器决定的。例如在浏览器地址栏中给出:http://localhost:8080/hello/AServlet?name=传智,那么其中”传智”是什么编码的呢?不同浏览器使用不同的编码,所以这是不确定的!

  • IE:使用GB2312;

  • FireFox:使用GB2312;

  • Chrome:使用UTF-8;

通常没有哪个应用要求用户在浏览器地址栏中输入请求数据的,所以大家只需了解一下即可。

在页面中发出请求

通常向服务器发送请求数据都需要先请求一个页面,然后用户在页面中输入数据。页面中有超链接和表单,通过超链接和表单就可以向服务器发送数据了。

因为页面是服务器发送到客户端浏览器的,所以这个页面本身的编码由服务器决定。而用户在页面中输入的数据也是由页面本身的编码决定的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<title>index.html</title>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
</head>

<body>
<form action="/hello/servlet/AServlet">
名称:<input type="text" name="name"/>
<input type="submit" value="提交"/>
</form>
<a href="/hello/servlet/AServlet?name=传智">链接</a>
</body>
</html>

当用户在index.html页面中输入数据时,都是UTF-8列表的。因为这个页面本身就是UTF-8编码的!

页面的编译就是页面中输入数据的编码。

GET请求解读编码

当客户端通过GET请求发送数据给服务器时,使用request.getParameter()获取的数据是被服务器误认为ISO-8859-1编码的,也就是说客户端发送过来的数据无论是UTF-8还是GBK,服务器都认为是ISO-8859-1,这就说明我们需要在使用request.getParameter()获取数据后,再转发成正确的编码。

例如客户端以UTF-8发送的数据,使用如下转码方式:

String name = request.getParameter(“name”);

name = new String(name.getBytes(“iso-8859-1”), “utf-8”);

POST请求解读编码

当客户端通过POST请求发送数据给服务器时,可以在使用request.getParameter()获取请求参数之前先通过request.setCharacterEncoding()来指定编码,然后再使用reuqest.getParameter()方法来获取请求参数,那么就是用指定的编码来读取了。

也就是说,如果是POST请求,服务器可以指定编码!但如果没有指定编码,那么默认还是使用ISO-8859-1来解读。

request.setCharacterEncoding(“utf-8”);

String name = request.getParameter(“name”);

响应编码

响应:服务器发送给客户端数据!响应是由response对象来完成,如果响应的数据不是字符数据,那么就无需去考虑编码问题。当然,如果响应的数据是字符数据,那么就一定要考虑编码的问题了。

response.getWriter().print(“传智”);

上面代码因为没有设置repsonse.getWriter()字符流的编码,所以服务器使用默认的编码(ISO-8859-1)来处理,因为ISO-8859-1不支持中文,所以一定会出现乱码的。

所以在使用response.getWriter()发送数据之前,一定要设置response.getWriter()的编码,这需要使用response.setCharacterEncoding()方法:

response.setCharacterEncoding(“utf-8”);

response.getWriter().print(“传智”);

上面代码因为在使用response.getWriter()输出之前已经设置了编码,所以输出的数据为utf-8编码。但是,因为没有告诉浏览器使用什么编码来读取响应数据,所以很可能浏览器会出现错误的解读,那么还是会出现乱码的。当然,通常浏览器都支持来设置当前页面的编码,如果用户在看到编码时,去设置浏览器的编码,如果设置的正确那么乱码就会消失。但是我们不能让用户总去自己设置编码,而且应该直接通知浏览器,服务器发送过来的数据是什么编码,这样浏览器就直接使用服务器告诉他的编码来解读!这需要使用content-type响应头。

response.setContentType(“text/html;charset=utf-8”);

response.getWriter().print(“传智”);

上面代码使用setContentType()方法设置了响应头content-type编码为utf-8,这不只是在响应中添加了响应头,还等于调用了一次response.setCharacterEncoding(“utf-8”),也就是说,通过我们只需要调用一次response.setContentType(“text/html;charset=utf-8”)即可,而无需再去调用response.setCharacterEncoding(“utf-8”)了。

在静态页面中,使用<meta>来设置content-type响应头,例如:

<meta http-equiv="content-type" content="text/html; charset=UTF-8">

URL编码

通过页面传输数据给服务器时,如果包含了一些特殊字符是无法发送的。这时就需要先把要发送的数据转换成URL编码格式,再发送给服务器。

其实需要我们自己动手给数据转换成URL编码的只有GET超链接,因为表单发送数据会默认使用URL编码,也就是说,不用我们自己来编码。

例如:”传智”这两个字通过URL编码后得到的是:”%E4%BC%A0%E6%99%BA”。URL编码是先需要把”传智”转换成字节,例如我们现在使用UTF-8把”传智”转换成字符,得到的结果是:”[-28, -68, -96, -26, -103, -70]”,然后再把所有负数加上256,得到[228, 188, 160, 230, 153, 186],再把每个int值转换成16进制,得到[E4, BC, A0, E6, 99, BA],最后再每个16进制的整数前面加上”%”。

通过URL编码,把”传智”转换成了”%E4%BC%A0%E6%99%BA”,然后发送给服务器!服务器会自动识别出数据是使用URL编码过的,然后会自动把数据转换回来。

当然,在页面中我们不需要自己去通过上面的过程把”传智”转换成”%E4%BC%A0%E6%99%BA”,而是使用Javascript来完成即可。当后面我们学习了JSP后,就不用再使用Javascript了。

1
2
3
4
5
<script type="text/javascript">
function _go() {
location = "/day05_2/AServlet?name=" + encodeURIComponent("传智+播客");
}
</script>

因为URL默认只支持ISO-8859-1,这说明在URL中出现中文和一些特殊字符可能无法发送到服务器。所以我们需要对包含中文或特殊字符的URL进行URL编码。

服务器会自动识别数据是否使用了URL编码,如果使用了服务器会自动把数据解码,无需我们自己动手解码。

String s = “传智”;

s = URLEncoder.encode(s, “utf-8”);// %E4%BC%A0%E6%99%BA

s = URLDecoderdecode(s, “utf-8”);//传智


Day 06

30/06/2020

JSP

JSP(Java Server Pages)是JavaWeb服务器端的动态资源。它与html页面的作用是相同的,显示数据和获取数据

JSP = html + Java脚本(代码片段) + JSP动作标签

JSP语法

JSP 脚本

JSP脚本就是Java代码片段,它分为三种:

  • <%...%>:Java语句;

  • <%=…%>:Java表达式;

  • <%!...%>:Java定义类成员;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>JSP演示</title>
</head>

<body>
<h1>JSP演示</h1>
<%
// Java语句
String s1 = "hello jsp";
// 不会输出到客户端,而是在服务器端的控制台打印
System.out.println(s1);
%>
<!-- 输出到客户端浏览器上 -->
输出变量:<%=s1 %><br/>
输出int类型常量:<%=100 %><br/>
输出String类型常量:<%="你好" %><br/>
<br/>
使用表达式输出常量是很傻的一件事,因为可以直接使用html即可,下面是输出上面的常量:<br/>
100<br/>
你好
</body>
</html>
内置对象 out

out对象在JSP页面中无需创建就可以使用,它的作用是用来向客户端输出。

1
2
3
4
5
6
7
8
<body>
<h1>out.jsp</h1>
<%
//向客户端输出
out.print("你好!");

%>
</body>

其中<%=…%>out.print()功能是相同的!它们都是向客户端输出,例如:

<%=s1%>等同于<% out.print(s1); %>

<%="hello"%>等同于<% out.print("hello"); %>,也等同于直接在页面中写hello一样。

多个<%...%>可以通用

在一个JSP中多个<%…%>是相通的。例如:

1
2
3
4
5
6
7
8
9
<body>
<h1>out.jsp</h1>
<%
String s = "hello";
%>
<%
out.print(s);
%>
</body>

循环打印表格:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<body>
<h1>表格</h1>

<table border="1" width="50%">
<tr>
<th>序号</th>
<th>用户名</th>
<th>密码</th>
</tr>
<%
for(int i = 0; i < 10; i++) {
%>
<tr>
<td><%=i+1 %></td>
<td>user<%=i %></td>
<td><%=100 + 1 %></td>
</tr>
<%
}
%>
</table>
</body>

JSP 原理

JSP是一种特殊的Servlet,当JSP页面首次被访问时,容器(Tomcat)会先把JSP编译成Servlet,然后再去执行Servlet。所以JSP其实就是一个Servlet!

2.png

JSP真身存放目录

JSP生成的Servlet存放在${CATALANA}/work目录下,我经常开玩笑的说,它是JSP的”真身”。我们打开看看其中的内容,了解一下JSP的”真身”。

你会发现,在JSP中的静态信息(例如<html>等)在”真身”中都是使用out.write()完成打印!这些静态信息都是作为字符串输出给了客户端。

JSP的整篇内容都会放到名为_jspService的方法中!你可能会说<@page>不在”真身”中,<%@page>我们明天再看。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void _jspService(final javax.servlet.http.HttpServletRequest request, 
final javax.servlet.http.HttpServletResponse response)
throws java.io.IOException, javax.servlet.ServletException {

final javax.servlet.jsp.PageContext pageContext;
javax.servlet.http.HttpSession session = null;
final javax.servlet.ServletContext application;
final javax.servlet.ServletConfig config;
javax.servlet.jsp.JspWriter out = null;
final java.lang.Object page = this;
javax.servlet.jsp.JspWriter _jspx_out = null;
javax.servlet.jsp.PageContext _jspx_page_context = null;


try {
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;

// JSP页面中的内容都会在这个位置出现!这时上面所说的对象已经创建完了,所以在JSP页面中是可以使用的。
}

JSP脚本

JSP脚本一共三种形式:

  • <%...%>:内容会直接放到”真身”中;

  • <%=…%>:内容会放到out.print()中,作为out.print()的参数;

  • <%!…%>:内容会放到_jspService()方法之外,被类直接包含;

前面已经讲解了<%...%><%=…%>,但还没有讲解<%!...%>的作用!

现在我们已经知道了,JSP其实就是一个类,一个Servlet类。<%!...%>的作用是在类中添加方法或成员的,所以<%!...%>中的内容不会出现在_jspService()中。

1
2
3
4
5
6
<%!
private String name;
public String hello() {
return "hello JSP!";
}
%>

JSP注释

我们现在已经知道JSP是需要先编译成.java,再编译成.class的。其中<%-- ... --%>中的内容在JSP编译成.java时会被忽略的,即JSP注释。

也可以在JSP页面中使用html注释:<!-- … -->,但这个注释在JSP编译成的.java中是存在的,它不会被忽略,而且会被发送到客户端浏览器。但是在浏览器显示服务器发送过来的html时,因为<!-- … -->是html的注释,所以浏览器是不会显示它的。

2.png

会话跟踪技术

什么是会话跟踪技术

我们需要先了解一下什么是会话!可以把会话理解为客户端与服务器之间的一次会晤,在一次会晤中可能会包含多次请求和响应。例如你给10086打个电话,你就是客户端,而10086服务人员就是服务器了。从双方接通电话那一刻起,会话就开始了,到某一方挂断电话表示会话结束。在通话过程中,你会向10086发出多个请求,那么这多个请求都在一个会话中。

在JavaWeb中,客户向某一服务器发出第一个请求开始,会话就开始了,直到客户关闭了浏览器会话结束。

在一个会话的多个请求中共享数据,这就是会话跟踪技术。例如在一个会话中的请求如下:

  • 请求银行主页;

  • 请求登录(请求参数是用户名和密码);

  • 请求转账(请求参数与转账相关的数据);

  • 请求信誉卡还款(请求参数与还款相关的数据)。

在这上会话中当前用户信息必须在这个会话中共享的,因为登录的是张三,那么在转账和还款时一定是相对张三的转账和还款!这就说明我们必须在一个会话过程中有共享数据的能力。

会话路径技术使用Cookie或session完成

我们知道HTTP协议是无状态协议,也就是说每个请求都是独立的!无法记录前一次请求的状态。但HTTP协议中可以使用Cookie来完成会话跟踪!

在JavaWeb中,使用session来完成会话跟踪,session底层依赖Cookie技术。

Cookie概述

Cookie翻译成中文是小甜点,小饼干的意思。在HTTP中它表示服务器送给客户端浏览器的小甜点。其实Cookie就是一个键和一个值构成的,随着服务器端的响应发送给客户端浏览器。然后客户端浏览器会把Cookie保存起来,当下一次再访问服务器时把Cookie再发送给服务器。

2.png

Cookie是由服务器创建,然后通过响应发送给客户端的一个键值对。客户端会保存Cookie,并会标注出Cookie的来源(哪个服务器的Cookie)。当客户端向服务器发出请求时会把所有这个服务器Cookie包含在请求中发送给服务器,这样服务器就可以识别客户端了!

Cookie规范
  • Cookie大小上限为4KB;

  • 一个服务器最多在客户端浏览器上保存20个Cookie;

  • 一个浏览器最多保存300个Cookie;

上面的数据只是HTTP的Cookie规范,但在浏览器大战的今天,一些浏览器为了打败对手,为了展现自己的能力起见,可能对Cookie规范”扩展”了一些,例如每个Cookie的大小为8KB,最多可保存500个Cookie等!但也不会出现把你硬盘占满的可能!

注意,不同浏览器之间是不共享Cookie的。也就是说在你使用IE访问服务器时,服务器会把Cookie发给IE,然后由IE保存起来,当你在使用FireFox访问服务器时,不可能把IE保存的Cookie发送给服务器。

Cookie与HTTP头

Cookie是通过HTTP请求和响应头在客户端和服务器端传递的:

  • Cookie:请求头,客户端发送给服务器端;
    • 格式:Cookie: a=A; b=B; c=C。即多个Cookie用分号离开;
  • Set-Cookie:响应头,服务器端发送给客户端;
    • 一个Cookie对应一个Set-Cookie:
    • Set-Cookie: a=A
    • Set-Cookie: b=B
    • Set-Cookie: c=C
Cookie的覆盖

如果服务器端发送重复的Cookie那么会覆盖原有的Cookie,例如客户端的第一个请求服务器端发送的Cookie是:Set-Cookie: a=A;第二请求服务器端发送的是:Set-Cookie: a=AA,那么客户端只留下一个Cookie,即:a=AA。

Cookie第一例

我们这个案例是,客户端访问AServlet,AServlet在响应中添加Cookie,浏览器会自动保存Cookie。然后客户端访问BServlet,这时浏览器会自动在请求中带上Cookie,BServlet获取请求中的Cookie打印出来。

2.png

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// AServlet.java
package cn.itcast.servlet;

import java.io.IOException;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 给客户端发送Cookie
* @author Administrator
*
*/
public class AServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");

String id = UUID.randomUUID().toString();//生成一个随机字符串
Cookie cookie = new Cookie("id", id);//创建Cookie对象,指定名字和值
response.addCookie(cookie);//在响应中添加Cookie对象
response.getWriter().print("已经给你发送了ID");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// BServlet.java
package cn.itcast.servlet;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
* 获取客户端请求中的Cookie
* @author Administrator
*
*/
public class BServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");

Cookie[] cs = request.getCookies();//获取请求中的Cookie
if(cs != null) {//如果请求中存在Cookie
for(Cookie c : cs) {//遍历所有Cookie
if(c.getName().equals("id")) {//获取Cookie名字,如果Cookie名字是id
response.getWriter().print("您的ID是:" + c.getValue());//打印Cookie值
}
}
}
}
}
什么是Cookie的生命

Cookie不只是有name和value,Cookie还有生命。所谓生命就是Cookie在客户端的有效时间,可以通过setMaxAge(int)来设置Cookie的有效时间。

  • cookie.setMaxAge(-1):cookie的maxAge属性的默认值就是-1,表示只在浏览器内存中存活。一旦关闭浏览器窗口,那么cookie就会消失。

  • cookie.setMaxAge(60*60):表示cookie对象可存活1小时。当生命大于0时,浏览器会把Cookie保存到硬盘上,就算关闭浏览器,就算重启客户端电脑,cookie也会存活1小时;

  • cookie.setMaxAge(0):cookie生命等于0是一个特殊的值,它表示cookie被作废!也就是说,如果原来浏览器已经保存了这个Cookie,那么可以通过Cookie的setMaxAge(0)来删除这个Cookie。无论是在浏览器内存中,还是在客户端硬盘上都会删除这个Cookie。

案例:显示上次访问时间
  • 创建Cookie,名为lasttime,值为当前时间,添加到response中;

  • 在AServlet中获取请求中名为lasttime的Cookie;

  • 如果不存在输出”您是第一次访问本站”,如果存在输出”您上一次访问本站的时间是xxx”;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// AServlet.java
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.setContentType("text/html;charset=utf-8");

Cookie cookie = new Cookie("lasttime", new Date().toString()); // 这里可能会出现小bug
cookie.setMaxAge(60 * 60);
response.addCookie(cookie);

Cookie[] cs = request.getCookies();
String s = "您是首次访问本站!";
if(cs != null) {
for(Cookie c : cs) {
if(c.getName().equals("lasttime")) {
s = "您上次的访问时间是:" + c.getValue();
}
}
}

response.getWriter().print(s);
}

Cookie的path

什么是Cookie的路径

现在有WEB应用A,向客户端发送了10个Cookie,这就说明客户端无论访问应用A的哪个Servlet都会把这10个Cookie包含在请求中!但是也许只有AServlet需要读取请求中的Cookie,而其他Servlet根本就不会获取请求中的Cookie。这说明客户端浏览器有时发送这些Cookie是多余的!

可以通过设置Cookie的path来指定浏览器,在访问什么样的路径时,包含什么样的Cookie。

Cookie路径与请求路径的关系

下面我们来看看Cookie路径的作用:

下面是客户端浏览器保存的3个Cookie的路径:

a: /cookietest;

b: /cookietest/servlet;

c: /cookietest/jsp;

下面是浏览器请求的URL:

A: http://localhost:8080/cookietest/AServlet

B: http://localhost:8080/cookietest/servlet/BServlet

C: http://localhost:8080/cookietest/jsp/CServlet

  • 请求A时,会在请求中包含a;

  • 请求B时,会在请求中包含a、b;

  • 请求C时,会在请求中包含a、c;

也就是说,请求路径如果包含了Cookie路径,那么会在请求中包含这个Cookie,否则不会请求中不会包含这个Cookie。

  • A请求的URL包含了”/cookietest”,所以会在请求中包含路径为”/cookietest”的Cookie;

  • B请求的URL包含了”/cookietest”,以及”/cookietest/servlet”,所以请求中包含路径为”/cookietest”和”/cookietest/servlet”两个Cookie;

  • B请求的URL包含了”/cookietest”,以及”/cookietest/jsp”,所以请求中包含路径为”/cookietest”和”/cookietest/jsp”两个Cookie;

设置Cookie的路径

设置Cookie的路径需要使用setPath()方法,例如:

cookie.setPath(“/cookietest/servlet”);

如果没有设置Cookie的路径,那么Cookie路径的默认值当前访问资源所在路径,例如:

  • 访问http://localhost:8080/cookietest/AServlet时添加的Cookie默认路径为/cookietest;

  • 访问http://localhost:8080/cookietest/servlet/BServlet时添加的Cookie默认路径为/cookietest/servlet;

  • 访问http://localhost:8080/cookietest/jsp/BServlet时添加的Cookie默认路径为/cookietest/jsp;

Cookie的domain

Cookie的domain属性可以让网站中二级域共享Cookie,次要!

百度你是了解的对吧!

http://www.baidu.com

http://zhidao.baidu.com

http://news.baidu.com

http://tieba.baidu.com

现在我希望在这些主机之间共享Cookie(例如在www.baidu.com中响应的cookie,可以在news.baidu.com请求中包含)。很明显,现在不是路径的问题了,而是主机的问题,即域名的问题。处理这一问题其实很简单,只需要下面两步:

  • 设置Cookie的path为”/“:c.setPath(“/“);

  • 设置Cookie的domain为”.baidu.com”:c.setDomain(“.baidu.com”)。

当domain为”.baidu.com”时,无论前缀是什么,都会共享Cookie的。但是现在我们需要设置两个虚拟主机:www.baidu.comnews.baidu.com

第一步:设置windows的DNS路径解析

找到C:\WINDOWS\system32\drivers\etc\hosts文件,添加如下内容

1
2
3
127.0.0.1       localhost
127.0.0.1 www.baidu.com
127.0.0.1 news.baidu.com

第二步:设置Tomcat虚拟主机

找到server.xml文件,添加<Host>元素,内容如下:

1
2
3
4
5
6
<Host name="www.baidu.com"  appBase="F:\webapps\www"
unpackWARs="true" autoDeploy="true"
xmlValidation="false" xmlNamespaceAware="false"/>
<Host name="news.baidu.com" appBase="F:\webapps\news"
unpackWARs="true" autoDeploy="true"
xmlValidation="false" xmlNamespaceAware="false"/>

第三步:创建A项目,创建AServlet,设置Cookie。

1
2
3
4
5
6
7
Cookie[] cs = request.getCookies();
if(cs != null) {
for(Cookie c : cs) {
String s = c.getName() + ": " + c.getValue() + "<br/>";
response.getWriter().print(s);
}
}

把B项目的WebRoot目录复制到F:\webapps\news目录下,并把WebRoot目录的名字修改为ROOT。

第五步:访问www.baidu.com\AServlet,然后再访问news.baidu.com\BServlet

Cookie保存中文

Cookie的name和value都不能使用中文,如果希望在Cookie中使用中文,那么需要先对中文进行URL编码,然后把编码后的字符串放到Cookie中。

向客户端响应中添加Cookie

1
2
3
4
5
String name = URLEncoder.encode("姓名", "UTF-8");
String value = URLEncoder.encode("张三", "UTF-8");
Cookie c = new Cookie(name, value);
c.setMaxAge(3600);
response.addCookie(c);

从客户端请求中获取Cookie

1
2
3
4
5
6
7
8
9
10
response.setContentType("text/html;charset=utf-8");
Cookie[] cs = request.getCookies();
if(cs != null) {
for(Cookie c : cs) {
String name = URLDecoder.decode(c.getName(), "UTF-8");
String value = URLDecoder.decode(c.getValue(), "UTF-8");
String s = name + ": " + value + "<br/>";
response.getWriter().print(s);
}
}

显示曾经浏览过的商品

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<%--index.jsp--%>
<body>
<h1>商品列表</h1>
<a href="/day06_3/GoodServlet?name=ThinkPad">ThinkPad</a><br/>
<a href="/day06_3/GoodServlet?name=Lenovo">Lenovo</a><br/>
<a href="/day06_3/GoodServlet?name=Apple">Apple</a><br/>
<a href="/day06_3/GoodServlet?name=HP">HP</a><br/>
<a href="/day06_3/GoodServlet?name=SONY">SONY</a><br/>
<a href="/day06_3/GoodServlet?name=ACER">ACER</a><br/>
<a href="/day06_3/GoodServlet?name=DELL">DELL</a><br/>

<hr/>
您浏览过的商品:
<%
Cookie[] cs = request.getCookies();
if(cs != null) {
for(Cookie c : cs) {
if(c.getName().equals("goods")) {
out.print(c.getValue());
}
}
}
%>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// GoodServlet
public class GoodServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String goodName = request.getParameter("name");
String goods = CookieUtils.getCookValue(request, "goods");

if(goods != null) {
String[] arr = goods.split(", ");
Set<String> goodSet = new LinkedHashSet(Arrays.asList(arr));
goodSet.add(goodName);
goods = goodSet.toString();
goods = goods.substring(1, goods.length() - 1);
} else {
goods = goodName;
}
Cookie cookie = new Cookie("goods", goods);
cookie.setMaxAge(1 * 60 * 60 * 24);
response.addCookie(cookie);

response.sendRedirect("/day06_3/index.jsp");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// CookieUtils
public class CookieUtils {
public static String getCookValue(HttpServletRequest request, String name) {
Cookie[] cs = request.getCookies();
if(cs == null) {
return null;
}
for(Cookie c : cs) {
if(c.getName().equals(name)) {
return c.getValue();
}
}
return null;
}
}

HttpSession

HttpSession概述

javax.servlet.http.HttpSession接口表示一个会话,我们可以把一个会话内需要共享的数据保存到HttpSession对象中!

获取HttpSession对象
  • HttpSession request.getSesssion():如果当前会话已经有了session对象那么直接返回,如果当前会话还不存在会话,那么创建session并返回;

  • HttpSession request.getSession(boolean):当参数为true时,与requeset.getSession()相同。如果参数为false,那么如果当前会话中存在session则返回,不存在返回null;

HttpSession是域对象

我们已经学习过HttpServletRequest、ServletContext,它们都是域对象,现在我们又学习了一个HttpSession,它也是域对象。它们三个是Servlet中可以使用的域对象,而JSP中可以多使用一个域对象,明天我们再讲解JSP的第四个域对象。

  • HttpServletRequest:一个请求创建一个request对象,所以在同一个请求中可以共享request,例如一个请求从AServlet转发到BServlet,那么AServlet和BServlet可以共享request域中的数据;

  • ServletContext:一个应用只创建一个ServletContext对象,所以在ServletContext中的数据可以在整个应用中共享,只要不启动服务器,那么ServletContext中的数据就可以共享;

  • HttpSession:一个会话创建一个HttpSession对象,同一会话中的多个请求中可以共享session中的数据;

下面是session的域方法:

  • void setAttribute(String name, Object value):用来存储一个对象,也可以称之为存储一个域属性,例如:session.setAttribute(“xxx”, “XXX”),在session中保存了一个域属性,域属性名称为xxx,域属性的值为XXX。请注意,如果多次调用该方法,并且使用相同的name,那么会覆盖上一次的值,这一特性与Map相同;

  • Object getAttribute(String name):用来获取session中的数据,当前在获取之前需要先去存储才行,例如:String value = (String) session.getAttribute(“xxx”);,获取名为xxx的域属性;

  • void removeAttribute(String name):用来移除HttpSession中的域属性,如果参数name指定的域属性不存在,那么本方法什么都不做;

  • Enumeration getAttributeNames():获取所有域属性的名称;

登录案例

需要的页面:

  • login.jsp:登录页面,提供登录表单;

  • index1.jsp:主页,显示当前用户名称,如果没有登录,显示您还没登录;

  • index2.jsp:主页,显示当前用户名称,如果没有登录,显示您还没登录;

Servlet:

  • LoginServlet:在login.jsp页面提交表单时,请求本Servlet。在本Servlet中获取用户名、密码进行校验,如果用户名、密码错误,显示”用户名或密码错误”,如果正确保存用户名session中,然后重定向到index1.jsp;

当用户没有登录时访问index1.jsp或index2.jsp,显示”您还没有登录”。如果用户在login.jsp登录成功后到达index1.jsp页面会显示当前用户名,而且不用再次登录去访问index2.jsp也会显示用户名。因为多次请求在一个会话范围,index1.jsp和index2.jsp都会到session中获取用户名,session对象在一个会话中是相同的,所以都可以获取到用户名!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<%--login.jsp--%>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>login.jsp</title>
</head>

<body>
<h1>login.jsp</h1>
<hr/>
<form action="/day06_4/LoginServlet" method="post">
用户名:<input type="text" name="username" /><br/>
<input type="submit" value="Submit"/>
</form>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%--index1.jsp--%>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>index1.jsp</title>
</head>

<body>
<h1>index1.jsp</h1>
<%
String username = (String)session.getAttribute("username");
if(username == null) {
out.print("您还没有登录!");
} else {
out.print("用户名:" + username);
}
%>
<hr/>
<a href="/day06_4/index2.jsp">index2</a>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<%--index2.jsp--%>
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>index2.jsp</title>
</head>

<body>
<h1>index2.jsp</h1>
<%
String username = (String)session.getAttribute("username");
if(username == null) {
out.print("您还没有登录!");
} else {
out.print("用户名:" + username);
}
%>
<hr/>
<a href="/day06_4/index1.jsp">index1</a>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// LoginServlet
public class LoginServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");

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

if(username.equalsIgnoreCase("itcast")) {
response.getWriter().print("用户名或密码错误!");
} else {
HttpSession session = request.getSession();
session.setAttribute("username", username);
response.sendRedirect("/day06_4/index1.jsp");
}
}
}

session的实现原理

session底层是依赖Cookie的!我们来理解一下session的原理吧!

当我首次去银行时,因为还没有账号,所以需要开一个账号,我获得的是银行卡,而银行这边的数据库中留下了我的账号,我的钱是保存在银行的账号中,而我带走的是我的卡号。

当我再次去银行时,只需要带上我的卡,而无需再次开一个账号了。只要带上我的卡,那么我在银行操作的一定是我的账号!

当首次使用session时,服务器端要创建session,session是保存在服务器端,而给客户端的session的id(一个cookie中保存了sessionId)。客户端带走的是sessionId,而数据是保存在session中。

当客户端再次访问服务器时,在请求中会带上sessionId,而服务器会通过sessionId找到对应的session,而无需再创建新的session。

2.png

session与浏览器

session保存在服务器,而sessionId通过Cookie发送给客户端,但这个Cookie的生命为-1,即只在浏览器内存中存在,也就是说如果用户关闭了浏览器,那么这个Cookie就丢失了。

当用户再次打开浏览器访问服务器时,就不会有sessionId发送给服务器,那么服务器会认为你没有session,所以服务器会创建一个session,并在响应中把sessionId中到Cookie中发送给客户端。     

你可能会说,那原来的session对象会怎样?当一个session长时间没人使用的话,服务器会把session删除了!这个时长在Tomcat中配置是30分钟,可以在${CATALANA}/conf/web.xml找到这个配置,当然你也可以在自己的web.xml中覆盖这个配置!

1
2
3
<session-config>
<session-timeout>30</session-timeout>
</session-config>

session失效时间也说明一个问题!如果你打开网站的一个页面开始长时间不动,超出了30分钟后,再去点击链接或提交表单时你会发现,你的session已经丢失了!

session其他常用API

  • String getId():获取sessionId;

  • int getMaxInactiveInterval():获取session可以的最大不活动时间(秒),默认为30分钟。当session在30分钟内没有使用,那么Tomcat会在session池中移除这个session;

  • void setMaxInactiveInterval(int interval):设置session允许的最大不活动时间(秒),如果设置为1秒,那么只要session在1秒内不被使用,那么session就会被移除;

  • long getCreationTime():返回session的创建时间,返回值为当前时间的毫秒值;

  • long getLastAccessedTime():返回session的最后活动时间,返回值为当前时间的毫秒值;

  • void invalidate():让session失效!调用这个方法会被session失效,当session失效后,客户端再次请求,服务器会给客户端创建一个新的session,并在响应中给客户端新session的sessionId;

  • boolean isNew():查看session是否为新。当客户端第一次请求时,服务器为客户端创建session,但这时服务器还没有响应客户端,也就是还没有把sessionId响应给客户端时,这时session的状态为新。

URL重写

我们知道session依赖Cookie,那么session为什么依赖Cookie呢?因为服务器需要在每次请求中获取sessionId,然后找到客户端的session对象。那么如果客户端浏览器关闭了Cookie呢?那么session是不是就会不存在了呢?

其实还有一种方法让服务器收到的每个请求中都带有sessioinId,那就是URL重写!在每个页面中的每个链接和表单中都添加名为jSessionId的参数,值为当前sessionid。当用户点击链接或提交表单时也服务器可以通过获取jSessionId这个参数来得到客户端的sessionId,找到sessoin对象。

1
2
3
4
5
6
7
8
9
<%--index.jsp--%>
<body>
<h1>URL重写</h1>
<a href='/day06_5/index.jsp;jsessionid=<%=session.getId() %>' >主页</a>

<form action='/day06_5/index.jsp;jsessionid=<%=session.getId() %>' method="post">
<input type="submit" value="提交"/>
</form>
</body>

也可以使用response.encodeURL()对每个请求的URL处理,这个方法会自动追加jsessionid参数,与上面我们手动添加是一样的效果。

1
2
3
4
5
<a href='<%=response.encodeURL("/day06_5/index.jsp") %>' >主页</a>

<form action='<%=response.encodeURL("/day06_5/index.jsp") %>' method="post">
<input type="submit" value="提交"/>
</form>

 使用response.encodeURL()更加”智能”,它会判断客户端浏览器是否禁用了Cookie,如果禁用了,那么这个方法在URL后面追加jsessionid,否则不会追加。

案例:一次性图片验证码

验证码有啥用

在我们注册时,如果没有验证码的话,我们可以使用URLConnection来写一段代码发出注册请求。甚至可以使用while(true)来注册!那么服务器就废了!

验证码可以去识别发出请求的是人还是程序!当然,如果聪明的程序可以去分析验证码图片!但分析图片也不是一件容易的事,因为一般验证码图片都会带有干扰线,人都看不清,那么程序一定分析不出来。

VerifyCode类

现在我们已经有了cn.itcast.utils.VerifyCode类,这个类可以生成验证码图片!下面来看一个小例子。

1
2
3
4
5
6
7
8
9
10
11
12
public void fun1() throws IOException {
// 创建验证码类
VerifyCode vc = new VerifyCode();
// 获取随机图片
BufferedImage image = vc.getImage();
// 获取刚刚生成的随机图片上的文本
String text = vc.getText();
System.out.println(text);
// 保存图片
FileOutputStream out = new FileOutputStream("F:/xxx.jpg");
VerifyCode.output(image, out);
}

在页面中显示动态图片

我们需要写一个VerifyCodeServlet,在这个Servlet中我们生成动态图片,然后它图片写入到response.getOutputStream()流中!然后让页面的元素指定这个VerifyCodServlet即可。

1
2
3
4
5
6
7
8
9
10
11
// VerifyCodeServlet
public class VerifyCodeServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
VerifyCode vc = new VerifyCode();
BufferedImage image = vc.getImage();
String text = vc.getText();
System.out.println("text:" + text);
VerifyCode.output(image, response.getOutputStream());
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
<%--index.jsp--%>
<script type="text/javascript">
function _change() {
var imgEle = document.getElementById("vCode");
imgEle.src = "/day06_6/VerifyCodeServlet?" + new Date().getTime();
}
</script>
...
<body>
<h1>验证码</h1>
<img id="vCode" src="/day06_6/VerifyCodeServlet"/>
<a href="javascript:_change()">看不清,换一张</a>
</body>

在注册页面中使用验证码

1
2
3
4
5
6
7
8
<form action="/day06_6/RegistServlet" method="post">
用户名:<input type="text" name="username"/><br/>
验证码:<input type="text" name="code" size="3"/>
<img id="vCode" src="/day06_6/VerifyCodeServlet"/>
<a href="javascript:_change()">看不清,换一张</a>
<br/>
<input type="submit" value="Submit"/>
</form>

RegistServlet

修改VerifyCodeServlet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class VerifyCodeServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
VerifyCode vc = new VerifyCode();
BufferedImage image = vc.getImage();
request.getSession().setAttribute("vCode", vc.getText());
VerifyCode.output(image, response.getOutputStream());
}
}
----------------------------------------------------------------------------------------
// RegisterServlet
public class RegistServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");

String username = request.getParameter("username");
String vCode = request.getParameter("code");

String sessionVerifyCode = (String)request.getSession().getAttribute("vCode");

if(vCode.equalsIgnoreCase(sessionVerifyCode) ) {
response.getWriter().print(username + ", 恭喜!注册成功!");
} else {
response.getWriter().print("验证码错误!");
}
}
}

总结验证码案例

  • VerifyCodeServlet:

    • 生成验证码:VerifyCode vc = new VerifyCode(); BufferedImage image = vc.getImage();

    • 在session中保存验证码文本:request.getSession.getAttribute(“vCode”, vc.getText());

    • 把验证码输出到页面:VerifyCode.output(image, response.getOutputStream);

  • regist.jsp:

    • 表单中包含username和code字段;

    • 在表单中给出指向VerifyCodeServlet,用来在页面中显示验证码图片;

    • 提供”看不清,换一张”链接,指向_change()函数;

    • 提交到RegistServlet;

  • RegistServlet:

    • 获取表单中的username和code;
  • 获取session中的vCode;

    • 比较code和vCode是否相同;
  • 相同说明用户输入的验证码正确,否则输入验证码错误。


Day 07

01/07/2020

JSP 指令

JSP指令的格式:<%@指令名 attr1="" attr2="" %>,一般都会把JSP指令放到JSP文件的最上方,但这不是必须的。

JSP中有三大指令:page、include、taglib,最为常用,也最为复杂的就是page指令了。

page 指令

page指令是最为常用的指定,也是属性最多的属性!

page指令没有必须属性,都是可选属性。例如<%@page %>,没有给出任何属性也是可以的!

在JSP页面中,任何指令都可以重复出现!

<%@ page language="java"%>

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

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

这也是可以的!

page指令的pageEncoding和contentType(重点)

pageEncoding指定当前JSP页面的编码!这个编码是给服务器看的,服务器需要知道当前JSP使用的编码,不然服务器无法正确把JSP编译成java文件。所以这个编码只需要与真实的页面编码一致即可!在MyEclipse中,在JSP文件上点击右键,选择属性就可以看到当前JSP页面的编码了。

contentType属性与response.setContentType()方法的作用相同!它会完成两项工作,一是设置响应字符流的编码,二是设置content-type响应头。例如:<%@ contentType="text/html;charset=utf-8"%>,它会使”真身”中出现response.setContentType(“text/html;charset=utf-8”)。

无论是page指令的pageEncoding还是contentType,它们的默认值都是ISO-8859-1,我们知道ISO-8859-1是无法显示中文的,所以JSP页面中存在中文的话,一定要设置这两个属性。

其实pageEncoding和contentType这两个属性的关系很”暧昧”:

  • 当设置了pageEncoding,而没设置contentType时: contentType的默认值为pageEncoding;

  • 当设置了contentType,而没设置pageEncoding时: pageEncoding的默认值与contentType;

也就是说,当pageEncoding和contentType只出现一个时,那么另一个的值与出现的值相同。如果两个都不出现,那么两个属性的值都是ISO-8859-1。所以通过我们至少设置它们两个其中一个!

page指令的import属性

import是page指令中一个很特别的属性!

import属性值对应”真身”中的import语句。

import属性值可以使逗号:<%@page import="java.net.*,java.util.*,java.sql.*"%>

import属性是唯一可以重复出现的属性:

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

但是,我们一般会使用多个page指令来导入多个包:

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

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

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

page指令的errorPage和isErrorPage

我们知道,在一个JSP页面出错后,Tomcat会响应给用户错误信息(500页面)!如果你不希望Tomcat给用户输出错误信息,那么可以使用page指令的errorPage来指定错误页!也就是自定义错误页面,例如:<%@page errorPage="xxx.jsp"%>。这时,在当前JSP页面出现错误时,会请求转发到xxx.jsp页面。

1
2
3
4
5
6
7
<%--a.jsp--%>
<%@ page import="java.util.*" pageEncoding="UTF-8"%>
<%@ page errorPage="b.jsp" %>
<%
if(true)
throw new Exception("哈哈~");
%>
1
2
3
4
5
6
7
<%--b.jsp--%>
<%@ page pageEncoding="UTF-8"%>
<html>
<body>
<h1>出错啦!</h1>
</body>
</html>

在上面代码中,a.jsp抛出异常后,会请求转发到b.jsp。在浏览器的地址栏中还是a.jsp,因为是请求转发!

而且客户端浏览器收到的响应码为200,表示请求成功!如果希望客户端得到500,那么需要指定b.jsp为错误页面。

1
2
3
4
5
6
7
8
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ page isErrorPage="true" %>
<html>
<body>
<h1>出错啦!</h1>
<%=exception.getMessage() %>
</body>
</html>

注意,当isErrorPage为true时,说明当前JSP为错误页面,即专门处理错误的页面。那么这个页面中就可以使用一个内置对象exception了。其他页面是不能使用这个内置对象的!

温馨提示:IE会在状态码为500时,并且响应正文的长度小于等于512B时不给予显示!而是显示”网站无法显示该页面”字样。这时你只需要添加一些响应内容即可,例如上例中的b.jsp中我给出一些内容,IE就可以正常显示了!

web.xml中配置错误页面

不只可以通过JSP的page指令来配置错误页面,还可以在web.xml文件中指定错误页面。这种方式其实与page指令无关,但想来想去还是在这个位置来说明比较合适!

web.xml

1
2
3
4
5
6
7
8
9
10
11
12
<error-page>
<error-code>404</error-code>
<location>/error404.jsp</location>
</error-page>
<error-page>
<error-code>500</error-code>
<location>/error500.jsp</location>
</error-page>
<error-page>
<exception-type>java.lang.RuntimeException</exception-type>
<location>/error.jsp</location>
</error-page>

<error-page>有两种使用方式:

  • <error-code><location>子元素;

  • <exception-type><location>子元素;

    其中<error-code>是指定响应码;<location>指定转发的页面;<exception-type>是指定抛出的异常类型。

  • 当出现404时,会跳转到error404.jsp页面;

  • 当出现RuntimeException异常时,会跳转到error.jsp页面;

  • 当出现非RuntimeException的异常时,会跳转到error500.jsp页面。

这种方式会在控制台看到异常信息!而使用page指令时不会在控制台打印异常信息。

page指令的authFlush和buffer

buffer表示当前JSP的输出流(out隐藏对象)的缓冲区大小,默认为8kb。

authFlush表示在out对象的缓冲区满时如何处理!当authFlush为true时,表示缓冲区满时把缓冲区数据输出到客户端;当authFlush为false时,表示缓冲区满时,抛出异常。authFlush的默认值为true。

这两个属性一般我们也不会去特意设置,都是保留默认值!

page指令的isELIgnored

后面我们会讲解EL表达式语言,page指令的isELIgnored属性表示当前JSP页面是否忽略EL表达式,默认值为false,表示不忽略(即支持)。

page指令的其他属性
  • language:只能是Java,这个属性可以看出JSP最初设计时的野心!希望JSP可以转换成其他语言!但是,到现在JSP也只能转换成Java代码;

  • info:JSP说明性信息;

  • isThreadSafe:默认为false,为true时,JSP生成的Servlet会去实现一个过时的标记接口SingleThreadModel,这时JSP就只能处理单线程的访问;

  • session:默认为true,表示当前JSP页面可以使用session对象,如果为false表示当前JSP页面不能使用session对象;

  • extends:指定当前JSP页面生成的Servlet的父类;

<jsp-config>(了解)

在web.xml页面中配置<jsp-config>也可以完成很多page指定的功能!

1
2
3
4
5
6
7
8
<jsp-config>
<jsp-property-group>
<url-pattern>*.jsp</url-pattern> <!--对所有jsp进行配置-->
<el-ignored>true</el-ignored> <!--忽略EL表达式-->
<page-encoding>UTF-8</page-encoding> <!--指定页面编码为utf-8-->
<scripting-invalid>true</scripting-invalid> <!--禁用Java脚本!如果在JSP页面中使用了Java脚本就会抛出异常。-->
</jsp-property-group>
</jsp-config>

include指令

include指令表示静态包含!即目的是把多个JSP合并成一个JSP文件!

include指令只有一个属性:file,指定要包含的页面,例如:<%@include file="b.jsp"%>

静态包含:当hel.jsp页面包含了lo.jsp页面后,在编译hel.jsp页面时,需要把hel.jsp和lo.jsp页面合并成一个文件,然后再编译成Servlet(Java文件)。

2.png

很明显,在ol.jsp中在使用username变量,而这个变量在hel.jsp中定义的,所以只有这两个JSP文件合并后才能使用。通过include指定完成对它们的合并!

taglib指令

这个指令需要在学习了自定义标签后才会使用,现在只能做了了解而已!

在JSP页面中使用第三方的标签库时,需要使用taglib指令来”导包”。例如:

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

其中prefix表示标签的前缀,这个名称可以随便起。uri是由第三方标签库定义的,所以你需要知道第三方定义的uri。

JSP 九大内置对象

什么是JSP九大内置对象

在JSP中无需创建就可以使用的9个对象,它们是:

  • out(JspWriter):等同与response.getWriter(),用来向客户端发送文本数据;

  • config(ServletConfig):对应”真身”中的ServletConfig;

  • page(当前JSP的真身类型):当前JSP页面的”this”,即当前对象;

  • pageContext(PageContext):页面上下文对象,它是最后一个没讲的域对象;

  • exception(Throwable):只有在错误页面中可以使用这个对象;

  • request(HttpServletRequest):即HttpServletRequest类的对象;

  • response(HttpServletResponse):即HttpServletResponse类的对象;

  • application(ServletContext):即ServletContext类的对象;

  • session(HttpSession):即HttpSession类的对象,不是每个JSP页面中都可以使用,如果在某个JSP页面中设置<%@page session="false"%>,说明这个页面不能使用session。

在这9个对象中有很多是极少会被使用的,例如:config、page、exception基本不会使用。

在这9个对象中有两个对象不是每个JSP页面都可以使用的:exception、session。

在这9个对象中有很多前面已经学过的对象:out、request、response、application、session、config。

通过”真身”来对照JSP

我们知道JSP页面的内容出现在”真身”的_jspService()方法中,而在_jspService()方法开头部分已经创建了9大内置对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public void _jspService(HttpServletRequest request, HttpServletResponse response)
throws java.io.IOException, ServletException {

PageContext pageContext = null;
HttpSession session = null;
ServletContext application = null;
ServletConfig config = null;
JspWriter out = null;
Object page = this;
JspWriter _jspx_out = null;
PageContext _jspx_page_context = null;


try {
response.setContentType("text/html;charset=UTF-8");
pageContext = _jspxFactory.getPageContext(this, request, response,
null, true, 8192, true);
_jspx_page_context = pageContext;
application = pageContext.getServletContext();
config = pageContext.getServletConfig();
session = pageContext.getSession();
out = pageContext.getOut();
_jspx_out = out;

    // 从这里开始,才是JSP页面的内容
}…

pageContext对象

在JavaWeb中一共四个域对象,其中Servlet中可以使用的是request、session、application三个对象,而在JSP中可以使用pageContext、request、session、application四个域对象。

pageContext 对象是PageContext类型,它的主要功能有:

  • 域对象功能;

  • 代理其它域对象功能;

  • 获取其他内置对象;

域对象功能

pageContext也是域对象,它的范围是当前页面。它的范围也是四个域对象中最小的!

  • void setAttribute(String name, Object value);

  • Object getAttrbiute(String name, Object value);

  • void removeAttribute(String name, Object value);

代理其它域对象功能

还可以使用pageContext来代理其它3个域对象的功能,也就是说可以使用pageContext向request、session、application对象中存取数据,例如:

1
2
3
4
pageContext.setAttribute("x", "X"); 	// 向pageContext中存储数据
pageContext.setAttribute("x", "XX", PageContext.REQUEST_SCOPE); // 向request中存储数据
pageContext.setAttribute("x", "XXX", PageContext.SESSION_SCOPE); // 向session中存储数据
pageContext.setAttribute("x", "XXXX", PageContext.APPLICATION_SCOPE); // 向application中存储数据
  • void setAttribute(String name, Object value, int scope):在指定范围中添加数据;

  • Object getAttribute(String name, int scope):获取指定范围的数据;

  • void removeAttribute(String name, int scope):移除指定范围的数据;

  • Object findAttribute(String name):依次在page、request、session、application范围查找名称为name的数据,如果找到就停止查找。这说明在这个范围内有相同名称的数据,那么page范围的优先级最高!

获取其他内置对象

一个pageContext对象等于所有内置对象,即1个当9个。这是因为可以使用pageContext对象获取其它8个内置对象:

  • JspWriter getOut():获取out内置对象;

  • ServletConfig getServletConfig():获取config内置对象;

  • Object getPage():获取page内置对象;

  • ServletRequest getRequest():获取request内置对象;

  • ServletResponse getResponse():获取response内置对象;

  • HttpSession getSession():获取session内置对象;

  • ServletContext getServletContext():获取application内置对象;

  • Exception getException():获取exception内置对象;

JSP 动作标签

JSP动作标签概述

动作标签的作用是用来简化Java脚本的!

JSP动作标签是JavaWeb内置的动作标签,它们是已经定义好的动作标签,我们可以拿来直接使用。

如果JSP动作标签不够用时,还可以使用自定义标签(今天不讲)。JavaWeb一共提供了20个JSP动作标签,但有很多基本没有用,这里只介绍一些有坐标的动作标签。

JSP动作标签的格式:<jsp:标签名 …>

<jsp:include>

<jsp:include>标签的作用是用来包含其它JSP页面的!你可能会说,前面已经学习了include指令了,它们是否相同呢?虽然它们都是用来包含其它JSP页面的,但它们的实现的级别是不同的!

include指令是在编译级别完成的包含,即把当前JSP和被包含的JSP合并成一个JSP,然后再编译成一个Servlet。

include动作标签是在运行级别完成的包含,即当前JSP和被包含的JSP都会各自生成Servlet,然后在执行当前JSP的Servlet时完成包含另一个JSP的Servlet。它与RequestDispatcher的include()方法是相同的!

1
2
3
4
5
<%--hel.jsp--%>
<body>
<h1>hel.jsp</h1>
<jsp:include page="lo.jsp" />
</body>
1
2
3
4
<%--lo.jsp--%>
<%
out.println("<h1>lo.jsp</h1>");
%>

2.png

其实<jsp:include>在”真身”中不过是一句方法调用,即调用另一个Servlet而已。

<jsp:forward>

forward标签的作用是请求转发!forward标签的作用与RequestDispatcher#forward()方法相同。

1
2
3
4
5
6
7
<%--hel.jsp--%>
<body>
<h1>
hel.jsp
</h1>
<jsp:forward page="lo.jsp"/>
</body>
1
2
3
4
<%--lo.jsp--%>
<%
out.println("<h1>lo.jsp</h1>");
%>

注意,最后客户端只能看到lo.jsp的输出,而看不到hel.jsp的内容。也就是说在hel.jsp中的<h1>hel.jsp</h1>是不会发送到客户端的。<jsp:forward>的作用是”别再显示我,去显示它吧!”。

<jsp:param>

还可以在<jsp:include><jsp:forward>标签中使用<jsp:param>子标签,它是用来传递参数的。下面用<jsp:include>来举例说明<jsp:param>的使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>a.jsp</title>
</head>

<body>
<h1>a.jsp</h1>
<hr/>
<jsp:include page="/b.jsp">
<jsp:param value="zhangSan" name="username"/> <%--传参--%>
</jsp:include>
</body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<head>
<title>b.jsp</title>
</head>

<body>
<h1>b.jsp</h1>
<hr/>
<%
String username = request.getParameter("username");
out.print("你好:" + username);
%>
</body>
</html>

JavaBean

JavaBean概述

什么是JavaBean

JavaBean是一种规范,也就是对类的要求。它要求Java类的成员变量提供getter/setter方法,这样的成员变量被称之为JavaBean属性。

JavaBean还要求类必须提供仅有的无参构造器,例如:public User() {…}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// User.java
public class User {
private String username;
private String password;

public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
JavaBean属性

JavaBean属性是具有getter/setter方法的成员变量。

  • 也可以只提供getter方法,这样的属性叫只读属性;

  • 也可以只提供setter方法,这样的属性叫只写属性;

  • 如果属性类型为boolean类型,那么读方法的格式可以是get或is。例如名为abc的boolean类型的属性,它的读方法可以是getAbc(),也可以是isAbc();

JavaBean属性名要求:前两个字母要么都大写,要么都小写:

1
2
3
4
5
6
7
public class User {
private String iD; // wrong
private String ID;
private String qQ; // wrong
private String QQ;
// …
}

JavaBean可能存在属性,但不存在这个成员变量,例如:

1
2
3
4
5
public class User {
public String getUsername() {
return "zhangSan";
}
}

上例中User类有一个名为username的只读属性!但User类并没有username这个成员变量!

还可以更变态一点:

1
2
3
4
5
6
7
8
9
10
11
public class User {
private String hello;

public String getUsername() {
return hello;
}

public void setUsername(String username) {
this.hello = username;
}
}

上例中User类中有一个名为username的属性,它是可读可写的属性!而Use类的成员变量名为hello!也就是说JavaBean的属性名取决于方法名称,而不是成员变量的名称。但通常没有人做这么变态的事情。

内省(了解)

内省的目标是得到JavaBean属性的读、写方法的反射对象,通过反射对JavaBean属性进行操作的一组API。例如User类有名为username的JavaBean属性,通过两个Method对象(一个是getUsenrmae(),一个是setUsername())来操作User对象。

如果你还不能理解内省是什么,那么我们通过一个问题来了解内省的作用。现在我们有一个Map,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
Map<String,String> map = new HashMap<String,String>();
map.put("username", "admin");
map.put("password", "admin123");
// ----------------------------------------------------
public class User {
private String username;
private String password;

public User(String username, String password) {
this.username = username;
this.password = password;
}
public User() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String toString() {
return "User [username=" + username + ", password=" + password + "]";
}
}

现在需要把map的数据封装到一个User对象中!User类有两个JavaBean属性,一个叫username,另一个叫password。

你可能想到的是反射,通过map的key来查找User类的Field!这么做是没有问题的,但我们要知道类的成员变量是私有的,虽然也可以通过反射去访问类的私有的成员变量,但我们也要清楚反射访问私有的东西是有”危险”的,所以还是建议通过getUsername和setUsername来访问JavaBean属性。

内省之获取BeanInfo

我们这里不想去对JavaBean规范做过多的介绍,所以也就不在多介绍BeanInfo的”出身”了。你只需要知道如何得到它,以及BeanInfo有什么。

通过java.beans.Introspector的getBeanInfo()方法来获取java.beans.BeanInfo实例。

1
BeanInfo beanInfo = Introspector.getBeanInfo(User.class);
得到所有属性描述符(PropertyDescriptor)

通过BeanInfo可以得到这个类的所有JavaBean属性的PropertyDescriptor对象。然后就可以通过PropertyDescriptor对象得到这个属性的getter/setter方法的Method对象了。

1
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();

每个PropertyDescriptor对象对应一个JavaBean属性:

  • String getName():获取JavaBean属性名称;

  • Method getReadMethod:获取属性的读方法;

  • Method getWriteMethod:获取属性的写方法。

完成Map数据封装到User对象中
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public void fun1() throws Exception {
Map<String,String> map = new HashMap<String,String>();
map.put("username", "admin");
map.put("password", "admin123");

BeanInfo beanInfo = Introspector.getBeanInfo(User.class);

PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();

User user = new User();
for(PropertyDescriptor pd : pds) {
String name = pd.getName();
String value = map.get(name);
if(value != null) {
Method writeMethod = pd.getWriteMethod();
writeMethod.invoke(user, value);
}
}

System.out.println(user);
}

commons-beanutils

提到内省,不能不提commons-beanutils这个工具。它底层使用了内省,对内省进行了大量的简化!

使用beanutils需要的jar包:

  • commons-beanutils.jar;

  • commons-logging.jar;

设置JavaBean属性
1
2
3
4
5
6
User user = new User();

BeanUtils.setProperty(user, "username", "admin");
BeanUtils.setProperty(user, "password", "admin123");

System.out.println(user);
获取JavaBean属性
1
2
3
4
5
6
User user = new User("admin", "admin123");

String username = BeanUtils.getProperty(user, "username");
String password = BeanUtils.getProperty(user, "password");

System.out.println("username=" + username + ", password=" + password);
封装Map数据到JavaBean对象中
1
2
3
4
5
6
7
8
9
Map<String,String> map = new HashMap<String,String>();
map.put("username", "admin");
map.put("password", "admin123");

User user = new User();

BeanUtils.populate(user, map);

System.out.println(user);

JSP与JavaBean相关的动作标签

在JSP中与JavaBean相关的标签有:

  • <jsp:useBean>:创建JavaBean对象;

  • <jsp:setProperty>:设置JavaBean属性;

  • <jsp:getProperty>:获取JavaBean属性;

我们需要先创建一个JavaBean类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// User.java
package tt;

public class User {
private String username;
private String password;

public User(String username, String password) {
this.username = username;
this.password = password;
}
public User() {
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String toString() {
return "User [username=" + username + ", password=" + password + "]";
}
}
<jsp:useBean>

<jsp:useBean>标签的作用是创建JavaBean对象:

  • 在当前JSP页面创建JavaBean对象;

  • 把创建的JavaBean对象保存到域对象中;

1
<jsp:useBean id="user1" class="tt.User" />

上面代码表示在当前JSP页面中创建User类型的对象,并且把它保存到page域中了。下面我们把<jsp:useBean>标签翻译成Java代码:

1
2
3
4
<%
tt.User user1 = new tt.User();
pageContext.setAttribute("user1", user1);
%>

这说明我们可以在JSP页面中完成下面的操作:

1
2
3
4
5
<jsp:useBean id="user1" class="tt.User" />
<%=user1 %>
<%
out.println(pageContext.getAttribute("user1"));
%>

<jsp:useBean>标签默认是把JavaBean对象保存到page域,还可以通过scope标签属性来指定保存的范围:

1
2
3
4
<jsp:useBean id="user1" class="tt.User" scope="page"/>
<jsp:useBean id="user2" class="tt.User" scope="request"/>
<jsp:useBean id="user3" class="tt.User" scope="session"/>
<jsp:useBean id="user4" class="tt.User" scope="applicatioin"/>

<jsp:useBean>标签其实不一定会创建对象!!!其实它会先在指定范围中查找这个对象,如果对象不存在才会创建,我们需要重新对它进行翻译:

1
<jsp:useBean id="user4" class="tt.User" scope="applicatioin"/>
1
2
3
4
5
6
7
<%
tt.User user4 = (tt.User)application.getAttribute("user4");
if(user4 == null) {
user4 = new tt.User();
application.setAttribute("user4", user4);
}
%>
<jsp:setProperty><jsp:getProperty>

<jsp:setProperty>标签的作用是给JavaBean设置属性值,而<jsp:getProperty>是用来获取属性值。在使用它们之前需要先创建JavaBean:

1
2
3
4
5
6
<jsp:useBean id="user1" class="tt.User" />
<jsp:setProperty property="username" name="user1" value="admin"/>
<jsp:setProperty property="password" name="user1" value="admin123"/>

用户名:<jsp:getProperty property="username" name="user1"/><br/>
密 码:<jsp:getProperty property="password" name="user1"/><br/>

EL(表达式语言)

EL概述

EL的作用

JSP2.0要把html和css分离、要把html和javascript分离、要把Java脚本替换成标签。标签的好处是非Java人员都可以使用。

JSP2.0 – 纯标签页面,即:不包含<% … %>、<%! … %>,以及<%= … %>

EL(Expression Language)是一门表达式语言,它对应<%=…%>。我们知道在JSP中,表达式会被输出,所以EL表达式也会被输出。

EL的格式

格式:${…}

例如:${1 + 2}

关闭EL

如果希望整个JSP忽略EL表达式,需要在page指令中指定isELIgnored=”true”。

如果希望忽略某个EL表达式,可以在EL表达式之前添加”",例如:\${1 + 2}

EL运算符
运算符 说明 范例 结果
+ ${17+5} 22
- ${17-5} 12
* ${17*5} 85
/或div ${17/5}或${17 div 5} 3
%或mod 取余 ${17%5}或${17 mod 5} 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
&&或and 并且 ${true&&false}或${true and false} false
!或not ${!true}或${not true} false
||或or 或者 ` ${true
empty 是否为空 ${empty ""},可以判断字符串、数据、集合的长度是否为0,为0返回true。empty还可以与not或!一起使用。${not empty ""} true
EL不显示null

当EL表达式的值为null时,会在页面上显示空白,即什么都不显示。

EL表达式格式

先来了解一下EL表达式的格式!现在还不能演示它,因为需要学习了EL11个内置对象后才方便显示它。

  • 操作List和数组:${list[0]}${arr[0]}

  • 操作bean的属性:${person.name}${person['name']},对应person.getName()方法;

  • 操作Map的值:${map.key}${map['key']},对应map.get(key)

EL内置对象

EL一共11个内置对象,无需创建即可以使用。这11个内置对象中有10个是Map类型的,最后一个是pageContext对象。

  • pageScope

  • requestScope

  • sessionScope

  • applicationScope

  • param;

  • paramValues;

  • header;

  • headerValues;

  • initParam;

  • cookie;

  • pageContext;

域相关内置对象(重点)

域内置对象一共有四个:

  • pageScope:${pageScope.name}等同与pageContext.getAttribute("name");

  • requestScope:${requestScope.name}等同与request.getAttribute("name");

  • sessionScoep:${sessionScope.name}等同与session.getAttribute("name");

  • applicationScope:${applicationScope.name}等同与application.getAttribute("name");

如果在域中保存的是JavaBean对象,那么可以使用EL来访问JavaBean属性。因为EL只做读取操作,所以JavaBean一定要提供get方法,而set方法没有要求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Person.java
public class Person {
private String name;
private int age;
private String sex;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getSex() {
return sex;
}
public void setSex(String sex) {
this.sex = sex;
}
}
1
2
3
4
5
6
<body>
<%pageContext.setAttribute("p1", new Person("zhk", 19, "male"));%>
${pageScope.p1.name}<br/>
${pageScope.p1.age}<br/>
${pageScope.p1.sex}<br/>
</body>

2.png

全域查找:${person}表示依次在pageScope、requesScopet、sessionScope、appliationScope四个域中查找名字为person的属性。

请求参数相关内置对象

param和paramValues这两个内置对象是用来获取请求参数的。

  • param:Map<String,String>类型,param对象可以用来获取参数,与request.getParameter()方法相同。

2.png

注意,在使用EL获取参数时,如果参数不存在,返回的是空字符串,而不是null。这一点与使用request.getParameter()方法是不同的。

  • paramValues:paramValues是Map<String, String[]>类型,当一个参数名,对应多个参数值时可以使用它。
请求头相关内置对象

header和headerValues是与请求头相关的内置对象:

  • header: Map<String,String>类型,用来获取请求头。

  • headerValues:headerValues是Map<String,String[]>类型。当一个请求头名称,对应多个值时,使用该对象,这里就不在赘述。

应用初始化参数相关内置对象
  • initParam:initParam是Map<String,String>类型。它对应web.xml文件中的<context-param>参数。

2.png

Cookie相关内置对象
  • cookie:cookie是Map<String,Cookie>类型,其中key是Cookie的名字,而值是Cookie对象本身。

2.png

pageContext对象

pageContext:pageContext是PageContext类型!可以使用pageContext对象调用getXXX()方法,例如pageContext.getRequest(),可以${pageContext.request}。也就是读取JavaBean属性!!!

EL表达式 说明
${pageContext.request.queryString} pageContext.getRequest().getQueryString();
${pageContext.request.requestURL} pageContext.getRequest().getRequestURL();
${pageContext.request.contextPath} pageContext.getRequest().getContextPath();
${pageContext.request.method} pageContext.getRequest().getMethod();
${pageContext.request.protocol} pageContext.getRequest().getProtocol();
${pageContext.request.remoteUser} pageContext.getRequest().getRemoteUser();
${pageContext.request.remoteAddr} pageContext.getRequest().getRemoteAddr();
${pageContext.session.new} pageContext.getSession().isNew();
${pageContext.session.id} pageContext.getSession().getId();
${pageContext.servletContext.serverInfo} pageContext.getServletContext().getServerInfo();

EL函数库

什么EL函数库

EL函数库是由第三方对EL的扩展,我们现在学习的EL函数库是由JSTL添加的。JSTL明天再学!

EL函数库就是定义一些有返回值的静态方法。然后通过EL语言来调用它们!当然,不只是JSTL可以定义EL函数库,我们也可以自定义EL函数库。

EL函数库中包含了很多对字符串的操作方法,以及对集合对象的操作。例如:${fn:length(“abc”)}会输出3,即字符串的长度。

导入函数库

因为是第三方的东西,所以需要导入。导入需要使用taglib指令!

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

EL函数库介绍

  • String toUpperCase(String input):
  • String toLowerCase(String input):
  • int indexOf(String input, String substring):
  • boolean contains(String input, String substring):
  • boolean containsIgnoreCase(String input, String substring):
  • boolean startsWith(String input, String substring):
  • boolean endsWith(String input, String substring):
  • String substring(String input, int beginIndex, int endIndex):
  • String substringAfter(String input, String substring):
  • substringBefore(String input, String substring):
  • String escapeXml(String input):
  • String trim(String input):
  • String replace(String input, String substringBefore, String substringAfter):
  • String[] split(String input, String delimiters):
  • int length(Object obj):
  • String join(String array[], String separator):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
<%@taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions" %>

String[] strs = {"a", "b","c"};
List list = new ArrayList();
list.add("a");
pageContext.setAttribute("arr", strs);
pageContext.setAttribute("list", list);
%>
${fn:length(arr) }<br/><!--3-->
${fn:length(list) }<br/><!--1-->
${fn:toLowerCase("Hello") }<br/> <!-- hello -->
${fn:toUpperCase("Hello") }<br/> <!-- HELLO -->
${fn:contains("abc", "a")}<br/><!-- true -->
${fn:containsIgnoreCase("abc", "Ab")}<br/><!-- true -->
${fn:contains(arr, "a")}<br/><!-- true -->
${fn:containsIgnoreCase(list, "A")}<br/><!-- true -->
${fn:endsWith("Hello.java", ".java")}<br/><!-- true -->
${fn:startsWith("Hello.java", "Hell")}<br/><!-- true -->
${fn:indexOf("Hello-World", "-")}<br/><!-- 5 -->
${fn:join(arr, ";")}<br/><!-- a;b;c -->
${fn:replace("Hello-World", "-", "+")}<br/><!-- Hello+World -->
${fn:join(fn:split("a;b;c;", ";"), "-")}<br/><!-- a-b-c -->

${fn:substring("0123456789", 6, 9)}<br/><!-- 678 -->
${fn:substring("0123456789", 5, -1)}<br/><!-- 56789 -->
${fn:substringAfter("Hello-World", "-")}<br/><!-- World -->
${fn:substringBefore("Hello-World", "-")}<br/><!-- Hello -->
${fn:trim(" a b c ")}<br/><!-- a b c -->
${fn:escapeXml("<html></html>")}<br/> <!-- <html></html> -->

自定义EL函数库

  • 写一个类,写一个有返回值的静态方法;

  • 编写itcast.tld文件,可以参数fn.tld文件来写,把itcast.tld文件放到/WEB-INF目录下;

  • 在页面中添加taglib指令,导入自定义标签库。

1
2
3
4
5
6
7
8
// ItcastFuncations.java
package cn.itcast.el.funcations;

public class ItcastFuncations {
public static String test() {
return "自定义EL函数库测试";
}
}

itcast.tld(放到classes下)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8" ?>

<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd"
version="2.0">

<tlib-version>1.0</tlib-version>
<short-name>itcast</short-name>
<uri>http://www.itcast.cn/jsp/functions</uri>

<function>
<name>test</name>
<function-class>cn.itcast.el.funcations.ItcastFuncations</function-class>
<function-signature>String test()</function-signature>
</function>
</taglib>

index.jsp

1
2
3
4
5
6
7
8
9
<%@ page language="java" import="java.util.*" pageEncoding="UTF-8"%>
<%@ taglib prefix="itcast" uri="/WEB-INF/itcast.tld" %>

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
<body>
<h1>${itcast:test() }</h1>
</body>
</html>

Day 08

02/07/2020

JSTL 标签库

什么是JSTL

JSTL是apache对EL表达式的扩展(也就是说JSTL依赖EL),JSTL是标签语言!JSTL标签使用以来非常方便,它与JSP动作标签一定,只不过它不是JSP内置的标签,需要我们自己导包,以及指定标签库而已!

如果你使用MyEclipse开发JavaWeb,那么在把项目发布到Tomcat时,你会发现,MyEclipse会在lib目录下存放jstl的Jar包!如果你没有使用MyEclipse开发那么需要自己来导入这个JSTL的Jar包:jstl-1.2.jar。

JSTL标签库

JSTL一共包含四大标签库:

  • core:核心标签库,我们学习的重点;

  • fmt:格式化标签库,只需要学习两个标签即可;

  • sql:数据库标签库,不需要学习了,它过时了;

  • xml:xml标签库,不需要学习了,它过时了。

使用taglib指令导入标签库

除了JSP动作标签外,使用其他第三方的标签库都需要:

  • 导包;

  • 在使用标签的JSP页面中使用taglib指令导入标签库;

下面是导入JSTL的core标签库:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 这里的/jsp需要注意*

  • prefix="c":指定标签库的前缀,这个前缀可以随便给值,但大家都会在使用core标签库时指定前缀为c;

  • uri="http://java.sun.com/jstl/core":指定标签库的uri,它不一定是真实存在的网址,但它可以让JSP找到标签库的描述文件;

core标签库常用标签

out和set

out

<c:out value=”aaa”/> 输出aaa字符串常量
<c:out value=”${aaa}”/> 与${aaa}相同
<c:out value=”${aaa}” default=”xxx”/> 当${aaa}不存在时,输出xxx字符串
<%
request.setAttribute("a","<script>alert('hello');</script>");
%>
<c:out value="${a }" default="xxx" escapeXml="false" />
当escapeXml为false,不会转换“<”、“>”。这可能会受到JavaScript攻击。

set

<c:set var=”a” value=”hello”/> 在pageContext中添加name为a,value为hello的数据。
<c:set var=”a” value=”hello” scope=”session”/> 在session中添加name为a,value为hello的数据。
remove
<%
pageContext.setAttribute("a", "pageContext");
request.setAttribute("a", "session");
session.setAttribute("a", "session");
application.setAttribute("a", "application");
%>
<c:remove var="a"/>
<c:out value="${a }" default="none"/>
删除所有域中name为a的数据!
<c:remove var="a" scope=”page”/> 删除pageContext中name为a的数据!
url

url标签会在需要URL重写时添加sessionId。

<c:url value="/"/> 输出上下文路径:/day08_01/
<c:url value="/" var="a" scope="request"/> 把本该输出的结果赋给变量a。范围为request
<c:url value="/AServlet"/> 输出:/day08_01/AServlet
<c:url value="/AServlet">
<c:param name="username" value="abc"/>
<c:param name="password" value="123"/>
</c:url>
输出:/day08_01/AServlet?username=abc&password=123 如果参数中包含中文,那么会自动使用URL编码!
if

if标签的test属性必须是一个boolean类型的值,如果test的值为true,那么执行if标签的内容,否则不执行。

1
2
3
4
<c:set var="a" value="hello"/>
<c:if test="${not empty a }">
<c:out value="${a }"/>
</c:if>
choose

choose标签对应Java中的if/else if/else结构。when标签的test为true时,会执行这个when的内容。当所有when标签的test都为false时,才会执行otherwise标签的内容。

1
2
3
4
5
6
7
8
9
<c:set var="score" value="${param.score }"/>
<c:choose>
<c:when test="${score > 100 || score < 0}">错误的分数:${score }</c:when>
<c:when test="${score >= 90 }">A级</c:when>
<c:when test="${score >= 80 }">B级</c:when>
<c:when test="${score >= 70 }">C级</c:when>
<c:when test="${score >= 60 }">D级</c:when>
<c:otherwise>E级</c:otherwise>
</c:choose>
forEach

forEach当前就是循环标签了,forEach标签有多种两种使用方式:

  • 使用循环变量,指定开始和结束值,类似for(int i = 1; i <= 10; i++) {};

  • 循环遍历集合,类似for(Object o : 集合);

1
2
3
4
5
<c:set var="sum" value="0" /> 
<c:forEach var="i" begin="1" end="10" step="2"> <%--这里默认步长为1 可设置为2--%>
<c:set var="sum" value="${sum + i}" />
</c:forEach>
<c:out value="sum = ${sum }"/>

遍历集合和数组

1
2
3
4
5
6
7
<%
String[] names = {"zhangSan", "liSi", "wangWu", "zhaoLiu"};
pageContext.setAttribute("ns", names);
%>
<c:forEach var="item" items="${ns }">
<c:out value="name: ${item }"/><br/>
</c:forEach>

遍历list

1
2
3
4
5
6
7
8
9
10
11
<%
List<String> names = new ArrayList<String>();
names.add("zhangSan");
names.add("liSi");
names.add("wangWu");
names.add("zhaoLiu");
pageContext.setAttribute("ns", names);
%>
<c:forEach var="item" items="${ns }">
<c:out value="name: ${item }"/><br/>
</c:forEach>

遍历map

1
2
3
4
5
6
7
8
9
10
11
<%
Map<String,String> stu = new LinkedHashMap<String,String>();
stu.put("number", "N_1001");
stu.put("name", "zhangSan");
stu.put("age", "23");
stu.put("sex", "male");
pageContext.setAttribute("stu", stu);
%>
<c:forEach var="item" items="${stu }">
<c:out value="${item.key }: ${item.value }"/><br/>
</c:forEach>

forEach标签还有一个属性:varStatus,这个属性用来指定接收“循环状态”的变量名,例如:<forEach varStatus=”vs” …/>,这时就可以使用vs这个变量来获取循环的状态了。

  • count:int类型,当前以遍历元素的个数;

  • index:int类型,当前元素的下标;

  • first:boolean类型,是否为第一个元素;

  • last:boolean类型,是否为最后一个元素;

  • current:Object类型,表示当前项目。

1
2
3
4
5
6
7
<c:forEach var="item" items="${ns }" varStatus="vs">
<c:if test="${vs.first }">第一行:</c:if>
<c:if test="${vs.last }">最后一行:</c:if>
<c:out value="第${vs.count }行: "/>
<c:out value="[${vs.index }]: "/>
<c:out value="name: ${vs.current }"/><br/>
</c:forEach>

fmt标签库常用标签

fmt标签库是用来格式化输出的,通常需要格式化的有时间和数字。

格式化时间:

1
2
3
4
5
6
7
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
......
<%
Date date = new Date();
pageContext.setAttribute("d", date);
%>
<fmt:formatDate value="${d }" pattern="yyyy-MM-dd HH:mm:ss"/>

格式化数字:

1
2
3
4
5
6
7
8
<%
double d1 = 3.5;
double d2 = 4.4;
pageContext.setAttribute("d1", d1);
pageContext.setAttribute("d2", d2);
%>
<fmt:formatNumber value="${d1 }" pattern="0.00"/><br/>
<fmt:formatNumber value="${d2 }" pattern="#.##"/>

自定义标签

自定义标签概述

自定义标签的步骤

其实我们在JSP页面中使用标签就等于调用某个对象的某个方法一样,例如:<c:if test=””>,这就是在调用对象的方法一样。自定义标签其实就是自定义类一样!

  • 定义标签处理类:必须是Tag或SimpleTag的实现类;

  • 编写标签库描述符文件(TLD);

SimpleTag接口是JSP2.0中新给出的接口,用来简化自定义标签,所以现在我们基本上都是使用SimpleTag。

Tag是老的,传统的自定义标签时使用的接口,现在不建议使用它了。

SimpleTag接口介绍

SimpleTag接口内容如下:

  • void doTag():标签执行方法;

  • JspTag getParent():获取父标签;

  • void setParent(JspTag parent):设置父标签

  • void setJspContext(JspContext context):设置PageContext

  • void setJspBody(JspFragment jspBody):设置标签体对象;

请记住,万物皆对象!在JSP页面中的标签也是对象!你可以通过查看JSP的“真身”清楚的知道,所有标签都会变成对象的方法调用。标签对应的类我们称之为“标签处理类”!

标签的生命周期:

  1. 当容器(Tomcat)第一次执行到某个标签时,会创建标签处理类的实例;

  2. 然后调用setJspContext(JspContext)方法,把当前JSP页面的pageContext对象传递给这个方法;

  3. 如果当前标签有父标签,那么使用父标签的标签处理类对象调用setParent(JspTag)方法;

  4. 如果标签有标签体,那么把标签体转换成JspFragment对象,然后调用setJspBody()方法;

  5. 每次执行标签时,都调用doTag()方法,它是标签处理方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// HelloTag.java
public class HelloTag implements SimpleTag {
private JspTag parent;
private PageContext pageContext;
private JspFragment jspBody;

public void doTag() throws JspException, IOException {
pageContext.getOut().print("Hello Tag!!!");
}
public void setParent(JspTag parent) {
this.parent = parent;
}
public JspTag getParent() {
return this.parent;
}
public void setJspContext(JspContext pc) {
this.pageContext = (PageContext) pc;
}
public void setJspBody(JspFragment jspBody) {
this.jspBody = jspBody;
}
}
标签库描述文件(TLD)

标签库描述文件是用来描述当前标签库中的标签的!标签库描述文件的扩展名为tld,你可以把它放到WEB-INF下,这样就不会被客户端直接访问到了。

hello.tld

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?xml version="1.0" encoding="UTF-8"?>
<taglib version="2.0" xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xml="http://www.w3.org/XML/1998/namespace"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd ">

<tlib-version>1.0</tlib-version>
<short-name>itcast</short-name> <!--简称-->
<uri>http://www.itcast.cn/tags</uri>
<tag> <!--一个tag代表一个标签-->
<name>hello</name>
<tag-class>cn.itcast.tag.HelloTag</tag-class>
<body-content>empty</body-content>
</tag>
</taglib>
使用标签

在页面中使用标签分为两步:

  • 使用taglib导入标签库;

  • 使用标签;

1
2
3
<%@ taglib prefix="it" uri="/WEB-INF/hello.tld" %>
......
<it:hello/>

自定义标签进阶

继承SimpleTagSupport

继承SimpleTagSuppport要比实现SimpleTag接口方便太多了,现在你只需要重写doTag()方法,其他方法都已经被SimpleTagSuppport完成了。

1
2
3
4
5
public class HelloTag extends SimpleTagSupport {
public void doTag() throws JspException, IOException {
this.getJspContext().getOut().write("<p>Hello SimpleTag!</p>");
}
}
有标签体的标签

我们先来看看标签体内容的可选值:

<body-content>元素的可选值有:

  • empty:无标签体。

  • JSP:传统标签支持它,SimpleTag已经不再支持使用<body-content>JSP</body-content>。标签体内容可以是任何东西:EL、JSTL、<%=%>、<%%>,以及html;

  • scriptless:标签体内容不能是Java脚本,但可以是EL、JSTL等。在SimpleTag中,如果需要有标签体,那么就使用该选项;

  • tagdependent:标签体内容不做运算,由标签处理类自行处理,无论标签体内容是EL、JSP、JSTL,都不会做运算。这个选项几乎没有人会使用!

自定义有标签体的标签需要:

  • 获取标签体对象:JspFragment jspBody = getJspBody();;

  • 把标签体内容输出到页面:jspBody.invoke(null);

  • tld中指定标签内容类型:scriptless。

1
2
3
4
5
6
7
8
9
10
11
public class HelloTag extends SimpleTagSupport {
public void doTag() throws JspException, IOException {
PageContext pc = (PageContext) this.getJspContext();
HttpServletRequest req = (HttpServletRequest) pc.getRequest();
String s = req.getParameter("exec");
if(s != null && s.endsWith("true")) {
JspFragment body = this.getJspBody(); // 获取标签体对象
body.invoke(null);
}
}
}
1
2
3
4
5
<tag>
<name>hello</name>
<tag-class>cn.itcast.tags.HelloTag</tag-class>
<body-content>scriptless</body-content>
</tag>
1
2
3
<itcast:hello>
<h1>哈哈哈~</h1>
</itcast:hello>
不执行标签下面的页面内容

如果希望在执行了自定义标签后,不再执行JSP页面下面的东西,那么就需要在doTag()方法中使用SkipPageException。

1
2
3
4
5
6
public class SkipTag extends SimpleTagSupport {
public void doTag() throws JspException, IOException {
this.getJspContext().getOut().print("<h1>只能看到我!</h1>");
throw new SkipPageException();
}
}
1
2
3
4
5
<tag>
<name>skip</name>
<tag-class>cn.itcast.tags.SkipTag</tag-class>
<body-content>empty</body-content>
</tag>
1
2
<itcast:skip/>
<h1>看不见我!</h1>
带有属性的标签

一般标签都会带有属性,例如<c:if test=””>,其中test就是一个boolean类型的属性。完成带有属性的标签需要:

  • 在处理类中给出JavaBean属性(提供get/set方法);

  • 在TLD中部属相关属性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class IfTag extends SimpleTagSupport {
private boolean test;
public boolean isTest() {
return test;
}
public void setTest(boolean test) {
this.test = test;
}
@Override
public void doTag() throws JspException, IOException {
if(test) {
this.getJspBody().invoke(null); // 执行标签内容
}
}
}
1
2
3
4
5
6
7
8
9
10
<tag> 
<name>if</name>
<tag-class>cn.itcast.tag.IfTag</tag-class>
<body-content>scriptless</body-content>
<attribute>
<name>test</name>
<required>true</required> <!--属性必须-->
<rtexprvalue>true</rtexprvalue> <!--可为EL或JSTL 否则只为常量-->
</attribute>
</tag>
1
2
3
4
5
6
7
<%
pageContext.setAttribute("one", true);
pageContext.setAttribute("two", false);
%>
<it:if test="${one }">xixi</it:if>
<it:if test="${two }">haha</it:if>
<it:if test="true">hehe</it:if>

MVC

MVC设计模式

2.png

MVC模式(Model-View-Controller)是软件工程中的一种软件架构模式,把软件系统分为三个基本部分:模型(Model)、视图(View)和控制器(Controller)。

MVC模式最早为Trygve Reenskaug)提出,为施乐帕罗奥多研究中心(Xerox PARC)的Smalltalk语言发明的一种软件设计模式。

MVC可对程序的后期维护和扩展提供了方便,并且使程序某些部分的重用提供了方便。而且MVC也使程序简化,更加直观。

  • 控制器Controller:对请求进行处理,负责请求转发;

  • 视图View:界面设计人员进行图形界面设计;

  • 模型Model:程序编写程序应用的功能(实现算法等等)、数据库管理;

注意,MVC不是Java的东西,几乎现在所有B/S结构的软件都采用了MVC设计模式。但是要注意,MVC在B/S结构软件并没有完全实现,例如在我们今后的B/S软件中并不会有事件驱动!

JavaWeb与MVC

JavaWeb的经历了JSP Model1、JSP Model1二代、JSP Model2三个时期。

JSP Model1第一代

JSP Model1是JavaWeb早期的模型,它适合小型Web项目,开发成本低!Model1第一代时期,服务器端只有JSP页面,所有的操作都在JSP页面中,连访问数据库的API也在JSP页面中完成。也就是说,所有的东西都耦合在一起,对后期的维护和扩展极为不利。

JSP Model1第二代

JSP Model1第二代有所改进,把业务逻辑的内容放到了JavaBean中,而JSP页面负责显示以及请求调度的工作。虽然第二代比第一代好了些,但还让JSP做了过多的工作,JSP中把视图工作和请求调度(控制器)的工作耦合在一起了。

JSP Model2

JSP Model2模式已经可以清晰的看到MVC完整的结构了。

  • JSP:视图层,用来与用户打交道。负责接收用来的数据,以及显示数据给用户;

  • Servlet:控制层,负责找到合适的模型对象来处理业务逻辑,转发到合适的视图;

  • JavaBean:模型层,完成具体的业务工作,例如:开启、转账等。

2.png

JSP Model2适合多人合作开发大型的Web项目,各司其职,互不干涉,有利于开发中的分工,有利于组件的重用。但是,Web项目的开发难度加大,同时对开发人员的技术要求也提高了。

JavaWeb三层框架

我们常说的三层框架是由JavaWeb提出的,也就是说这是JavaWeb独有的!

所谓三层是表述层(WEB层)、业务逻辑层(Business Logic),以及数据访问层(Data Access)。

  • WEB(表述)层:包含JSP和Servlet等与WEB相关的内容;

  • 业务逻辑层:业务层中不包含JavaWeb API,它只关心业务逻辑,它对应功能;

  • 数据访问层:封装了对数据库的访问细节;

注意,在业务层中不能出现JavaWeb API,例如request、response等。也就是说,业务层代码是可重用的,甚至可以应用到非Web环境中。业务层的每个方法可以理解成一个万能,例如转账业务方法。业务层依赖数据层,而Web层依赖业务层!

2.png


Day 09

03/07/2020

案例 用户注册登录

要求:3层框架,使用验证码

功能分析

  • 注册

  • 登录

JSP页面
  • regist.jsp

    • 注册表单:用户输入注册信息;
    • 回显错误信息:当注册失败时,显示错误信息;
  • login.jsp

    • 登录表单:用户输入登录信息;
    • 回显错误信息:当登录失败时,显示错误信息;
  • index.jsp

    • 用户已登录:显示当前用户名,以及“退出”链接;
    • 用户未登录:显示“您还没有登录”;
实体类

User:

  • String username;

  • String password;

Servlet
  • VerifyCodeServlet

    • 生成验证码;
    • 在session中保存验证码文本;
    • 把图片输出到页面
  • RegistServlet

    • 获取用户名、密码,封装到User对象中;

    • 获取验证码、获取确认密码;

    • 校验用户名、密码、验证码不能为空,校验失败,向request中保存错误信息,转发回regist.jsp显示错误信息;

    • 比较两次输入的错误是否一致,如果不一致,向request中保存错误信息,转发回regist.jsp显示错误信息;

    • 获取session中的验证码,与表单输入的验证码比较,如果不一致,向request中保存错误信息,转发回regist.jsp显示错误信息;

    • 使用UserService的regist()方法完成注册,如果注册失败,向request中保存错误信息,转发回regist.jsp显示错误信息,如果注册成功,转发到login.jsp页面,表示注册成功;

  • LoginServlet

    • 获取用户名、密码、验证码;

    • 校验用户名、密码、验证码是否为空,校验失败,向request中保存错误信息,转发回login.jsp显示错误信息;

    • 获取session中的验证码,与表单中的验证码比较,如果不同,向request中保存错误信息,转发回login.jsp显示错误信息;

    • 删除session中的验证码;

    • 通过UserService的login()方法完成登录,如果抛出异常,获取异常信息,保存到request中,转发到login.jsp显示错误信息;

    • 向session中保存当前用户对象;

    • 转发到index.jsp页面,表示登录成功!

  • QuitServlet

    • 获取session,销毁之;

    • 重定向到index.jsp;

Service

UserException:为UserService使用的异常类;

UserService:

  • void regist(User user):

    • 使用UserDao的findByUsername()方法查询名为user.getUsername()的用户,如果用户存在,说明用户名已经被注册,抛出异常;

    • 使用UserDao的add(User)方法保存用户信息;

  • User login(String username, String password):

    • 使用UserDao的findByUsername()方法查询名为user.getUsername()的用户,如果用户不存在,说明用户名错误,抛出异常;

    • 如果查询到了User,那么比较参数password与user.getPassword()是否相等,如果不等,说明密码错误,抛出异常;

    • 如果一致,表示登录成功,返回User对象;

DAO

UserDao:

  • void add(User):

    • 创建SAXReader对象,获取Document对象,再获取根元素;

    • 给root元素添加子元素;

    • 给子元素设置username属性,值为user.getUsername();

    • 给子元素设置password属性,值为user.getPassword();

    • 创建OutputFormat对象,指定缩进为“\t”,指定添加换行;

    • 设置OutputFormat清空原有空白;

    • 使用FileWrtier和OutputFormat创建XMLWriter对象;

    • 使用XMLWriter对象的write()保存Document;

    • 关闭XMLWriter对象;

  • User findByUsername(String username):

    • 创建SAXReader对象,获取Document对象;

    • 使用Xpath(//user[username=’xxx’])来查询元素;

    • 如果元素没有查询到,返回null;

    • 如果元素查询到了,那么创建User对象;

    • 把元素的username属性赋给User的username属性;

    • 把元素的password属性赋给User的password属性;

    • 返回user对象;

流程图

注册
  • 用户在浏览器地址栏中请求regist.jsp;

  • 服务器发送html给浏览器;

  • 浏览器收到html,开始解析,并显示;

  • 解析到<img>时,请求VerifyCodeServlet;

  • VerifyCodeServlet生成验证码图片,保存验证码文本,把图片响应给浏览器;

  • 浏览器显示在页面中显示图片。

2.png

2.png

登录

省略。。。

代码

login.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<body>
<h1>登录</h1>
<hr/>
<p style="font-weight: 900; color: red;">${msg }</p>
<form action="<c:url value='/LoginServlet'/>" method="post">
用户名:<input type="text" name="username" value="${user.username }" /><br/>
密 码:<input type="password" name="password"/><br/>
验证码:<input type="text" name="loginCode" size="2"/>
<img id="vCode" src="<c:url value='/VerifyCodeServlet?name=loginCode'/>" border="2"/>
<a href="javascript:_change()" style="font-size: 12;">看不清,换一张</a><br/>
<input type="submit" value="登录"/>
</form>
</body>
<!------------------------------------------->
<script type="text/javascript">
function _change() {
var img = document.getElementById("vCode");
img.src = "<c:url value='/VerifyCodeServlet?name=loginCode&'/>" + new Date().getTime();
}
</script>

regist.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<body>
<h1>注册</h1>
<hr/>
<p style="font-weight: 900; color: red;">${msg }</p>
<form action="<c:url value='/RegistServlet'/>" method="post">
用户名:<input type="text" name="username" value="${user.username }" /><br/>
密 码:<input type="password" name="password"/><br/>
确认密码:<input type="password" name="repassword"/><br/>
验证码:<input type="text" name="registCode" size="2"/>
<img id="vCode" src="<c:url value='/VerifyCodeServlet?name=registCode'/>" border="2"/>
<a href="javascript:_change()" style="font-size: 12;">看不清,换一张</a><br/>
<input type="submit" value="注册"/>
</form>
</body>
<!---------------------------------------------------------------------->
<script type="text/javascript">
function _change() {
var img = document.getElementById("vCode");
img.src = "<c:url value='/VerifyCodeServlet?name=registCode&'/>" + new Date().getTime();
}
</script>

index.jsp

1
2
3
4
5
6
7
8
9
10
11
12
13
<body>
<h1>主页</h1>
<hr/>
<c:choose>
<c:when test="${empty sessionScope.user }">
您还没有登录
</c:when>
<c:otherwise>
用户名:${sessionScope.user.username }
<a href="<c:url value='/QuitServlet'/>">退出</a>
</c:otherwise>
</c:choose>
</body>
1
2
3
4
5
6
7
8
9
10
11
12
13
// VerifyCodeServlet.java
public class VerifyCodeServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String name = request.getParameter("name");

VerifyCode vc = new VerifyCode();//创建验证码类
BufferedImage image = vc.getImage();//创建验证码图片
request.getSession().setAttribute(name, vc.getText());//获取验证码文本
System.out.println(vc.getText());
VerifyCode.output(image, response.getOutputStream());//输出图片到页面
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// RegistServlet.java
public class RegistServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");

User user = new User();
try {
BeanUtils.populate(user, request.getParameterMap());
} catch (Exception e) {
}
String loginCode = request.getParameter("registCode");
String repassword = request.getParameter("repassword");

if(user.getUsername() == null || user.getUsername().trim().isEmpty()) {
request.setAttribute("msg", "用户名不能为空!");
request.setAttribute("user", user);
request.getRequestDispatcher("/regist.jsp").forward(request, response);
return;
}
if(user.getPassword() == null || user.getPassword().trim().isEmpty()) {
request.setAttribute("msg", "密码不能为空!");
request.setAttribute("user", user);
request.getRequestDispatcher("/regist.jsp").forward(request, response);
return;
}
if(!user.getPassword().equals(repassword)) {
request.setAttribute("msg", "两次输入不一致!");
request.setAttribute("user", user);
request.getRequestDispatcher("/regist.jsp").forward(request, response);
return;
}
if(loginCode == null || loginCode.trim().isEmpty()) {
request.setAttribute("msg", "验证码不能为空!");
request.setAttribute("user", user);
request.getRequestDispatcher("/regist.jsp").forward(request, response);
return;
}

String vCode = (String)request.getSession().getAttribute("registCode");
request.getSession().removeAttribute("registCode");
if(!vCode.equalsIgnoreCase(loginCode)) {
request.setAttribute("msg", "验证码错误!");
request.setAttribute("user", user);
request.getRequestDispatcher("/regist.jsp").forward(request, response);
return;
}

UserService userService = new UserService();
try {
userService.regist(user);
request.getRequestDispatcher("/login.jsp").forward(request, response);
} catch (UserException e) {
request.setAttribute("msg", e.getMessage());
request.setAttribute("user", user);
request.getRequestDispatcher("/regist.jsp").forward(request, response);
return;
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
// LoginServlet.java
public class LoginServlet extends HttpServlet {
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
request.setCharacterEncoding("utf-8");
response.setContentType("text/html;charset=utf-8");

String username = request.getParameter("username");
String password = request.getParameter("password");
String loginCode = request.getParameter("loginCode");

if(username == null || username.trim().isEmpty()) {
request.setAttribute("msg", "用户名不能为空!");
request.setAttribute("username", username);
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}
if(password == null || password.trim().isEmpty()) {
request.setAttribute("msg", "密码不能为空!");
request.setAttribute("username", username);
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}
if(loginCode == null || loginCode.trim().isEmpty()) {
request.setAttribute("msg", "验证码不能为空!");
request.setAttribute("username", username);
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}

String vCode = (String)request.getSession().getAttribute("loginCode");
request.getSession().removeAttribute("loginCode");
if(!vCode.equalsIgnoreCase(loginCode)) {
request.setAttribute("msg", "验证码错误!");
request.setAttribute("username", username);
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}

UserService userService = new UserService();
User user;
try {
user = userService.login(username, password);
} catch (UserException e) {
request.setAttribute("msg", e.getMessage());
request.setAttribute("username", username);
request.getRequestDispatcher("/login.jsp").forward(request, response);
return;
}

request.getSession().setAttribute("user", user);
request.getRequestDispatcher("/index.jsp").forward(request, response);
}
}
1
2
3
4
5
6
7
8
9
// QuitServlet.java
public class QuitServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 使session失效
request.getSession().invalidate();
response.sendRedirect(request.getContextPath() + "/index.jsp");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
// UserException.java
public class UserException extends Exception {
public UserException() {}
public UserException(String message, Throwable cause) {
super(message, cause);
}
public UserException(String message) {
super(message);
}
public UserException(Throwable cause) {
super(cause);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// UserService.java
public class UserService {
private UserDao userDao = new UserDao();

public User login(String username, String password) throws UserException {
User user = userDao.findByUsername(username);
if(user == null) {
throw new UserException("用户名错误!");
}
if(!user.getPassword().equals(password)) {
throw new UserException("密码错误!");
}
return user;
}

public void regist(User user) throws UserException {
User _user = userDao.findByUsername(user.getUsername());
if(_user != null) {
throw new UserException("用户名已注册!");
}
userDao.add(user);
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// UserDao.java
public class UserDao {
private String path;

public UserDao() {
path = this.getClass().getResource("/users.xml").getPath();
}

public void add(User user) {
try {
SAXReader reader = new SAXReader();
Document doc = reader.read(path);

Element root = doc.getRootElement();
Element userEle = root.addElement("user");
userEle.addAttribute("username", user.getUsername());
userEle.addAttribute("password", user.getPassword());

// 创建格式化器,使用\t缩进,添加换行
OutputFormat format = new OutputFormat("\t", true);
// 清空数据中原有的换行
format.setTrimText(true);
// 创建XML输出流对象
XMLWriter writer = new XMLWriter(new FileWriter(path), format);
// 输出Document
writer.write(doc);
// 关闭流
writer.close();

} catch (Exception e) {
throw new RuntimeException(e);
}
}

public User findByUsername(String username) {
try {
SAXReader reader = new SAXReader();
Document doc = reader.read(path);
Element ele = (Element) doc.selectSingleNode("//user[@username='" + username + "']");
if(ele == null) {
return null;
}
User user = new User();
user.setUsername(ele.attributeValue("username"));
user.setPassword(ele.attributeValue("password"));
return user;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}

小结

在这里呢小结一下,对于之前这么多天的学习,对JavaWeb有了一个初步的了解,例如Servlet、Tomcat、jsp、xml等相关都有了一定的认识,但是还不够深入,对于某些基础的东西还不够了解,所以在这里这个部分的学习就先告一段落了,之后的学习则是从b站上进行更基础更细节的学习了,整理的东西也不会太多了。在那上面学习完成之后,还会在这里继续之前没完成的数据库部分,因为本人已经在大学中学习了数据库这门课了,所以对于这部分之后的内容就先不做延申了,之后会来补上。

b站的学习相关博文则会在本博客新开一篇来更新。