JavaWeb之 Servlet(2万6千字详解)

前言

博主将用 CSDN 记录 Java 后端开发学习之路上的经验,并将自己整理的编程经验和知识分享出来,希望能帮助到有需要的小伙伴。
博主也希望和一直在坚持努力学习的小伙伴们共勉!唯有努力钻研,多思考勤动手,方能在编程道路上行至所向。
由于博主技术知识有限,博文中难免会有出错的地方,还望各位大佬包涵并批评指正,博主会及时改正;如果本文对小伙伴你有帮助的话,求求给博主一个赞支持一下,可以一起交流,一起加油!!

本文是博主在学习B站尚硅谷的JavaWeb网课时整理的学习笔记,在这里感谢其优质网课,如果有兴趣的小伙伴也可以去看看。
本文编写过程中,参考了以下几位 csdn博主的博客,写的非常好,有兴趣的小伙伴也可以去看看。
Servlet是什么
JavaWeb(专栏)
(尚硅谷)JavaWeb新版教程03-Tomcat-Servlet

1. Servlet 简介

1627234763207
  • Servlet是 JavaWeb最为核心的内容,它是 Java提供的一门动态Web资源开发技术

  • 使用Servlet就可以实现,根据不同的用户在页面上动态显示不同内容。

  • Servlet是JavaEE规范之一,其实本质上就是一个接口,将来我们需要自己定义类来实现Servlet 接口,并由Web服务器运行实现了Servlet接口的类。

1627234972853

那 Servlet到底是干什么的呢?答案很简单,接口的作用就是用于规范代码

在这里插入图片描述

Servlet 接口定义的是一套处理网络请求的规范,所有实现了 Servlet接口的类,都需要实现它的五个接口方法。

其中最主要的是两个声明周期的方法**init()destory(),还有一个处理请求的方法service()。也就是说,所有实现 Servlet接口的类,或者说,所有想要处理网络请求**的类,都需要回答以下三个问题:

  • 你初始化时要做什么?

  • 你接收到请求时要做什么?

  • 你销毁时要做什么?

刚刚说 Servlet是一个规范,那实现了 Servlet接口的类,就能处理浏览器发送的请求了吗?

  • 答案是不能。我们不会在 Servlet实现类中写监听 8080端口的代码,Servlet 实现类也不会直接和浏览器打交道。

那浏览器发送的请求是怎么来到 Servlet实现类的呢?

  • 答案是使用 Servlet 容器,比如最常用的**Tomcat**。Servlet 实现类都是要部署在一个容器中的,不然 Servlet实现类根本不能起作用。

所以换而言之,Tomcat 才是直接与浏览器打交道的家伙,它负责监听端口,当浏览器发送请求过来后,Tomcat根据浏览器所访问的URL等信息,确定要将请求交给哪个 Servlet实现类去处理,然后调用那个 Servlet实现类的service()方法,service()方法便会返回一个响应对象,最后再由Tomcat 把这个响应对象返回给浏览器。

Servlet 本身在 Tomcat中是“非常被动”的一个角色,处理网络请求与响应不是它的主要职责,它其实更偏向于业务代码。所谓的请求和响应对象都是由 Tomcat 传给 Servlet用来处理请求和响应的工具,但 Servlet本身不处理这些,而是另外调用其他类的方法去处理。

2. Servlet 前世今生

Tomcat其实是 Web服务器Servlet容器的结合体。

  1. Web服务器的所做的工作本质上是:

    • 将某个主机上的资源映射为一个URL供外界(如:浏览器)访问。
  2. 那什么是 Servlet容器呢?

    • Servlet容器,顾名思义里面存放的是 Servlet 实现类对象

我们怎样才能通过 Web服务器映射的URL访问到资源呢?肯定是需要创建一个Web项目来处理浏览器所发送的请求的,主要有三个过程:

  1. 接收请求(收到请求)

  2. 处理请求(处理请求)

  3. 响应请求(返回处理结果)

任何一个Web项目,必然包括这三个步骤。其中接收请求和响应请求是共性功能,而且没有差异性。于是,我们就可以把接收请求和响应请求这两个步骤抽取出来并封装进Web服务器中。

在这里插入图片描述

但对于不同请求的处理功能逻辑是不同的。没关系,我们将处理请求的过程抽取出来封装成一个类,该类必须要实现 Servlet接口,也就是 Servlet实现类,而这个类就交给程序员自己编写。我们把 Servlet实现类存放在 Web项目中,可以把它看作是一个资源。

当然,随着互联网的发展,出现了三层架构,所以一些处理逻辑又从 Servlet实现类中抽取出来,封装到 Service层(业务层)和Dao层(持久层)中。

在这里插入图片描述

但是Servlet 并不擅长往浏览器输出 HTML页面,所以后来就出现了 Thymeleaf技术(后面会介绍)。

慢慢的,等Spring 家族出现后,Servlet 开始退居幕后,取而代之的是 SpringMVCSpringMVC的核心组件DispacterServlet 其实本质上就是一个 Servlet接口的实现子类,但是它已经自立门户,在原来 HttpServlet(Servlet子类)的基础上,又封装了一层逻辑。本处我们不再具体展开,而是专注于讲解 Servlet。

3. Servlet 执行流程

Servlet(Server Applet,服务器小程序,是服务器的一小部分),全称 Java Servlet,是用 Java编写的服务器端程序。其主要功能在于能够交互式地浏览和修改数据,生成动态 Web内容。

狭义的 Servlet是指 Java语言实现的一个接口,而广义的 Servlet是指任何实现了 Servlet接口的类。一般情况下,我们将 Servlet 理解为后者。在后面的描述中,Servlet 就代指任何实现了 Servlet接口的类。

Servlet 可以运行于所有支持 Java 的应用服务器中。从原理上讲,Servlet 可以响应任何类型的请求,但绝大多数情况下Servlet 只用于基于 HTTP协议的Web服务器

Servlet的工作流程:Servlet 由 Web服务器调用,Web服务器在收到浏览器对某个 Servlet的访问请求后:

  1. Web服务器首先检查是否已经装载并创建了该Servlet的实例对象。如果是,则直接执行第④步,否则,执行第②步。
  2. 装载并创建该 Servlet的一个实例对象。
  3. 调用 Servlet实例对象的init()方法。
  4. 创建一个用于封装 HTTP请求消息的**HttpServletRequest对象和一个代表HTTP响应消息的HttpServletResponse**对象,然后调用 Servlet中的service()方法,并将请求和响应对象作为参数传递进去。
  5. Web服务器被停止或重新启动之前,Servlet引擎将卸载所有 Servlet,并在卸载之前调用Servlet的destroy()方法。

最早支持 Servlet标准的是 JavaSoftJava Web Server,此后,一些其它的基于 Java的Web服务器开始支持标准的 Servlet。执行流程如下图:

img

以 Tomcat服务器为例,Servlet 简单的执行流程如下:

  1. 浏览器向Tomcat服务器请求某个 Servlet 的实例对象;
  2. Tomcat 加载该 Servlet 类到内存中;
  3. Tomcat 调用 init()方法初始化该 Servlet并实例化该Servlet对象;
  4. Tomcat 调用该Servlet 类中的 service() 方法, service() 方法根据不同请求的方式再去调用doGet()方法或者 doPost()方法,此外还有doHead()doPut()doTrace()doDelete()doOptions()等方法;
  5. 最后Tomcat 调用 Servlet中的destroy()方法销毁该Servlet对象。

详细的执行流程如下:

  1. 加载和实例化 Servlet。这项操作一般是动态执行的。然而,Tomcat 通常会提供一个管理的选项,用于在 Tomcat 启动时强制装载和初始化特定的 Servlet;
  2. Tomcat 创建一个 Servlet的实例;
  3. 第一个浏览器的请求到达 Tomcat;
  4. Tomcat 调用 Servlet 的 init() 方法(可配置为 Tomcat 创建 Servlet 实例时调用:在 Web.xml文件中 <servlet> 标签下配置<load-on-startup> 标签,配置的值为整型,值越小 Servlet 的启动优先级越高);
  5. 一个浏览器的请求到达 Tomcat;
  6. Tomcat 创建一个请求对象,用于处理浏览器请求;
  7. Tomcat 创建一个响应对象,用于响应浏览器请求;
  8. Tomcat 调用 Servlet 的 service() 方法,并传递请求对象和响应对象作为方法的参数;
  9. service() 方法获得关于请求对象的信息,处理请求,访问其他资源,获得需要的信息;
  10. service() 方法使用响应对象的方法,将响应对象传回 Tomcat,最终Tomcat 将响应传回浏览器。service()方法还可能调用其它方法以处理请求,如doGet()doPost() 或者程序员自己开发的新的方法;
  11. 对于更多的浏览器的相同请求,Tomcat 创建新的请求对象和响应对象,仍然激活此 Servlet 的 service()方法,将这两个对象作为参数传递给它,如此重复以上的循环。但无需再调用init()方法。一般 Servlet 只初始化一次(只有一个对象),当 Tomcat 不再需要该 Servlet 时(一般当 Server 关闭时),Tomcat 调用 Servlet 的 destroy()方法将其销毁。

小结:

  1. Servlet就是一群人来制定 Java应用程序中使用 Web时的各种规范,统一接口,其他内部实现由厂商自己实现,Tomcat、jetty、jboss等等应运而生。

  2. Web服务器习惯处理静态资源,所以需要一个帮助程序来帮忙处理动态请求(如当前时间)。Web服务器会将动态请求转发给帮助程序,帮助程序处理后,返回处理后的静态结果给Web服务器。这样就避免了Web服务器处理动态资源。所以,Servlet 的本质是一个帮助程序。 如下图:

img

3.Servlet执行流程分为三个阶段:init(初始化),service(运行),destroy(销毁)

下面我们试试自己创建一个Servlet 程序,并通过浏览器访问该程序。

4. Servlet 快速入门

需求分析: 编写一个Servlet类,并使用IDEA中 Tomcat插件进行部署,最终通过浏览器访问所编写的 Servlet程序。 具体的实现步骤为:

第一步:使用 Maven工具创建一个Web项目Web-demo,并在pom.xml文件中导入 ServletAPI 的依赖坐标。

<dependency>
    <groupId>javax.servlet</groupId>
    <artifactId>javax.servlet-api</artifactId>
    <version>3.1.0</version>
    <!--
      此处为什么需要添加<scope>标签?
      provided 指的是该依赖只在编译和测试过程中有效,最后生成的war包时不会加入该依赖
      因为Tomcat的lib目录中已经有servlet-api这个jar包,如果在生成war包的时候再加入,就会和Tomcat中的jar包冲突,导致报错
    -->
    <scope>provided</scope>
</dependency>

第二步:在 Web项目中的 main/src文件夹下定义一个ServletDemo1类,实现 Servlet接口,并重写接口中所有方法,在service()方法中输出一句话。

import javax.servlet.*;
import java.io.IOException;

public class ServletDemo1 implements Servlet {

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("servlet hello world~");
    }
    
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    public ServletConfig getServletConfig() {
        return null;
    }

    public String getServletInfo() {
        return null;
    }

    public void destroy() {

    }
}

第三步:在ServletDemo1类上使用@WebServlet注解,该注解的作用是配置该 Servlet程序的访问路径(相当于我们给Tomcat中部署的 Web项目设置访问路径URL,也就是在Application context属性中设置的值)。

@WebServlet("/demo1")
public class ServletDemo1 implements Servlet {
    ...
}

第四步:启动Tomcat,在浏览器中输入URL地址访问该Servlet程序。

http://localhost:8080/Web-demo/demo1

第五步:浏览器访问后,在IDEA 控制台会打印出servlet hello world~,说明ServletDemo1程序已经成功运行并成功处理了浏览器请求。

我们以快速入门的这个程序为例,简单看一下 Servlet 程序的执行流程。如下图:

1627236923139

1. 浏览器发出http://localhost:8080/Web-demo/demo1请求,从请求的UEL中可以解析出三部分内容,分别是localhost:8080Web-demodemo1

  • 根据localhost:8080可以找到要访问的Tomcat Web服务器。
  • 根据Web-demo可以找到部署在 Tomcat服务器上的Web-demo项目。
  • 根据demo1可以找到要访问的是项目中的哪个 Servlet类,其根据@WebServlet注解中设置的值进行匹配。

2. 找到ServletDemo1这个类后,Tomcat Web服务器就会为ServletDemo1这个类创建一个对象,然后调用对象中的service()方法。

  • ServletDemo1类实现了 Servlet接口,所以类中必然会重写service()方法供 Tomcat Web服务器进行调用。
  • service()方法中有 ServletRequestServletResponse两个形参,ServletRequest封装的是请求数据,ServletResponse封装的是响应数据,后期我们可以通过这两个参数实现前后端的数据交互。(后面我们会详细讲解这几个参数)

至此,Servlet 的入门案例就已经完成,大家可以按照上面的步骤进行练习了。

5. 两种配置 Servlet程序 URL的方式

5.1 使用 注解来配置 Servlet程序 的 URL

在快速入门案例中,我们知道,一个 Servlet程序在编写好后,要想被浏览器访问到,就需要配置其访问路径:即urlPattern。一个Servlet程序,可以配置多个urlPattern,也就是说,同一个 Servlet 程序可以被映射到多个URL路径上,多个URL路径可以访问同一个 Servlet程序。 比如:

@WebServlet(urlPatterns = {"/demo1", "/demo2"})

代码举例:

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.annotation.WebServlet;

/**
* urlPattern: 一个Servlet可以配置多个访问路径
*/
@WebServlet(urlPatterns = {"/demo1", "/demo2"})
public class ServletDemo1 implements Servlet {

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("servletdemo1 & servletdemo2");
    }
    
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    public ServletConfig getServletConfig() {
        return null;
    }

    public String getServletInfo() {
        return null;
    }

    public void destroy() {

    }
}

在浏览器上输入:
http://localhost:8080/Web-demo/demo1
http://localhost:8080/Web-demo/demo2
这两个地址都能访问到ServletDemo1程序。

5.1.1 urlPattern 的配置规则

精确匹配

1627273174144

/**
 * UrlPattern:
 * * 精确匹配
 */
@WebServlet(urlPatterns = "/user/select")
public class ServletDemo3 implements Servlet {

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("精确访问 Servlet");
    }
    
    ...// 省略其他几个方法
}
  • ServletDemo3程序访问路径为:http://localhost:8080/Web-demo/user/select
目录匹配:使用 * 符号代表任意路径

1627273184095

/**
 * UrlPattern:
 * * 目录匹配: /user/*
 */
@WebServlet(urlPatterns = "/user/*")
public class ServletDemo4 implements Servlet {

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("目录访问 Servlet");
    }
    
    ...// 省略其他几个方法
}
  • ServletDemo4程序的访问路径为:http://localhost:8080/Web-demo/user/任意

思考:

  1. 访问路径http://localhost:8080/Web-demo/user是否能访问到ServletDemo4程序?
  2. 访问路径http://localhost:8080/Web-demo/user/a/b是否能访问到ServletDemo4程序?
  3. 访问路径http://localhost:8080/Web-demo/user/select是访问到ServletDemo3程序还是ServletDemo4程序?

答案是:是;是;访问ServletDemo3

因此我们可以得到的结论是/user/*中的/*代表的是零或多个层级访问目录,同时精确匹配优先级要高于目录匹配。

扩展名匹配

1627273194118

/**
 * UrlPattern:
 * * 扩展名匹配: *.do
 */
@WebServlet(urlPatterns = "*.do")
public class ServletDemo5 implements Servlet {

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("扩展名匹配 Servlet");
    }
    
    ...// 省略其他几个方法
}
  • ServletDemo5程序的访问路径为:http://localhost:8080/Web-demo/任意.do

注意:

  1. 如果路径配置的不是扩展名方式,那么在路径的前面就必须要加**/**,否则会报错。如下:

1627274483755

  1. 如果路径配置的是*.do,那么在*.do的前面不能加上/,即不能是"/*.do",否则会报错。如下:

1627274368245

任意匹配

1627273201370

/**
 * UrlPattern:
 * * 任意匹配: /
 */
@WebServlet(urlPatterns = "/")
public class ServletDemo6 implements Servlet {

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("扩展名匹配 Servlet");
    }
    
    ...// 省略其他几个方法
}
  • ServletDemo6程序的访问路径为:http://localhost:8080/Web-demo/任意
/**
 * UrlPattern:
 * * 任意匹配: /*
 */
@WebServlet(urlPatterns = "/*")
public class ServletDemo7 implements Servlet {

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("扩展名匹配 Servlet");
    }
    
    ...// 省略其他几个方法
}
  • ServletDemo7程序的访问路径为:http://localhost:8080/Web-demo/任意

注意: 路径//*的区别?

  1. 当我们的项目中的 Servlet 程序的urlpattern配置为 /,就会覆盖掉 Tomcat中的 DefaultServlet类,当其他所有Servlet程序的url-pattern都匹配不上浏览器的某个请求URL时,最后都会匹配这个urlpattern配置为 /的 Servlet程序。

  2. 当我们的项目中配置了 Servlet 程序的urlpattern配置为 /,意味着该 Servlet程序可以匹配任意请求URL,但是精确匹配仍是排在第一位的。

  3. DefaultServlet是用来处理静态资源,如果配置了/会把默认的DefaultServlet类覆盖掉,就会引发请求静态资源的时候没有走默认的路径而是走了自定义的 Servlet类,最终导致静态资源不能被访问。

5.1.2 小结

  1. urlPattern总共有四种配置方式,分别是精确匹配、目录匹配、扩展名匹配、任意匹配

  2. 五种配置的优先级为 精确匹配 > 目录匹配> 扩展名匹配 > /* > /,无需记,以最终运行结果为准。

5.2 使用 XML文件来配置 Servlet程序 的 URL

前面对应 Servlet程序的URL配置,我们都是使用 @WebServlet 这个注解,但 Servlet接口 从 3.0版本后才开始支持注解配置,3.0版本前只支持使用 XML文件的配置方法。

由于浏览器通过URL地址访问 Web服务器中的 Web项目和资源,所以 Servlet 程序若想被浏览器访问,就必须把 Servlet程序映射到一个URL地址上,这个工作需要在Web.xml文件中使用<servlet>元素和<servlet-mapping>元素完成。

  • <servlet>元素用于注册 Servlet,它包含有两个主要的子元素:<servlet-name><servlet-class>,分别用于设置 Servlet的注册名称和 Servlet的完整类名。

  • 一个<servlet-mapping>元素用于映射一个已注册的 Servlet的一个对外访问路径,它包含有两个子元素:<servlet-name><url-pattern>,分别用于指定 Servlet的 注册名称和该 Servlet的对外访问路径

对于 Web.xml文件的配置步骤有两步:

第一步:编写一个Servlet类(此时类上不需要加上@WebServlet这个注解了)

public class ServletDemo8 implements Servlet {

    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {
        System.out.println("使用XML配置文件映射 Servlet");
    }
    
    ...// 省略其他几个方法
}

第二步:在web.xml文件中配置该 Servlet程序的URL

<?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">
    
    <!-- 
     下面的元素用于注册 Servlet,相当于告诉Web服务器存在一个Servlet,名称为ServletDemo8,完整类名为...
    -->
    <servlet>
        <!-- Servlet的名称,名字任意-->
        <servlet-name>demo8</servlet-name>
        <!-- Servlet的类全名-->
        <servlet-class>com.Web.ServletDemo8</servlet-class>
    </servlet>

    <!-- 
     下面的元素用于映射一个已注册的Servlet的对外访问路径,相当于告诉Web服务器,若想访问ServletDemo8,就需要通过 /demo8 这个url路径访问
    -->
    <servlet-mapping>
        <!-- Servlet的名称,要和上面的名称一致-->
        <servlet-name>demo8</servlet-name>
        <!-- Servlet的访问路径-->
        <url-pattern>/demo8</url-pattern>
    </servlet-mapping>
</Web-app>

同一个 Servlet 可以被映射到多个URL路径上,即多个<servlet-mapping>元素的<servlet-name>的设置值可以是同一个 Servlet的注册名, 但是映射的<url-pattern>的值可以不一样。例如:

<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello</url-pattern>
 </servlet-mapping>

<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/hello.html</url-pattern>
</servlet-mapping>

通过上面的配置,当浏览器想访问名称是HelloServle的 Servlet程序时,可以使用如下的几个地址去访问:

  • http://localhost:8080/Web-demo/hello

  • http://localhost:8080/Web-demo/hello.html

显然,HelloServlet程序被映射到了多个URL上。

这种通过 XML文件配置方式和注解比起来,确认麻烦很多,所以建议大家使用注解方式来开发。但是大家也要认识 XML文件的配置方式,因为并不是所有的项目都是基于注解开发的。

6. Servlet 接口详解

下面我们将介绍Servlet 接口中的方法以及方法中的形参。

首先查看Servlet 接口中的接口方法:

在这里插入图片描述

Servlet 接口中包含了五个方法,这些方法最难的地方在于,传入方法中的形参是什么。不过幸好Tomcat已经事先帮我们把形参对象封装好传入方法中了,不需要我们再去设置。

除此之外,既不需要我们自己写 TCP连接,也不需要我们解析 HTTP请求,更不需要我们把结果转换成 HTTP响应,ServletRequest 对象和ServletResponse对象已经帮我们把这些难事搞定了(注:这两个对象我们会在后面详细介绍)。

所以,在 Servlet实现类里面主要写的代码都是业务逻辑,我们自己编写的 Servlet程序和原始的、底层的解析、连接等没有丝毫关系。最难的这些操作,Tomcat 已经帮我们封装成形参对象传入方法中了。

上面我们说过,Servlet 接口的作用是为了保证了方法名的规范性和一致性,若方法名不一致,Tomcat 将无法通过方法名对其实现类的方法进行调用。

因此我们自己写的 Servlet程序,只需要实现 Servlet接口,并编写处理浏览器所发送的请求的业务逻辑就好了。

在这里插入图片描述

总的来说,Tomcat 已经帮我们完成了底层的操作,并且传入了三个形参,分别是:ServletConfigServletRequestServletResponse。他们的解析将在下面分别介绍。

6.1 ServletConfig 类

ServletConfig 类即Servlet配置类,也就是我们在 Web.xml文件中配置的 <servlet> 标签中的内容。它封装了 Servlet程序的一些参数信息,如果需要这些信息,我们可以从ServletConfig 对象中获取。

如下图为TestServlet类实例化以及ServletConfig类实例化过程:

在这里插入图片描述

过程解释:

  • 首先,Tomcat 解析 Web.xml配置文件,在<servlet-mapping>标签中通过<url-pattern>找到对应的<servlet-name>中设置的 TestServlet。

  • 然后Tomcat再去<servlet>标签中通过<servlet-name>找到对应的<servlet-class>中设置的TestServlet 的全类名,最后通过 Java的反射机制便可将 TestServlet 类实例化为对象。

  • 在 TestServlet实例化的同时,Tomcat 也会对ServletConfig类进行实例化。

  • 最后,Tomcat 会将 ServletConfig 的实例对象作为参数传递给 TestServlet对象中的init()方法。

6.2 ServletRequest 和 ServletResponse 类

ServletRequest类是接收请求和发送请求的类,Tomcat 已经处理并封装好了,不需要我们编写的 Servlet程序操心。

当浏览器发送的HTTP请求到达 Tomcat之后,Tomcat 通过字符串解析,把各个请求头(Header)、请求地址(URL)、请求参数等都封装进ServletRequest对象中,然后将ServletRequest对象传递给负责处理请求的 Servlet程序。

在 Servlet程序 中通过调用ServletRequest对象中的方法,就可以得到浏览器发送的请求信息。如下:

在这里插入图片描述

至于ServletResponse类,当浏览器发送的HTTP请求到达 Tomcat之后,Tomcat 也会将一个ServletResponse对象传递给 Servlet程序,不过此时它还是一个空对象。

在 Servlet程序 处理完请求后会得到处理结果,这时 Servlet程序就会通过ServletResponse对象的write()方法,将处理结果写入ServletResponse对象内部的缓冲区。

Servlet程序处理请求结束后,会将封装了处理结果的ServletResponse对象返回给 Tomcat,Tomcat 遍历ServletResponse对象中缓冲区存储的信息,最后组装成 HTTP响应发给浏览器。

如下图是ServletRequest/ServletResponse 对象的传递流程:

img

6.3 Servlet 接口中的方法

上面我们介绍完ServletConfigServletRequestServletResponse类后,对于Servlet 接口中的方法形参有了一些了解。接下来我们就介绍一下Servlet 接口中的5个方法。如下图:

在这里插入图片描述

Servlet接口中包含了5个方法,其中 init()service()destory() 这3个方法是声明周期方法init()destory()方法在 Servlet程序存活期间各自只会执行一次,即分别在 Servlet程序被 Tomcat创建和销毁时。而service()方法则在每次有新请求到来时都会被调用。也就是说,我们主要的业务代码需要写在service()方法中。

但是,浏览器发送的请求基本只有两种类型:GET/POST。所以我们必须在service()方法中对不同的请求类型进行判断。如下:

在这里插入图片描述

  • 其中第一行的request.getMethod()方法是获取浏览器发送请求的类型:GET/POST/PUT...等等,我们需要判断浏览器发送请求的类型,并根据不同的请求类型对请求进行处理。

那能不能将上面的代码简化呢?因为每一个 Servlet程序都要写这些重复的判断请求类型代码的话,工作量会很大且没有意义,我们不想自己写这些判断请求类型的代码。

我们去看看 JavaAPI给我们提供的 Servlet 接口库是否有抽象类或者接口已经帮我们写好这些代码了,如果有,那我们的 Servlet程序直接继承或者实现它不就行了吗?

7. Servlet 体系结构

要想解决上面的问题,我们需要先对 Servlet接口的体系结构进行了解。如下图:

1627240593506

因为我们将来开发B/S架构的Web项目,都是针对 HTTP协议,所以我们自定义的 Servlet程序,其实都继承自 HttpServlet抽象类。

我们分别来看看 这几个抽象类都有些什么吧。

7.1 GenericServlet 抽象类

GenericServlet抽象类:该类实现了 Servlet接口(Generic:通用的)。 GenericServlet类源码如下:

在这里插入图片描述

GenericServlet抽象类的作用如下:

  • 定义了一个全局变量ServletConfig对象,提升了init()方法中原本是形参的ServletConfig对象的作用域,使得ServletConfig对象变成了全局变量,方便其他方法使用。
  • init() 方法中还另外调用了一个init()空参方法,如果我们希望 Servlet程序在被创建时做一些特定的初始化操作,可以在继承GenericServlet类后,重写该init()空参方法。
  • 为了方便在其他类中也可以获得ServletConfig对象,于是写了一个getServletConext()方法。

不过令人沮丧的是,GenericServlet类中的service()方法还是一个空方法,这并没有完成我们最初简化代码开发的设想。那继续看一下HttpServlet 抽象类

7.2 HttpServlet 抽象类

HttpServlet 抽象类,它继承自GenericServlet类。HttpServlet类源码如下:

在这里插入图片描述

HttpServlet 本身是一个抽象类,它定义了很多全局变量,我们看看它里面的service()方法有没有实现对浏览器发送请求的类型进行判断。源码如下:

在这里插入图片描述

显而易见,HttpServlet类中的service()方法帮我们完成了对复杂的浏览器发送请求的类型判断,而这正是我们想要的。

service()方法中,如果请求类型为 GET类型,则会调用doGet()方法处理该请求;如果请求类型为 POST类型,则会调用doPost()方法处理该请求。

问题是HttpServlet 类为什么还要声明成抽象类呢?它的文档中注释了:

在这里插入图片描述

可以发现,继承了HttpServlet类的 Servlet程序,必须重写doGet()doPost()doPut()doDelete()等方法中的至少一个。

补充:一个类声明成抽象类,一般有两个原因:

  1. 它内部有抽象方法。

  2. 它没有抽象方法,但是该类不希望被实例化。

其实,HTTPServlet类做成抽象类,是为了不被实例化。

在这里插入图片描述

它为什么不希望被实例化,而且要求子类重写doGet()doPost()等方法呢?我们来看一下源码:

在这里插入图片描述

HttpServlet中的doGet()方法,使用了protected修饰,意思是希望子类能重写该方法。

那如果我们没有重写doGet()doPost()等方法,又会怎么样?

在这里插入图片描述

源码告诉我们,浏览器页面会显示405错误:(http.method_get_not_supported)

也就是说,HttpServlet类虽然在service()方法中帮我们完成了对浏览器发送请求的类型的判断。但是在实际开发中,针对每一种请求类型,具体的业务逻辑代码都是不同的(换句话说,HttpServlet只能帮我们把不同类型的请求分类,而并不能帮我们处理请求)。

由于HttpServlet无法知晓具体的请求想干什么,所以它索性抽象出了 7个方法,并且提供了方法的默认实现:即报 405、400错误。这些错误提示我们HttpServlet类并不支持处理具体的请求业务。因此,我们必须重写这些方法,用来处理具体的请求业务。

在这里插入图片描述

以上就是不能让HttpServlet类被实例化的原因了,不然如果不小心调用了HttpServlet对象提供的doXxx()方法去处理请求,就会出错。

而这也就是模板方法模式:父类把能写的逻辑都写完,把不确定的业务代码抽象成一个方法,并调用它。当子类重写了该方法后,整个业务代码就活了。

我们自己编写的 Servlet程序继承了HttpServlet类,当 Tomcat 以后调用HttpServlet类的service()方法时,在该方法内部会调用doXxx()方法。由于我们的 Servlet程序重写了HttpServlet类的doXxx()方法,因此service()方法最终调用的是我们自己编写的doXxx()方法。

补充:Java子类调用父类的方法的步骤:

  1. 子类的对象调用方法时,会首先在子类中查找是否存在该方法,如果子类中没有该方法,再到父类中查找。
  2. 如果该方法中又调用了其他方法,那么还是按照之前的顺序,先在子类中查找,再在父类中查找。

如下图:

在这里插入图片描述

小结

  • 最后我们明白了,我们自己编写一个 Servlet程序来处理浏览器发送的请求,只需要继承HttpServlet类,然后在 Servlet程序中根据不同的请求类型和请求业务,重写HttpServlet类中的doGet()doPost()doPut()doDelete()等方法即可。
  • 其他的事就不需要我们管了,方法形参由 Tomcat帮我们封装好传递进来,请求类型的判断由HttpServlet类的service()方法帮我们完成。

8. Servlet 生命周期

8.1 生命周期与三个重要方法的对应

生命周期:Servlet 生命周期指它从被创建到被销毁的整个过程。对应 Servlet 中的三个方法:init(), service(), destroy()

默认情况下:

  • 第一次接收到浏览器发送的请求时,负责处理请求的 Servlet程序会进行实例化(调用构造方法)、初始化(调用init()方法)、然后处理请求(调用service()方法)。
  • 从第二次接收请求开始,Servlet 只会调用service()方法处理请求,没有实例化和初始化的过程了。
  • 当Tomcat 容器关闭时,容器中所有的 Servlet 实例都会被销毁,此时调用销毁方法destroy(),实例随后会被 Java的垃圾收集器所回收。

8.2 生命周期的演示

我么通过重写 Servlet接口中的这三个方法来看一下 Servlet 的整个生命周期过程,之前我们说过,如果子类中重写了父类的方法,那么调用的是子类重写后的方法。

Web-demo项目中创建Demo02Servlet类。如下:

// 演示Servlet的生命周期
@WebServlet(urlPatterns = "/demo2")
public class Demo02Servlet extends HttpServlet {
    
    public Demo02Servlet(){
        System.out.println("正在实例化....");
    }

    @Override
    public void init() throws ServletException {
        System.out.println("正在初始化.....");
    }

    @Override
    protected void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("正在服务.....");
    }

    @Override
    public void destroy() {
        System.out.println("正在销毁......");
    }
}

在浏览器中发送第一次请求,访问:http://localhost:8080/Web-demo/demo2

控制台输出如下:

在这里插入图片描述

然后刷新网页,在网页上每一次刷新,就代表着重新发送一次请求,这个时候控制台输出信息只有:正在服务…。如下图:

在这里插入图片描述

最后点击停止运行按钮,Tomcat 容器就会对所有的 Servlet实例 进行销毁,控制台输出如下:

在这里插入图片描述

通过上面案例我们发现:

  1. 对于每一个 Servlet 实例,Tomcat 只会创建一个,所有相同的请求都由对应的处理该请求的 Servlet实例去处理。

  2. 默认情况下,第一次发送请求时,Tomcat 才会去实例化和初始化 Servlet程序,然后由 Servlet程序处理请求。

    • 这样做的好处是: 提高系统的启动速度。就是说只在有请求时,才创建 Servlet 对象。
    • 这样做的缺点是: 处理第一次发送的请求时,系统耗时较长。

结论:

  • 如果需要提高系统的启动速度,当前默认情况就是这样。
  • 如果需要提高系统的响应速度,我们就应该设置 Servlet 的初始化时机。

8.3 Servlet 的初始化时机

默认是第一次接收请求时,进行实例化,初始化 Servlet。

我们可以通过修改Web.xml文件中 Servlet程序 的 <load-on-startup>标签来设置 Servlet程序启动的先后顺序,数字越小,启动顺序越靠前,最小值为0(这个优先级表示随着 Tomcat 的启动,直接对 Servlet程序进行实例化和初始化操作,无需等到有请求到来)。

修改Web.xml配置文件如下:

<servlet>
    <servlet-name>Demo02Servlet</servlet-name>
    <servlet-class>com.servlets.Demo02Servlet</servlet-class>
    <!--设置初始化优先级-->
    <load-on-startup>1</load-on-startup>
</servlet>

启动 Tomcat时控制台展示如下,可以看出,在部署好 Tomcat 之前 Demo02Servlet就进行了实例化和初始化:

在这里插入图片描述

补充:我们也可以使用注解@WebServlet(urlPatterns = "/demo2", loadOnStartup = 1)来设置 Servlet程序初始化的优先级。loadOnstartup的取值有两类情况:

  • 负整数:第一次有请求访问时再初始化 Servlet对象。
  • 0 或正整数:服务器启动时就创建 Servlet对象,数字越小优先级越高。

9. Servlet 线程不安全问题

Servlet 在Tomcat容器中是:单例的、多线程情况下是线程不安全的。

单例:所有相同的请求都由对应处理该请求的同一个 Servlet实例去响应处理。

线程不安全:假如有两个浏览器线程同时请求同一个 Servlet实例,第一个线程需要根据 Servlet程序中的某个成员变量值去做逻辑判断。但是在第一个线程进行逻辑判断的过程中,第二个线程改变了这个成员变量的值,从而导致第一个线程的判断结果发生了变化。这就是线程不安全问题。如下图:

eecd046a42bd4eeb9ffa819433084d0c

解决多线程不安全问题的注意事项:

  1. 尽量的不要在 Servlet程序中定义成员变量。
  2. 如果不得不定义成员变量,那么一定不要去:①修改成员变量的值;②根据成员变量的值做一些逻辑判断。

10. 小结一下 Tomcat 和 Servlet的执行流程

一个 Web 项目,需要部署到 Tomcat 容器中去,并不是把 Web 项目的源代码部署进去,而是把 Web 项目的部署包artifact部署到 Tomcat中去。所以我们需要先生成 Web项目的 部署包,在IDEA 2022中,创建 Web 项目时,就会自动生成其对应的部署包了。不过在旧版的 IDEA 中,则需要在 Project Structure中手动生成 Web项目的部署包。

若是已经生成了 Web项目的部署包之后,又需要在 Web 项目中另外引入新的 jar 包,这个后面引入的 jar 包在已部署的包中不会自动更新。此时在Project Structure中会出现 Problems提示,我们根据其提示操作就可以解决该问题。另外,我们也可以直接把第三方 jar 包直接放到已部署的 Web 项目中的 WEB-INF目录下,这样该 jar 包也会自动添加进部署包中,不过这样做的缺点是该后添 jar 包只能给该Web项目独享,其他的项目不能再使用此 jar包。

最后就是要在 Tomcat 中部署 Web 项目的部署包了。首先进入 Tomcat 模板中的 Deployment模块,添加需要部署的 Web项目的 Artifact部署包,然后会出现一个Web项目的根目录Application context,其内设置的目录路径就是 Web 项目在 Tomcat 容器中的根目录路径context root,我们可以对其进行更改。 当Tomcat 启动后,会自动打开指定的浏览器,然后去访问Web项目的URL

1.假设在 Tomcat 中设置的 URL 为http://localhost:8080/demo,启动 Tomcat 后,浏览器便会访问该 URL,此时会向 Tomcat 服务器中部署的 Web 项目中请求资源 demo,于是 Tomcat 便会到 Web 项目中寻找该资源。

  • 若通过注解@WebServlet没有直接找到,Tomcat 便解析 Web.xml配置文件,在<servlet-mapping> 标签中找到了 <url-patter>标签中值为/demo所对应的<servlet-name>标签中的类名。
  • 然后去<servlet>标签中找到相匹配的 Servlet 类,然后通过反射生成该类的实例对象,并自动调用其 sevice()方法,最后在使用结束后销毁该实例对象。

2.假设在 Tomcat 中设置的 URL 为http://localhost:8080/test.html,启动 Tomcat 后,在浏览器访问该 URL,则是向 Tomcat 服务器中部署的 Web 项目中请求资源 test.html,于是 Tomcat 便会到 Web 项目中寻找该资源,找到该资源后,便给浏览器返回响应,于是在浏览器中便会显示对应的test.html静态界面,

浏览器启动后,可能会报错 404。404:意味着找不到指定的资源。 假设我们的网址是:http://localhost:8080/pro01/(/pro01是根目录,没有指定要访问的资源路径), 那么表明客户端请求的资源是 Web 项目中的 index.html,它也是客户端默认访问的资源路径。

我们也可以通过Web.xml中的<welcome-file-list>标签进行设置客户端默认访问的资源路径(在 Tomcat的Web.xml中设置,或者在自己项目的Web.xml中设置)。

**浏览器启动后,可能会报错 405。405:表示当前请求的方法不支持。**比如,我们的 form表单中的method=post , 那么 Servlet 必须重写doPost()方法。否则会报 405 错误。

还有可能会出现空指针异常或者是NumberFormatException

最后注意:web.xml文件中的<url-pattern>标签中的值中以斜杠开头。

  • 23
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林二月er

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值