Servlet(控制器)

目录


Java Web 设计模式

image

Servlet 介绍

Servlet 是 SUN 公司提供的一套规范,名称就叫 Servlet 规范,它也是 JavaEE 规范之一。我们可以通过访问官方 API 学习和查阅里面的内容。

打开官方 API 网址,在左上部分找到 javax.servlet 包,在左下部分找到 Servlet,如下图显示:

image

通过阅读 API,我们可以得到如下信息:

  1. Servlet 是一个运行在 Web 服务端的 Java 小程序。
  2. 它可以用于接收和响应客户端的请求,主要功能在于交互式地浏览和修改数据,生成动态的 Web 内容。
  3. 要想实现 Servlet 功能,可以实现 Servlet 接口、继承 GenericServlet 或者 HttpServlet。
  4. 每次请求都会执行 service 方法。
  5. Servlet 还支持配置。

image

Servlet 主要执行以下工作:

使用 Servlet,可以收集来自网页表单的用户输入,可以呈现来自数据库或者其他来源的记录,还可以动态地创建网页(JSP)。

  1. 读取客户端(浏览器)发送的显式的数据。这包括网页上的 HTML 表单,或者也可以是来自 Applet 或自定义的 HTTP 客户端程序的表单。
  2. 读取客户端(浏览器)发送的隐式的 HTTP 请求数据。这包括 Cookies、媒体类型和浏览器能理解的压缩格式等。
  3. 处理数据并生成结果。这个过程可能需要访问数据库,执行 RMI 或 CORBA 调用,认用 Web 服务,或者直接计算得出对应的响应。
  4. 发送显式的数据(即文档)到客户端(浏览器)。该文档的格式可以是多种多样自包括文本文件(HTML 或 XML)、二进制文件(GIF 图像)、Excel 等。
  5. 发送隐式的 HTTP 响应到客户端(浏览器)。这包括告诉浏览器或其他客户端被返国的文档类型(例如 HTML)、设置 Cookie 和缓存参数,以及其他类似的任务。

Servlet 的优势

Java Servlet 通常情况下与使用 CGI(common gateway interface,公共网关接口)实现的程序可以达到异曲同工的效果。但是相比于 CGI,Servlet 有以下几点优势:

  1. 性能明显更好。
  2. 在 Web 服务器的地址空间内执行。这样它就没有必要再创建一个单独的进程来处理每个客户端请求。
  3. Servlet 是独立于平台的,因为它们是用 Java 编写的。
  4. 服务器上的 Java 安全管理器具有一定的限制,以保护服务器计算机上的资源。因此 Servlet 是可信的。
  5. Java 类库的全部功能对 Servlet 来说都是可用的。它可以通过 Socket 和 RMI 机制与 Applet、数据库或其他软件进行交互。

Servlet 基础使用

Servlet 编写步骤

1)编码

  1. 前期准备-创建 Java Web 工程;

  2. 编写一个普通类继承 GenericServlet 并重写 service 方法;

  3. 在 web.xml 配置 Servlet;

2)测试

  1. 在 Tomcat 中部署项目;

  2. 在浏览器访问 Servlet。

image

Servlet 执行过程

image

  1. 浏览器使用 Socket(IP+端口)与服务器建立连接。
  2. 浏览器将请求数据按照 HTTP 打成一个数据包(请求数据包)发送给服务器。
  3. 服务器解析请求数据包并创建请求对象(request)和响应对象(response)。
    • 请求对象是 HttpServletRquest 接口的一个实现。
    • 响应对象是 HttpServletResponse 接口的一个实现,响应对象用于存放 Servlet 处理自的结果。
  4. 服务器将解析之后的数据存放到请求对象(request)里面。
  5. 服务器依据请求资源路径找到相应的 Servlet 配置,通过反射创建 Servlet 实例。
  6. 服务器调用其 service() 方法,在在调用 serviceO方法时,会将事先创建好的请求对象(request)和响应对象(response)作为参数进行传递。
  7. 在 Servlet 内部,可以通过 reques st 获得请求数据,或者通过 response 设置响应数据
  8. 服务器从 response 中获取数据 按照 HTTP 打成一个数据包(响应数据包),发送给浏览器。
  9. 浏览器解析响应数据包,取出相应的数据,生成相应的界面。

Servlet 类视图

  • 在 Servlet 的 API 介绍中,除了继承 GenericServlet 外还可以继承 HttpServlet。
  • 通过查阅 servlet 的类视图,我们看到 GenericServlet 还有一个子类 HttpServlet。
  • 同时,在 service 方法中还有参数 ServletRequest 和 ServletResponse。

它们的关系如下图所示:

image


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 的生命周期主要有初始化阶段、处理客户端请求阶段和终止阶段

  1. 初始化阶段

    • Servlet 容器加载 Servlet,加载完成后,Servlet 容器会创建一个 Servlet 实例并调用 init() 方法,init() 方法只会调用一次。
    • Servlet 容器会在以下几种情况加载 Servlet:
      1. Servlet 容器启动时自动加载某些 Servlet,这样需要在 web.xml 文件中添加。
      2. 在 Servlet 容器启动后,客户首次向 Servlet 发送请求。
      3. Servlet 类文件被更新后,重新加载。
  2. 处理客户端请求阶段

    • 每收到一个客户端请求,服务器就会产生一个新的线程去处理。对于用户的 Servlet 请求,Servlet 容器会创建一个特定于请求的 ServletRequest 和 ServletResponse。
    • 对于 Tomcat 来说,它会将传递来的参数放入一个哈希表中,这是一个 String->String[]的键值映射。
  3. 终止阶段

    • 当 Web 应用被终止,或者 Servlet 容器终止运行,又或者 Servlet 重新加载 Servlet 新实例时,Servlet 容器会调用 Servlet 的 destroy() 方法。

通过分析 Servlet 的生命周期可以发现,它的实例化和初始化只会在请求第一次到达 Servlet 时执行,而销毁只会在 Tomcat 服务器停止时执行。

由此我们得出一个结论,Servlet 对象只会创建一次,销毁一次。所以,每一个 Servlet 只有一个实例对象。如果一个对象实例在应用中是唯一的存在,那么我们就说它是单实例的,即运用了单例模式。

image

如下是一个典型的 Servlet 生命周期方案:

image

  1. 第一个到达服务器的 HTTP 请求被委派到 Servlet 容器。
  2. Servlet 容器在调用 service() 方法之前加载 Servlet。
  3. Servlet 容器处理由多个线程产生的多个请求,每个线程执行一个单一的 Servlet 实例的 service() 方法。

Servlet 执行时一般要实现的方法

Servlet 类要继承的 GenericServlet 与 HttpServlet 类说明:

  1. GenericServlet 类是一个实现了 Servlet 的基本特征和功能的基类,其完整名称为 javax.Servlet.GenericServlet,它实现了 Servlet 和 ServletConfig 接口。
  2. 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()
  1. init() 方法在 Servlet 的生命周期中仅执行一次,在 Servlet 引擎创建 Servlet 对象后执行。 Servlet 在调用 init() 方法时,会传递一个包含 Servlet 的配置和运行环境信息的 ServletConfig 对象。如果初始化代码中要使用到 ServletConfig 对象,则初始化代码就只能在 Servlet 的 init() 方法中编写,而不能在构造方法中编写。默认的 init(方法通常是符合要求的,不过也可以根据需要进行覆盖,比如管理服务器端资源、初始化数据库连接等,默认的 inti()方法设置了 Servlet的初始化参数,并用它的 ServeltConfig 对象参数来启动配置,所以覆盖 init() 方法时,应调用 super.init() 以确保仍然执行这些任务。

  2. service() 方法是 Servlet 的核心,用于响应对 Servlet 的访问请求。对于 HttpServlet,每当客户请求一个 HttpServlet 对象时,该对象的 serviceO方法就要被调用,HttpServlet 默认的 serviceo方法的服务功能就是调用与 HTTP 请求的方法相应的 do 功能:doPostO和 doGet0,所以对于 HttpServlet,一般都是重写 doPostO和 doGet()方法。

  3. destroy() 方法在 Servlet 的生命周期中也仅执行一次,即在服务器停止卸载 Servlet 之前被调用,把 Servlet 作为服务器进程的一部分关闭。默认的 destroy() 方法通常是符合要求的,但也可以覆盖,来完成与 init() 方法相反的功能。比如在卸载 Servlet 时将统计数字保存在文件中,或是关闭数据库连接或 I/O 流。

  4. getServletConfig() 方法返回一个 ServletConfig 对象,该对象用来返回初始化参数和 ServletContext。ServletContext 接口提供有关 Servlet 的环境信息。

  5. getServletInfo() 方法提供有关 Servlet 的描述信息,如作者、版本、版权。可以对它进行覆盖。

  6. 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);
    }
}

启动两个浏览器,输入不同的参数,访问之后发现输出的结果都是一样,所以出现线程安全问题:

image

通过上面的测试我们发现,在 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 才会接收和响应来自客户端的请求。

image

方式二:/开头+通配符

此种方式,只要符合目录结构即可,不用考虑结尾是什么。

例如:映射为:/servlet/*

image

方式三:通配符+固定格式结尾

此种方式,只要符合固定结尾格式即可,其前面的访问URI无须关心(注意协议,主机和端口必须正确)

例如:映射为:*.do

image

优先级

通过测试我们发现,Servlet 支持多种配置方式,但是由此也引出了一个问题,当有两个及以上的 Servlet 映射都符合请求 URL 时,由谁来响应呢?

注意:HTTP 协议的特征是一请求一响应的规则。那么有一个请求,必然有且只有一个响应。所以,映射规则的优先级如下:

  1. 精确匹配
  2. /开头+通配符
  3. 通配符+固定格式结尾

image


多路径映射 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>

启动服务,测试运行结果:

image


启动时即创建 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>

image


默认 Servlet

默认 Servlet 是由 Web 服务器提供的一个 Servlet,它配置在 Tomcat 的 conf 目录下的 web.xml 中。如下图所示:

image

它的映射路径是<url-pattern>/<url-pattern>。在我们发送请求时,首先会在我们应用中的 web.xml 中查找映射配置,找到就执行。当找不到对应的 Servlet 路径时,就会去找默认的 Servlet,由默认 Servlet 处理。所以,一切都是 Servlet。

Servlet 关系总图

image


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>

常用方法

image

示例:

/**
 * 演示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);
    }
}

image


ServletContext

ServletContext 概述

ServletContext 对象是应用上下文对象

每一个应用有且只有一个 ServletContext 对象,它可以实现让应用中所有 Servlet 间的数据共享。

生命周期

  1. 出生: 应用一加载,该对象就被创建出来了。一个应用只有一个实例对象(Servlet 和 ServletContext 都是单例的)。

  2. 活着:只要应用一直提供服务,该对象就一直存在。

  3. 死亡:应用停止(或者服务器挂了),该对象消亡。

域对象概念

  • 域对象指的是对象有作用域,即有作用范围

  • 域对象的作用,域对象可以实现数据共享。不同作用范围的域对象,共享数据的能力不一样。

  • 在 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 获取的方法,如下图所示:

image

示例 Servlet 都是继承自 HttpServlet,而 HttpServlet 又是 GenericServlet 的子类,所以我们在获取 ServletContext 时,如果当前 Servlet 没有用到它自己的初始化参数时,就可以不用再定义初始化参数了,而是直接改成下图所示的代码即可:

image


配置

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

image

image

步骤二:编写 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

image

步骤四:测试

image

注解源码分析

/**
 * 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 工程中,用于发送请求的对象。

常用请求对象

常用的请求对象是ServletRequestHttpServletRequest,它们的区别就是是否和 HTTP 协议有关

image

常用方法

image

获取各种路径

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)域:可以在一次请求范围内进行数据共享。一般用于请求转发的多个资源中共享数据。
  • 作用范围:当前请求(一次请求,和当前请求的转发之中。

image

请求对象操作共享数据的方法:

返回值 方法名 说明
void setAttribute(String name, Object value) 向请求域对象中存储数据
Object getAttribute(String name) 通过名称获取请求域对象中的数据
void removeAttribute(String name) 通过名称移除请求域对象中的数据

请求转发

请求转发:客户端的一次请求到达后,发现需要借助其他 Servlet 来实现功能。

特点:

  • 浏览器地址不变
  • 域对象中的数据不丢失
  • 负责转发的 Servlet 的响应正文会丢失
  • 由转发的目的地(Servlet)来响应客户端

image

请求转发代码示例:

  • 中转 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 接口

类结构图如下:

image

常用方法

image

注意:

  • response 获取的流无需手动关闭(close),由服务器关闭即可。
  • response 得到的字符流和字节流互斥,只能选其一。

image

字节流响应对象及中文乱码问题

常用方法:

返回值 方法名 说明
ServletOutputStream getOutputStream() 获取响应字节输出流对象
void setContentType("text/html;charset=UTF-8") 设置响应内容类型,解决中文乱码问题

中文乱码问题:

  • 问题:IDEA 编写的 String str = "字节流中文乱码问题",使用字节流输出,会不会产生中文乱码?
  • 答案:会产生乱码。
  • 原因:String str = "字节流中文乱码问题"; 在保存时用的是 IDEA 创建文件使用的字符集 UTF-8。在到浏览器上显示,Chrome 浏览器和 IE 浏览器默认的字符集是 GB2312(GBK),存和取用的不是同一个码表,就会产生乱码。
  • 引申:如果产生了乱码,就是存和取用的不是同一个码表
  • 解决方案:把存和取的码表统一。

解决方法详解:

  1. 解决方法一:修改浏览器的编码,使用右键——编码——改成UTF-8。IE 和火狐浏览器可以直接右键设置字符集。而 chrome 需要安装插件,很麻烦。(不建议使用,尽量不要求用户做什么事情)
  2. 解决方法二:向页面上输出一个 meta 标签:<meta http-equiv="content-type" content="text/html;charset=UTF-8">,其实它就是指挥了浏览器,使用哪个编码进行显示。(不建议使用,因为不好记)
  3. 解决方法三:设置响应消息头,告知浏览器响应正文的MIME类型和字符集:response.setHeader("Content-Type","text/html;charset=UTF-8");
  4. 解决方法四:(推荐使用)本质就是设置了一个响应消息头: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);
    }
}

image


字符流响应对象及中文乱码问题

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);
    }
}

image


响应消息头:设置缓存时间

使用缓存的一般都是静态资源,动态资源一般不能缓存。

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);
    }

}

image

image


响应消息头:定时刷新

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);
    }

}

image


请求重定向

  • 原始 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);
    }
}

image


文件下载

首先在工程的 web 目录下新建一个目录 uploads,并且拷贝一张图片到目录中,如下图所示:

image

文件下载的 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);
    }

}

image

会话管理

会话管理概述

什么是会话:

这里的会话,指的是 Web 开发中的一次通话过程,当打开浏览器,访问网站地址后,会话开始,当关闭浏览器(或者到了过期时间),会话结束。

会话管理的作用:

什么时候会用到会话管理呢?最常见的就是购物车,当我们登录成功后,把商品加入到购物车之中,此时我们无论再浏览什么商品,当点击购物车时,那些加入的商品都仍在购物车中。

在我们的实际开发中,还有很多地方都离不开会话管理技术。比如,我们在论坛发帖,没有登录的游客身份是不允许发帖的。所以当我们登录成功后,无论我们进入哪个版块发帖,只要权限允许的情况下,服务器都会认识我们,从而让我们发帖,因为登录成功的信息一直保留在服务器端的会话中。

通过上面的两个例子,我们可以看出,它是为我们共享数据用的,并且是在不同请求间实现数据共享。也就是说,如果我们需要在多次请求间实现数据共享,就可以考虑使用会话管理技术了。

会话管理分类:

在 JavaEE 的项目中,会话管理分为两类,分别是:客户端会话管理技术和服务端会话管理技术。

  • 客户端会话管理技术:它是把要共享的数据保存到了客户端(也就是浏览器端)。每次请求时,把会话信息带到服务器,从而实现多次请求的数据共享。

  • 服务端会话管理技术:它本质仍是采用客户端会话管理技术,只不过保存到客户端的是一个特殊的标识,并且把要共享的数据保存到了服务端的内存对象中。每次请求时,把这个标识带到服务器端,然后使用这个标识,找到对应的内存空间,从而实现数据共享。

客户端会话管理技术:Cookie

它是客户端浏览器的缓存文件,里面记录了客户浏览器访问网站的一些内容。同时,也是 HTTP 请求和响应消息头的一部分。

作用:

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 的,以秒为单位)。

image

/**
 * 通过指定的名称和值构造一个Cookie
 *
 * Cookie的名称必须遵循RFC 2109规范。这就意味着,它只能包含ASCII字母数字字符,
 * 不能包含逗号、分号或空格或以$字符开头。
 * 创建后无法更改cookie的名称。
 *
 * 该值可以是服务器选择发送的任何内容。
 * 它的价值可能只有服务器才感兴趣。
 * 创建之后,可以使用setValue方法更改cookie的值。
 */
public Cookie(String name, String value) {
	validation.validate(name);
	this.name = name;
	this.value = value;
}

image

/**
 * 添加Cookie到响应中。此方法可以多次调用,用以添加多个Cookie。
 */
public void addCookie(Cookie cookie);

image

/**
 * 这是HttpServletRequest中的方法。
 * 它返回一个Cookie的数组,包含客户端随此请求发送的所有Cookie对象。
 * 如果没有符合规则的cookie,则此方法返回null。
 */
 public Cookie[] getCookies();

1)需求说明

创建一个 Cookie,设置 Cookie 的 path,通过不同的路径访问,从而查看请求携带 Cookie 的情况。

2)案例目的

通过此案例的讲解,可以清晰的描述出,客户浏览器何时带 cookie 到服务器端,何时不带。

3)案例步骤

第一步:编写 Servlet

  1. 在 demo1 中写一个 cookie 到客户端
  2. 在 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 中就无法获取了,如下图所示:

image

image

image

5)路径问题的分析及总结

问题:demo2 和 demo3 谁能取到 Cookie?

答案:demo2 能取到,demo3 取不到。

分析:

  1. 首先,我们要知道如何确定一个 cookie ?那就是使用 cookie 的三个属性组合:domain + path + name
  2. 这里面,同一个应用的domain是一样的,在我们的案例中都是 localhost。并且,我们取的都是同一个 cookie,所以 name 也是一样的,都是 pathquestion。
  3. 那么,不一样的只能是 path 了。但是示例中没有设置过 cookie 的 path 属性,这就表明 path 是有默认值的。
  4. 接下来,我们打开这个 cookie 来看一看,在 IE 浏览器访问一次 PathQuestionDemo1 这个 Servlet:

image

我们是通过 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 接口中的两个方法获取的,如下图所示:

image

两个方法的区别:

image


HttpSession 常用方法

image


HttpSession 入门案例

1)需求说明

在请求 HttpSessionDemo1 这个 Servlet 时,携带用户名信息,并且把信息保存到会话域中,然后从 HttpSessionDemo2 这个 Servlet 中获取登录信息。

2)案例目的

通过本案例认识到会话域的作用,即多次请求间的数据共享。因为是两次请求,请求域肯定不一样了,所以不能用请求域实现。

最终掌握 HttpSession 对象的获取和使用。

3)原理分析

HttpSession 虽然是服务端会话管理技术的对象,但它本质仍是一个 Cookie,是一个由服务器自动创建的特殊的 Cookie,Cookie 的名称是 JSESSIONID,其值是服务器分配的一个唯一的标识。

当我们使用 HttpSession 时,浏览器在没有禁用 Cookie 的情况下,都会把这个 Cookie 带到服务器端,然后根据唯一标识去查找对应的 HttpSession 对象,找到了,我们就可以直接使用了。

下图就是入门案例中,HttpSession 分配的唯一标识,可以看到两次请求的 JSESSIONID 的值是一样的:

image


HttpSession 的钝化和活化

什么是持久态?

  • 把长时间不用,但还不到过期时间的 HttpSession 进行序列化,写到磁盘上。

  • 我们把 HttpSession 持久态也叫做钝化(与钝化相反的,我们叫活化)。

什么时候使用持久化?

  • 第一种情况:当访问量很大时,服务器会根据 getLastAccessTime 来进行排序,对长时间不用,但是还没到过期时间的 HttpSession 进行持久化。

  • 第二种情况:当服务器进行重启的时候,为了保持客户 HttpSession 中的数据,也要对 HttpSession 进行持久化。

注意:

  • HttpSession 的持久化由服务器来负责管理,我们不用关心。

  • 只有实现了序列化接口的类才能被序列化,否则不行。

posted @ 2021-10-02 11:20  Juno3550  阅读(242)  评论(0编辑  收藏  举报