Servlet(控制器)
Java Web 设计模式
Servlet 介绍
Servlet 是 SUN 公司提供的一套规范,名称就叫 Servlet 规范,它也是 JavaEE 规范之一。我们可以通过访问官方 API 学习和查阅里面的内容。
打开官方 API 网址,在左上部分找到 javax.servlet 包,在左下部分找到 Servlet,如下图显示:
通过阅读 API,我们可以得到如下信息:
- Servlet 是一个运行在 Web 服务端的 Java 小程序。
- 它可以用于接收和响应客户端的请求,主要功能在于交互式地浏览和修改数据,生成动态的 Web 内容。
- 要想实现 Servlet 功能,可以实现 Servlet 接口、继承 GenericServlet 或者 HttpServlet。
- 每次请求都会执行 service 方法。
- Servlet 还支持配置。
Servlet 主要执行以下工作:
使用 Servlet,可以收集来自网页表单的用户输入,可以呈现来自数据库或者其他来源的记录,还可以动态地创建网页(JSP)。
- 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 Applet 或自定义的 HTTP 客户端程序的表单。
- 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 Cookies、媒体类型和浏览器能理解的压缩格式等。
- 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,认用 Web 服务,或者直接计算得出对应的响应。
- 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样自包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。
- 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返国的文档类型(例如 HTML)、设置 Cookie 和缓存参数,以及其他类似的任务。
Servlet 的优势
Java Servlet 通常情况下与使用 CGI(common gateway interface,公共网关接口)实现的程序可以达到异曲同工的效果。但是相比于 CGI,Servlet 有以下几点优势:
- 性能明显更好。
- 在 Web 服务器的地址空间内执行。这样它就没有必要再创建一个单独的进程来处理每个客户端请求。
- Servlet 是独立于平台的,因为它们是用 Java 编写的。
- 服务器上的 Java 安全管理器具有一定的限制,以保护服务器计算机上的资源。因此 Servlet 是可信的。
- Java 类库的全部功能对 Servlet 来说都是可用的。它可以通过 Socket 和 RMI 机制与 Applet、数据库或其他软件进行交互。
Servlet 基础使用
Servlet 编写步骤
1)编码
-
前期准备-创建 Java Web 工程;
-
编写一个普通类继承 GenericServlet 并重写 service 方法;
-
在 web.xml 配置 Servlet;
2)测试
-
在 Tomcat 中部署项目;
-
在浏览器访问 Servlet。
Servlet 执行过程
- 浏览器使用 Socket(IP+端口)与服务器建立连接。
- 浏览器将请求数据按照 HTTP 打成一个数据包(请求数据包)发送给服务器。
- 服务器解析请求数据包并创建请求对象(request)和响应对象(response)。
- 请求对象是 HttpServletRquest 接口的一个实现。
- 响应对象是 HttpServletResponse 接口的一个实现,响应对象用于存放 Servlet 处理自的结果。
- 服务器将解析之后的数据存放到请求对象(request)里面。
- 服务器依据请求资源路径找到相应的 Servlet 配置,通过反射创建 Servlet 实例。
- 服务器调用其 service() 方法,在在调用 serviceO方法时,会将事先创建好的请求对象(request)和响应对象(response)作为参数进行传递。
- 在 Servlet 内部,可以通过 reques st 获得请求数据,或者通过 response 设置响应数据
- 服务器从 response 中获取数据 按照 HTTP 打成一个数据包(响应数据包),发送给浏览器。
- 浏览器解析响应数据包,取出相应的数据,生成相应的界面。
Servlet 类视图
- 在 Servlet 的 API 介绍中,除了继承 GenericServlet 外还可以继承 HttpServlet。
- 通过查阅 servlet 的类视图,我们看到 GenericServlet 还有一个子类 HttpServlet。
- 同时,在 service 方法中还有参数 ServletRequest 和 ServletResponse。
它们的关系如下图所示:
Servlet 编写方式
我们在实现 Servlet 功能时,可以选择以下三种方式:
第一种:实现 Servlet 接口,接口中的方法必须全部实现。
- 使用此种方式,表示接口中的所有方法在需求方面都有重写的必要。此种方式支持最大程度的自定义。
第二种:继承 GenericServlet,service 方法必须重写,其他方可根据需求,选择性重写。
- 使用此种方式,表示只在接收和响应客户端请求这方面有重写的需求,而其他方法可根据实际需求选择性重写,使我们的开发 Servlet 变得简单。但是,此种方式是和 HTTP 协议无关的。
第三种:继承 HttpServlet。
- 它是 javax.servlet.http 包下的一个抽象类,是 GenericServlet 的子类。
如果我们选择继承 HttpServlet 时,只需要重写 doGet 和 doPost 方法,不需要覆盖 service 方法。
- 使用此种方式,表示我们的请求和响应需要和 HTTP 协议相关。也就是说,我们是通过 HTTP 协议来访问的。那么每次请求和响应都符合 HTTP 协议的规范。请求的方式就是 HTTP 协议所支持的方式(HTTP 协议支持 7 种请求方式:GET、POST、PUT、DELETE、TRACE、OPTIONS、HEAD)。
- 为了实现代码的可重用性,通常我们只需要在 doGet 或者 doPost 方法任意一个中提供具体功能即可,而另外的那个方法只需要调用提供了功能的方法。
Servlet 生命周期
对象的生命周期,就是对象从生到死的过程,即:出生——活着——死亡。用更偏向于开发的官方说法,就是对象从被创建到销毁的过程。
Servlet 的生命周期主要有初始化阶段、处理客户端请求阶段和终止阶段:
-
初始化阶段
- Servlet 容器加载 Servlet,加载完成后,Servlet 容器会创建一个 Servlet 实例并调用 init() 方法,init() 方法只会调用一次。
- Servlet 容器会在以下几种情况加载 Servlet:
- Servlet 容器启动时自动加载某些 Servlet,这样需要在 web.xml 文件中添加。
- 在 Servlet 容器启动后,客户首次向 Servlet 发送请求。
- Servlet 类文件被更新后,重新加载。
-
处理客户端请求阶段
- 每收到一个客户端请求,服务器就会产生一个新的线程去处理。对于用户的 Servlet 请求,Servlet 容器会创建一个特定于请求的 ServletRequest 和 ServletResponse。
- 对于 Tomcat 来说,它会将传递来的参数放入一个哈希表中,这是一个 String->String[]的键值映射。
-
终止阶段
- 当 Web 应用被终止,或者 Servlet 容器终止运行,又或者 Servlet 重新加载 Servlet 新实例时,Servlet 容器会调用 Servlet 的 destroy() 方法。
通过分析 Servlet 的生命周期可以发现,它的实例化和初始化只会在请求第一次到达 Servlet 时执行,而销毁只会在 Tomcat 服务器停止时执行。
由此我们得出一个结论,Servlet 对象只会创建一次,销毁一次。所以,每一个 Servlet 只有一个实例对象
。如果一个对象实例在应用中是唯一的存在,那么我们就说它是单实例的,即运用了单例模式。
如下是一个典型的 Servlet 生命周期方案:
- 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。
- Servlet 容器在调用 service() 方法之前加载 Servlet。
- Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。
Servlet 执行时一般要实现的方法
Servlet 类要继承的 GenericServlet 与 HttpServlet 类说明:
- GenericServlet 类是一个实现了 Servlet 的基本特征和功能的基类,其完整名称为 javax.Servlet.GenericServlet,它实现了 Servlet 和 ServletConfig 接口。
- HtpServlet 类是 GenericServlet 的子类,其完整名称为javax.Servlet.HttpServlet,它提供了处理 HTTP 的基本构架。如果一个 Servlet 类要充分使用 HTTP 的功能,就应该继 HttpServlet。在 HttpServlet 类及其子类中,除可以调用 HttpServlet 类内部新定义的方法外,可以调用包括 Servlet、ServletConfig 接口和 GenericServlet 类中的一些方法。
Servlet 若继承上述类,执行时一般要实现的方法:
publle void init(servletconfig config)
public void service(servletRequest request, servletResponse response) public void destroy()
public Servletconfig getservletConfig()
publle string getservletInfo()
-
init()
方法在 Servlet 的生命周期中仅执行一次,在 Servlet 引擎创建 Servlet 对象后执行。 Servlet 在调用 init() 方法时,会传递一个包含 Servlet 的配置和运行环境信息的 ServletConfig 对象。如果初始化代码中要使用到 ServletConfig 对象,则初始化代码就只能在 Servlet 的 init() 方法中编写,而不能在构造方法中编写。默认的 init(方法通常是符合要求的,不过也可以根据需要进行覆盖,比如管理服务器端资源、初始化数据库连接等,默认的 inti()方法设置了 Servlet的初始化参数,并用它的 ServeltConfig 对象参数来启动配置,所以覆盖 init() 方法时,应调用 super.init() 以确保仍然执行这些任务。 -
service()
方法是 Servlet 的核心,用于响应对 Servlet 的访问请求。对于 HttpServlet,每当客户请求一个 HttpServlet 对象时,该对象的 serviceO方法就要被调用,HttpServlet 默认的 serviceo方法的服务功能就是调用与 HTTP 请求的方法相应的 do 功能:doPostO和 doGet0,所以对于 HttpServlet,一般都是重写 doPostO和 doGet()方法。 -
destroy()
方法在 Servlet 的生命周期中也仅执行一次,即在服务器停止卸载 Servlet 之前被调用,把 Servlet 作为服务器进程的一部分关闭。默认的 destroy() 方法通常是符合要求的,但也可以覆盖,来完成与 init() 方法相反的功能。比如在卸载 Servlet 时将统计数字保存在文件中,或是关闭数据库连接或 I/O 流。 -
getServletConfig()
方法返回一个 ServletConfig 对象,该对象用来返回初始化参数和 ServletContext。ServletContext 接口提供有关 Servlet 的环境信息。 -
getServletInfo()
方法提供有关 Servlet 的描述信息,如作者、版本、版权。可以对它进行覆盖。 -
doXxx()
方法客户端可以用 HTTP 中规定的各种请求方式来访问 Servlet,Servlet 采取不同的访问方式进行处理。不管用哪种请求方式访问 Servlet,Servlet 引擎都会调用 Servlet 的 service() 方法,service() 方法是所有请求方式的入口。- doGet() 用于处理 GET 请求;
- doPost() 用于处理 POST 请求;
- doHead() 用于处理 HEAD 请求;
- doPut() 用于处理 PUT 请求;
- doDelete() 用于处理 DELETE 请求;
- doTrace() 用于处理 TRACE 请求;
- doOptions() 用于处理 OPTIONS 请求。
Servlet 线程安全
由于 Servlet 运用了单例模式,即在整个应用中,每一个 Servlet 类只有一个实例对象,所以我们需要分析这个唯一的实例中的类成员是否线程安全。
接下来,我们来看下面的的示例:
public class ServletDemo extends HttpServlet {
//1.定义用户名成员变量
//private String username = null;
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
String username = null;
//synchronized (this) {
//2.获取用户名
username = req.getParameter("username");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//3.获取输出流对象
PrintWriter pw = resp.getWriter();
//4.响应给客户端浏览器
pw.print("welcome:" + username);
//5.关流
pw.close();
//}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
启动两个浏览器,输入不同的参数,访问之后发现输出的结果都是一样,所以出现线程安全问题:
通过上面的测试我们发现,在 Servlet 中定义了类成员后,多个浏览器都会共享类成员的数据。每一个浏览器端就代表是一个线程,那么多个浏览器就是多个线程,所以测试的结果说明了多个线程会共享 Servlet 类成员中的数据。那么,其中任何一个线程修改了数据,都会影响其他线程。因此,我们可以认为 Servlet 不是线程安全的。
分析产生这个问题的根本原因,其实就是因为 Servlet 是单例,单例对象的类成员只会随类实例化时初始化一次,之后的操作都可能会改变,而不是重新初始化。
要解决这个线程安全问题,需要在 Servlet 中定义类成员时慎重。
- 如果类成员是共用的,并且只会在初始化时赋值,其余时间都是获取的话,那么是没问题的。
- 但如果类成员并非共用,或者每次使用都有可能对其赋值(如上图示例),那么就要考虑线程安全问题了,解决方案是把它定义到 doGet 或者 doPost 方法中。
Servlet 映射配置
Servlet 支持三种映射方式,以达到灵活配置的目的。
首先编写一个Servlet,代码如下:
public class ServletDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("ServletDemo5接收到了请求");
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
方式一:精确映射
此种方式,只有和映射配置一模一样时,Servlet 才会接收和响应来自客户端的请求。
方式二:/开头+通配符
此种方式,只要符合目录结构即可,不用考虑结尾是什么。
例如:映射为:/servlet/*
-
访问 http://localhost:8585/servlet/itheima 或 http://localhost:8585/servlet/itcast.do 这两个 URL 都可以。
-
因为用的
*
,表示 /servlet/ 后面的内容是什么都可以。
方式三:通配符+固定格式结尾
此种方式,只要符合固定结尾格式即可,其前面的访问URI无须关心(注意协议,主机和端口必须正确)
例如:映射为:*.do
- 访问 URL:http://localhost:8585/servlet/itcast.do 或 http://localhost:8585/itheima.do 这两个 URL 都可以。
- 因为都是以 .do 作为结尾,而前面用 * 号通配符配置的映射。
优先级
通过测试我们发现,Servlet 支持多种配置方式,但是由此也引出了一个问题,当有两个及以上的 Servlet 映射都符合请求 URL 时,由谁来响应呢?
注意:HTTP 协议的特征是一请求一响应的规则。那么有一个请求,必然有且只有一个响应。所以,映射规则的优先级如下:
- 精确匹配
- /开头+通配符
- 通配符+固定格式结尾
多路径映射 Servlet
这其实是给一个 Servlet 配置多个访问映射,从而可以根据不同请求 URL 实现不同的功能。
示例 Servlet:
public class ServletDemo extends HttpServlet {
/**
* 根据不同的请求URL,做不同的处理规则
* @param req
* @param resp
* @throws ServletException
* @throws IOException
*/
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
//1. 获取当前请求的 URI
String uri = req.getRequestURI();
uri = uri.substring(uri.lastIndexOf("/"), uri.length());
//2. 判断是1号请求还是2号请求
if("/servletDemo7".equals(uri)){
System.out.println("ServletDemo7执行1号请求的业务逻辑:商品单价7折显示");
}else if("/demo7".equals(uri)){
System.out.println("ServletDemo7执行2号请求的业务逻辑:商品单价8折显示");
}else {
System.out.println("ServletDemo7执行基本业务逻辑:商品单价原价显示");
}
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req, resp);
}
}
web.xml 配置 Servlet:
<servlet>
<servlet-name>servletDemo7</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo7</servlet-class>
</servlet>
<!--映射路径1-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/demo7</url-pattern>
</servlet-mapping>
<!--映射路径2-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/servletDemo7</url-pattern>
</servlet-mapping>
<!--映射路径3-->
<servlet-mapping>
<servlet-name>servletDemo7</servlet-name>
<url-pattern>/servlet/*</url-pattern>
</servlet-mapping>
启动服务,测试运行结果:
启动时即创建 Servlet
Servlet 的创建默认情况下是请求第一次到达 Servlet 时创建的。但是我们知道,Servlet 是单例的,也就是说在应用中只有唯一的一个实例,所以在 Tomcat 启动加载应用的时候就创建也是一个很好的选择。那么两者有什么区别呢?
-
第一种:应用加载时创建 Servlet。
- 它的优势是在服务器启动时,就把需要的对象都创建完成了,从而在使用的时候减少了创建对象的时间,提高了首次执行的效率。
- 它的弊端也同样明显,因为在应用加载时就创建了 Servlet 对象,因此,有可能导致内存中充斥着大量用不上的 Servlet 对象,造成了内存的浪费。
-
第二种:请求第一次访问是创建 Servlet。
- 它的优势就是减少了对服务器内存的浪费,因为那些一直没有被访问过的 Servlet 对象就不会被创建,同时也提高了服务器的启动时间。
- 而它的弊端就是,如果有一些要在应用加载时就做的初始化操作,那么它就没法完成,从而要考虑其他技术实现。
通过上面的分析可得出,当需要在应用加载就要完成一些工作时,就需要选择第一种方式;当有很多 Servlet 且其使用时机并不确定时,就选择第二种方式
。
在 web.xml 中是支持对 Servlet 的创建时机进行配置的,配置的方式如下:
<servlet>
<servlet-name>servletDemo3</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo3</servlet-class>
<!-- 配置Servlet的创建顺序,当配置此标签时,Servlet就会改为应用加载时创建
配置项的取值只能是正整数(包括0),数值越小,表明创建的优先级越高。
-->
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo3</servlet-name>
<url-pattern>/servletDemo3</url-pattern>
</servlet-mapping>
默认 Servlet
默认 Servlet 是由 Web 服务器提供的一个 Servlet,它配置在 Tomcat 的 conf 目录下的 web.xml 中。如下图所示:
它的映射路径是<url-pattern>/<url-pattern>
。在我们发送请求时,首先会在我们应用中的 web.xml 中查找映射配置,找到就执行。当找不到对应的 Servlet 路径时,就会去找默认的 Servlet,由默认 Servlet 处理。所以,一切都是 Servlet。
Servlet 关系总图
ServletConfig
ServletConfig 简介
概念
- ServletConfig 是 Servlet 的配置参数对象。
- 在 Servlet 规范中,允许为每个 Servlet 都提供一些初始化配置。所以,每个 Servlet 都一个自己的 ServletConfig。
- 它的作用是在 Servlet 初始化期间,把一些配置信息传递给 Servlet。
生命周期
- 由于 ServletConfig 是在初始化阶段读取了 web.xml 中为 Servlet 准备的初始化配置,并把配置信息传递给 Servlet,所以生命周期与 Servlet 相同。
- 这里需要注意的是,如果 Servlet 配置了
<load-on-startup>1</load-on-startup>
,那么 ServletConfig 也会在应用加载时创建。
ServletConfig 使用
获取
ServletConfig 可以为每个 Servlet 都提供初始化参数,所以肯定可以在每个 Servlet 中都配置。
public class ServletDemo8 extends HttpServlet {
// 定义 Servlet 配置对象 ServletConfig
private ServletConfig servletConfig;
/**
* 在初始化时为 ServletConfig 赋值
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 输出ServletConfig
System.out.println(servletConfig);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
web.xml:
<servlet>
<servlet-name>servletDemo8</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo8</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo8</servlet-name>
<url-pattern>/servletDemo8</url-pattern>
</servlet-mapping>
配置
上面我们已经准备好了 Servlet,同时也获取到了它的 ServletConfig 对象,而如何配置初始化参数,则需要使用<servlet>
标签中的<init-param>
标签来配置。
即 Servlet 的初始化参数都是配置在 Servlet 的声明部分的,并且每个 Servlet 都支持有多个初始化参数,并且初始化参数都是以键值对的形式存在的。
配置示例:
<servlet>
<servlet-name>servletDemo8</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo8</servlet-class>
<!--配置初始化参数-->
<init-param>
<!--用于获取初始化参数的key-->
<param-name>encoding</param-name>
<!--初始化参数的值-->
<param-value>UTF-8</param-value>
</init-param>
<!--每个初始化参数都需要用到init-param标签-->
<init-param>
<param-name>servletInfo</param-name>
<param-value>This is Demo8</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo8</servlet-name>
<url-pattern>/servletDemo8</url-pattern>
</servlet-mapping>
常用方法
示例:
/**
* 演示Servlet的初始化参数对象
* @author 黑马程序员
* @Company http://www.itheima.com
*/
public class ServletDemo8 extends HttpServlet {
// 定义 Servlet 配置对象 ServletConfig
private ServletConfig servletConfig;
/**
* 在初始化时为 ServletConfig 赋值
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 1. 输出ServletConfig
System.out.println(servletConfig);
// 2. 获取Servlet的名称
String servletName= servletConfig.getServletName();
System.out.println(servletName);
// 3. 获取字符集编码
String encoding = servletConfig.getInitParameter("encoding");
System.out.println(encoding);
// 4. 获取所有初始化参数名称的枚举
Enumeration<String> names = servletConfig.getInitParameterNames();
//遍历names
while(names.hasMoreElements()){
//取出每个name(key)
String name = names.nextElement();
//根据key获取value
String value = servletConfig.getInitParameter(name);
System.out.println("name:"+name+",value:"+value);
}
// 5. 获取ServletContext对象
ServletContext servletContext = servletConfig.getServletContext();
System.out.println(servletContext);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
ServletContext
ServletContext 概述
ServletContext 对象是应用上下文对象
。
每一个应用有且只有一个 ServletContext 对象,它可以实现让应用中所有 Servlet 间的数据共享。
生命周期
-
出生: 应用一加载,该对象就被创建出来了。一个应用只有一个实例对象(Servlet 和 ServletContext 都是单例的)。
-
活着:只要应用一直提供服务,该对象就一直存在。
-
死亡:应用停止(或者服务器挂了),该对象消亡。
域对象概念
-
域对象指的是
对象有作用域,即有作用范围
。 -
域对象的作用,域对象可以实现数据共享。不同作用范围的域对象,共享数据的能力不一样。
-
在 Servlet 规范中,一共有 4 个域对象,ServletContext 就是其中一个。
-
ServletContext 是 web 应用中最大的作用域,叫
application 域
。每个应用只有一个 application 域,它可以实现整个应用间的数据共享功能。
ServletContext 使用
获取
只需要调用 ServletConfig 对象的getServletContext()
方法就可以了:
public class ServletDemo9 extends HttpServlet {
// 定义 Servlet 配置对象 ServletConfig
private ServletConfig servletConfig;
/**
* 在初始化时为 ServletConfig 赋值
* @param config
* @throws ServletException
*/
@Override
public void init(ServletConfig config) throws ServletException {
this.servletConfig = config;
}
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 获取 ServletContext 对象
ServletContext servletContext = servletConfig.getServletContext();
System.out.println(servletContext);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
web.xml:
<servlet>
<servlet-name>servletDemo9</servlet-name>
<servlet-class>com.itheima.web.servlet.ServletDemo9</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>servletDemo9</servlet-name>
<url-pattern>/servletDemo9</url-pattern>
</servlet-mapping>
更简洁的获取方法
:
在实际开发中,如果每个 Servlet 对 ServletContext 都使用频繁的话,那么每个 Servlet 里定义 ServletConfig,再获取 ServletContext 的代码将非常多,造成大量的重复代码。
而 Servlet 规范的定义中也为我们想到了这一点,所以它在 GenericServlet 中,已经为我们声明好了 ServletContext 获取的方法,如下图所示:
示例 Servlet 都是继承自 HttpServlet,而 HttpServlet 又是 GenericServlet 的子类,所以我们在获取 ServletContext 时,如果当前 Servlet 没有用到它自己的初始化参数时,就可以不用再定义初始化参数了,而是直接改成下图所示的代码即可:
配置
ServletContext 既然被称之为应用上下文对象,那么它的配置就是针对整个应用的配置,而非某个特定 Servlet 的配置。它的配置被称为应用的初始化参数配置。
配置的方式,需要在<web-app>
标签中使用<context-param>
来配置初始化参数。具体代码如下:
<!--配置应用初始化参数-->
<context-param>
<!--用于获取初始化参数的 key-->
<param-name>servletContextInfo</param-name>
<!--初始化参数的值-->
<param-value>This is application scope</param-value>
</context-param>
<!--每个应用初始化参数都需要用到 context-param 标签-->
<context-param>
<param-name>globalEncoding</param-name>
<param-value>UTF-8</param-value>
</context-param>
Servlet 注解开发
Servlet 3.0 规范
在大概十多年前,那会还是 Servlet 2.5 的版本的天下,它最明显的特征就是 Servlet 的配置要求配在 web.xml 中。
从 2007 年开始到 2009 年底的这个时间段中,软件开发开始逐步的演变,基于注解的配置理念开始逐渐出现,大量注解配置思想开始用于各种框架的设计中,例如:Spring 3.0 版本的 Java Based Configuration、JPA 规范、Apache 旗下的 struts2 和 mybatis 的注解配置开发等等。
JavaEE6 规范也是在这个期间设计并推出的,与之对应就是它里面包含了新的 Servlet 规范:Servlet 3.0 版本。
使用示例
配置步骤
步骤一:创建 Java Web 工程,并移除 web.xml
步骤二:编写 Servlet
public class ServletDemo1 extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doPost(req, resp);
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
System.out.println("Servlet Demo1 Annotation");
}
}
步骤三:使用注解配置 Servlet
步骤四:测试
注解源码分析
/**
* WebServlet注解
* @since Servlet 3.0
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface WebServlet {
/**
* 指定Servlet的名称。
* 相当于xml配置中<servlet>标签下的<servlet-name>
*/
String name() default "";
/**
* 用于映射Servlet访问的url映射
* 相当于xml配置时的<url-pattern>
*/
String[] value() default {};
/**
* 相当于xml配置时的<url-pattern>
*/
String[] urlPatterns() default {};
/**
* 用于配置Servlet的启动时机
* 相当于xml配置的<load-on-startup>
*/
int loadOnStartup() default -1;
/**
* 用于配置Servlet的初始化参数
* 相当于xml配置的<init-param>
*/
WebInitParam[] initParams() default {};
/**
* 用于配置Servlet是否支持异步
* 相当于xml配置的<async-supported>
*/
boolean asyncSupported() default false;
/**
* 用于指定Servlet的小图标
*/
String smallIcon() default "";
/**
* 用于指定Servlet的大图标
*/
String largeIcon() default "";
/**
* 用于指定Servlet的描述信息
*/
String description() default "";
/**
* 用于指定Servlet的显示名称
*/
String displayName() default "";
}
请求对象
请求对象介绍
请求,顾名思义,就是客户端希望从服务器端索取一些资源,因此向服务器发出的询问。在 B/S 架构中,就是客户浏览器向服务器发出询问。在 JavaEE 工程中,客户浏览器发出询问,要遵循 HTTP 协议所规定的。
请求对象,就是在 JavaEE 工程中,用于发送请求的对象。
常用请求对象
常用的请求对象是ServletRequest
和HttpServletRequest
,它们的区别就是是否和 HTTP 协议有关。
常用方法
获取各种路径
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class requestServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//本机地址:服务器地址
String localAddr = request.getLocalAddr();
//本机名称:服务器名称
String localName = request.getLocalName();
//本机端口:服务器端口
int localPort = request.getLocalPort();
//来访者ip
String remoteAddr = request.getRemoteAddr();
//来访者主机
String remoteHost = request.getRemoteHost();
//来访者端口
int remotePort = request.getRemotePort();
//统一资源标识符
String URI = request.getRequestURI();
//统一资源定位符
String URL = request.getRequestURL().toString();
//获取查询字符串
String queryString = request.getQueryString();
//获取Servlet映射路径
String servletPath = request.getServletPath();
//输出内容
System.out.println("getLocalAddr() is :"+localAddr);
System.out.println("getLocalName() is :"+localName);
System.out.println("getLocalPort() is :"+localPort);
System.out.println("getRemoteAddr() is :"+remoteAddr);
System.out.println("getRemoteHost() is :"+remoteHost);
System.out.println("getRemotePort() is :"+remotePort);
System.out.println("getRequestURI() is :"+URI);
System.out.println("getRequestURL() is :"+URL);
System.out.println("getQueryString() is :"+queryString);
System.out.println("getServletPath() is :"+servletPath);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
获取请求头信息
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Enumeration;
public class requestServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//1.根据名称获取头的值 一个消息头一个值
String value = request.getHeader("Accept-Encoding");
System.out.println("getHeader():"+value);
//2.根据名称获取头的值 一个头多个值
Enumeration<String> values = request.getHeaders("Accept");
while(values.hasMoreElements()){
System.out.println("getHeaders():"+values.nextElement());
}
//3.获取请求消息头的名称的枚举
Enumeration<String> names = request.getHeaderNames();
while(names.hasMoreElements()){
String name = names.nextElement();
String value1 = request.getHeader(name);
System.out.println(name+":"+value1);
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
获取请求参数
1)获取请求参数
准备一个表单页面:
<form action="/requestServlet" method="post">
用户名:<input type="text" name="username" /><br/>
密码:<input type="password" name="password" /><br/>
确认密码:<input type="password" name="password" /><br/>
性别:<input type="radio" name="gender" value="1" checked>男
<input type="radio" name="gender" value="0">女
<br/>
<input type="submit" value="注册" />
</form>
方法示例:
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.Enumeration;
@WebServlet("/requestServlet")
public class requestServlet extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 方式一
String username = request.getParameter("username");
String[] password = request.getParameterValues("password"); // 当表单中有多个名称是一样时,得到是一个字符串数组
String gender = request.getParameter("gender");
System.out.println(username+","+ Arrays.toString(password)+","+gender); // user,[123, 123],1
// 方式二
// 1.获取请求正文名称的枚举
Enumeration<String> names = request.getParameterNames();
// 2.遍历正文名称的枚举
while(names.hasMoreElements()){
String name = names.nextElement();
String value = request.getParameter(name);
System.out.println(name+":"+value);
/*
username:user
password:123
gender:1
*/
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
2)封装请求参数到实体类中
通过上面的示例方法可以获取到请求参数,但是如果参数过多,在进行传递时,方法的形参定义将会变得非常难看。此时我们应该用一个对象来描述这些参数,它就是实体类。
实体类示例:
import java.util.Arrays;
public class Student {
// 成员变量名要与表单name值一致
private String username;
private String password;
private String[] hobby;
public Student() {
}
public Student(String username, String password, String[] hobby) {
this.username = username;
this.password = password;
this.hobby = hobby;
}
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[] getHobby() {
return hobby;
}
public void setHobby(String[] hobby) {
this.hobby = hobby;
}
@Override
public String toString() {
return "Student{" +
"username='" + username + '\'' +
", password='" + password + '\'' +
", hobby=" + Arrays.toString(hobby) +
'}';
}
}
我们现在要做的就是把表单中提交过来的数据填充到实体类中。
使用 apache 的 commons-beanutils 实现封装:
private void test(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
Users user = new Users();
System.out.println("封装前:"+user.toString());
try{
BeanUtils.populate(user, request.getParameterMap()); // 就一句代码
}catch(Exception e){
e.printStackTrace();
}
System.out.println("封装后:"+user.toString());
}
以流的方式读取请求信息
import javax.servlet.ServletException;
import javax.servlet.ServletInputStream;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/*
流对象获取数据
*/
@WebServlet("/servletDemo")
public class ServletDemo extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// 字符流(必须是post方式)
/*BufferedReader br = req.getReader();
String line;
while((line = br.readLine()) != null) {
System.out.println(line);
}*/
// br.close(); // 由request获取的流对象无需手动关闭,由服务器自动关闭即可
// 字节流
ServletInputStream is = req.getInputStream();
byte[] arr = new byte[1024];
int len;
while((len = is.read(arr)) != -1) {
System.out.println(new String(arr, 0, len));
}
// is.close();
}
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
doGet(req,resp);
}
}
请求的中文乱码问题
POST 请求
public class RequestDemo extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
//1.获取请求正文
/*POST方式:
* 问题:取的时候会不会有乱码
* 答案:会。因为是在获取的时候就已经乱码
* 解决办法:
* 是request对象的编码出问题了,因此设置request对象的字符集
* request.setCharacterEncoding("GBK"); 它只能解决POST的请求方式,GET方式解决不了
* 结论:
* 请求正文的字符集和响应正文的字符集没有关系。各是各的
*/
request.setCharacterEncoding("UTF-8");
String username = request.getParameter("username");
// 输出到控制台
System.out.println(username);
// 输出到浏览器:注意响应的乱码问题已经解决了
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.write(username);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
GET 请求
GET 方式请求的正文是在地址栏中,在 Tomcat8.5 版本及以后,Tomcat 服务器已经帮我们解决了,所以不会有乱码问题。
而如果我们使用的不是 Tomcat 服务器,或者 Tomcat 版本是 8.5 以前,那么 GET 方式仍然会有乱码问题。解决方式如下:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/*
* GET方式:正文在地址栏
* username=%D5%C5%C8%FD
* %D5%C5%C8%FD是已经被编过一次码了
*
* 解决办法:
* 使用正确的码表对已经编过码的数据进行解码。
* 就是把取出的内容转成一个字节数组,但是要使用正确的码表。(ISO-8859-1)
* 再使用正确的码表进行编码
* 把字节数组再转成一个字符串,需要使用正确的码表,是看浏览器当时用的是什么码表
*/
String username = request.getParameter("username");
byte[] by = username.getBytes("ISO-8859-1");
username = new String(by, "GBK");
//输出到浏览器:注意响应的乱码问题已经解决了
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.write(username);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
请求转发
请求域
- 请求(Request)域:可以在一次请求范围内进行数据共享。一般用于请求转发的多个资源中共享数据。
- 作用范围:当前请求(一次请求,和当前请求的转发之中。
请求对象操作共享数据的方法:
返回值 | 方法名 | 说明 |
---|---|---|
void | setAttribute(String name, Object value) | 向请求域对象中存储数据 |
Object | getAttribute(String name) | 通过名称获取请求域对象中的数据 |
void | removeAttribute(String name) | 通过名称移除请求域对象中的数据 |
请求转发
请求转发:客户端的一次请求到达后,发现需要借助其他 Servlet 来实现功能。
特点:
- 浏览器地址不变
- 域对象中的数据不丢失
- 负责转发的 Servlet 的响应正文会丢失
- 由转发的目的地(Servlet)来响应客户端
请求转发代码示例:
- 中转 Servlet:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.拿到请求调度对象
RequestDispatcher rd = request.getRequestDispatcher("/RequestDemo7"); // 如果是给浏览器看的,/可写可不写。如果是给服务器看的,一般情况下,/都是必须的。
// 2.放入数据到请求域中
request.setAttribute("CityCode", "bj-010");
// 3.实现真正的转发操作
rd.forward(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
- 目标 Servlet:
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 获取请求域中的数据
String value = (String)request.getAttribute("CityCode");
response.getWriter().write("welcome to request demo:"+value);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
请求转发与重定向的区别
-
当使用请求转发时,Servlet 容器将使用一个内部的方法来调用目标页面,新的页面继续处理同一个请求,而浏览器将不会知道这个过程(即服务器行为)。与之相反,重定向的含义是第一个页面通知浏览器发送一个新的页面请求。因为当使用重定向时,浏览器中所显示的 URL 会变成新页面的 URL(浏览器行为)。而当使用转发时,该 URL 会保持不变。
-
重定向的速度比转发慢,因为浏览器还得发出一个新的请求。
-
同时,由于重定向产生了一个新的请求,所以经过一次重定向后,请求内的对象将无法使用。
总结:
- 重定向:两次请求,浏览器行为,地址栏改变,请求域中的数据会丢失。
- 请求转发:一次请求,服务器行为,地址栏不变,请求域中的数据不丢失。
怎么选择是重定向还是转发呢?
-
通常情况下转发更快,而且能保持请求内的对象,所以它是第一选择。但是由于在转发之后,浏览器中 URL 仍然指向开始页面,此时如果重载当前页面,开始页面将会被重新调用。如果不想看到这样的情况,则选择重定向。
-
不要仅仅为了把变量传到下一个页面而使用 session 作用域,那会无故增大变量的作用域,转发也许可以帮助解决这个问题。
- 重定向:以前的请求中存放的变量全部失效,并进入一个新的请求作用域。
- 转发:以前的请求中存放的变量不会失效,就像把两个页面拼到了一起。
请求包含
我们都知道 HTTP 协议的特点是一请求,一响应的方式,所以绝对不可能出现有多个 Servlet 同时响应的方式。那么我们就需要用到“请求包含”,把多个 Servlet 的响应内容合并输出。
请求包含:可以合并其他 Servlet 中的功能,一起响应给客户端。
特点:
- 浏览器地址不变
- 域对象中的数据不丢失
- 被包含的 Servlet 响应头会丢失
- 这种包含是“动态包含”:各编译各的,只是最后合并输出
代码示例:
- 被包含 Servlet:
@WebServlet("/RequestDemo1")
public class RequestDemo1 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getWriter().write("include request demo1");
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
- 最终 Servlet:
public class RequestDemo2 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getWriter().write("include request demo2");
// 1.拿到请求调度对象
RequestDispatcher rd = request.getRequestDispatcher("/RequestDemo1");
// 2.实现包含的操作
rd.include(request, response);
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
浏览器响应结果:
include request demo2
include request demo1
响应对象
响应对象介绍
什么是响应:
响应,它表示了服务器端收到请求,同时也已经处理完成,把处理的结果告知用户。简单来说,指的就是服务器把请求的处理结果告知客户端。在 B/S 架构中,响应就是把结果带回浏览器。
响应对象,顾名思义就是用于在 JavaWeb 工程中实现上述功能的对象。
常用响应对象:
常用响应对象
响应对象也是 Servlet 规范中定义的,它包括了协议无关的和协议相关的。
-
协议无关的对象标准是:ServletResponse 接口
-
协议相关的对象标准是:HttpServletResponse 接口
类结构图如下:
常用方法
注意:
- response 获取的流无需手动关闭(close),由服务器关闭即可。
- response 得到的字符流和字节流互斥,只能选其一。
字节流响应对象及中文乱码问题
常用方法:
返回值 | 方法名 | 说明 |
---|---|---|
ServletOutputStream | getOutputStream() | 获取响应字节输出流对象 |
void | setContentType("text/html;charset=UTF-8") | 设置响应内容类型,解决中文乱码问题 |
中文乱码问题:
- 问题:IDEA 编写的 String str = "字节流中文乱码问题",使用字节流输出,会不会产生中文乱码?
- 答案:会产生乱码。
- 原因:String str = "字节流中文乱码问题"; 在保存时用的是 IDEA 创建文件使用的字符集 UTF-8。在到浏览器上显示,Chrome 浏览器和 IE 浏览器默认的字符集是 GB2312(GBK),存和取用的不是同一个码表,就会产生乱码。
- 引申:如果产生了乱码,就是存和取用的不是同一个码表
- 解决方案:把存和取的码表统一。
解决方法详解:
- 解决方法一:修改浏览器的编码,使用右键——编码——改成UTF-8。IE 和火狐浏览器可以直接右键设置字符集。而 chrome 需要安装插件,很麻烦。(不建议使用,尽量不要求用户做什么事情)
- 解决方法二:向页面上输出一个 meta 标签:<meta http-equiv="content-type" content="text/html;charset=UTF-8">,其实它就是指挥了浏览器,使用哪个编码进行显示。(不建议使用,因为不好记)
- 解决方法三:设置响应消息头,告知浏览器响应正文的MIME类型和字符集:response.setHeader("Content-Type","text/html;charset=UTF-8");
- 解决方法四:(推荐使用)本质就是设置了一个响应消息头:
response.setContentType("text/html;charset=UTF-8")
;
示例代码:
public class ResponseDemo extends HttpServlet {
/**
* 演示字节流输出的乱码问题
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String str = "字节流输出中文的乱码问题"; // UTF-8的字符集。 解决方法一:浏览器显示也需要使用UTF-8的字符
// 1.拿到字节流输出对象
ServletOutputStream sos = response.getOutputStream();
// 解决方法二:sos.write("<meta http-equiv='content-type' content='text/html;charset=UTF-8'>".getBytes());
// 解决方法三:response.setHeader("Content-Type","text/html;charset=UTF-8");
// 解决方法四:
response.setContentType("text/html;charset=UTF-8");
// 2.把str转换成字节数组之后输出到浏览器
sos.write(str.getBytes("UTF-8"));
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
字符流响应对象及中文乱码问题
public class ResponseDemo extends HttpServlet {
/**
* 演示:字符流输出中文乱码
* @param request
* @param response
* @throws ServletException
* @throws IOException
*/
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String str = "字符流输出中文乱码";
// 设置响应正文的MIME类型和字符集
response.setContentType("text/html;charset=UTF-8");
// 1.获取字符输出流
PrintWriter out = response.getWriter();
// 2.使用字符流输出中文
out.write(str);
/**
* 问题:out.write(str); 直接输出,会不会产生乱码?
*
* 答案:会产生乱码
*
* 原因:
* UTF-8(存)————>PrintWriter ISO-8859-1(取) 乱
* PrintWirter ISO-8859-1(存)————>浏览器 GBK(取) 乱
*
* 解决办法:
* 改变PrintWriter的字符集,PrintWriter是从response对象中获取的,因此设置response的字符集。
* 注意:设置response的字符集,需要在拿流之前。
* response.setCharacterEncoding("UTF-8");
*
* response.setContentType("text/html;charset=UTF-8");
* 此方法,其实是做了两件事:
* 1. 设置响应对象的字符集(包括响应对象取出的字符输出流)
* 2. 告知浏览器响应正文的MIME类型和字符集
*/
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
响应消息头:设置缓存时间
使用缓存的一般都是静态资源,动态资源一般不能缓存。
public class ResponseDemo extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String str = "设置缓存时间";
/*
* 设置缓存时间,其实就是设置响应消息头:Expires,其值是一个毫秒数。
* 使用的是:response.setDateHeader();
*
* 缓存1小时,是在当前时间的毫秒数上加上1小时之后的毫秒值
*/
response.setDateHeader("Expires",System.currentTimeMillis()+1*60*60*1000);
response.setContentType("text/html;charset=UTF-8");
response.getOutputStream().write(str.getBytes());
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
响应消息头:定时刷新
public class ResponseDemo extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String str = "用户名和密码不匹配,2秒后转向登录页面...";
response.setContentType("text/html;charset=UTF-8");
PrintWriter out = response.getWriter();
out.write(str);
// 定时刷新,其实就是设置一个响应消息头
response.setHeader("Refresh", "2;URL=/login.html"); // Refresh设置的时间单位是秒,如果刷新到其他地址,需要在时间后面拼接上地址
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
请求重定向
- 原始 Servlet:
public class ResponseDemo6 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.设置响应状态码
// response.setStatus(302);
// 2.定向到哪里去: 其实就是设置响应消息头,Location
// response.setHeader("Location", "ResponseDemo7");
//使用重定向方法
response.sendRedirect("ResponseDemo7"); // 此行做了什么事,请看上面
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
- 目标 Servlet:
public class ResponseDemo7 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
response.getWriter().write("welcome to ResponseDemo7");
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
文件下载
首先在工程的 web 目录下新建一个目录 uploads,并且拷贝一张图片到目录中,如下图所示:
文件下载的 Servlet:
public class ResponseDemo8 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
/*
* 文件下载的思路:
* 1.获取文件路径
* 2.把文件读到字节输入流中
* 3.告知浏览器,以下载的方式打开(告知浏览器下载文件的MIME类型)
* 4.使用响应对象的字节输出流输出到浏览器上
*/
// 1.获取文件路径(绝对路径)
ServletContext context = this.getServletContext();
String filePath = context.getRealPath("/uploads/6.jpg");//通过文件的虚拟路径,获取文件的绝对路径
// 2.通过文件路径构建一个字节输入流
InputStream in = new FileInputStream(filePath);
// 3.设置响应消息头
response.setHeader("Content-Type", "application/octet-stream"); // 注意下载的时候,设置响应正文的MIME类型,用application/octet-stream
response.setHeader("Content-Disposition", "attachment;filename=1.jpg"); // 告知浏览器以下载的方式打开
// 4.使用响应对象的字节输出流输出
OutputStream out = response.getOutputStream();
int len = 0;
byte[] by = new byte[1024];
while((len = in.read(by)) != -1){
out.write(by, 0, len);
}
in.close();
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
会话管理
会话管理概述
什么是会话:
这里的会话,指的是 Web 开发中的一次通话过程,当打开浏览器,访问网站地址后,会话开始,当关闭浏览器(或者到了过期时间),会话结束。
会话管理的作用:
什么时候会用到会话管理呢?最常见的就是购物车,当我们登录成功后,把商品加入到购物车之中,此时我们无论再浏览什么商品,当点击购物车时,那些加入的商品都仍在购物车中。
在我们的实际开发中,还有很多地方都离不开会话管理技术。比如,我们在论坛发帖,没有登录的游客身份是不允许发帖的。所以当我们登录成功后,无论我们进入哪个版块发帖,只要权限允许的情况下,服务器都会认识我们,从而让我们发帖,因为登录成功的信息一直保留在服务器端的会话中。
通过上面的两个例子,我们可以看出,它是为我们共享数据用的,并且是在不同请求间实现数据共享。也就是说,如果我们需要在多次请求间实现数据共享,就可以考虑使用会话管理技术了。
会话管理分类:
在 JavaEE 的项目中,会话管理分为两类,分别是:客户端会话管理技术和服务端会话管理技术。
-
客户端会话管理技术:它是把要共享的数据保存到了客户端(也就是浏览器端)。每次请求时,把会话信息带到服务器,从而实现多次请求的数据共享。
-
服务端会话管理技术:它本质仍是采用客户端会话管理技术,只不过保存到客户端的是一个特殊的标识,并且把要共享的数据保存到了服务端的内存对象中。每次请求时,把这个标识带到服务器端,然后使用这个标识,找到对应的内存空间,从而实现数据共享。
客户端会话管理技术:Cookie
Cookie 概述
它是客户端浏览器的缓存文件,里面记录了客户浏览器访问网站的一些内容。同时,也是 HTTP 请求和响应消息头的一部分。
作用:
Cookie 可以保存客户端浏览器访问网站的相关内容(需要客户端不禁用 Cookie),从而在每次访问需要同一个内容时,先从本地缓存获取,使资源共享,提高效率。
Cookie 常用属性
属性名称 | 属性作用 | 是否重要 |
---|---|---|
name | cookie 的名称 | 必要属性 |
value | cookie 的值(不能是中文) | 必要属性 |
path | cookie 的路径 | 重要 |
domain | cookie 的域名 | 重要 |
maxAge | cookie 的生存时间 | 重要 |
version | cookie 的版本号 | 不重要 |
comment | cookie 的说明 | 不重要 |
详解:
-
Cookie 有大小和个数限制:
- 每个网站最多只能存 20 个cookie,且大小不能超过 4kb。
- 同时,所有网站的 cookie 总数不超过 300 个。
-
maxAge 值:
-
当要删除 Cookie 时,可以设置 maxAge 值为 0。
-
当不设置 maxAge 时,使用的是浏览器的内存。当关闭浏览器之后,Cookie 将丢失。
-
设置了此值,就会保存成缓存文件(值必须是大于 0 的,以秒为单位)。
Cookie 常用方法
创建 Cookie
/**
* 通过指定的名称和值构造一个Cookie
*
* Cookie的名称必须遵循RFC 2109规范。这就意味着,它只能包含ASCII字母数字字符,
* 不能包含逗号、分号或空格或以$字符开头。
* 创建后无法更改cookie的名称。
*
* 该值可以是服务器选择发送的任何内容。
* 它的价值可能只有服务器才感兴趣。
* 创建之后,可以使用setValue方法更改cookie的值。
*/
public Cookie(String name, String value) {
validation.validate(name);
this.name = name;
this.value = value;
}
向浏览器添加 Cookie
/**
* 添加Cookie到响应中。此方法可以多次调用,用以添加多个Cookie。
*/
public void addCookie(Cookie cookie);
获取客户端 Cookie
/**
* 这是HttpServletRequest中的方法。
* 它返回一个Cookie的数组,包含客户端随此请求发送的所有Cookie对象。
* 如果没有符合规则的cookie,则此方法返回null。
*/
public Cookie[] getCookies();
Cookie 的 Path :客户浏览器何时带 cookie 到服务器端,何时不带
1)需求说明
创建一个 Cookie,设置 Cookie 的 path,通过不同的路径访问,从而查看请求携带 Cookie 的情况。
2)案例目的
通过此案例的讲解,可以清晰的描述出,客户浏览器何时带 cookie 到服务器端,何时不带。
3)案例步骤
第一步:编写 Servlet
- 在 demo1 中写一个 cookie 到客户端
- 在 demo2 和 demo3 中分别去获取 cookie
- demo1 的 Servlet 映射是 /servlet/PathQuestionDemo1
- demo2 的 Servlet 映射是 /servlet/PathQuestionDemo2
- demo3 的 Servlet 映射是 /PathQuestionDemo3
/**
* 写一个 cookie 到客户端
*/
public class PathQuestionDemo1 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.创建一个Cookie
Cookie cookie = new Cookie("pathquestion", "CookiePathQuestion");
// 2.设置cookie的最大存活时间
cookie.setMaxAge(Integer.MAX_VALUE);
// 3.把cookie发送到客户端
response.addCookie(cookie); // setHeader("Set-Cookie", "cookie的值")
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
/**
* 获取Cookie,名称是pathquestion
*/
public class PathQuestionDemo2 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.获取所有的cookie
Cookie[] cs = request.getCookies();
// 2.遍历cookie的数组
for(int i=0; cs!=null && i<cs.length; i++){
if("pathquestion".equals(cs[i].getName())){
// 找到了我们想要的cookie,输出cookie的值
response.getWriter().write(cs[i].getValue());
return;
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
/**
* 获取Cookie,名称是pathquestion
*/
public class PathQuestionDemo3 extends HttpServlet {
public void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// 1.获取所有的cookie
Cookie[] cs = request.getCookies();
// 2.遍历cookie的数组
for(int i=0;cs!=null && i<cs.length;i++){
if("pathquestion".equals(cs[i].getName())){
// 找到了我们想要的cookie,输出cookie的值
response.getWriter().write(cs[i].getValue());
return;
}
}
}
public void doPost(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
doGet(request, response);
}
}
第二步:配置 Servlet
<!-- Demo1:设置Cookie -->
<servlet>
<servlet-name>PathQuestionDemo1</servlet-name>
<servlet-class>com.web.servlet.pathquestion.PathQuestionDemo1</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PathQuestionDemo1</servlet-name>
<url-pattern>/servlet/PathQuestionDemo1</url-pattern>
</servlet-mapping>
<!-- Demo2:获取Cookie -->
<servlet>
<servlet-name>PathQuestionDemo2</servlet-name>
<servlet-class>com.web.servlet.pathquestion.PathQuestionDemo2</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PathQuestionDemo2</servlet-name>
<url-pattern>/servlet/PathQuestionDemo2</url-pattern>
</servlet-mapping>
<!-- Demo3:获取Cookie -->
<servlet>
<servlet-name>PathQuestionDemo3</servlet-name>
<servlet-class>com.web.servlet.pathquestion.PathQuestionDemo3</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>PathQuestionDemo3</servlet-name>
<url-pattern>/PathQuestionDemo3</url-pattern>
</servlet-mapping>
4)测试结果
通过分别运行 PathQuestionDemo1,2 和 3 这三个 Servlet,我们发现由 demo1 写的 Cookie,在 demo2 中可以取到,但是到了 demo3 中就无法获取了,如下图所示:
5)路径问题的分析及总结
问题:demo2 和 demo3 谁能取到 Cookie?
答案:demo2 能取到,demo3 取不到。
分析:
- 首先,我们要知道如何确定一个 cookie ?那就是使用 cookie 的三个属性组合:
domain + path + name
。 - 这里面,同一个应用的domain是一样的,在我们的案例中都是 localhost。并且,我们取的都是同一个 cookie,所以 name 也是一样的,都是 pathquestion。
- 那么,不一样的只能是 path 了。但是示例中没有设置过 cookie 的 path 属性,这就表明 path 是有默认值的。
- 接下来,我们打开这个 cookie 来看一看,在 IE 浏览器访问一次 PathQuestionDemo1 这个 Servlet:
我们是通过 demo1 写的 cookie,demo1 的访问路径是 http://localhost:9090/servlet/PathQuestionDemo1 。通过比较两个路径:请求资源地址和 cookie 的 path,可以看出:cookie 的 path 默认值是 URI 中去掉资源的部分
。
在上述案例中:
访问 URL | URI 部分 | Cookie 的 Path | 是否携带 Cookie | 能否取到 Cookie |
---|---|---|---|---|
http://localhost:9090/servlet/PathQuestionDemo2 | /servlet/PathQuestionDemo2 | /servlet/ | 带 | 能取到 |
http://localhost:9090/PathQuestionDemo3 | /PathQuestionDemo3 | /servlet/ | 不带 | 不能取到 |
总结:客户端什么时候带 cookie 到服务器,什么时候不带?
- 就是看 URI 和 cookie 的 path 比较。
URI.startWith(cookie 的 path)
:如果返回的是 true 就带,如果返回的是 false 就不带。
服务端会话管理技术:Session
HttpSession 对象概述
HttpSession 是 Servlet 规范中提供的一个接口。该接口的实现由 Servlet 规范的实现提供商提供。
由于 Tomcat 服务器对 Servlet 规范进行了实现,所以 HttpSession 接口的实现由 Tomcat 提供。该对象用于提供一种通过多个页面请求或访问网站,来标识用户并存储有关该用户的信息的方法。简单说它就是一个服务端的会话对象,用于存储用户的会话数据。
同时,它也是 Servlet 规范中四大域对象之一的会话域对象。并且它也是用于实现数据共享的,但它与前面介绍的应用域和请求域是有区别的。
域对象 | 作用范围 | 使用场景 |
---|---|---|
ServletContext | 整个应用范围 | 当前项目中需要数据共享时,可以使用此域对象。 |
ServletRequest | 当前请求范围 | 在请求或者当前请求转发时需要数据共享可以使用此域对象。 |
HttpSession | 会话返回 | 在当前会话范围中实现数据共享;可以在多次请求中实现数据共享。 |
HttpSession 对象的获取
HttpSession 的获取是通过 HttpServletRequest 接口中的两个方法获取的,如下图所示:
两个方法的区别:
HttpSession 常用方法
HttpSession 入门案例
1)需求说明
在请求 HttpSessionDemo1 这个 Servlet 时,携带用户名信息,并且把信息保存到会话域中,然后从 HttpSessionDemo2 这个 Servlet 中获取登录信息。
2)案例目的
通过本案例认识到会话域的作用,即多次请求间的数据共享。因为是两次请求,请求域肯定不一样了,所以不能用请求域实现。
最终掌握 HttpSession 对象的获取和使用。
3)原理分析
HttpSession 虽然是服务端会话管理技术的对象,但它本质仍是一个 Cookie,是一个由服务器自动创建的特殊的 Cookie,Cookie 的名称是 JSESSIONID,其值是服务器分配的一个唯一的标识。
当我们使用 HttpSession 时,浏览器在没有禁用 Cookie 的情况下,都会把这个 Cookie 带到服务器端,然后根据唯一标识去查找对应的 HttpSession 对象,找到了,我们就可以直接使用了。
下图就是入门案例中,HttpSession 分配的唯一标识,可以看到两次请求的 JSESSIONID 的值是一样的:
HttpSession 的钝化和活化
什么是持久态?
-
把长时间不用,但还不到过期时间的 HttpSession 进行序列化,写到磁盘上。
-
我们把 HttpSession 持久态也叫做钝化(与钝化相反的,我们叫活化)。
什么时候使用持久化?
-
第一种情况:当访问量很大时,服务器会根据 getLastAccessTime 来进行排序,对长时间不用,但是还没到过期时间的 HttpSession 进行持久化。
-
第二种情况:当服务器进行重启的时候,为了保持客户 HttpSession 中的数据,也要对 HttpSession 进行持久化。
注意:
-
HttpSession 的持久化由服务器来负责管理,我们不用关心。
-
只有实现了序列化接口的类才能被序列化,否则不行。