# Web 基础概念简介

# #1、服务器与客户端

# #①线下的服务器与客户端

./images

# #②线上的服务器与客户端

./images

# #③客户端的各种形式

# #[1] PC 端网页

./images

# #[2] 移动端

./images

# #[3] Iot 设备

./images

# #④服务器的各种形式

点我查看完整内容

# #2、服务器端应用程序

我们要开发的就是服务器端应用程序

./images

# #3、业务

项目中的功能就是业务。

# #4、请求和响应

# #①发生在饭馆的请求和响应

./images

# #②项目中的请求和响应

./images

# #5、项目的逻辑构成

  • 请求:请求是项目中最基本的逻辑单元,就像万事万物都由原子构成

    举例:点超链接跳转到注册页面

  • 功能:一个功能包含很多个请求

    举例:注册用户功能

    • 请求 1:点超链接跳转到注册页面
    • 请求 2:发送请求获取短信验证码
    • 请求 3:检查用户名是否可用
    • 请求 4:提交表单完成注册
  • 模块:一个模块包含很多功能

    举例:用户信息管理模块

    • 功能 1:用户注册功能
    • 功能 2:用户登录功能
    • 功能 3:个人中心 —— 账户安全功能
    • 功能 4:个人中心 —— 账户绑定功能
    • 功能 5:个人中心 —— 收货地址功能
    • 功能 6:个人中心 —— 我的银行卡功能
  • 子系统:根据项目规模的不同,子系统这层逻辑概念可能有也可能没有。如果设置了子系统,那么子系统中也必然包含很多模块。其实庞大项目的子系统已经相当于一个项目了,甚至比小型项目整个都大。

    举例:认证中心子系统

    • 模块 1:用户信息管理模块
    • 模块 2:权限管理模块
    • 模块 3:授权管理模块
    • 模块 4:权限检查模块
  • 项目:为了解决现实生活中的实际问题开发一个项目,这个项目就是为这个需求提供的一整套解决方案。

    举例:电商项目

    • 子系统 1:认证中心子系统
    • 子系统 2:商品管理子系统
    • 子系统 3:购物车子系统
    • 子系统 4:仓储子系统
    • 子系统 5:物流子系统
    • 子系统 6:订单子系统

./images

# #6、架构

# #①概念

『架构』其实就是项目的『结构』。只不过『结构』这个词太小了,不适合用来描述项目这么大的东西,所以换了另一个更大的词:架构。所以当我们聊一个项目的架构时,我们聊的是项目是由哪些部分组成的。

# #②发展演变历程

# #[1] 单一架构

一个项目就是一个工程,这样的结构就是单一架构,也叫 all in one。我们现在的 JavaWeb 阶段、SSM 阶段都是学习单一架构开发技术。

# #[2] 分布式架构

一个项目中包含很多工程,每个工程作为一个模块。模块之间存在调用关系。分布式架构阶段的技术分为两类:

  • Java 框架:SpringBoot、SpringCloud、Dubbo 等等。
  • 中间件:Redis、ElasticSearch、FastDFS、Nginx、Zookeeper、RabbitMQ 等等。

./images

# #③单一架构技术体系

  • 视图:用户的操作界面 + 数据的动态显示
    • 前端技术:HTML/CSS/JavaScript
    • 服务器端页面模板技术:Thymeleaf
  • 控制层:处理请求 + 跳转页面
    • 服务器:Tomcat
    • 控制器:Servlet
    • 域对象:request、session、servletContext
    • 过滤器:Filter
    • 监听器:Listener
    • 异步交互:Ajax
  • 业务逻辑层:业务逻辑计算
  • 持久化层:操作数据库

./images

# #7、本阶段技术体系

./images

# #8、本阶段案例简介

./images


# HTTP 协议

# #1、介绍

HTTP:Hyper Text Transfer Protocol 超文本传输协议。HTTP 最大的作用就是确定了请求和响应数据的格式。浏览器发送给服务器的数据:请求报文;服务器返回给浏览器的数据:响应报文。

image-20220131181213563

# #2、请求报文

# #①在开发者工具中浏览报文源码

image-20220125045122504

# #②请求报文的三个部分

./images

HTTP Request Message - HTTP Requests Documentation

# #③请求行 (request line)

作用:展示当前请求的最基本信息

POST /dynamic/target.jsp HTTP/1.1

  • 请求方式
  • 访问地址
  • HTTP 协议的版本

# #④请求消息头 (request header)

作用:通过具体的参数对本次请求进行详细的说明

格式:键值对,键和值之间使用冒号隔开

往服务器发送请求时,浏览器本身会发一些信息在请求消息头中

相对比较重要的请求消息头:

名称 功能
Host 服务器的主机地址
Accept 声明当前请求能够接受的『媒体类型』
Referer 当前请求来源页面的地址
Content-Length 请求体内容的长度
Content-Type 请求体的内容类型,这一项的具体值是媒体类型中的某一种
Cookie 浏览器访问服务器时携带的 Cookie 数据

# 空行

空行是用于分割请求数据的行,且是必须的。即使第四部分的请求数据是空的,也必须有空行分割。

# #⑤请求体 (request message)

作用:作为请求的主体,发送数据给服务器。具体来说其实就是 POST 请求方式下的请求参数。

__三种__情况:

# #[1] get 方式 -> 没有请求体,只有 queryString

==get 请求没有请求体!!!== 只有 queryString, 也就是一般我们发送请求时 url 后面那串信息 ( ? 后面的)

# #[2] post 方式 ->form data

含义:当前请求体是一个表单提交的请求参数。

image-20220125045220785

查看源码后,发现格式如下:

username=tom&password=123456

  • 每一组请求参数是一个键值对
  • 键和值中间是等号
  • 键值对之间是 & 号

# #[3] json 方式 ->Request Payload

含义:整个请求体以某种特定格式来组织数据,例如 JSON 格式。

./images

# #3、请求方式

# #①HTTP 协议已定义的请求方式

HTTP1.1 中共定义了八种请求方式:

  • GET:从服务器端获取数据
  • POST:将数据保存到服务器端
  • PUT:命令服务器对数据执行更新
  • DELETE:命令服务器删除数据
  • HEAD
  • CONNECT
  • OPTIONS
  • TRACE

# #②GET 请求

  • 特征 1:没有请求体
  • 特征 2:请求参数附着在 URL 地址后面
  • 特征 3:请求参数在浏览器地址栏能够直接被看到,存在安全隐患
  • 特征 4:在 URL 地址后面携带请求参数,数据容量非常有限。如果数据量大,那么超出容量的数据会丢失
  • 特征 5:从报文角度分析,请求参数是在请求行中携带的,因为访问地址在请求行

# #③POST 请求

  • 特征 1:有请求体
  • 特征 2:请求参数放在请求体中
  • 特征 3:请求体发送数据的空间没有限制
  • 特征 4:可以发送各种不同类型的数据
  • 特征 5:从报文角度分析,请求参数是在请求体中携带的
  • 特征 6:由于请求参数是放在请求体中,所以浏览器地址栏看不到

# #4、媒体类型

# #①HTTP 协议中的 MIME 类型

Multipurpose Internet Mail Extensions

# #②用途

为了让用户通过浏览器和服务器端交互的过程中有更好、更丰富的体验,HTTP 协议需要支持丰富的数据类型。

# #③MIME 类型定义参考

我们可以通过查看 Tomcat 解压目录下 conf/web.xml 配置文件,了解 HTTP 协议中定义的 MIME 类型。

<mime-mapping>
	<extension>mp4</extension>
	<mime-type>video/mp4</mime-type>
</mime-mapping>
<mime-mapping>
	<extension>doc</extension>
	<mime-type>application/msword</mime-type>
</mime-mapping>
<mime-mapping>
	<extension>json</extension>
	<mime-type>application/json</mime-type>
</mime-mapping>
<mime-mapping>
	<extension>html</extension>
	<mime-type>text/html</mime-type>
</mime-mapping>

从上面的例子中可以看出:MIME 的基本格式是

大类 / 具体类型

MIME 类型在 HTTP 报文中对应的是内容类型:Content-type

# #5、响应报文

./images

# #①响应状态行

HTTP/1.1 200 OK

  • HTTP 协议版本
  • 响应状态码
  • 响应状态的说明文字

# #②响应消息头

  • 响应体的说明书。
  • 服务器端对浏览器端设置数据,例如:服务器端返回 Cookie 信息
名称 功能
Content-Type 响应体的内容类型
Content-Length 响应体的内容长度
Set-Cookie 服务器返回新的 Cookie 信息给浏览器
location 重定向的情况下,告诉浏览器访问下一个资源的地址

# 空行

用于区分正文部分,且是必须的。表示该空行之后的内容均为正文内容。

# #③响应体

服务器返回的数据主体,有可能是各种数据类型。

  • HTML 页面
  • 图片
  • 视频
  • 以下载形式返回的文件
  • CSS 文件
  • JavaScript 文件

image-20220201040501271

# #④响应状态码

作用:以编码的形式告诉浏览器当前请求处理的结果

状态码 含义
200 服务器成功处理了当前请求,成功返回响应
302 重定向
400 [SpringMVC 特定环境] 请求参数问题
403 没有权限
404 找不到目标资源
405 请求方式和服务器端对应的处理方式不一致
406 [SpringMVC 特定环境] 请求扩展名和实际返回的响应体类型不一致
50X 服务器端内部错误,通常都是服务器端抛异常了

404 产生的具体原因:

  • 访问地址写错了,确实是没有这个资源
  • 访问了 WEB-INF 目录下的资源
  • Web 应用启动的时候,控制台已经抛出异常,导致整个 Web 应用不可用,访问任何资源都是 404
  • 服务器端缓存

# Servlet 概述

# 先说下 Tomcat

新建 project,idea 默认会给你 project 一个同名的 module, 一般是不用所以把他 src 给删了。然后创建新的 module, 要是 idea 里面一开始选了一个 javaee 的 module, 那么会除了 src 还会自带那个__web 文件夹__, 里面有一些需要的,然后还会自带一个部署包,这个部署包才是真正部署到 tomcat 里面去的,而不是我们源代码,这个部署的包叫做 artifact.

现在我们要是在我们源代码里面用到别的第三方的类 (jar 包), 我们就引入到比如说一个 lib 文件夹里,我们首先要把我们这个__module 的依赖加上这个第三方的 jar 包__,__然后需要再在部署的包里面也加上 (project setting - artifacts - 然后加,或者看 Problem 一般告诉你加)__必须要在部署包加,不然就跟没有用那个包一样会出错.

不过要是你这个第三方的类是在 web 文件下面的 WEB-INF 下面新建一个 lib 文件来存,这个样子好像就不用特意再去给部署包加了好像是 (不过这样不方便,其他 module 用不了,不像我们放到项目那一级就可以让所有 module 使用)

https://heavy_code_industry.gitee.io/code_heavy_industry/pro001-javaweb/lecture/chapter05/

image-20220127162516194

然后部署的时候那个 application context 就是 context root, 也就是我们页面一开始的 url (一般直接改成 /), 与我们 module 名啊啥的没关系.

因为后面没写会默认去访问 index.html/index.htm/index.jsp

image-20220127172329609

如果我们有个其他名的欢迎页,可以再 tomcat 的 web.xml 或者我们当前换个 module 的 web.xml 设置好就可以了

image-20220127172725440

或者我们在那个 url 写上一开始访问的文件 ([IP]:[端口][context root (我们上面设为了 "/")[文件比如说 “hello.html”]]

# #1、Servlet 名字

Servlet=Server+applet

Server:服务器

applet:小程序

Servlet 含义是服务器端的小程序

其实就是我们可以按照客户端的请求创造对应的 servlet 类然后这个类继承 HttpServlet, 然后要重写一些方法,也就是我们客户端请求发送后,我们干些什么

# #2、Servlet 在整个 Web 应用中起到的作用

# #①生活中的例子

./images

# #②对应 Web 应用

./images

# #③具体细节

./images

# #④Servlet 扮演角色

./images

在整个 Web 应用中,Servlet 主要负责处理请求、协调调度功能。我们可以把 Servlet 称为 Web 应用中的 **『控制器』**


# Servlet HelloWorld

# #1、HelloWorld 分析

# #①目标

在页面上点击超链接,由 Servlet 处理这个请求,并返回一个响应字符串:Hello,I am Servlet

# #②思路

./images

# #2、具体操作

# #①第一步:创建动态 Web module

./images

# #②第二步:创建超链接

<!-- /Web应用地址/Servlet地址 -->
<a href="/app/helloServlet">Servlet Hello World</a>

!!! 如果 Web 应用地址 (context root) 只是 / 那么用 /Servlet地址 便足够

# #③第三步:创建 HelloServlet 的 Java 类

public class HelloServlet implements Servlet {
    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

    }

    @Override
    public ServletConfig getServletConfig() {
        return null;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        // 控制台打印,证明这个方法被调用了
        System.out.println("我是HelloServlet,我执行了!");

        // 返回响应字符串
        // 1、获取能够返回响应数据的字符流对象
        PrintWriter writer = servletResponse.getWriter();

        // 2、向字符流对象写入数据
        writer.write("Hello,I am Servlet");
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {

    }
}

# #④第四步:配置 HelloServlet

配置文件位置:WEB-INF/web.xml

image-20220125045506448image-20220127172945731

<!-- 配置Servlet本身 -->
<servlet>
    <!-- 全类名太长,给Servlet设置一个简短名称 -->
    <servlet-name>HelloServlet</servlet-name>

    <!-- 配置Servlet的全类名 -->
    <servlet-class>com.atguigu.servlet.HelloServlet</servlet-class>
</servlet>

<!-- 将Servlet和访问地址关联起来 -->
<servlet-mapping>
    <servlet-name>HelloServlet</servlet-name>
    <url-pattern>/helloServlet</url-pattern>
</servlet-mapping>

『虚拟路径』:Servlet 并不是文件系统中实际存在目录或文件,所以为了方便浏览器访问,我们创建了虚拟出来的路径来访问它。

image-20220127132850218

image-20220202095404132

# #⑤小结

  • 需求:在浏览器上点超链接能够访问 Java 程序

./images

# sidenote (注解方式注册 servlet (不需要在 web.xml 里面那种方式了)):

image-20220201053253318

直接在注解里面写 /Web应用地址/(对应的)Servlet地址(可能是多个) 即可

# #3、梳理概念

# #①原生 Tomcat

./images

# #②IDEA 中的 Tomcat 实例

./images

# #③IDEA 中的 Web 工程

./images

# #④根据 Web 工程生成的 war 包

./images

# #⑤Web 工程中的资源

# #[1] 静态资源

  • HTML 文件
  • CSS 文件
  • JavaScript 文件
  • 图片文件

# #[2] 动态资源

  • Servlet

# #⑥访问资源的地址

# #[1] 静态资源

/Web 应用名称 / 静态资源本身的路径

# #[2] 动态资源

/Web 应用名称 / 虚拟路径

# #⑦Web 应用名称

./images

# #⑧总体的逻辑结构

./images


# Servlet 生命周期

image-20220131144814828

image-20220131145204054

image-20220131145243473

因此!!!image-20220131145329837

image-20220131145644468

注意我们可能有多个我们自己写的继承 httpServlet 的子类,这每一个可能对应着用户的请求,这里不同请求指的不是 methods 不同,而是请求的地址或者资源不同。我们已经在 web.xml 配置好了各种相关设置.(注意一个我们创造 httpServlet 子类可能对应多个不同用户请求的地址或资源,也就是那个 servlet 处理多个业务,之后会说到,当然和这个也是需要在 web.xml 中配置好)

继续说,我们可能有多个 httpServlet 子类,然后这些都是只有在用户请求第一次的时候,那个有着对应请求地址的那个 httpServlet 子类才会被实例化,初始化,然后服务那个请求.

不过要是我们在 web.xml 给那个 httpServlet 子类配置了 <load-on-startup>数字</load-on-startup> 中的数字为 1 的话,那就会让这个 httpServlet 子类在我们启动 tomcat 容器的时候就实例化并初始化这个 httpServlet 子类 (数字越小启动越快)

可能其他 httpServlet 子类也有这个设置,分别是 2,3,4,5,… 那么就会按照大小,小的先被实例化然后初始化

然后之后用户发请求,我们这些都已经实例化并且初始化好了,用户一发请求我们就可以服务了

每个 servlet 都是单例,线程不安全的

image-20220131150944344

image-20220131151039541

如果非要定义成员变量,那就__不要__

  1. 拿成员变量的值做什么判断等等等
  2. 不要修改那个成员变量的值

# #1、从 Servlet 接口说起

./images

# #2、Servlet 创建对象的时机

# #①验证方式

在 HelloServlet 的构造器中执行控制台打印

public HelloServlet(){
    System.out.println("我来了!HelloServlet对象创建!");
}

# #②打印结果

我来了!HelloServlet 对象创建! 我是 HelloServlet,我执行了! 我是 HelloServlet,我执行了! 我是 HelloServlet,我执行了! 我是 HelloServlet,我执行了! 我是 HelloServlet,我执行了! 我是 HelloServlet,我执行了!

# #③结论

  • 默认情况下:Servlet 在第一次接收到请求的时候才创建对象
  • 创建对象后,所有的 URL 地址匹配的请求都由这同一个对象来处理
  • Tomcat 中,每一个请求会被分配一个线程来处理,所以可以说:Servlet 是单实例,多线程方式运行的。
  • 既然 Servlet 是多线程方式运行,所以有线程安全方面的可能性,所以不能在处理请求的方法中修改公共属性。

# #④修改 Servlet 创建对象的时机

修改 web.xml 中 Servlet 的配置:

<!-- 配置Servlet本身 -->
<servlet>
    <!-- 全类名太长,给Servlet设置一个简短名称 -->
    <servlet-name>HelloServlet</servlet-name>

    <!-- 配置Servlet的全类名 -->
    <servlet-class>com.atguigu.servlet.HelloServlet</servlet-class>

    <!-- 配置Servlet启动顺序 -->
    <load-on-startup>1</load-on-startup>
</servlet>

效果:Web 应用启动的时候创建 Servlet 对象

image-20220125045649999

友情提示:将来配置 SpringMVC 的时候会看到这样的配置。

# #3、其他环节

./images

# #4、Servlet 容器

# #①容器

在开发使用的各种技术中,经常会有很多对象会放在容器中。

# #②容器提供的功能

容器会管理内部对象的整个生命周期。对象在容器中才能够正常的工作,得到来自容器的全方位的支持。

  • 创建对象
  • 初始化
  • 工作
  • 清理

# #③容器本身也是对象

  • 特点 1:往往是非常大的对象
  • 特点 2:通常的单例的

# #④典型 Servlet 容器产品举例

  • Tomcat
  • jetty
  • jboss
  • Weblogic
  • WebSphere
  • glassfish

# #5、总结

名称 时机 次数
创建对象 默认情况:接收到第一次请求 修改启动顺序后:Web 应用启动过程中 一次
初始化操作 创建对象之后 一次
处理请求 接收到请求 多次
销毁操作 Web 应用卸载之前 一次

小提示:

我们学习任何一章的知识,通常都包括两类:

  • 现在用得上的 —— 优先级高
  • 以后才用的 —— 优先级低

生命周期部分就属于以后才用的知识。


# ServletConfig 和 ServletContext

# #1、类比

./images

# #2、ServletConfig 接口

# #①接口概览

./images

# #②接口方法介绍

方法名 作用
getServletName() 获取HelloServlet定义的 Servlet 名称
getServletContext() 获取 ServletContext 对象
getInitParameter() 获取配置 Servlet 时设置的『初始化参数』,根据名字获取值
getInitParameterNames() 获取所有初始化参数名组成的 Enumeration 对象

# #③初始化参数举例

<!-- 配置Servlet本身 -->
<servlet>
    <!-- 全类名太长,给Servlet设置一个简短名称 -->
    <servlet-name>HelloServlet</servlet-name>

    <!-- 配置Servlet的全类名 -->
    <servlet-class>com.atguigu.servlet.HelloServlet</servlet-class>

    <!-- 配置初始化参数 -->
    <init-param>
        <param-name>goodMan</param-name>
        <param-value>me</param-value>
    </init-param>

    <!-- 配置Servlet启动顺序 -->
    <load-on-startup>1</load-on-startup>
</servlet>

# #④体验

在 HelloServlet 中增加代码:

public class HelloServlet implements Servlet {

    // 声明一个成员变量,用来接收init()方法传入的servletConfig对象
    private ServletConfig servletConfig;

    public HelloServlet(){
        System.out.println("我来了!HelloServlet对象创建!");
    }

    @Override
    public void init(ServletConfig servletConfig) throws ServletException {

        System.out.println("HelloServlet对象初始化");

        // 将Tomcat调用init()方法时传入的servletConfig对象赋值给成员变量
        this.servletConfig = servletConfig;

    }

    @Override
    public ServletConfig getServletConfig() {

        // 返回成员变量servletConfig,方便使用
        return this.servletConfig;
    }

    @Override
    public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException {

        // 控制台打印,证明这个方法被调用了
        System.out.println("我是HelloServlet,我执行了!");

        // 返回响应字符串
        // 1、获取能够返回响应数据的字符流对象
        PrintWriter writer = servletResponse.getWriter();

        // 2、向字符流对象写入数据
        writer.write("Hello,I am Servlet");

        // =============分割线===============
        // 测试ServletConfig对象的使用
        // 1.获取ServletConfig对象:在init()方法中完成
        System.out.println("servletConfig = " + servletConfig.getClass().getName());

        // 2.通过servletConfig对象获取ServletContext对象
        ServletContext servletContext = this.servletConfig.getServletContext();
        System.out.println("servletContext = " + servletContext.getClass().getName());

        // 3.通过servletConfig对象获取初始化参数
        Enumeration<String> enumeration = this.servletConfig.getInitParameterNames();
        while (enumeration.hasMoreElements()) {
            String name = enumeration.nextElement();
            System.out.println("name = " + name);

            String value = this.servletConfig.getInitParameter(name);
            System.out.println("value = " + value);
        }
    }

    @Override
    public String getServletInfo() {
        return null;
    }

    @Override
    public void destroy() {
        System.out.println("HelloServlet对象即将销毁,现在执行清理操作");
    }
}

打印效果:

我是 HelloServlet,我执行了! servletConfig = org.apache.catalina.core.StandardWrapperFacade servletContext = org.apache.catalina.core.ApplicationContextFacade name = goodMan value = me

引申:

  • 广义 Servlet:javax.servlet 包下的一系列接口定义的一组 Web 开发标准。遵循这一套标准,不同的 Servlet 容器提供了不同的实现。
  • 狭义 Servlet:javax.servlet.Servlet 接口和它的实现类,也就是实际开发时使用的具体的 Servlet。

Servlet 标准和 JDBC 标准对比

Servlet 标准 JDBC 标准
javax.servlet 包下的一系列接口 javax.sql 包下的一系列接口
Servlet 容器厂商提供的具体实现类 数据库厂商提供的实现类(数据库驱动)

同样都体现了面向接口编程的思想,同时也体现了解耦的思想:只要接口不变,下层方法有任何变化,都不会影响上层方法。

./images

# #3、ServletContext 接口

# #①简介

  • 代表:整个 Web 应用
  • 是否单例:是
  • 典型的功能:
    • 获取某个资源的真实路径:getRealPath ()
    • 获取整个 Web 应用级别的初始化参数:getInitParameter ()
    • 作为 Web 应用范围的域对象
      • 存入数据:setAttribute ()
      • 取出数据:getAttribute ()

# #②体验

# #[1] 配置 Web 应用级别的初始化参数

<!-- 配置Web应用的初始化参数 -->
<context-param>
    <param-name>handsomeMan</param-name>
    <param-value>alsoMe</param-value>
</context-param>

# #[2] 获取参数

String handsomeMan = servletContext.getInitParameter("handsomeMan");
System.out.println("handsomeMan = " + handsomeMan);

# Servlet 继承关系

# #1、提出问题

  • 为什么 IDEA 创建 Servlet 之后不再实现 Servlet 接口而是继承 HttpServlet 类
  • Servlet 接口 HttpServlet 类有什么关系?
  • doXxx () 方法 service () 方法有什么关系?

# #2、类型关系

./imagesimage-20220129153641023

# #3、方法关系

image-20220131134357706

./imagesimage-20220131141539484

image-20220131143035519

  • 也就是说 Servlet 总接口,然后 GenericServlet 抽象类实现这个接口一部分功能 (不包括 service)

  • 然后我们的 HTTPServlet 抽象类继承 GenericServlet 抽象类,在我们 HTTPServlet 里面,service 方法不再是抽象的

  • 但是我们 HTTPServlet 里面的 service 方法只是会接收请求然后会按照请求是什么__Method 而去调用对应的自己有的 doMethod 方法 (比如说 doPost)__, 而这个 HTTPServlet 里面这些 doMethod 方法只是会报错 (405 错误)

  • 所以我们需要自己创建 HTTPServlet 的子类,然后在这个子类 (可以叫任何名, 就是需要在 web.xml 里面配置好名字对应的请求地址等等等) 里面我们重写这个 doMethod (不管是 doPost 还是 doGet 还是啥的), 或者直接重写__service 方法__方法

  • 一旦前端有请求 (ajax, 表单,等等等) 过来,tomcat 就会帮我们调用我们的 service 方法,然后发现是什么 Method 然后会调用我们创建的 HTTPServlet 的子类里面的 doMethod 方法,然后那个方法里面我们可以做各种操作.

  • 这些方法其实都是 tomcat 帮我们调用的,而 servlet 的生命周期也是 tomcat 帮忙维护的


# 动态 Web 工程内编写路径

# #1、为什么要写路径?

./images

  • 整个系统要根据功能拆分成许许多多独立资源
  • 资源之间既要完成自身的功能又要和其他资源配合
  • 写路径就是为了从一个资源跳转到下一个资源

# #2、为什么写路径这事有点复杂?

# #①先开发再部署

./images

  • 工程目录:我们写代码的地方,但是在服务器上运行的不是这个。
  • 部署目录:经过 Java 源文件编译目录重组后,IDEA 就替我们准备好了可以在服务器上运行的部署目录。
  • 区别:因为从工程目录到部署目录经过了目录重组,所以它们的目录结构是不同的。
  • 基准:用户通过浏览器访问服务器,而服务器上运行的是部署目录,所以写路径的时候参考部署目录而不是工程目录。
  • 对应关系工程目录下的 web 目录对应部署目录的根目录,同时部署目录的根目录也是路径中的 Web 应用根目录

./images

# #②路径的各个组成部分

从最前面一直到 Web 应用名称这里都是固定写法,到资源名这里要看具体是什么资源。

./images

# #[1] 具体文件

我们写代码的时候都是在工程目录下操作,所以参照工程目录来说最方便。按照工程目录的目录结构来说,从 web 目录开始按照实际目录结构写就好了(不包括 web 目录本身)。

./images

# #[2]Servlet

访问 Servlet 的路径是我们在 web.xml 中配置的,大家可能注意到了,url-pattern 里面的路径我们也是斜杠开头的,但是这个开头的斜杠代表 Web 应用根目录

./images

同样是开头的斜杠,超链接路径中的开头斜杠代表服务器根目录,Servlet 地址开头的斜杠,代表 Web 应用根目录,怎么记呢?请看下面的准则:

# #3、准则

用通俗的大白话来解释:一个路径由谁来解析,其实就是这个路径是谁来用。

路径类型 解析方式
由浏览器解析的路径 开头斜杠代表服务器根目录
由服务器解析的路径 开头斜杠代表 Web 应用根目录

./images

那么具体来说,哪些路径是浏览器解析的,哪些路径是服务器解析的呢?

  • 浏览器解析的路径举例:
    • 所有 HTML 标签中的路径
    • 重定向过程中指定的路径
  • 服务器解析的路径举例:
    • 所有 web.xml 中配置的路径
    • 请求转发过程中指定的路径

# #4、写路径的步骤

./images

# #5、动态获取上下文路径

# #①上下文路径的概念

上下文路径(context path)=/Web 应用名称

# #②动态获取

由于项目部署的时候,上下文路径是可以变化的,所以写死有可能发生错误。此时我们通过 request 对象动态获取上下文路径就不用担心这个问题了。调用下面这个方法,每一次获取的都是当前环境下实际的上下文路径的值。

request.getContextPath()

如果本节让你感觉很复杂,建议你放慢节奏,尝试下面的步骤:

  • 第一步:先弄清楚每个『名词概念』,清楚的知道我们提到的每一个名词指的是什么。
  • 第二步:弄清楚底层运行原理,其实就是工程目录和部署目录的区别。
  • 第三步:按照我们介绍的步骤一步一步慢慢写,写一步想一想,弄清楚各个部分的对应关系。

# 请求转发和重定向

# #1、接力

发一个请求给 Servlet,接力棒就传递到了 Servlet 手中。而绝大部分情况下,Servlet 不能独自完成一切,需要把接力棒继续传递下去,此时我们就需要请求的 **『转发』『重定向』**。

# #2、转发

image-20220201045826298

image-20220201050237020

本质:转交

完整定义:在请求的处理过程中,Servlet 完成了自己的任务,需要把请求转交给下一个资源继续处理。

./images

代码:

request.getRequestDispatcher("/fruit/apple/red/sweet/big.html").forward(request, response);

类比:

代码 类比
request 小货车
getRequestDispatcher (“转发地址”) 告诉司机要去哪
forward(request, response) 出发

关键:由于转发操作的核心部分是在服务器端完成的,所以浏览器感知不到,整个过程中浏览器只发送一次请求

./images

# #3、重定向

image-20220201050043111

image-20220201050344668

本质:一种特殊的响应

完整定义:在请求的处理过程中,Servlet 完成了自己的任务,然后以一个响应的方式告诉浏览器:“要完成这个任务还需要你另外再访问下一个资源”。

./images

代码:

response.sendRedirect("/app/fruit/apple/red/sweet/big.html");

./images

关键:由于重定向操作的核心部分是在浏览器端完成的,所以整个过程中浏览器共发送两次请求

image-20220201051726892

上方我们把用 dao 层的 java 类获取到的数据库的数据给存进当前这个 session 里面当做保存作用域了

注意上方我们用的重定向可能会替换成 thymeleaf 相关的写法 -> 应为我们需要在 index.html 与取出的数据做结合

# #4、对比

转发 重定向
一次请求 两次请求
浏览器地址栏显示的是第一个资源的地址 浏览器地址栏显示的是第二个资源的地址
全程使用的是同一个 request 对象 全程使用的是不同的 request 对象
在服务器端完成 在浏览器端完成
目标资源地址由服务器解析 目标资源地址由浏览器解析
目标资源可以在 WEB-INF 目录下 目标资源不能在 WEB-INF 目录下
目标资源仅限于本应用内部 目标资源可以是外部资源

image-20220201050747801

# #5、转发和重定向的应用场景

可以简单的判断:能用转发的先用转发,如果转发不行,再使用重定向。

  • 需要通过同一个 request 对象把数据携带到目标资源:只能用转发
  • 如果希望前往下一个资源之后,浏览器刷新访问的是第二个资源:只能用重定向

# 获取请求参数

# #1、请求参数的概念

浏览器在给服务器发送请求的同时,携带的参数数据。

# #2、浏览器端发送请求参数的基本形式

  • URL 地址后面附着的请求参数

/orange/CharacterServlet?username = 汤姆

  • 表单
  • Ajax 请求(将来会学到)

# #3、服务器端对请求参数的封装

总体上来说,服务器端将请求参数封装为 Map<String, String []>

  • 键:请求参数的名字
  • 值:请求参数的值组成的数组

# #4、获取请求参数的方法

方法名 返回值类型
request.getParameterMap() Map<String, String[]>
request.getParameter (“请求参数的名字”) String
request.getParameterValues (“请求参数的名字”) String []
request.getParameterNames() Enumeration

# #5、测试

# #①HTML 代码

<!-- 测试请求参数的表单 -->
<form action="/orange/ParamServlet" method="post">

    <!-- 单行文本框 -->
    <!-- input标签配合type="text"属性生成单行文本框 -->
    <!-- name属性定义的是请求参数的名字 -->
    <!-- 如果设置了value属性,那么这个值就是单行文本框的默认值 -->
    个性签名:<input type="text" name="signal" value="单行文本框的默认值" /><br/>

    <!-- 密码框 -->
    <!-- input标签配合type="password"属性生成密码框 -->
    <!-- 用户在密码框中填写的内容不会被一明文形式显示 -->
    密码:<input type="password" name="secret" /><br/>

    <!-- 单选框 -->
    <!-- input标签配合type="radio"属性生成单选框 -->
    <!-- name属性一致的radio会被浏览器识别为同一组单选框,同一组内只能选择一个 -->
    <!-- 提交表单后,真正发送给服务器的是name属性和value属性的值 -->
    <!-- 使用checked="checked"属性设置默认被选中 -->
    请选择你最喜欢的季节:
    <input type="radio" name="season" value="spring" />春天
    <input type="radio" name="season" value="summer" checked="checked" />夏天
    <input type="radio" name="season" value="autumn" />秋天
    <input type="radio" name="season" value="winter" />冬天

    <br/><br/>

    你最喜欢的动物是:
    <input type="radio" name="animal" value="tiger" />路虎
    <input type="radio" name="animal" value="horse" checked="checked" />宝马
    <input type="radio" name="animal" value="cheetah" />捷豹

    <br/>

    <!-- 多选框 -->
    <!-- input标签和type="checkbox"配合生成多选框 -->
    <!-- 多选框被用户选择多个并提交表单后会产生『一个名字携带多个值』的情况 -->
    你最喜欢的球队是:
    <input type="checkbox" name="team" value="Brazil"/>巴西
    <input type="checkbox" name="team" value="German" checked="checked"/>德国
    <input type="checkbox" name="team" value="France"/>法国
    <input type="checkbox" name="team" value="China" checked="checked"/>中国
    <input type="checkbox" name="team" value="Italian"/>意大利

    <br/>

    <!-- 下拉列表 -->
    <!-- 使用select标签定义下拉列表整体,在select标签内设置name属性 -->
    你最喜欢的运动是:
    <select name="sport">
        <!-- 使用option属性定义下拉列表的列表项 -->
        <!-- 使用option标签的value属性设置提交给服务器的值,在option标签的标签体中设置给用户看的值 -->
        <option value="swimming">游泳</option>
        <option value="running">跑步</option>

        <!-- 使用option标签的selected="selected"属性设置这个列表项默认被选中 -->
        <option value="shooting" selected="selected">射击</option>
        <option value="skating">溜冰</option>
    </select>

    <br/>

    <br/><br/>

    <!-- 表单隐藏域 -->
    <!-- input标签和type="hidden"配合生成表单隐藏域 -->
    <!-- 表单隐藏域在页面上不会有任何显示,用来保存要提交到服务器但是又不想让用户看到的数据 -->
    <input type="hidden" name="userId" value="234654745" />

    <!-- 多行文本框 -->
    自我介绍:<textarea name="desc">多行文本框的默认值</textarea>

    <br/>

    <!-- 普通按钮 -->
    <button type="button">普通按钮</button>

    <!-- 重置按钮 -->
    <button type="reset">重置按钮</button>

    <!-- 表单提交按钮 -->
    <button type="submit">提交按钮</button>
</form>

# #②Java 代码

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 获取包含全部请求参数的Map
    Map<String, String[]> parameterMap = request.getParameterMap();

    // 遍历这个包含全部请求参数的Map
    Set<String> keySet = parameterMap.keySet();

    for (String key : keySet) {

        String[] values = parameterMap.get(key);

        System.out.println(key + "=" + Arrays.asList(values));
    }

    System.out.println("---------------------------");

    // 根据请求参数名称获取指定的请求参数值
    // getParameter()方法:获取单选框的请求参数
    String season = request.getParameter("season");
    System.out.println("season = " + season);

    // getParameter()方法:获取多选框的请求参数
    // 只能获取到多个值中的第一个
    String team = request.getParameter("team");
    System.out.println("team = " + team);

    // getParameterValues()方法:取单选框的请求参数
    String[] seasons = request.getParameterValues("season");
    System.out.println("Arrays.asList(seasons) = " + Arrays.asList(seasons));

    // getParameterValues()方法:取多选框的请求参数
    String[] teams = request.getParameterValues("team");
    System.out.println("Arrays.asList(teams) = " + Arrays.asList(teams));

}

# 请求响应设置字符集

# #1、请求

image-20220127174030998

需要设置好编码,不然接收到的中文可能会被解读成乱码

image-20220127174326155

# #①GET 请求

# #[1] 设置之前

发送请求参数:

<a href="/orange/CharacterServlet?username=汤姆">测试请求字符集设置:GET请求</a>

接收到的数据:

username = ?±¤?§?

# #[2] 设置方式

./images

在 server.xml 第 71 行的 Connector 标签中增加 URIEncoding 属性:

<Connector port="8080" protocol="HTTP/1.1"
		   connectionTimeout="20000"
		   redirectPort="8443" 
		   URIEncoding="UTF-8"
		   />

重启 Tomcat 实例即可。再重新测试的结果是:

username = 汤姆

# #②POST 请求

# #[1] 设置之前

发送请求参数:

<!-- 测试请求字符集设置:POST请求 -->
<form action="/orange/CharacterServlet" method="post">

    用户名:<input type="text" name="username" /><br/>

    <button type="submit">保存</button>

</form>

接收到的数据:

username = ???

# #[2] 设置方式

// 使用request对象设置字符集
request.setCharacterEncoding("UTF-8");

// 获取请求参数
String username = request.getParameter("username");

System.out.println("username = " + username);

接收到的数据:

username = 林志玲

# #[3] 需要注意的问题

不能在设置字符集之前获取请求参数!下面是错误的示范

// 获取请求参数(先获取请求参数会导致设置字符集失效)
String username = request.getParameter("username");

// 使用request对象设置字符集
request.setCharacterEncoding("UTF-8");

System.out.println("username = " + username);

# #2、响应

# #①设置之前

服务器端代码:

PrintWriter writer = response.getWriter();

writer.write("志玲姐姐你好!");

浏览器显示:

???

# #②设置方式一

编码字符集和解码字符集一致

// 设置服务器端的编码字符集
response.setCharacterEncoding("UTF-8");

PrintWriter writer = response.getWriter();

writer.write("<!DOCTYPE html>                  ");
writer.write("<html>                           ");
writer.write("<head>                           ");
writer.write("<!-- 设置浏览器端的解码字符集 -->");
writer.write("    <meta charset='UTF-8'>       ");
writer.write("    <title>Title</title>         ");
writer.write("</head>                          ");
writer.write("<body>                           ");
writer.write("<p>志玲姐姐你好!</p>            ");
writer.write("</body>                          ");
writer.write("</html>                          ");

# #③设置方式二

response.setContentType("text/html;charset=UTF-8");

# #④需要注意的问题

response.getWriter () 不能出现在设置字符集操作的前面(两种方式都不行)。


# 保存作用域

image-20220201061846332

# request 一次请求相应范围

image-20220201060437326

image-20220201060830084

# session 一次会话范围有效

只要是 sessionId 对了就能取到之前在这个有着这个 sessionId 的 session 里面设置的 key and value

# application (ServletContext) 一次应用程序范围有效

image-20220201061825003

都可以获取到,因为都是一个应用程序,都是 public 的

但是 tomcat 停止了那就获取不到了


# MVC

# #1、提出问题

./images

我们对 HTML 的新的期待:既能够正常显示页面,又能在页面中包含动态数据部分。而动态数据单靠 HTML 本身是无法做到的,所以此时我们需要引入服务器端动态视图模板技术。

image-20220201213345098

image-20220201214049914

# #2、从三层结构到 MVC

# #①MVC 概念

M:Model 模型

V:View 视图

C:Controller 控制器

MVC 是在表述层开发中运用的一种设计理念。主张把封装数据的『模型』显示用户界面的『视图』、** 协调调度的『控制器』** 分开。

好处:

  • 进一步实现各个组件之间的解耦
  • 让各个组件可以单独维护
  • 将视图分离出来以后,我们后端工程师和前端工程师的对接更方便

# #②MVC 和三层架构之间关系

./images

# #3、前后端工程师对接方式

  • 服务器端渲染:前端工程师把前端页面一整套做好交给后端工程师
  • 前后端分离:开会商量 JSON 格式,然后分头开发。在后端程序尚不可用时,前端工程师会使用 Mock.js 生成假数据使用,在后端程序可用后再连接实际后端程序获取真实数据。

查看详细内容 (opens new window)

./images


# IOC

image-20220202174916178

就是现在又是 controller 层,service 层,dao 层,互相之间有依赖,不想这样

  • 我们可以创建一个接口 BeanFactory, 有一个方法叫做 getBeanByID, 然后我们可以写个 xml 配置文件,把每一个 (controller 层,service 层,dao 层的对象都和一个 ID 进行匹对成为一个个 bean 标签 (在配置文件 xml 里面)).

image-20220202164839794

  • 我们接着写个这个接口的实现类,在这类的 getBeanByID 方法里面我们可以读取那个 xml 配置文件 (io 流) 然后转换各种 api 转换成一个个 document 对象,接着获取每个节点,反正最终目的就是将我们的 (每一个!) 一个个 ID 和对应 ID 的那个类 (不管是 controller 层,service 层,dao 层的对象) 给实例化出来,把他们放进一个 hashmap 里面,ID 作为 key, 实例化出来的对象为 value. 之后 return 那个 hashmap.get (传进来的 ID)
  • 现在,比如说 DispatcherServlet 类里面需要获取到一个 controller 对象,就可以从 request.getServletPath, 然后各种 api 获取到想要访问的组件 (具体哪个 controller 类)(这只是一个 string 类型的字符串), 有了这个 controller 类我们 (一般都是在 xml 里面配置都是用这个 controller 类的名字) 拿他的名字作为 ID 取出对应的已经实例化好的存在了 BeanFactory 的实现类里面的那个 hashmap 里面了。这个取出来的就是我们想要的类型 (也就是那个类的类型), 然后用反射获取所有方法,看请求参数是什么方法,看请求传了什么其他参数,然后将这些参数整合作为参数传入且调用那个多出来的能类型的实例化对象里面的对应参数的方法.

注意!controller, 以前还没用 DispatcherServlet 的时候还是一个个 servlet 类,代表每个组件,现在则改为 controller,(DispatcherServlet 主要用来按照 request 请求哪个组件,请求参数 (之后传给那个想要用的 controller 里面想要用的方法传这些参数) 有哪些,想调用这个 controller 什么方法,这些都是通过 request 请求,或者 session 绑定的信息,或者我们 servletConfig/servletContext 以及配置文件的设置实现的,然后负责反射创造出来那个想要的 controller, 然后反射调用对应方法传参数). 然后每一个 controller 就是用来接收请求,具体业务还是要靠 service 层,service 依赖 dao 层做出操作.

但是现在比如说我们在一个 controller 类里面,这里面需要用到 service 层对象,但我们这里就不像在 DispatcherServlet 类里面可以从 request 访问哪个 service 层,因为这跟 request 无关,并不是像 controller 一样按照访问的具体 XXX.do 等等,DispatcherServlet 知道了是 XXX 然后就会调用 (分配给) XXX controller.

所以我们还需要再那个配置文件 xml 中把依赖关系搞好:

image-20220202170329988

上方 controller 因为需要用到 service 层所以作为属性标签 (property) 包括进去

service 层因为需要用到 dao 层所以作为属性标签 (property) 包括进去

注意 ref 对应的 id 值是一模一样

然后现在在实现 BeanFactory 的那个实现类里面需要将依赖添加好,因为我们之前只是把每个 ID 和对应类实例化出来一个然后存进了一个 hashmap 里面

组装 bean 之间的关系:

我们可以通过各种判断读取每个 bean 标签有没有这个 property 标签

如果有我们就可以读取这个 property 标签的 ref 的值,这时候再从 hashmap 里面取出这个 ref 的值对应的 ID 对应的实例化对象.(因为这个操作是在已经把所有 bean 标签的 ID 存的值和 class 存的值 (实例化出来) 存入了 hashmap, 然后才开始组装关系,所以可以取到.)

现在因为我们比如说 fruitController 需要用到 fruitService, 所以我们现在有了从 hashmap 取出的 fruitService 实例,我们接着可以从 hashmap 找出这个 fruitController, 然后通过反射把 fruitService 设置为 fruitController 里面的一个成员 (也就是我们上面 xml 写的 property 标签里面的 name 属性的值) 的值,这样我们 fruitController 里面的那个成员就存的是这个 fruitService 的实例化对象了

这样,虽然我们 fruitController 写的那个本来是 new fruitService () 的那个成员现在可以设为等于 null 了 (需要设为 null 不然编译都不通过).

注意我们需要在 DispatcherServlet 里面的 init 初始化的时候,就 BeanFacotry bf = new 那个实现类

这么做就可以让我们所有那些类都会被实例化,然后里面比如说 fruitController 实例化对象里面有个变量叫做 X 本来存的应该是一个 fruitService 的实例化对象,不过现在在 fruitController 的类里面是 null, 不过其实在运行时,因为在 DispatcherServlet 里面的 init 里面创造了那个 BeanFacotry 的实现类,也就是说那个实现类的构造函数会触发,那个实现类的构造函数里面就包含了把解析 xml 配置文件,把每个 bean 的 ID 和对应的实例化对象存入 hashmap 里面, 并且把每个需要依赖其他层的都设置好了 (比如说 fruitController 里面的 X 变量的值设为了我们 hashmap 里面存的那个 fruitService 对象), 这个样子之后执行 fruitController 里面的代码都是通过从那个 hashmap 取出那个实例化出来的对象,而那个实例化 fruitController 对象里面的 X 变量正是 fruitService 实例化对象,而不是 null, 所以其他操作可以正常运行.

这么做的原因就是只有解耦,主要还是看那个配置文件里面写的值.

控制反转 (IOC->Inversion of Control)

image-20220202175741945

依赖注入 (DI->Dependency Injection)

image-20220202175954665

大复习 (MVC 等等等)

看 https://www.bilibili.com/video/BV1AS4y177xJ?p=55

image-20220202181843744

# 加餐 最凝练的 CRUD

# #1、建模

# #①物理建模

CREATE DATABASE `view-demo`CHARACTER SET utf8;
USE `view-demo`;
CREATE TABLE t_soldier(
    soldier_id INT PRIMARY KEY AUTO_INCREMENT,
    soldier_name CHAR(100),
    soldier_weapon CHAR(100)
);

# #②逻辑建模

public class Soldier {
    
    private Integer soldierId;
    private String soldierName;
    private String soldierWeapon;

# #2、总体架构

./images

# #3、搭建持久化层所需环境

# #①导入 jar 包

commons-dbutils-1.6.jar druid-1.1.9.jar hamcrest-core-1.3.jar junit-4.12.jar mysql-connector-java-5.1.37-bin.jar

# #②创建 jdbc.properties

维护基本连接信息

driverClassName=com.mysql.jdbc.Driver
url=jdbc:mysql://192.168.198.100:3306/view-demo
username=root
password=atguigu
initialSize=10
maxActive=20
maxWait=10000

# #③创建 JDBCUtils 工具类

import com.alibaba.druid.pool.DruidDataSourceFactory;

import javax.sql.DataSource;
import java.io.InputStream;
import java.sql.Connection;
import java.sql.SQLException;
import java.util.Properties;

public class JDBCUtil {

    // 将数据源对象设置为静态属性,保证大对象的单一实例
    private static DataSource dataSource;

    static {

        // 1.创建一个用于存储外部属性文件信息的Properties对象
        Properties properties = new Properties();

        // 2.使用当前类的类加载器加载外部属性文件:jdbc.properties
        InputStream inputStream = JDBCUtil.class.getClassLoader().getResourceAsStream("jdbc.properties");

        try {

            // 3.将外部属性文件jdbc.properties中的数据加载到properties对象中
            properties.load(inputStream);

            // 4.创建数据源对象
            dataSource = DruidDataSourceFactory.createDataSource(properties);

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

    /**
     * 从数据源中获取数据库连接
     * @return 数据库连接对象
     */
    public static Connection getConnection() {

        Connection connection = null;

        try {
            connection = dataSource.getConnection();
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);
        }

        return connection;

    }

    /**
     * 释放数据库连接
     * @param connection 要执行释放操作的连接对象
     */
    public static void releaseConnection(Connection connection) {

        if (connection != null) {

            try {
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();

                throw new RuntimeException(e);
            }

        }

    }
    
}

测试能否正常连接数据库:

public class DemoTest {

    @Test
    public void testConnection() {
        Connection connection = JDBCUtil.getConnection();
        System.out.println("connection = " + connection);
    }

}

# #④BaseDao

public class BaseDao<T> {

    private QueryRunner queryRunner = new QueryRunner();

    /**
     * 通用的增删改方法
     * @param sql
     * @param param
     * @return
     */
    public int update(String sql, Object ... param) {

        Connection connection = JDBCUtil.getConnection();

        int count = 0;
        try {
            count = queryRunner.update(connection, sql, param);
        } catch (SQLException e) {
            e.printStackTrace();

            throw new RuntimeException(e);

        } finally {
            
            // 关闭数据库连接
            JDBCUtil.releaseConnection(connection);
            
        }

        return count;
    }

    /**
     * 查询单个对象的通用方法
     * @param clazz
     * @param sql
     * @param param
     * @return
     */
    public T getBean(Class<T> clazz, String sql, Object ... param) {

        Connection connection = JDBCUtil.getConnection();

        T bean = null;
        try {
            bean = queryRunner.query(connection, sql, new BeanHandler<>(clazz), param);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            
            // 关闭数据库连接
            JDBCUtil.releaseConnection(connection);
            
        }

        return bean;
    }

    /**
     * 查询集合对象的通用方法
     * @param clazz
     * @param sql
     * @param param
     * @return
     */
    public List<T> getBeanList(Class<T> clazz, String sql, Object ... param) {

        Connection connection = JDBCUtil.getConnection();

        List<T> beanList = null;

        try {
            beanList = queryRunner.query(connection, sql, new BeanListHandler<>(clazz), param);
        } catch (SQLException e) {
            e.printStackTrace();
            throw new RuntimeException(e);
        } finally {
            
            // 关闭数据库连接
            JDBCUtil.releaseConnection(connection);
            
        }

        return beanList;
    }
}

# #4、搭建表述层所需环境

本质上就是 Thymeleaf 所需要的环境

# #①导入 jar 包

attoparser-2.0.5.RELEASE.jar javassist-3.20.0-GA.jar log4j-1.2.15.jar ognl-3.1.26.jar slf4j-api-1.7.25.jar slf4j-log4j12-1.7.25.jar thymeleaf-3.0.12.RELEASE.jar unbescape-1.1.6.RELEASE.jar

# #②创建 ViewBaseServlet

import org.thymeleaf.TemplateEngine;
import org.thymeleaf.context.WebContext;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;

import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class ViewBaseServlet extends HttpServlet {

    private TemplateEngine templateEngine;

    @Override
    public void init() throws ServletException {

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

        // 2.创建Thymeleaf解析器对象
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(servletContext);

        // 3.给解析器对象设置参数
        // ①HTML是默认模式,明确设置是为了代码更容易理解
        templateResolver.setTemplateMode(TemplateMode.HTML);

        // ②设置前缀
        String viewPrefix = servletContext.getInitParameter("view-prefix");

        templateResolver.setPrefix(viewPrefix);

        // ③设置后缀
        String viewSuffix = servletContext.getInitParameter("view-suffix");

        templateResolver.setSuffix(viewSuffix);

        // ④设置缓存过期时间(毫秒)
        templateResolver.setCacheTTLMs(60000L);

        // ⑤设置是否缓存
        templateResolver.setCacheable(true);

        // ⑥设置服务器端编码方式
        templateResolver.setCharacterEncoding("utf-8");

        // 4.创建模板引擎对象
        templateEngine = new TemplateEngine();

        // 5.给模板引擎对象设置模板解析器
        templateEngine.setTemplateResolver(templateResolver);

    }

    protected void processTemplate(String templateName, HttpServletRequest req, HttpServletResponse resp) throws IOException {
        // 1.设置响应体内容类型和字符集
        resp.setContentType("text/html;charset=UTF-8");

        // 2.创建WebContext对象
        WebContext webContext = new WebContext(req, resp, getServletContext());

        // 3.处理模板数据
        templateEngine.process(templateName, webContext, resp.getWriter());
    }
}

# #③配置 web.xml

<!-- 在上下文参数中配置视图前缀和视图后缀 -->
<context-param>
    <param-name>view-prefix</param-name>
    <param-value>/WEB-INF/view/</param-value>
</context-param>
<context-param>
    <param-name>view-suffix</param-name>
    <param-value>.html</param-value>
</context-param>

# #④创建 view 目录

./images

# #5、功能清单

  • 显示首页:浏览器通过 index.html 访问首页 Servlet,然后再解析对应的模板视图
  • 显示列表:在首页点击超链接,跳转到目标页面把所有士兵的信息列表显示出来
  • 删除信息:在列表上点击删除超链接,执行信息的删除操作
  • 新增信息:
    • 在列表页面点击超链接跳转到新增士兵信息的表单页面
    • 在新增信息的表单页面点击提交按钮执行保存
  • 更新信息:
    • 在列表上点击更新超链接,跳转到更新士兵信息的表单页面:表单回显
    • 在更新信息的表单页面点击提交按钮执行更新

# #6、显示首页

# #①目标

浏览器访问 index.html,通过首页 Servlet,渲染视图,显示首页。

# #②思路

./images

# #③代码

# #[1] 创建 PortalServlet

<servlet>
    <servlet-name>PortalServlet</servlet-name>
    <servlet-class>com.atguigu.demo.servlet.PortalServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>PortalServlet</servlet-name>
    <url-pattern>/index.html</url-pattern>
</servlet-mapping>

Servlet 代码:

public class PortalServlet extends ViewBaseServlet {

    @Override
    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String viewName = "portal";

        super.processTemplate(viewName, request, response);

    }

    @Override
    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    }
}

# #[2] 创建 portal.html

./images

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>士兵信息管理系统</title>
</head>
<body>

    <a th:href="@{/SoldierServlet?method=showList}">显示士兵信息列表</a>

</body>
</html>

# #7、显示列表

# #①目标

在目标页面显示所有士兵信息,士兵信息是从数据库查询出来的

# #②思路

./images

# #③代码

# #[1]ModelBaseServlet

创建这个基类的原因是:我们希望每一个模块能够对应同一个 Servlet,这个模块所需要调用的所有方法都集中在同一个 Servlet 中。如果没有这个 ModelBaseServlet 基类,我们 doGet ()、doPost () 方法可以用来处理请求,这样一来,每一个方法都需要专门创建一个 Servlet(就好比咱们之前的 LoginServlet、RegisterServlet 其实都应该合并为 UserServlet)。

public class ModelBaseServlet extends ViewBaseServlet {

    protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 在doGet()方法中调用doPost()方法,这样就可以在doPost()方法中集中处理所有请求
        doPost(request, response);
    }

    protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 1.从请求参数中获取method对应的数据
        String method = request.getParameter("method");

        // 2.通过反射调用method对应的方法
        // ①获取Class对象
        Class<? extends ModelBaseServlet> clazz = this.getClass();

        try {
            // ②获取method对应的Method对象
            Method methodObject = clazz.getDeclaredMethod(method, HttpServletRequest.class, HttpServletResponse.class);
            
            // ③打开访问权限
            methodObject.setAccessible(true);
            
            // ④通过Method对象调用目标方法
            methodObject.invoke(this, request, response);
        } catch (Exception e) {
            e.printStackTrace();
            
            throw new RuntimeException(e);
        }
    }

}

# #[2]SoldierDao.selectSoldierList()

./images

./images

接口方法:

public interface SoldierDao {

    List<Soldier> selectSoldierList();

}

实现类方法:

public class SoldierDaoImpl extends BaseDao<Soldier> implements SoldierDao {
    @Override
    public List<Soldier> selectSoldierList() {
        
        String sql = "select soldier_id soldierId,soldier_name soldierName,soldier_weapon soldierWeapon from t_soldier";
        
        return getBeanList(Soldier.class, sql);
    }
}

# #[3]SoldierService.getSoldierList()

./images

接口方法:

public interface SoldierService {

    List<Soldier> getSoldierList();

}

实现类方法:

public class SoldierServiceImpl implements SoldierService {

    private SoldierDao soldierDao = new SoldierDaoImpl();

    @Override
    public List<Soldier> getSoldierList() {

        List<Soldier> soldierList = soldierDao.selectSoldierList();

        return soldierList;
    }
}

# #[4]SoldierServlet.showList()

protected void showList(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.调用Service方法获取集合数据
    List<Soldier> soldierList = soldierService.getSoldierList();

    // 2.将集合数据存入请求域
    request.setAttribute("soldierList", soldierList);

    // 3.渲染视图(在渲染的过程中,已经包含了转发)
    processTemplate("list", request, response);
}

# #8、删除功能

# #①目标

点击页面上的超链接,把数据库表中的记录删除。

# #②思路

# #[1] 先不考虑后续

./images

# #[2] 加上后续返回响应页面

./images

# #③代码

# #[1] 完成删除超链接

./images

<a th:href="@{/SoldierServlet(soldierId=${soldier.soldierId},method='remove')}">删除</a>

关于 @{地址} 附加请求参数的语法格式:

  • 只有一个请求参数:@{地址 (请求参数名 = 普通字符串)} 或 @{地址 (请求参数名 =${需要解析的表达式})}
  • 多个请求参数:@

官方文档中的说明如下:

./images

# #[2] Servlet 方法

protected void remove(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数中获取士兵信息的id值
    String soldierId = request.getParameter("soldierId");

    // 2.调用Service方法执行删除操作
    soldierService.remove(soldierId);

    // 3.后续……
    // 方案一:还是直接前往list.html,需要重新查询soldierList数据,代码重复
    // 1.调用Service方法获取集合数据
    // List<Soldier> soldierList = soldierService.getSoldierList();

    // 2.将集合数据存入请求域
    // request.setAttribute("soldierList", soldierList);

    // processTemplate("list", request, response);

    // 方案二:直接调用隔壁的showList()
    // 也能实现需求,但是总感觉这样调用方法破坏了程序的结构
    // showList(request, response);

    // 方案三:通过请求转发的方式间接调用showList()方法
    // request.getRequestDispatcher("/SoldierServlet?method=showList").forward(request, response);

    // 方案四:通过请求重定向的方式间接调用showList()方法
    response.sendRedirect(request.getContextPath() + "/SoldierServlet?method=showList");
}

# #[3] Service 方法

@Override
public void remove(String soldierId) {
    soldierDao.delete(soldierId);
}

# #[4] Dao 方法

@Override
public void delete(String soldierId) {
    String sql = "delete from t_soldier where soldier_id=?";

    update(sql, soldierId);
}

# #9、前往新增信息的表单页面

# #①创建超链接

<a th:href="@{/SoldierServlet?method=toAddPage}">前往新增页面</a>

# #②Servlet

protected void toAddPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    processTemplate("add-page", request, response);
}

# #③创建表单页面

./images

<form th:action="@{/SoldierServlet}" method="post">

    <input type="hidden" name="method" value="saveSoldier" />

    士兵姓名:<input type="text" name="soldierName" /><br/>
    士兵武器:<input type="text" name="soldierWeapon" /><br/>

    <button type="submit">保存</button>

</form>

# #10、执行保存

# #①目标

提交表单后,将表单数据封装为 Soldier 对象,然后将 Soldier 对象保存到数据库。

# #②思路

./images

# #③代码

# #[1] Servlet 方法

protected void saveSoldier(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    
    // 1.获取请求参数
    String soldierName = request.getParameter("soldierName");
    String soldierWeapon = request.getParameter("soldierWeapon");
    
    // 2.创建Soldier对象
    Soldier soldier = new Soldier(null, soldierName, soldierWeapon);

    // 3.调用Service方法
    soldierService.saveSoldier(soldier);

    // 4.重定向请求
    response.sendRedirect(request.getContextPath() + "/SoldierServlet?method=showList");
}

# #[2] Service 方法

@Override
public void saveSoldier(Soldier soldier) {

    soldierDao.insertSoldier(soldier);

}

# #[3] Dao 方法

@Override
public void insertSoldier(Soldier soldier) {

    String sql = "insert into t_soldier(soldier_name,soldier_weapon) values(?,?)";

    update(sql, soldier.getSoldierName(), soldier.getSoldierWeapon());
}

# #11、前往修改信息的表单页面

./images

# #①创建超链接

<a th:href="@{/SoldierServlet(soldierId=${soldier.soldierId},method=toEditPage)}">编辑</a>

# #②Servlet 方法

protected void toEditPage(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.从请求参数获取soldierId
    String soldierId = request.getParameter("soldierId");

    // 2.根据soldierId查询Soldier对象
    Soldier soldier = soldierService.getSoldierById(soldierId);

    // 3.将Soldier对象存入请求域
    request.setAttribute("soldier", soldier);

    // 4.前往更新的表单页面
    processTemplate("edit-page", request, response);

}

# #③Service 方法

@Override
public Soldier getSoldierById(String soldierId) {
    return soldierDao.selectSoldierByPrimaryKey(soldierId);
}

# #④Dao 方法

@Override
public Soldier selectSoldierByPrimaryKey(String soldierId) {
    String sql = "select soldier_id soldierId,soldier_name soldierName,soldier_weapon soldierWeapon from t_soldier where soldier_id=?";

    return getBean(Soldier.class, sql, soldierId);
}

# #⑤表单页面

<form th:action="@{/SoldierServlet}" method="post">

    <input type="hidden" name="method" value="updateSoldier" />
    <input type="hidden" name="soldierId" th:value="${soldier.soldierId}" />

    士兵姓名:<input type="text" name="soldierName" th:value="${soldier.soldierName}" /><br/>
    士兵武器:<input type="text" name="soldierWeapon" th:value="${soldier.soldierWeapon}" /><br/>

    <button type="submit">更新</button>

</form>

# #12、执行更新

./images

# #①Servlet 方法

protected void updateSoldier(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.获取请求参数
    String soldierIdOrigin = request.getParameter("soldierId");
    Integer soldierId = Integer.parseInt(soldierIdOrigin);
    String soldierName = request.getParameter("soldierName");
    String soldierWeapon = request.getParameter("soldierWeapon");

    // 2.封装对象
    Soldier soldier = new Soldier(soldierId, soldierName, soldierWeapon);

    // 3.调用Service方法执行更新
    soldierService.updateSoldier(soldier);

    // 4.重定向请求
    response.sendRedirect(request.getContextPath() + "/SoldierServlet?method=showList");
}

# #②Service 方法

@Override
public void updateSoldier(Soldier soldier) {

    soldierDao.updateSoldier(soldier);

}

# #③Dao 方法

@Override
public void updateSoldier(Soldier soldier) {
    String sql = "update t_soldier set soldier_name=?,soldier_weapon=? where soldier_id=?";
    update(sql, soldier.getSoldierName(), soldier.getSoldierWeapon(), soldier.getSoldierId());
}

# #13、请求字符集设置

  • 设置请求体字符集需要调用 request.setCharacterEncoding (“UTF-8”);
  • request.setCharacterEncoding (“UTF-8”); 要求在所有 request.getParameter () 前面
  • 在执行子类 Servlet 方法时,其实都是先调用父类 ModelBaseServlet 的 doPost () 方法
  • doPost () 方法中包含获取 method 请求参数的操作
  • 所以最前面的 request.getParameter () 在 doPost () 方法中
  • 所以 request.setCharacterEncoding (“UTF-8”); 要放在 doPost () 方法的 request.getParameter () 前面
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 0.在所有request.getParameter()前面设置解析请求体的字符集
    request.setCharacterEncoding("UTF-8");

    // 1.从请求参数中获取method对应的数据
    String method = request.getParameter("method");
    
    // ……

# 提出问题与解决方案核心代码

# #1、提出问题

./images

保持用户登录状态,背后的底层逻辑是:服务器在接收到用户请求的时候,有办法判断这个请求来自于之前的某一个用户。所以保持登录状态,本质上是保持 **『会话状态』**

# #2、解决方案

# #①结论

使用 HttpSession 对象,将数据存入会话域就能保持会话状态。

HttpSession session = request.getSession();
session.setAttribute("user", user);

# #②demo 体验

# #[1] 准备环境

参考会话控制 demo 原始纯净版

# #[2] 创建将数据存入会话域的 Servlet

public class HelloWorldServlet extends ModelBaseServlet {
    protected void setValue(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        // 1.通过request对象获取session对象
        HttpSession session = request.getSession();

        // 2.设置数据名称和数据的值
        String attrName = "sessionHelloAttrName";
        String attrValue = "sessionHelloAttrValue";

        // 3.将数据存入会话域
        session.setAttribute(attrName, attrValue);

        // 4.渲染视图
        processTemplate("page-target", request, response);
    }
}

# #[3] 在其他页面从会话域取出数据

./images

<div th:if="${#strings.isEmpty(session.sessionHelloAttrName)}">
    没有从会话域获取到数据
</div>
<div th:if="${not #strings.isEmpty(session.sessionHelloAttrName)}">
    <p>从会话域读取到:<span th:text="${session.sessionHelloAttrName}"></span></p>
</div>

# #[4] 操作效果

./images


# Cookie 的工作机制

# #1、HTTP 协议和会话控制

HTTP 协议本身是无状态的。单靠 HTTP 协议本身无法判断一个请求来自于哪一个浏览器,所以也就没法识别用户的身份状态。

./images

# #2、Cookie 介绍

# #①本质

  • 在浏览器端临时存储数据
  • 键值对
  • 键和值都是字符串类型
  • 数据量很小

# #②Cookie 在浏览器和服务器之间的传递

# #[1] 没有 Cookie 的状态

在服务器端没有创建 Cookie 并返回的情况下,浏览器端不会保存 Cookie 信息。双方在请求和响应的过程中也不会携带 Cookie 的数据。

# #[2] 创建 Cookie 对象并返回

// 1.创建Cookie对象
Cookie cookie = new Cookie("cookie-message", "hello-cookie");

// 2.将Cookie对象添加到响应中
response.addCookie(cookie);

// 3.返回响应
processTemplate("page-target", request, response);

# #[3] 服务器端返回 Cookie 的响应消息头

./images

# #[4] 浏览器拿到 Cookie 之后

浏览器拿到 Cookie 之后,以后的每一个请求都会携带 Cookie 信息。

./images

# #[5] 服务器端读取 Cookie 的信息

// 1.通过request对象获取Cookie的数组
Cookie[] cookies = request.getCookies();

// 2.遍历数组
for (Cookie cookie : cookies) {
    System.out.println("cookie.getName() = " + cookie.getName());
    System.out.println("cookie.getValue() = " + cookie.getValue());
    System.out.println();
}

# #③Cookie 时效性

# #[1] 理论

  • 会话级 Cookie
    • 服务器端并没有明确指定 Cookie 的存在时间
    • 在浏览器端,Cookie 数据存在于内存中
    • 只要浏览器还开着,Cookie 数据就一直都在
    • 浏览器关闭,内存中的 Cookie 数据就会被释放
  • 持久化 Cookie
    • 服务器端明确设置了 Cookie 的存在时间
    • 在浏览器端,Cookie 数据会被保存到硬盘上
    • Cookie 在硬盘上存在的时间根据服务器端限定的时间来管控,不受浏览器关闭的影响
    • 持久化 Cookie 到达了预设的时间会被释放

服务器端返回 Cookie 时附带过期时间的响应消息头如下:

./images

服务器通知浏览器删除 Cookie 时的响应消息头如下:

./images

# #[2] 代码

// ※给Cookie设置过期时间
// 正数:Cookie的过期时间,以秒为单位
// 负数:表示这个Cookie是会话级的Cookie,浏览器关闭时释放
// 0:通知浏览器立即删除这个Cookie
cookie.setMaxAge(20);

# #[3] 会话和持久化 Cookie 对比

./images

# #④Cookie 的 domain 和 path

上网时间长了,本地会保存很多 Cookie。对浏览器来说,访问互联网资源时不能每次都把所有 Cookie 带上。浏览器会使用 Cookie 的 domain 和 path 属性值来和当前访问的地址进行比较,从而决定是否携带这个 Cookie。

./images


# Session 的工作机制

image-20220201040731977

image-20220201041012003

# #1、文字描述

前提:浏览器正常访问服务器

  • 服务器端没调用 request.getSession () 方法:什么都不会发生
  • 服务器端调用了 request.getSession () 方法
    • 服务器端检查当前请求中是否携带了 JSESSIONID 的 Cookie
      • :根据 JSESSIONID 在服务器端查找对应的 HttpSession 对象
        • 能找到:将找到的 HttpSession 对象作为 request.getSession () 方法的返回值返回
        • 找不到:服务器端新建一个 HttpSession 对象作为 request.getSession () 方法的返回值返回
      • :服务器端新建一个 HttpSession 对象作为 request.getSession () 方法的返回值返回

第一次客户端发送请求没有请求头里面没有 session id, 之后请求一般都会带上 session id

image-20220201041840393

image-20220201042139206

session 保存作用域

image-20220201042947928image-20220201045604497

# #2、流程图描述

./images

# #3、代码验证

// 1.调用request对象的方法尝试获取HttpSession对象
HttpSession session = request.getSession();

// 2.调用HttpSession对象的isNew()方法
boolean wetherNew = session.isNew();

// 3.打印HttpSession对象是否为新对象
System.out.println("wetherNew = " + (wetherNew?"HttpSession对象是新的":"HttpSession对象是旧的"));

// 4.调用HttpSession对象的getId()方法
String id = session.getId();

// 5.打印JSESSIONID的值
System.out.println("JSESSIONID = " + id);

# #4、时效性

# #①为什么 Session 要设置时限

用户量很大之后,Session 对象相应的也要创建很多。如果一味创建不释放,那么服务器端的内存迟早要被耗尽。

# #②设置时限的难点

从服务器端的角度,很难精确得知类似浏览器关闭的动作。而且即使浏览器一直没有关闭,也不代表用户仍然在使用。

# #③服务器端给 Session 对象设置最大闲置时间

  • 默认值:1800 秒

./images

最大闲置时间生效的机制如下:

./images

# #④代码验证

// ※测试时效性
// 获取默认的最大闲置时间
int maxInactiveIntervalSecond = session.getMaxInactiveInterval();
System.out.println("maxInactiveIntervalSecond = " + maxInactiveIntervalSecond);

// 设置默认的最大闲置时间
session.setMaxInactiveInterval(15);

# #⑤强制 Session 立即失效

session.invalidate();

# 过滤器简介

# #1、通过类比了解过滤器作用

# #①坐地铁

./images

# #②登录检查

./images

# #2、过滤器的三要素

# #①拦截

过滤器之所以能够对请求进行预处理,关键是对请求进行拦截,把请求拦截下来才能够做后续的操作。而且对于一个具体的过滤器,它必须明确它要拦截的请求,而不是所有请求都拦截。

# #②过滤

根据业务功能实际的需求,看看在把请求拦截到之后,需要做什么检查或什么操作,写对应的代码即可。

# #③放行

过滤器完成自己的任务或者是检测到当前请求符合过滤规则,那么可以将请求放行。所谓放行,就是让请求继续去访问它原本要访问的资源。

友情提示:将来学习 SpringMVC 时,会学习 SpringMVC 中的『拦截器』,同样具备三要素。


# HelloWorld

# #1、思路

./images

# #2、操作步骤

# #①准备工作

  • 创建 module
  • 加入 Thymeleaf 环境
  • 完成首页访问功能
  • 创建 Target01Servlet 以及 target01.html
  • 创建 SpecialServlet 以及 special.html

# #②创建 Filter

# #[1] 创建 Target01Filter 类

  • 要点 1:实现 javax.servlet.Filter 接口
  • 要点 2:在 doFilter () 方法中执行过滤
  • 要点 3:如果满足过滤条件使用 chain.doFilter (request, response); 放行
  • 要点 4:如果不满足过滤条件转发或重定向请求
    • 附带问题:Thymeleaf 模板渲染。这里我们选择的解决办法是跳转到一个 Servlet,由 Servlet 负责执行模板渲染返回页面。
public class Target01Filter implements Filter {
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {

        // 1.打印一句话表明Filter执行了
        System.out.println("过滤器执行:Target01Filter");

        // 2.检查是否满足过滤条件
        // 人为设定一个过滤条件:请求参数message是否等于monster
        // 等于:放行
        // 不等于:将请求跳转到另外一个页面
        // ①获取请求参数
        String message = request.getParameter("message");

        // ②检查请求参数是否等于monster
        if ("monster".equals(message)) {

            // ③执行放行
            // FilterChain对象代表过滤器链
            // chain.doFilter(request, response)方法效果:将请求放行到下一个Filter,
            // 如果当前Filter已经是最后一个Filter了,那么就将请求放行到原本要访问的目标资源
            chain.doFilter(request, response);

        }else{

            // ④跳转页面
            request.getRequestDispatcher("/SpecialServlet?method=toSpecialPage").forward(request, response);

        }

    }

    @Override
    public void destroy() {

    }
}

# #[2] 配置 Target01Filter 类

这一步也可以叫『注册』。

<!-- 配置Target01Filter -->
<filter>
    <!-- 配置Filter的友好名称 -->
    <filter-name>Target01Filter</filter-name>

    <!-- 配置Filter的全类名,便于Servlet容器创建Filter对象 -->
    <filter-class>com.atguigu.filter.filter.Target01Filter</filter-class>
</filter>

<!-- 配置Filter要拦截的目标资源 -->
<filter-mapping>
    <!-- 指定这个mapping对应的Filter名称 -->
    <filter-name>Target01Filter</filter-name>

    <!-- 通过请求地址模式来设置要拦截的资源 -->
    <url-pattern>/Target01Servlet</url-pattern>
</filter-mapping>

# 过滤器生命周期

# #1、回顾 Servlet 生命周期

Servlet 生命周期 (opens new window)

# #2、Filter 生命周期

和 Servlet 生命周期类比,Filter 生命周期的关键区别是:在 Web 应用启动时创建对象

生命周期阶段 执行时机 执行次数
创建对象 Web 应用启动时 一次
初始化 创建对象后 一次
拦截请求 接收到匹配的请求 多次
销毁 Web 应用卸载前 一次

# 过滤器匹配规则

本节要探讨的是在 filter-mapping 中如何将 Filter 同它要拦截的资源关联起来。

# #1、精确匹配

指定被拦截资源的完整路径:

<!-- 配置Filter要拦截的目标资源 -->
<filter-mapping>
    <!-- 指定这个mapping对应的Filter名称 -->
    <filter-name>Target01Filter</filter-name>

    <!-- 通过请求地址模式来设置要拦截的资源 -->
    <url-pattern>/Target01Servlet</url-pattern>
</filter-mapping>

# #2、模糊匹配

相比较精确匹配,使用模糊匹配可以让我们创建一个 Filter 就能够覆盖很多目标资源,不必专门为每一个目标资源都创建 Filter,提高开发效率。

# #①前杠后星

在我们配置了 url-pattern 为 /user/* 之后,请求地址只要是 /user 开头的那么就会被匹配。

<filter-mapping>
    <filter-name>Target02Filter</filter-name>

    <!-- 模糊匹配:前杠后星 -->
    <!--
        /user/Target02Servlet
        /user/Target03Servlet
        /user/Target04Servlet
    -->
    <url-pattern>/user/*</url-pattern>
</filter-mapping>

极端情况:/* 匹配所有请求

# #②前星后缀

下面我们使用 png 图片来测试后缀拦截的效果,并不是只能拦截 png 扩展名。

# #[1] 创建一组 img 标签

<img th:src="@{/./images/img017.png}"/><br/>
<img th:src="@{/./images/img018.png}"/><br/>
<img th:src="@{/./images/img019.png}"/><br/>
<img th:src="@{/./images/img020.png}"/><br/>
<img th:src="@{/./images/img024.png}"/><br/>
<img th:src="@{/./images/img025.png}"/><br/>

# #[2] 创建 Filter

<filter>
    <filter-name>Target04Filter</filter-name>
    <filter-class>com.atguigu.filter.filter.Target04Filter</filter-class>
</filter>
<filter-mapping>
    <filter-name>Target04Filter</filter-name>
    <url-pattern>*.png</url-pattern>
</filter-mapping>

# #③前杠后缀,星号在中间

配置方式如下:

<url-pattern>/*.png</url-pattern>

按照这个配置启动 Web 应用时会抛出异常:

java.lang.IllegalArgumentException: Invalid /*.png in filter mapping

** 结论:这么配是 * 不允许 * 的!

# #3、匹配 Servlet 名称 [了解]

<filter-mapping>
    <filter-name>Target05Filter</filter-name>

    <!-- 根据Servlet名称匹配 -->
    <servlet-name>Target01Servlet</servlet-name>
</filter-mapping>

# 过滤器链

# #1、概念

  • 多个 Filter 的拦截范围如果存在重合部分,那么这些 Filter 会形成 Filter 链
  • 浏览器请求重合部分对应的目标资源时,会依次经过 Filter 链中的每一个 Filter。
  • Filter 链中每一个 Filter 执行的顺序是由 web.xml 中 filter-mapping 配置的顺序决定的。

./images

# #2、测试

# #①准备工作

创建超链接访问一个普通的 Servlet 即可。

# #②创建多个 Filter 拦截 Servlet

<filter-mapping>
    <filter-name>TargetChain03Filter</filter-name>
    <url-pattern>/Target05Servlet</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>TargetChain02Filter</filter-name>
    <url-pattern>/Target05Servlet</url-pattern>
</filter-mapping>
<filter-mapping>
    <filter-name>TargetChain01Filter</filter-name>
    <url-pattern>/Target05Servlet</url-pattern>
</filter-mapping>

控制台打印效果:

过滤器执行:Target03Filter [模糊匹配 前杠后星 /*] 测试 Filter 链:TargetChain03Filter 测试 Filter 链:TargetChain02Filter 测试 Filter 链:TargetChain01Filter


# 观察者模式

二十三种设计模式之一:

images

  • 观察者:监控『被观察者』的行为,一旦发现『被观察者』触发了事件,就会调用事先准备好的方法执行操作。
  • 被观察者:『被观察者』一旦触发了被监控的事件,就会被『观察者』发现。

# 监听器简介

# #1、概念

监听器:专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动。 Servlet 监听器:Servlet 规范中定义的一种特殊类,它用于监听 Web 应用程序中的 ServletContext,HttpSession 和 HttpServletRequest 等域对象的创建与销毁事件,以及监听这些域对象中的属性发生修改的事件。

# #2、分类

./images

  • 域对象监听器
  • 域对象的属性域监听器
  • Session 域中数据的监听器

# #3、监听器列表

# #①ServletContextListener

作用:监听 ServletContext 对象的创建与销毁

方法名 作用
contextInitialized(ServletContextEvent sce) ServletContext 创建时调用
contextDestroyed(ServletContextEvent sce) ServletContext 销毁时调用

ServletContextEvent 对象代表从 ServletContext 对象身上捕获到的事件,通过这个事件对象我们可以获取到 ServletContext 对象。

# #②HttpSessionListener

作用:监听 HttpSession 对象的创建与销毁

方法名 作用
sessionCreated(HttpSessionEvent hse) HttpSession 对象创建时调用
sessionDestroyed(HttpSessionEvent hse) HttpSession 对象销毁时调用

HttpSessionEvent 对象代表从 HttpSession 对象身上捕获到的事件,通过这个事件对象我们可以获取到触发事件的 HttpSession 对象。

# #③ServletRequestListener

作用:监听 ServletRequest 对象的创建与销毁

方法名 作用
requestInitialized(ServletRequestEvent sre) ServletRequest 对象创建时调用
requestDestroyed(ServletRequestEvent sre) ServletRequest 对象销毁时调用

ServletRequestEvent 对象代表从 HttpServletRequest 对象身上捕获到的事件,通过这个事件对象我们可以获取到触发事件的 HttpServletRequest 对象。另外还有一个方法可以获取到当前 Web 应用的 ServletContext 对象。

# #④ServletContextAttributeListener

作用:监听 ServletContext 中属性的创建、修改和销毁

方法名 作用
attributeAdded(ServletContextAttributeEvent scab) 向 ServletContext 中添加属性时调用
attributeRemoved(ServletContextAttributeEvent scab) 从 ServletContext 中移除属性时调用
attributeReplaced(ServletContextAttributeEvent scab) 当 ServletContext 中的属性被修改时调用

ServletContextAttributeEvent 对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getServletContext() 获取 ServletContext 对象

# #⑤HttpSessionAttributeListener

作用:监听 HttpSession 中属性的创建、修改和销毁

方法名 作用
attributeAdded(HttpSessionBindingEvent se) 向 HttpSession 中添加属性时调用
attributeRemoved(HttpSessionBindingEvent se) 从 HttpSession 中移除属性时调用
attributeReplaced(HttpSessionBindingEvent se) 当 HttpSession 中的属性被修改时调用

HttpSessionBindingEvent 对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getSession() 获取触发事件的 HttpSession 对象

# #⑥ServletRequestAttributeListener

作用:监听 ServletRequest 中属性的创建、修改和销毁

方法名 作用
attributeAdded(ServletRequestAttributeEvent srae) 向 ServletRequest 中添加属性时调用
attributeRemoved(ServletRequestAttributeEvent srae) 从 ServletRequest 中移除属性时调用
attributeReplaced(ServletRequestAttributeEvent srae) 当 ServletRequest 中的属性被修改时调用

ServletRequestAttributeEvent 对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取修改或添加的属性名
getValue() 获取被修改或添加的属性值
getServletRequest () 获取触发事件的 ServletRequest 对象

# #⑦HttpSessionBindingListener

作用:监听某个对象在 Session 域中的创建与移除

方法名 作用
valueBound(HttpSessionBindingEvent event) 该类的实例被放到 Session 域中时调用
valueUnbound(HttpSessionBindingEvent event) 该类的实例从 Session 中移除时调用

HttpSessionBindingEvent 对象代表属性变化事件,它包含的方法如下:

方法名 作用
getName() 获取当前事件涉及的属性名
getValue() 获取当前事件涉及的属性值
getSession() 获取触发事件的 HttpSession 对象

# #⑧HttpSessionActivationListener

作用:监听某个对象在 Session 中的序列化与反序列化。

方法名 作用
sessionWillPassivate(HttpSessionEvent se) 该类实例和 Session 一起钝化到硬盘时调用
sessionDidActivate(HttpSessionEvent se) 该类实例和 Session 一起活化到内存时调用

HttpSessionEvent 对象代表事件对象,通过 getSession () 方法获取事件涉及的 HttpSession 对象。


# ServletContextListener

# #1、实用性

将来学习 SpringMVC 的时候,会用到一个 ContextLoaderListener,这个监听器就实现了 ServletContextListener 接口,表示对 ServletContext 对象本身的生命周期进行监控。

# #2、具体用法

# #①创建监听器类

public class AtguiguListener implements ServletContextListener {
    @Override
    public void contextInitialized(
            // Event对象代表本次事件,通过这个对象可以获取ServletContext对象本身
            ServletContextEvent sce) {
        System.out.println("Hello,我是ServletContext,我出生了!");

        ServletContext servletContext = sce.getServletContext();
        System.out.println("servletContext = " + servletContext);
    }

    @Override
    public void contextDestroyed(ServletContextEvent sce) {
        System.out.println("Hello,我是ServletContext,我打算去休息一会儿!");
    }
}

# #②注册监听器

<!-- 每一个listener标签对应一个监听器配置,若有多个监听器,则配置多个listener标签即可 -->
<listener>
    <!-- 配置监听器指定全类名即可 -->
    <listener-class>com.atguigu.listener.AtguiguListener</listener-class>
</listener>

事件触发过程中控制台日志的打印:

Connected to server [2021-03-20 04:23:20,982] Artifact pro10-listener:war exploded: Artifact is being deployed, please wait… 三月 20, 2021 4:23:21 下午 org.apache.catalina.deploy.WebXml setVersion 警告: Unknown version string [4.0]. Default version will be used. Hello,我是 ServletContext,我出生了! servletContext = org.apache.catalina.core.ApplicationContextFacade@6a66017e [2021-03-20 04:23:21,426] Artifact pro10-listener:war exploded: Artifact is deployed successfully [2021-03-20 04:23:21,426] Artifact pro10-listener:war exploded: Deploy took 444 milliseconds 三月 20, 2021 4:23:30 下午 org.apache.catalina.startup.HostConfig deployDirectory 信息: Deploying web application directory D:\software\apache-tomcat-7.0.57\webapps\manager 三月 20, 2021 4:23:31 下午 org.apache.catalina.startup.HostConfig deployDirectory 信息: Deployment of web application directory D:\software\apache-tomcat-7.0.57\webapps\manager has finished in 124 ms [2021-03-20 04:24:06,422] Artifact pro10-listener:war exploded: Artifact is being deployed, please wait… Hello,我是 ServletContext,我打算去休息一会儿! Hello,我是 ServletContext,我出生了! servletContext = org.apache.catalina.core.ApplicationContextFacade@2a55374c [2021-03-20 04:24:07,115] Artifact pro10-listener:war exploded: Artifact is deployed successfully [2021-03-20 04:24:07,115] Artifact pro10-listener:war exploded: Deploy took 694 milliseconds


# 第一节 Ajax 概

# #1、服务器端渲染

./images

# #2、Ajax 渲染(局部更新)

./images

# #3、前后端分离

彻底舍弃服务器端渲染,数据全部通过 Ajax 方式以 JSON 格式来传递。

# #4、同步与异步

Ajax 本身就是 Asynchronous JavaScript And XML 的缩写,直译为:异步的 JavaScript 和 XML。在实际应用中 Ajax 指的是:不刷新浏览器窗口不做页面跳转局部更新页面内容的技术。

**『同步』『异步』** 是一对相对的概念,那么什么是同步,什么是异步呢?

# #①同步

多个操作按顺序执行,前面的操作没有完成,后面的操作就必须等待。所以同步操作通常是串行的。

./images

# #②异步

多个操作相继开始并发执行,即使开始的先后顺序不同,但是由于它们各自是在自己独立的进程或线程中完成,所以互不干扰,** 谁也 * 不用等 *

./images

# #5、Axios 简介

使用原生的 JavaScript 程序执行 Ajax 极其繁琐,所以一定要使用框架来完成。而 Axios 就是目前最流行的前端 Ajax 框架。

Axios 官网 (opens new window)

./images

使用 Axios 和使用 Vue 一样,导入对应的 *.js 文件即可。官方提供的 script 标签引入方式为:

<script src="https://unpkg.com/axios/dist/axios.min.js"></script>

我们可以把这个 axios.min.js 文件下载下来保存到本地来使用。


# Axios 基本用法

# #0、在前端页面引入开发环境

<script type="text/javascript" src="/demo/static/vue.js"></script>
<script type="text/javascript" src="/demo/static/axios.min.js"></script>

# #1、发送普通请求参数

# #①前端代码

HTML 标签:

<div id="app">
    <button @click="commonParam">普通请求参数</button>
</div>

Vue+axios 代码:

new Vue({
    "el":"#app",
    "data":{},
    "methods":{
        "commonParam":function () {
            axios({
                "method":"post",
                "url":"/demo/AjaxServlet?method=commonParam",
                "params":{
                    "userName":"tom",
                    "userPwd":"123456"
                }
            }).then(function (response) {
                console.log(response);
            }).catch(function (error) {
                console.log(error);
            });
        }
    }
});

效果:所有请求参数都被放到 URL 地址后面了,哪怕我们现在用的是 POST 请求方式。

./images

# #②后端代码

public class AjaxServlet extends ModelBaseServlet {
    protected void commonParam(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

        String userName = request.getParameter("userName");
        String userPwd = request.getParameter("userPwd");

        System.out.println("userName = " + userName);
        System.out.println("userPwd = " + userPwd);

        response.setContentType("text/html;charset=UTF-8");
        response.getWriter().write("服务器端返回普通文本字符串作为响应");

    }
}

P.S.:由于我们不需要 Thymeleaf 了,所以 ModelBaseServlet 可以跳过 ViewBaseServlet 直接继承 HttpServlet。

./images

# #③axios 程序接收到的响应对象结构

./images

属性名 作用
config 调用 axios (config 对象) 方法时传入的 JSON 对象
data 服务器端返回的响应体数据
headers 响应消息头
request 原生 JavaScript 执行 Ajax 操作时使用的 XMLHttpRequest
status 响应状态码
statusText 响应状态码的说明文本

# #④服务器端处理请求失败后

catch(function (error) {     // catch()服务器端处理请求出错后,会调用

    console.log(error);         // error就是出错时服务器端返回的响应数据
    console.log(error.response);        // 在服务器端处理请求失败后,获取axios封装的JSON格式的响应数据对象
    console.log(error.response.status); // 在服务器端处理请求失败后,获取响应状态码
    console.log(error.response.statusText); // 在服务器端处理请求失败后,获取响应状态说明文本
    console.log(error.response.data);   // 在服务器端处理请求失败后,获取响应体数据

});

在给 catch () 函数传入的回调函数中,error 对象封装了服务器端处理请求失败后相应的错误信息。其中,axios 封装的响应数据对象,是 error 对象的 response 属性。response 属性对象的结构如下图所示:

./images

可以看到,response 对象的结构还是和 then () 函数传入的回调函数中的 response 是一样的:

./images

回调函数:开发人员声明,但是调用时交给系统来调用。像单击响应函数、then ()、catch () 里面传入的都是回调函数。回调函数是相对于普通函数来说的,普通函数就是开发人员自己声明,自己调用:

function sum(a, b) {
    return a+b;
}

var result = sum(3, 2);
console.log("result="+result);

# #2、发送请求体 JSON

# #①前端代码

HTML 代码:

<button @click="requestBodyJSON">请求体JSON</button>

Vue+axios 代码:

……
"methods":{
    "requestBodyJSON":function () {
        axios({
            "method":"post",
            "url":"/demo/AjaxServlet?method=requestBodyJSON",
            "data":{
                "stuId": 55,
                "stuName": "tom",
                "subjectList": [
                    {
                        "subjectName": "java",
                        "subjectScore": 50.55
                    },
                    {
                        "subjectName": "php",
                        "subjectScore": 30.26
                    }
                ],
                "teacherMap": {
                    "one": {
                        "teacherName":"tom",
                        "tearcherAge":23
                    },
                    "two": {
                        "teacherName":"jerry",
                        "tearcherAge":31
                    },
                },
                "school": {
                    "schoolId": 23,
                    "schoolName": "atguigu"
                }
            }
        }).then(function (response) {
            console.log(response);
        }).catch(function (error) {
            console.log(error);
        });
    }
}
……

效果:

./images

P.S.:Chrome 浏览器中将『请求负载』显示为英文:『Request Payload』。

# #②后端代码

# #[1] 加入 Gson 包

Gson 是 Google 研发的一款非常优秀的 JSON 数据解析和生成工具,它可以帮助我们将数据在 JSON 字符串和 Java 对象之间互相转换。

./images

# #[2] Servlet 代码

protected void requestBodyJSON(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.由于请求体数据有可能很大,所以Servlet标准在设计API的时候要求我们通过输入流来读取
    BufferedReader reader = request.getReader();

    // 2.创建StringBuilder对象来累加存储从请求体中读取到的每一行
    StringBuilder builder = new StringBuilder();

    // 3.声明临时变量
    String bufferStr = null;

    // 4.循环读取
    while((bufferStr = reader.readLine()) != null) {
        builder.append(bufferStr);
    }

    // 5.关闭流
    reader.close();

    // 6.累加的结果就是整个请求体
    String requestBody = builder.toString();

    // 7.创建Gson对象用于解析JSON字符串
    Gson gson = new Gson();

    // 8.将JSON字符串还原为Java对象
    Student student = gson.fromJson(requestBody, Student.class);
    System.out.println("student = " + student);

    System.out.println("requestBody = " + requestBody);

    response.setContentType("text/html;charset=UTF-8");
    response.getWriter().write("服务器端返回普通文本字符串作为响应");
}

P.S.:看着很麻烦是吧?别担心,将来我们有了 SpringMVC 之后,一个 **@RequestBody** 注解就能够搞定,非常方便!

# #3、服务器端返回 JSON 数据

# #①前端代码

axios({
    "method":"post",
    "url":"/demo/AjaxServlet?method=responseBodyJSON"
}).then(function (response) {
    console.log(response);
}).catch(function (error) {
    console.log(error);
});

then () 中获取到的 response 在控制台打印效果如下:我们需要通过 data 属性获取响应体数据

./images

# #②后端代码

# #[1] 加入 Gson 包

仍然需要 Gson 支持,不用多说

./images

# #[2] Servlet 代码

protected void responseBodyJSON(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {

    // 1.准备数据对象
    Student student = new Student();
    student.setStuId(10);
    student.setStuName("tom");
    student.setSchool(new School(11,"atguigu"));
    student.setSubjectList(Arrays.asList(new Subject("java", 95.5), new Subject("php", 93.3)));

    Map<String, Teacher> teacherMap = new HashMap<>();
    teacherMap.put("t1", new Teacher("lili", 25));
    teacherMap.put("t2", new Teacher("mary", 26));
    teacherMap.put("t3", new Teacher("katty", 27));

    student.setTeacherMap(teacherMap);

    // 2.创建Gson对象
    Gson gson = new Gson();

    // 3.将Java对象转换为JSON对象
    String json = gson.toJson(student);

    // 4.设置响应体的内容类型
    response.setContentType("application/json;charset=UTF-8");
    response.getWriter().write(json);

}