0 从Servlet出发

Servlet是一个相当古老的技术了,可以说现在的任何一家公司都不再直接使用servlet来写项目,但是这并不能否认他的重要性,把目光放到现在的市场上,可以看到流行的spring全家桶也是基于Servlet开发出来的。各种框架追寻到底层,还是Servlet。

学习Servlet的最大好处就是可以为我们打下坚实的基础,进而在往后的框架学习过程中学得更加得心应手。

1 Tomcat

Tomcat 服务器是一个免费的开放源代码的运行JAVA的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试 jsp 程序的首选,它的底层是Socket的一个程序,同时它也是一个JSP和Serlvet的容器。

1.1 为什么我们需要用到Tomcat

如果你学过html,css,你会知道你写的页面只能自己访问,别人不能远程访问你写的页面,Tomcat就是提供能够让别人访问自己写的页面的一个程序

Tomcat和servlet在网络中的位置

1.2 Tomcat的目录结构

Tomcat目录结构

1.2.1 bin目录

bin目录主要是用来存放tomcat的命令,主要有两大类,一类是以.sh结尾的(linux命令),另一类是以.bat结尾的(windows命令)。同时很多环境变量的设置也都在此处,例如可以设置JDK路径、tomcat路径 。

  • startup 用来启动tomcat
  • shutdown 用来关闭tomcat
  • 修改catalina可以设置tomcat的内存

1.2.2 conf目录

conf目录主要是用来存放tomcat的一些配置文件。

  • server.xml可以设置端口号、设置域名或IP、默认加载的项目、请求编码
  • web.xml可以设置tomcat支持的文件类型
  • context.xml可以用来配置数据源之类的
  • tomcat-users.xml用来配置管理tomcat的用户与权限
  • 在Catalina目录下可以设置默认加载的项目

1.2.3 lib目录

lib目录主要用来存放tomcat运行需要加载的jar包。
例如,像连接数据库的jdbc的包我们可以加入到lib目录中来。

1.2.4 logs目录

logs目录用来存放tomcat在运行过程中产生的日志文件,非常重要的是在控制台输出的日志。
在windows环境中,控制台的输出日志在catalina.xxxx-xx-xx.log文件中
在linux环境中,控制台的输出日志在catalina.out文件中

1.2.5 temp目录

temp目录用户存放tomcat在运行过程中产生的临时文件。(清空不会对tomcat运行带来影响)

1.2.6 webapp目录

webapps目录用来存放应用程序,当tomcat启动时会去加载webapps目录下的应用程序。可以以文件夹、war包、jar包的形式发布应用。
当然,你也可以把应用程序放置在磁盘的任意位置,只要在配置文件中映射好就行。

1.2.7 work目录

work目录用来存放tomcat在运行时的编译后文件,例如JSP编译后的文件。
清空work目录,然后重启tomcat,可以达到清除缓存的作用。

1.3 Tomcat的web应用实例

我们可以在webapps中建立了webtest目录,下面放置我们的html文件,jsp文件,图片等等,那么webtest就被当做一个web应用被管理起来。

举例:在webapps下创建一个web站点,在web站点下创建一个html文件,然后访问这个html文件。

web应用实例

以下是访问结果:

访问结果

对于一个web站点,它内部的文件目录是有规范的。这个规范是约定熟成的。

web站点内部规范

为什么要这样设置web站点⽬录呢?

假如说我有一个需求:我有多个html⽂件,想把其中的⼀个html⽂件作为我web站点的⾸⻚。 如果没有WEB-INF⽬录下的web.xml⽂件⽀持,是⽆法解决我的这个需求的。

下面将webtest站点下的welcome.html页面作为我网站的首页,那么我们需要做的是:

  • 在web站点下面创建一个WEB-INF目录
  • 在WEB-INF目录中创建一个web.xml文件

添加欢迎页面

添加配置文件

web.xml我们不可能会写,所以可以在webapps⽬录下其他的站点中抄⼀份过来【复制ROOT/WEB-INF/web.xml的⽂件到⾃⼰的站点中】在web.xml中添加以下代码

1
2
3
<welcome-file-list>
<welcome-file>welcome.html</welcome-file>
</welcome-file-list>

最后web.xml文件中的内容为:

1
2
3
4
5
6
7
8
9
10
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">

<welcome-file-list>
<welcome-file>welcome.html</welcome-file>
</welcome-file-list>
</web-app>

访问结果:

带欢迎界面的访问结果

1.4 Tomcat的体系结构

Tomcat体系结构

1.5 Tomcat访问WEB资源的流程图

Tomcat访问WEB资源的流程图

2 Servlet

2.1 Servlet定义

我们编写Java程序想要在⽹上实现聊天、发帖这样⼀些的交互性功能,普通的Java技术是⾮常难完成的。于是sun公司就提供了一种名为Servlet的技术供我们使⽤,Servlet其实就是⼀个遵循Servlet开发的java类,Servlet由服务器调⽤的,运⾏在服务器端。

2.2 HTTP协议

2.1.1 HTTP协议的定义

超⽂本传输协议(HTTP,HyperText Transfer Protocol)是互联⽹上应⽤最为⼴泛的⼀种⽹络协议。所有的WWW⽂件都必须遵守这个标准。它是TCP/IP协议的⼀个应⽤层协议。

简单来说,HTTP协议就是客户端和服务器交互的⼀种通迅的格式

例子:在浏览器点击⼀个链接,浏览器就为我打开这个链接的网页

原理:当在浏览器中点击这个链接的时候,浏览器会向服务器发送⼀段⽂本,告诉服务器请求打开的是哪⼀个网页。服务器收到请求后,就返回⼀段⽂本给浏览器,浏览器会将该⽂本解析,然后显示出来。

这段文本就是遵循HTTP协议规范的。

2.1.2 HTTP1.0和HTTP1.1的区别

  • HTTP1.0协议中,客户端与web服务器建立连接后,只能获得⼀个web资源【短连接,获取资源后就断开连接】
  • HTTP1.1协议中,允许客户端与web服务器建立连接后,在⼀个连接上获取多个web资源【保持连接】

2.1.3 HTTP请求

浏览器向服务器请求某个web资源时,称之为浏览器向服务器发送了⼀个http请求。

⼀个完整http请求应该包含三个部分:

  • 请求⾏【描述客户端的请求⽅式、请求的资源名称,以及使⽤的HTTP协议版本号】
  • 多个消息头【描述客户端请求哪台主机,以及客户端的⼀些环境信息等】
  • 一个空行

2.1.3.1 请求行

请求行:GET/java.html HTTP/1.1

请求行中的GET称之为请求方式,请求方式有:POST、GET、HEAD、OPTIONS、DELETE、TRACE、PUT。

常用的有:POST、GET

⼀般来说,当我们点击超链接,通过地址栏访问都是get请求方式。通过表单提交的数据⼀般是post方式。可以简单理解GET方式用来查询数据,POST方式用来提交数据,get的提交速度比post快

GET方式:在URL地址后附带的参数是有限制的,其数据容量通常不能超过1K。

POST方式:可以在请求的实体内容中向服务器发送数据,传送的数据量⽆限制。

2.1.1.2 请求头

  • Accept:text/html,img/* 【浏览器告诉服务器,它支持的数据类型】
  • Accept-Charset:ISO-8859-1 【浏览器告诉服务器,它支持哪种字符集】
  • Accept-Encoding:gzip,compress 【浏览器告诉服务器,它支持的压缩格式】
  • Accept-Language:en-us,zn-cn 【浏览器告诉服务器,它的语言环境】
  • HOST:www.jegret.cn 【浏览器告诉服务器,它想访问哪台主机】
  • If-Modified-Since:Tue,11 Jul 2000 20:32:55 GMT【浏览器告诉服务器,缓冲数据的时间】
  • Referer:http://www.jegret.cn 【浏览器告诉服务器,客户机是从哪个页面来的,常用与做防盗链】
  • User-Agent:Mozilla/4.0(compatible;MSIE 5.5;Windows NT 5.0) 【浏览器告诉服务器,浏览器的内核是什么】
  • Cookie:浏览器告诉服务器,带来的Cookie是什么
  • Connection:close/Keep-Alive 【浏览器告诉服务器,请求完断开连接还是保持连接】
  • Date:Tue,11 Jul 2000 20:32:55 GMT 【浏览器告诉服务器,请求的时间】

2.1.4 HTTP响应

一个HTTP响应代表着服务器向浏览器回送数据。

一个完整的HTTP响应应该包含四个部分:

  • 一个状态行【用与描述服务器对请求的处理结果】
  • 多个消息头【用与描述服务器的基本信息,以及数据的描述,服务器通过这些数据的描述信息,可以通知客户端如何处理等一会它回送的数据】
  • 一个空行
  • 实体内容【服务器向客户端回送数据】

2.1.4.1 状态行

格式:HTTP版本号 状态码 原因叙述

状态行:HTTP/1.1 200 OK

状态码用与表示服务器对请求的处理结果,它是一个三位的十进制数。响应状态码分为5类

状态码 含义
100~199 表示成功接受请求,要求客户端继续提交下一次请求才能完成整个处理过程
200~299 表示成功接受请求并已完成整个处理过程,常用200
300~399 为完成请求,客户需进一步细化请求。例如,请求的资源已移动一个新地址,常用302、307和304
400~499 客户端的请求有错误,常用404
500~599 服务器端出现错误,常用500

2.1.4.2 响应头

  • Location:http://www.jegret.cn/index.html【服务器告诉浏览器要跳转到哪个页面】
  • Server:apache tomcat【服务器告诉浏览器,服务器的型号是什么】
  • Content-Encoding:gzip 【服务器告诉浏览器数据压缩的格式】
  • Content-Length:80 【服务器告诉浏览器回送数据的长度】
  • Content-Language:zh-cn 【服务器告诉浏览器,服务器的语言环境】
  • Content-Type:text/html; charset=GB2312 【服务器告诉浏览器,回送数据的类型】
  • Last-Modified:Tue, 11 Jul 2000 18:23:51 GMT【服务器告诉浏览器该资源上次更新时间】
  • Refresh:1;url=http://www.jegret.cn【服务器告诉浏览器要定时刷新】
  • Content-Disposition:attachment; filename=aaa.zip【服务器告诉浏览器以下载方式打开数据】
  • Transfer-Encoding:chunked 【服务器告诉浏览器数据以分块方式回送】
  • Set-Cookie:SS=Q0=5Lb_nQ; path=/search【服务器告诉浏览器要保存Cookie】
  • Expires:-1【服务器告诉浏览器不要设置缓存】
  • Cache-Control:no-cache 【服务器告诉浏览器不要设置缓存】
  • Pragma:no-cache 【服务器告诉浏览器不要设置缓存】
  • Connection:close/Keep-Alive 【服务器告诉浏览器连接方式】
  • Date:Tue,11 Jul 2000 20:32:55 GMT 【服务器告诉浏览器回送数据的时间】

2.3 一个简易的Servlet程序示例

2.3.1 编写逻辑代码

我们创建一个类实现Servlet接口,然后我们就可以发现我们一共有五个方法需要重写

  • init:初始化
  • getServletConfig:获取servlet配置
  • service:服务
  • getServletInfo:获取servlet信息
  • destroy:销毁

service方法是我们写逻辑代码的地方, 这里我们调用ServletResponse对象的方法向浏览器输出一个“Hello World!!!”以作示例。

HelloWorld示例

2.3.2 配置xml文件

光写servlet文件还不行,Tomcat还不知道如何访问这个servlet,这时我们就需要来编写xml文件来配置浏览器访问servlet的路径。

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
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
version="4.0">
<!--
当浏览器输入 http://localhost:8080/MyServlet 时会发生以下事情:
1.发现Tomcat的web.xml文件中有配置这个映射路径 <url-pattern>/MyServlet</url-pattern>
2.查看映射路径的servlet名字是什么 <servlet-name>MyServlet</servlet-name>
3.通过映射路径的servlet名字查找到对应的配置servlet的名字 <servlet-name>MyServlet</servlet-name>
4.通过servlet的名字查找servlet编译后class文件存在的位置 <servlet-class>cn.egret.MyServlet</servlet-class>
5.执行MyServlet
-->

<servlet>
<!--为MyServlet配置一个名字-->
<servlet-name>MyServlet</servlet-name>
<!--类的存放位置在哪里【要带上包名】-->
<servlet-class>cn.egret.MyServlet</servlet-class>
</servlet>

<!--配置MyServlet的映射路径-->
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<!--外界访问MyServlet的路径-->
<url-pattern>/MyServlet</url-pattern>
</servlet-mapping>
</web-app>

这个时候我们就可以通过Tomcat访问到我们需要的servlet。

访问MyServlet

2.4 Servlet生命周期

Servlet的生命周期可分为以下5个步骤:

  1. 加载Servlet。当Tomcat第⼀次访问Servlet的时候,Tomcat会负责创建Servlet的实例
  2. 初始化。当Servlet被实例化后,Tomcat会调用init()方法初始化这个对象
  3. 处理服务。当浏览器访问Servlet的时候,Servlet 会调用service()方法处理请求
  4. 销毁。当Tomcat关闭时或者检测到Servlet要从Tomcat删除的时候会自动调用destroy()方法,让该实例释放掉所占的资源。⼀个Servlet如果长时间不被使用的话,也会被Tomcat自动销毁
  5. 卸载。当Servlet调用完destroy()方法后,等待垃圾回收。如果有需要再次使⽤这个Servlet,会重新调⽤init()方法进行初始化操作。

简单总结:只要访问Servlet,service()就会被调用。init()只有第⼀次访问Servlet的时候才会被调用。 destroy()只有在Tomcat关闭的时候才会被调用。

2.5 通过继承HttpServlet编写Servlet程序

如果每次编写程序都实现Servlet接口,那么每次我们都需要去实现五个接口,这显然太麻烦了。所以为了方便,在实际开发中我们一般会通过继承HttpServlet来编写Servlet程序,HttpServlet类实现了Servlet接⼝的所有⽅法。编写Servlet时,只需要继承HttpServlet,重写我们需要的方法即可。

HttpServlet类在原有Servlet接口上添加了⼀些与HTTP协议处理⽅法,它比Servlet接口的功能更为强大。
⼀般我们开发的时候,都是重写doGet()和doPost()方法的。

HttpServlet

2.6 Servlet调用图

Servlet调用图

2.7 Servlet的一些小细节

2.7.1 一个servlet可以被多次映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<servlet>
<!--为MyServlet配置一个名字-->
<servlet-name>MyServlet</servlet-name>
<!--类的存放位置在哪里【要带上包名】-->
<servlet-class>cn.egret.MyServlet</servlet-class>
</servlet>

<!--配置MyServlet的映射路径-->
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<!--外界访问MyServlet的路径-->
<url-pattern>/MyServlet1</url-pattern>
</servlet-mapping>

<!--配置MyServlet的映射路径-->
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<!--外界访问MyServlet的路径-->
<url-pattern>/MyServlet2</url-pattern>
</servlet-mapping>

⽆论我访问的是http://localhost:8080/MyServlet1还是http://localhost:8080/MyServlet2。我访问的都是MyServlet。

2.7.2 Servlet映射的URL可以使用通配符

简单来说:

  • 以”/’开头和以”/*”结尾的是用来做路径映射的。
  • /* 匹配所有
  • 以前缀”*.”开头的是用来做扩展映射的。
  • “/” 是用来定义default servlet映射的。会匹配到/login这样的路径型url,不会匹配到模式为*.jsp这样的后缀型url
  • 剩下的都是用来定义详细映射的。比如: /aa/bb/cc.action

注意:为什么定义”/*.action”这样一个看起来很正常的匹配在启动tomcat时会报错?因为这个匹配即属于路径映射,也属于扩展映射,导致容器无法判断。

在Servlet映射到的URL中也可以使用通配符,但是只能有两种固定的格式:

  • 一种格式是 *.扩展名
  • 另一种格式是以正斜杠(/)开头并以“/*”结尾。

如果.扩展名和正斜杠(/)开头并以“/”结尾两种通配符同时出现,匹配的是哪一个呢?*

  • 看谁的匹配度高,谁就被选择
  • *.扩展名的优先级最低

示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/*</url-pattern> <!--匹配所有-->
</servlet-mapping>

<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>*.html</url-pattern> <!--匹配以.html结尾的所有-->
</servlet-mapping>

<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/action/*</url-pattern> <!--路径映射-->
</servlet-mapping>

<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/</url-pattern> <!--default servlet映射-->
</servlet-mapping>

<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<url-pattern>/aa/bb/cc.action</url-pattern> <!--详细映射-->
</servlet-mapping>

2.8 Servlet是单例的

2.8.1 Servlet是单例的原因

浏览器多次对Servlet的请求,⼀般情况下,服务器只创建⼀个Servlet对象,也就是说,Servlet对象⼀旦创建了,就会驻留在内存中,为后续的请求做服务,直到服务器关闭

2.8.2 每次访问请求对象和响应对象都是新的

对于每次访问请求,Servlet引擎都会创建⼀个新的HttpServletRequest请求对象和⼀个新的HttpServletResponse响应对象,然后将这两个对象作为参数传递给它调⽤的Servlet的service()方法,service⽅法再根据请求⽅式分别调⽤doXXX⽅法。

2.8.3 线程安全

当多个⽤户访问Servlet的时候,服务器会为每个⽤户创建⼀个线程。当多个⽤户并发访问Servlet共享资源的时候就会出现线程安全问题。

线程安全的原则:

  • 如果⼀个变量需要多个⽤户共享,则应当在访问该变量的时候,加同步机制synchronized (对象){}
  • 如果⼀个变量不需要共享,则直接在 doGet() 或者 doPost()定义.这样不会存在线程安全问题

2.9 在web访问任何资源都是在访问Servlet

⽆论在web中访问什么资源【包括JSP】,都是在访问Servlet。没有手工配置缺省Servlet的时候,你访问静态图⽚,静态网页,缺省Servlet会在你web站点中寻找该图片或网页,如果有就返回给浏览器,没有就报404错误

1
2
3
4
<servlet-mapping>
<servlet-name>default</servlet-name>
<url-pattern>/</url-pattern> <!--缺省Servlet的URL配置-->
</servlet-mapping>

2.10 ServletConfig对象

通过ServletConfig对象我们可以方便地读取到web.xml配置的初始化参数。

web.xml文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<servlet>
<!--为MyServlet配置一个名字-->
<servlet-name>MyServlet</servlet-name>
<!--类的存放位置在哪里【要带上包名】-->
<servlet-class>cn.egret.MyServlet2</servlet-class>

<!--初始化参数-->
<init-param>
<param-name>testName</param-name>
<param-value>HelloHelloHelloHelloHelloHello</param-value>
</init-param>
</servlet>

<!--配置MyServlet的映射路径-->
<servlet-mapping>
<servlet-name>MyServlet</servlet-name>
<!--外界访问MyServlet的路径-->
<url-pattern>/MyServlet</url-pattern>
</servlet-mapping>

MyServlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyServlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
ServletConfig servletConfig = this.getServletConfig();
String value = servletConfig.getInitParameter("testName");
System.out.println(value);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

访问结果:

2.11 ServletContext对象

当Tomcat启动的时候,就会创建⼀个ServletContext对象。它代表着当前web站点

ServletContext对象的用处:

  • ServletContext既然代表着当前web站点,那么所有Servlet都共享着⼀个ServletContext对象,所以Servlet之间可以通过ServletContext实现通讯
  • ServletConfig获取的是配置的是单个Servlet的参数信息,ServletContext可以获取的是配置整个web站点的参数信息
  • 利⽤ServletContext读取web站点的资源⽂件
  • 实现Servlet的转发【⽤ServletContext转发不多,主要⽤request转发】

2.11.1 实现Servlet之间的通信

ServletContext对象可以被称之为域对象。域对象可以简单理解成⼀个容器【类似于Map集合】
实现Servlet之间通讯就要用到ServletContext的setAttribute(String name,Object obj)方法

  • 第⼀个参数是关键字
  • 第⼆个参数是你要存储的对象

接下来是一个示例

MyServlet2代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyServlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//获取到ServletContext对象
ServletContext servletContext = this.getServletContext();
String value = "hellllllllllll";

// name作为关键字,value作为值存进域对象【类型于Map集合】
servletContext.setAttribute("name", value);
resp.getWriter().write("Here is MyServlet2");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

MyServlet3代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyServlet3 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取ServletContext对象
ServletContext servletContext = this.getServletContext();
// 通过关键字获取存储在域对象的值
String value = (String) servletContext.getAttribute("name");
System.out.println(value);

resp.getWriter().write("Here is MyServlet3");
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

注意:要先访问MyServlet2,把数据填充进去,再访问MyServlet3,才能把数据读取出来。

以下是访问结果:

访问MyServlet2

MyServlet2示例

访问MyServlet3

MyServlet3示例

读取数据并输出

ServletContext对象访问结果

2.11.2 获取web站点配置的信息

web.xml⽂件⽀持对整个站点进⾏配置参数信息【所有Servlet都可以取到该参数信息】

1
2
3
4
<context-param>
<param-name>name</param-name>
<param-value>testValue</param-value>
</context-param>

以下是MyServlet2代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class MyServlet2 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 获取到ServletContext对象
ServletContext servletContext = this.getServletContext();

// 获取值
String value = servletContext.getInitParameter("name");
System.out.println("MyServlet2:" + value);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

以下是MyServlet3代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MyServlet3 extends HttpServlet {

@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {

// 获取到ServletContext对象
ServletContext servletContext = this.getServletContext();

// 获取值
String value = servletContext.getInitParameter("name");
System.out.println("MyServlet3:" + value);
}

@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
super.doPost(req, resp);
}
}

依次访问MyServlet2MyServlet3,可见两者都能获取这个站点的配置信息。

ServletContext对象获取配置信息

3 Response对象

3.1 response、request对象

Tomcat收到客户端的http请求,会针对每⼀次请求,分别创建⼀个代表请求的request对象、和代表响应的response对象。
既然request对象代表http请求,那么我们获取浏览器提交过来的数据,找request对象即可。
response对象代表http响应,那么我们向浏览器输出数据,找response对象即可。

3.2 HttpServletResponse对象

http响应由状态行、实体内容、消息头、⼀个空行组成。HttpServletResponse对象就封装http响应的信息。

3.2.1 调用getOutputStream()方法向浏览器输出数据

3.2.1.1 print()方法

1
2
3
4
5
6
7
8
9
10
11
//  获取到OutputStream流
ServletOutputStream servletOutputStream = response.getOutputStream();
// 向浏览器输出数据
servletOutputStream.print("aaaa"); // 输出成功

servletOutputStream.print("大中国"); // 输出失败

/**
为什么会出现异常呢?在IO中我们学过,outputStream是输出二进制数据的,print()⽅法接收了⼀个字符串,print()方法要把“中国”改成二进制数据,Tomcat使⽤ISO 8859-1编码对其进行转换,“中国”根本对ISO8859-1编码不支持,所以出现了异常。
*/

3.2.1.2 write()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//  获取到OutputStream流
ServletOutputStream servletOutputStream = response.getOutputStream();
// 向浏览器输出数据
servletOutputStream.write("aaaa".getBytes()); // 输出成功

servletOutputStream.write("你好呀我是Egret".getBytes()); // 输出成功

/**
"你好呀我是Egret".getBytes() 这句代码在转成byte[]数组的时候默认查的是gb2312编码,而"你好呀我是Egret"支持gb2312编码,所以可以正常显示出来。
*/

//程序要实现通用性,应该使用的是UTF-8编码,我们在字符串转换成字节数组时指定UTF-8编码,看看会怎么样。
response.getOutputStream().write("你好呀我是Egret".getBytes("UTF-8"));// 乱码
//原因是这样的:在向服务器输出的中文是UTF-8编码的,⽽浏览器采用的是GBK,GBK想显示UTF-8的中文数据,所以才会乱码

//设置头信息,告诉浏览器我回送的数据编码是UTF-8的
response.setHeader("Content-Type", "text/html;charset=UTF-8");
response.getOutputStream().write("你好呀我是Egret".getBytes("UTF-8"));// 输出成功

3.2.2 调用getWriter()方法向浏览器输出数据

对于getWriter()方法而言,是Writer的子类,那么只能向浏览器输出字符数据,不能输出⼆进制数据, 使用getWriter()方法输出中文数据,代码如下:

1
2
3
4
5
6
//设置浏览器⽤UTF-8编码显示数据,
response.setContentType("text/html;charset=UTF-8");

//获取到printWriter对象
PrintWriter printWriter = response.getWriter();
printWriter.write("你好呀我是Egret");

getOutputStream()方法和getWriter()方法的一些问题:

  • **getWriter()和getOutputStream()**两个方法不能同时调用。如果同时调用就会出现异常。
  • Servlet程序向ServletOutputStream或PrintWriter对象中写入的数据将被Servlet引擎从response里面获取,Servlet引擎将这些数据当作响应消息的正文,然后再与响应状态行和各响应头组合后输出到客户端。
  • Servletserice()方法结束后【也就是doPost()或者doGet()结束后】,Servlet引擎将检查getWriter或getOutputStream方法返回的输出流对象是否已经调用过close方法,如果没有,Servlet引擎将调用close方法关闭该输出流对象。

有关问题的解决方法:https://juejin.cn/post/6844904128431259662

3.2.3 重定向

重定向(Redirect)就是通过各种方法将各种网络请求重新定个方向转到其他位置(如:网页重定向、域名重定向、路由选中的变化也是对数据报文经由路径的一种重定向)。

1
2
// 重定向到index.jsp⻚⾯
response.sendRedirect("/egret/index.jsp");

出重定向是通过302状态码和跳转地址实现的。于是乎,我们设置http消息头就可以实现重定向跳转。

1
2
3
4
5
6
7
8
9
// 设置状态码是302
response.setStatus(302);

// HttpServletResponse把常⽤的状态码封装成静态常量了,所以我们可以使用
// SC_MOVED_TEMPORARILY代表着302
response.setStatus(HttpServletResponse.SC_MOVED_TEMPORARILY);

//跳转的地址是index.jsp⻚⾯
response.setHeader("Location", "/egret/index.jsp");

其实**sendRedirect()方法就是对setStatus()setHeader()**进行封装,原理就是setStatus()和setHeader()

4 Request对象

HttpServletRequest对象代表客户端的请求,当客户端通过HTTP协议访问服务器时,HTTP请求头中的所有信息都封装在这个对象中,开发⼈员通过这个对象的方法,可以获得客户这些信息。
简单来说,要得到浏览器信息,就找HttpServletRequest对象

4.1 HttpServletRequest常用方法

4.1.1 获得客户机【浏览器】信息

  • getRequestURL方法返回客户端发出请求时的完整URL。
  • getRequestURI方法返回请求行中的资源名部分。
  • getQueryString 方法返回请求行中的参数部分。
  • getPathInfo方法返回请求URL中的额外路径信息。额外路径信息是请求URL中的位于Servlet的路径之后和查询参数之前的内容,它以“/”开头。
  • getRemoteAddr方法返回发出请求的客户机的IP地址
  • getRemoteHost方法返回发出请求的客户机的完整主机名
  • getRemotePort方法返回客户机所使用的网络端⼝号
  • getLocalAddr方法返回WEB服务器的IP地址。
  • getLocalName方法返回WEB服务器的主机名

4.1.2 获得客户机请求头

  • getHeader方法
  • getHeaders方法
  • getHeaderNames方法

4.1.3 请求参数

  • getParameter方法
  • getParameterValues(String name)方法
  • getParameterNames方法
  • getParameterMap方法
  • setAttribute(String name, Object o)方法

4.2 获取数据

4.2.1 POST方式

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
<form action="/egret/MyServlet" method="post">
<table>
<tr>
<td>用户名</td>
<td><input type="text" name="username"></td>
</tr>
<tr>
<td>密码</td>
<td><input type="password" name="password"></td>
</tr>
<tr>
<td>性别</td>
<td>
<input type="radio" name="gender" value="男">
<input type="radio" name="gender" value="⼥">
</td>
</tr>
<tr>
<td>爱好</td>
<td>
<input type="checkbox" name="hobbies" value="游泳">游泳
<input type="checkbox" name="hobbies" value="跑步">跑步
<input type="checkbox" name="hobbies" value="⻜翔">⻜翔
</td>
</tr>
<input type="hidden" name="aaa" value="my name is zhongfucheng">
<tr>
<td>你的家乡</td>
<td>
<select name="address">
<option value="⼴州">⼴州</option>
<option value="深圳">深圳</option>
<option value="北京">北京</option>
</select>
</td>
</tr>
<tr>
<td>详细说明:</td>
<td>
<textarea cols="30" rows="2" name="textarea"></textarea>
</td>
</tr>
<tr>
<td><input type="submit" value="提交"></td>
<td><input type="reset" value="重置"></td>
</tr>
</table>

MyServlet:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//设置request字符编码的格式
request.setCharacterEncoding("UTF-8");

//通过html的name属性,获取到值
String username = request.getParameter("username");
String password = request.getParameter("password");
String gender = request.getParameter("gender");

//复选框和下拉框有多个值,获取到多个值
String[] hobbies = request.getParameterValues("hobbies");
String[] address = request.getParameterValues("address");

//获取到文本域的值
String description = request.getParameter("textarea");

//得到隐藏域的值
String hiddenValue = request.getParameter("aaa");

// 接下来是各种实际操作

4.2.2 GET方式

常见的get方式提交数据有:使用超链接sendRedirect()

1
2
3
4
5
<!--超链接方式-->
<a href="/zhongfucheng/Servlet111?username=xxx">使⽤超链接将数据带给浏览器</a>

<!--sendRedirect()-->
sendRedirect("servlet的地址?参数名="+参数值 &"参数名="+参数值);

MyServlet:

1
2
3
//  接收以username为参数名带过来的值
String username = request.getParameter("username");
System.out.println(username);

4.3 中文乱码问题

POST:当我们点击提交按钮的时候,数据封装进了Form Data中,http请求中把实体主体带过去了【传输的数据称之为实体主体】,既然request对象封装了 http请求,所以request对象可以解析到发送过来的数据,于是只要把编码设置成UTF-8就可以解决乱码问题了。

1
request.setCharacterEncoding("utf-8");

GET:GET方式的数据是从消息行带过去的,没有封装到request对象里面,所以使用request设置编码是无效的。

要解决get方式乱码问题也不难,我们既然知道Tomcat默认的编码是ISO 8859-1,那么get方式由消息体带过去给浏览器的时候肯定是用ISO 8859-1编码了。

1
2
3
4
5
6
7
8
//  此时得到的数据已经是被ISO 8859-1编码后的字符串了,这个是乱码
String name = request.getParameter("username");

// 乱码通过反向查ISO 8859-1得到原始的数据
byte[] bytes = name.getBytes("ISO8859-1");

// 通过原始的数据,设置正确的码表,构建字符串
String value = new String(bytes, "UTF-8");

总结:

  • post方式直接改request对象的编码
  • get方式需要手工转换编码
  • get方式也可以修改Tomcat服务器的编码,不推荐,因为会太依赖服务器了!
  • 提交数据能用post就用post

4.4 转发

转发和重定向的功能一样,也是为了实现页面跳转。

1
2
3
4
5
6
7
8
//  以username为关键字存egret值,转发可以保存这个设置的值
request.setAttribute("username", "zhongfucheng");

// 获取到requestDispatcher对象,跳转到index.jsp
RequestDispatcher requestDispatcher = request.getRequestDispatcher("/index.jsp");

// 调⽤requestDispatcher对象的forward()实现转发,传⼊request和response⽅法
requestDispatcher.forward(request, response);

4.4.1 转发的时序图

转发的时序图

4.4.2 转发的禁忌

如果在调⽤forward方法之前,在Servlet程序中写⼊的部分内容已经被真正地传送到了客户端, forward⽅法将抛出IllegalStateException异常。 也就是说:不要在转发之前写数据给浏览器。

如果在调⽤forward方法之前向Servlet引擎的缓冲区中写入了内容,只要写入到缓冲区中的内容还没有被真正输出到客户端,forward方法就可以被正常执行,原来写入到输出缓冲区中的内容将被清空,但是,已写⼊到HttpServletResponse对象中的响应头字段信息保持有效。

4.4.3 重定向和转发的区别

4.4.3.1 实际发生位置不同,地址栏不同

  • 转发是由服务器进行跳转的,在转发的时候,浏览器的地址栏是没有发生变化的,在我访问MyServlet1的时候,即使跳转到了MyServlet3的页面,浏览器的地址还是MyServlet1的。也就是说浏览器是不知道该跳转的动作,转发是对浏览器透明的。通过上面的转发时序图我们也可以发现,实现转发只是⼀次的http请求,⼀次转发中request和response对象都是同⼀个。这也解释了,为什么可以使用request作为域对象进行Servlet之间的通讯。
  • 重定向是由浏览器进行跳转的,进行重定向跳转的时候,浏览器的地址会发生变化的。曾经介绍过:实现重定向的原理是由response的状态码和Location头组合而实现的。这是由浏览器进行的页面跳转实现重定向会发出两个http请求,request域对象是无效的,因为它不是同⼀个request对象

4.4.3.2 用法不同

转发和重定向的时候,资源地址究竟怎么写。

有的时候要把应用名写上,有的时候不用把应用名写上。很容易把⼈搞晕。记住⼀个原则:给服务器用的直接从资源名开始写,给浏览器用的要把应用名写上

1
2
3
4
5
//  转发时"/"代表的是本应用程序的根目录【JavaWebProject】
request.getRequestDispatcher("/资源名URI").forward(request,response);

// 重定向时"/"代表的是webapps目录
response.send("/web应用/资源名 URI");

4.4.3.3 能够去往的URL的范围不⼀样

  • 转发是服务器跳转只能去往当前web应用的资源
  • 重定向是服务器跳转,可以去往任何的资源

4.4.3.4 传递数据的类型不同

  • 转发的request对象可以传递各种类型的数据
  • 包括对象重定向只能传递字符串

4.4.3.5 跳转的时间不同

  • 转发:执行到跳转语句时就会立刻跳转
  • 重定向:整个页面执行完之后才执行跳转

4.4.3.6 转发和重定向的使用场景

转发是带着转发前的请求的参数的。重定向是新的请求
典型的应用场景:

  • 转发:访问 Servlet 处理业务逻辑,然后forward到 jsp 显示处理结果,浏览器里URL不变
  • 重定向:提交表单,处理成功后 redirect 到另⼀个 jsp,防止表单重复提交,浏览器里URL变了

5 Cookie和Session

5.1 会话技术

会话:指⽤户开⼀个浏览器,访问⼀个网站,只要不关闭该浏览器,不管该用户点击多少个超链接,访问多少资源,直到⽤户关闭浏览器,整个这个过程我们称为⼀次会话。

网页之间的交互是通过HTTP协议传输数据的,而HTTP协议是⽆状态的协议。⽆状态的协议是什么意思呢?⼀旦数据提交完后,浏览器和服务器的连接就会关闭,再次交互的时候需要重新建立新的连接。
服务器⽆法确认用户的信息,于是乎,W3C就提出了:给每⼀个用户都发⼀个通行证,⽆论谁访问的时候都需要携带通行证,这样服务器就可以从通行证上确认用户的信息。通行证就是Cookie。
Cookie的流程:浏览器访问服务器,如果服务器需要记录该用户的状态,就使用response向浏览器发送⼀个Cookie,浏览器会把Cookie保存起来。当浏览器再次访问服务器的时候,浏览器会把请求的网址连同Cookie⼀同交给服务器。

Cookie具有不可跨域名性。浏览器判断⼀个网站是否能操作另⼀个网站的Cookie的依据是域名。所以⼀般来说,当我访问baidu的时候,浏览器只会把baidu颁发的Cookie带过去,⽽不会带上google的Cookie。

5.2.1 Cookie的API

  • Cookie类用于创建⼀个Cookie对象
  • response接口中定义了⼀个addCookie方法,它用于在其响应头中增加⼀个相应的Set-Cookie头字段
  • request接口中定义了⼀个getCookies方法,它用于获取客户端提交的Cookie

常用接口:

  • public Cookie(String name,String value)
  • setValue与getValue方法
  • setMaxAge与getMaxAge方法
  • setPath与getPath方法
  • setDomain与getDomain方法
  • getName方法

5.2.2 Cookie的使用

发送一个Cookie:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//设置response的编码
response.setContentType("text/html;charset=UTF-8");

//创建Cookie对象,指定名称和值
Cookie cookie = new Cookie("username", "egret");

//设置Cookie的时间,必须要
cookie.setMaxAge(1000);

//向浏览器给⼀个Cookie
response.addCookie(cookie);

//对Unicode字符进行编码,这是发送中文的时候
Cookie cookie = new Cookie("country", URLEncoder.encode(name, "UTF-8"));

接收Cookie:

1
2
3
4
5
6
7
Cookie[] cookies = request.getCookies();
for (int i = 0; cookies != null && i < cookies.length; i++) {
String name = cookies[i].getName();
// 经过URLEncoding就要URLDecoding,解码中文
String value = URLDecoder.decode(cookies[i].getValue(), "UTF-8");
printWriter.write(name + "------" + value);
}

5.2.3 Cookie的有效期

Cookie的有效期是通过**setMaxAge()**来设置的。

  • 如果MaxAge为正数,浏览器会把Cookie写到硬盘中,只要还在MaxAge秒之前,登陆网站时该Cookie就有效【不论关闭了浏览器还是电脑】
  • 如果MaxAge为负数,Cookie是临时性的,仅在本浏览器内有效,关闭浏览器Cookie就失效了,Cookie不会写到硬盘中。Cookie默认值就是-1。所以如果没设置Cookie的有效期,在硬盘中就找不到对应的⽂件。
  • 如果MaxAge为0,则表示删除该Cookie。Cookie机制没有提供删除Cookie对应的⽅法,把MaxAge设置为0等同于删除Cookie。

5.3 Session

5.3.1 Session的定义

Session是另⼀种记录浏览器状态的机制。不同的是Cookie保存在浏览器中,Session保存在服务器中。用户使用浏览器访问服务器的时候,服务器把用户的信息以某种的形式记录在服务器,这种记录就是Session。

如果说Cookie是检查用户身上的”通行证“来确认用户的身份,那么Session就是通过检查服务器上的”客户明细表“来确认用户的身份的。

Session相当于在服务器中建立了⼀份“客户明细表”。

Session比Cookie使用更加方便,Session可以解决Cookie解决不了的事情:Session可以存储对象,而Cookie只能存储字符串。

5.3.2 Session的API

  • long getCreationTime();【获取Session被创建时间】
  • String getId();【获取Session的id】
  • long getLastAccessedTime();【返回Session最后活跃的时间】
  • ServletContext getServletContext();【获取ServletContext对象】
  • void setMaxInactiveInterval(int var1);【设置Session超时时间】
  • int getMaxInactiveInterval();【获取Session超时时间】
  • Object getAttribute(String var1);【获取Session属性】
  • Enumeration getAttributeNames();【获取Session所有的属性名】
  • void setAttribute(String var1, Object var2);【设置Session属性】
  • void removeAttribute(String var1);【移除Session属性】
  • void invalidate();【销毁该Session】
  • boolean isNew();【该Session是否为新的】

5.3.3 Session的应用

Session有着request和ServletContext类似的方法。其实Session也是⼀个域对象。Session作为⼀种记录浏览器状态的机制,只要Session对象没有被销毁,Servlet之间就可以通过Session对象实现通讯。

⼀般来讲,当我们要存进的是用户级别的数据就⽤Session,用户级别即是:只要浏览器不关闭,希望数据还在,就使用Session来保存。

MyServlet1:

1
2
3
4
//  得到Session对象
HttpSession session = request.getSession();
// 设置Session属性
session.setAttribute("name", "egret");

MyServler2:

1
2
3
4
//获取到从MyServlet1的Session存进去的值
HttpSession session = request.getSession();
String value = (String) session.getAttribute("name");
System.out.println(value); // 得到egret

5.3.4 Session的超时时间

Session在用户第⼀次访问服务器Servlet,jsp等动态资源就会被自动创建,Session对象保存在内存⾥,这也就为什么上⾯的例子可以直接使用request对象获取得到Session对象。如果访问HTML,IMAGE等静态资源Session不会被创建。
Session生成后,只要用户继续访问,服务器就会更新Session的最后访问时间,⽆论是否对Session进⾏读写,服务器都会认为Session活跃了⼀次。
由于会有越来越多的用户访问服务器,因此Session也会越来越多。为了防止内存溢出,服务器会把长时间没有活跃的Session从内存中删除,这个时间也就是Session的超时时间

Session的超时时间默认是30分钟,有三种方式可以对Session的超时时间进行修改。

第一种方式:在tomcat/conf/web.xml文件中设置,时间值为20分钟(单位为分钟),所有的WEB应用都有效

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

第二种方式:在单个的web.xml文件中设置,对单个web应用有效,如果有冲突,以自己的web应用为准

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

第三种方式:通过setMaxInactiveInterval()方法设置,单位为秒

1
2
//  设置Session最长超时时间为60秒,这里的单位是秒
session.setMaxInactiveInterval(60);

5.3.5 Session的原理

Session之所以可以识别不同的用户,依靠的就是Cookie

Cookie是服务器自动颁发给浏览器的,不用我们手工创建的。该Cookie的maxAge值默认是**-1,也就是说仅当前浏览器使用,不会将该Cookie存在硬盘中**。

当我们访问一个Servlet时,服务器就会创建⼀个Session对象,然后执行我们的程序代码,并自动颁发个Cookie给用户浏览器。然后当我们用同⼀个浏览器访问Servlet时,浏览器会把Cookie的值通过http协议带过去给服务器,这个时候服务器就知道用哪⼀个Session。而当我们使用新会话的浏览器访问Servlet的时候,该新浏览器并没有Cookie,服务器⽆法辨认使用哪⼀个Session,所以这时就不知道该用哪个Session来执行程序。

5.3.6 URL重写

当前浏览器把Cookie禁用之后,浏览器在发请求的时候,就不会把cookie带到服务器端了(其中最重要的也包括JSESSIONID),因为禁用Cookie之后浏览器拒绝一切站点向浏览器写入cookie的(注意再禁用之前是否已经有一些已经存在的cookie了),这样的话,多个请求就不能在服务器端拿到同一个session对象了(因为发送请求的时候没有把JSESSIONID的值传给服务器)。

HttpServletResponse类提供了两个URL地址重写的⽅法:

  • encodeURL(String url)
  • encodeRedirectURL(String url)

注意:这两个方法会自动判断该浏览器是否⽀持Cookie,如果支持Cookie的话,重写后的URL 地址就不会带有JSESSIONID了。

1
2
3
String url = "/egret/MyServlet";
response.sendRedirect(response.encodeURL(url));
// 重写后的url: /egret/MyServlet?jsessionid=session的id

URL地址重写的原理:将Session的id信息重写到URL地址中。服务器解析重写后URL,获取Session的id。这样⼀来,即使浏览器禁用掉了Cookie,但Session的id通过服务器端传递,还是可以使用Session来记录用户的状态

5.4 Cookie和Session的区别

从存储方式上比较

  • Cookie只能存储字符串,如果要存储非ASCII字符串还要对其编码。
  • Session可以存储任何类型的数据,可以把Session看成是⼀个容器

从隐私安全上比较

  • Cookie存储在浏览器中,对客户端是可见的,信息容易泄露出去,如果使用Cookie,最好将Cookie加密。
  • Session存储在服务器上,对客户端是透明的,不存在敏感信息泄露问题。

从有效期上比较

  • Cookie保存在硬盘中,只需要设置maxAge属性为比较大的正整数,即使关闭浏览器,Cookie还是存在的。
  • Session保存在服务器中,设置maxInactiveInterval属性值来确定Session的有效期,并且Session依赖于名为JSESSIONID的Cookie,该Cookie默认的maxAge属性为-1。如果关闭了浏览器,该Session虽然没有从服务器中消亡,但也就失效了。

从对服务器的负担比较

  • Session是保存在服务器的,每个用户都会产生⼀个Session,如果是并发访问的用户非常多,是不能使用Session的,Session会消耗大量的内存。
  • Cookie是保存在客户端的。不占用服务器的资源。像baidu、淘宝这样的大型网站,⼀般都是使用Cookie来进⾏会话跟踪。

从浏览器的支持上比较

  • 如果浏览器禁用了Cookie,那么Cookie是⽆用的了!
  • 如果浏览器禁用了Cookie,Session可以通过URL地址重写来进行会话跟踪。

从跨域名上比较

  • Cookie可以设置domain属性来实现跨域名
  • Session只在当前的域名内有效,不可跨域名