# 一、SpringMVC 简介

# 1、什么是 MVC

MVC 是一种软件架构的思想,将软件按照模型、视图、控制器来划分

M:Model,模型层,指工程中的 JavaBean,作用是处理数据

JavaBean 分为两类:

  • 一类称为实体类 Bean:专门存储业务数据的,如 Student、User 等
  • 一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问。

V:View,视图层,指工程中的 html 或 jsp 等页面,作用是与用户进行交互,展示数据

C:Controller,控制层,指工程中的 servlet,作用是接收请求和响应浏览器

MVC 的工作流程:
用户通过视图层发送请求到服务器,在服务器中请求被 Controller 接收,Controller 调用相应的 Model 层处理请求,处理完毕将结果返回到 Controller,Controller 再根据请求处理的结果找到相应的 View 视图,渲染数据后最终响应给浏览器

# 2、什么是 SpringMVC

SpringMVC 是 Spring 的一个后续产品,是 Spring 的一个子项目

SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案。在表述层框架历经 Strust、WebWork、Strust2 等诸多产品的历代更迭之后,目前业界普遍选择了 SpringMVC 作为 Java EE 项目表述层开发的首选方案

注:三层架构分为表述层(或表示层)、业务逻辑层、数据访问层,表述层表示前台页面和后台 servlet

# 3、SpringMVC 的特点

  • Spring 家族原生产品,与 IOC 容器等基础设施无缝对接
  • 基于原生的 Servlet,通过了功能强大的前端控制器 DispatcherServlet,对请求和响应进行统一处理
  • 表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案
  • 代码清新简洁,大幅度提升开发效率
  • 内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可
  • 性能卓著,尤其适合现代大型、超大型互联网项目要求

# 二、HelloWorld

# 1、开发环境

IDE:idea 2019.2

构建工具:maven3.5.4

服务器:tomcat7

Spring 版本:5.3.1

# 2、创建 maven 工程

# a > 添加 web 模块
# b > 打包方式:war
# c > 引入依赖
<dependencies>
    <!-- SpringMVC -->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-webmvc</artifactId>
        <version>5.3.1</version>
    </dependency>

    <!-- 日志 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>

    <!-- ServletAPI -->
    <dependency>
        <groupId>javax.servlet</groupId>
        <artifactId>javax.servlet-api</artifactId>
        <version>3.1.0</version>
        <scope>provided</scope> //provided代表之后这个module被打包成war包就不会带上这个api了,因为服务器本身就提提供了servletAPI的包(依赖范围->provided:运行时无效,Tomcat会有servlet-api,但是编译期需要,所有要有这个依赖,要不然无法通过编译。所以它不需要被打包,因为服务器有。)
    </dependency>

    <!-- Spring5和Thymeleaf整合包 -->
    <dependency>
        <groupId>org.thymeleaf</groupId>
        <artifactId>thymeleaf-spring5</artifactId>
        <version>3.0.12.RELEASE</version>
    </dependency>
</dependencies>

注:由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其他靠传递性导入。

# 3、配置 web.xml

  • 我们需要有一个前端控制器的 xml 配置文件,放到 resources 下

image-20220207224457998

  • 然后需要注册 SpringMVC 的前端控制器 DispatcherServlet, 想用这个前端控制器来控制请求就必须先在 xml 里面注册他 (然后顺便设置一下前端控制器 DispatcherServlet 的配置文件的名字以及名称和什么时候被创建以及初始化)
# a > 默认配置方式

此配置作用下,SpringMVC 的配置文件默认位于 WEB-INF 下,默认名称为-servlet.xml,例如,以下配置所对应 SpringMVC 的配置文件位于 WEB-INF 下,文件名为__springMVC-servlet.xml__

<!-- 配置SpringMVC的前端控制器,对浏览器发送的请求统一进行处理 -->
<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
</servlet>
<servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <!--
        设置springMVC的核心控制器所能处理的请求的请求路径
        /所匹配的请求可以是/login或.html或.js或.css方式的请求路径
        但是/不能匹配.jsp请求路径的请求
(因为jsp本质上就是servlet,不需要做dispatcherServlet做拦截处理,要是用了/*就会让.jsp的访问交由dispatcherServlet来处理,然后把.jsp当做是普通资源的申请就会找不到)
这也是为什么使用"/"而不是"/*"
    -->
    <url-pattern>/</url-pattern>
</servlet-mapping>

因为我们之前是一个请求就对应一个 Servlet

而 jsp 请求,因为 jsp 本身就是一个 Servlet,所以请求 jsp 时已经有特定的 Servlet 来处理该请求了(就是它自己)

如果用前端控制器来处理了 jsp,就会用前端控制器这个 Servlet 来处理,而不会调用该 jsp 所对应的 Servlet 进行处理

# b > 扩展配置方式

可通过 init-param (初始化参数) 标签设置 SpringMVC 配置文件的位置和名称 (所以就不像上面的那个一样了),通过 load-on-startup 标签设置 SpringMVC 前端控制器 DispatcherServlet 的初始化时间

<!-- 配置SpringMVC的前端控制器,对浏览器发送的请求统一进行处理 -->
<servlet>
    <servlet-name>springMVC</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <!-- 通过初始化参数指定SpringMVC配置文件的位置和名称 -->
    <init-param>
        <!-- contextConfigLocation为固定值 -->
        <param-name>contextConfigLocation</param-name>
        <!-- 使用classpath:表示从类路径查找配置文件,例如maven工程中的src/main/resources -->
        <param-value>classpath:springMVC.xml</param-value>
    </init-param>
    <!-- 
 		作为框架的核心组件,在启动过程中有大量的初始化操作要做
		而这些操作放在第一次请求时才执行会严重影响访问速度
		因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时
	-->
    <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
    <servlet-name>springMVC</servlet-name>
    <!--
        设置springMVC的核心控制器所能处理的请求的请求路径
        /所匹配的请求可以是/login或.html或.js或.css方式的请求路径
        但是/不能匹配.jsp请求路径的请求
    -->
    <url-pattern>/</url-pattern>
</servlet-mapping>

注:

标签中使用 / 和 /* 的区别:

/ 所匹配的请求可以是 /login 或.html 或.js 或.css 方式的请求路径,但是 / 不能匹配.jsp 请求路径的请求

因此就可以避免在访问 jsp 页面时,该请求被 DispatcherServlet 处理,从而找不到相应的页面

/* 则能够匹配所有请求,例如在使用过滤器时,若需要对所有请求进行过滤,就需要使用 /* 的写法

上面还把那个 springMVC 的配置文件生成在 classpath 下,也就是 src/main/resources.

对于 maven 来说,我们应该把 (除了 web.xml 的) 配置文件都放到 src/main/resources, 也就是 classpath 之下,所以我们喜欢这种扩展配置方式

# 4、创建请求控制器

由于前端控制器对浏览器发送的请求进行了统一的处理,但是__具体的请求有不同的处理过程,因此需要创建处理具体请求的类,即请求控制器 (Controller 层的)__

请求控制器中每一个处理请求的方法成为控制器方法

因为 SpringMVC 的控制器由一个 POJO(普通的 Java 类)担任,因此需要通过 @Controller 注解将其标识为一个控制层组件,交给 Spring 的 IoC 容器管理,此时 SpringMVC 才能够识别控制器的存在

@Controller
public class HelloController {
    
}

# 5、创建 springMVC 的配置文件

注意是在 SpringMVC (既前端控制器) 的 xml 配置文件里,而不是 web.xml!!!

<!-- 自动扫描包 -->
<context:component-scan base-package="com.atguigu.mvc.controller"/>

<!-- 配置Thymeleaf视图解析器 -->
<bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver">
    <property name="order" value="1"/>
    <property name="characterEncoding" value="UTF-8"/>
    <property name="templateEngine">
        <bean class="org.thymeleaf.spring5.SpringTemplateEngine">
            <property name="templateResolver">
                <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver">
    
                    <!-- 视图前缀 -->
                    <property name="prefix" value="/WEB-INF/templates/"/>
    
                    <!-- 视图后缀 -->
                    <property name="suffix" value=".html"/>
                    <property name="templateMode" value="HTML5"/>
                    <property name="characterEncoding" value="UTF-8" />
                </bean>
            </property>
        </bean>
    </property>
</bean>

<!-- 
   处理静态资源,例如html、js、css、jpg
  若只设置该标签,则只能访问静态资源,其他请求则无法访问
  此时必须设置<mvc:annotation-driven/>解决问题
 -->
<mvc:default-servlet-handler/>

<!-- 开启mvc注解驱动 -->
<mvc:annotation-driven>
    <mvc:message-converters>
        <!-- 处理响应中文内容乱码 -->
        <bean class="org.springframework.http.converter.StringHttpMessageConverter">
            <property name="defaultCharset" value="UTF-8" />
            <property name="supportedMediaTypes">
                <list>
                    <value>text/html</value>
                    <value>application/json</value>
                </list>
            </property>
        </bean>
    </mvc:message-converters>
</mvc:annotation-driven>

# 6、测试 HelloWorld

# a > 实现对首页的访问

在请求控制器中创建处理请求的方法

// @RequestMapping注解:处理请求和控制器方法之间的映射关系
// @RequestMapping注解的value属性可以通过请求地址匹配请求,/表示的当前工程的上下文路径
// localhost:8080/springMVC/
@RequestMapping("/")
public String index() {
    //设置视图名称
    return "index";
}

这里的 @RequestMapping("/") 注解意味着如果我们的请求路径为 "/" 也就是我们的上下文路径 (ApplicationContext 的路径) 的时候就会来执行我们当前被这个注解表示的方法,这个方法返回值就会被我们的视图解析器加上前缀和后缀 (我们之前配的), 也就是说最后会跳转到我们 / WEB-INF/templates/index.html 这个页面,就算一般来说 WEB-INF 下的都访问不了 (只能通过内部转发才可以)

注意这个 @RequestMapping(value=XXX) 注解的值可以通过请求方式,请求报文,请求参数等进行匹配!都可以!

# b > 通过超链接跳转到指定页面

在主页 index.html 中设置超链接

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
    <h1>首页</h1>
    <a th:href="@{/hello}">HelloWorld</a><br/>
</body>
</html>

在请求控制器中创建处理请求的方法

@RequestMapping("/hello")
public String HelloWorld() {
    return "target";
}

# 7、总结

浏览器发送请求,若请求地址符合前端控制器的 url-pattern (就是当初我们设置的 "/", 不包括任何.jsp 的访问,.jsp 都交给 jsp 他自己去处理),该请求就会被前端控制器 DispatcherServlet 处理。前端控制器会读取 SpringMVC 的核心配置文件,通过扫描组件__找到控制器 (所以是要在前端控制器配置文件而不是 web.xml 里面开启 Spring IOC 的注解扫描等等等),将__请求地址和控制器中 @RequestMapping 注解的 value 属性值进行匹配,若匹配成功,该注解所标识的控制器方法就是处理请求的方法。处理__请求的方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析,加上前缀和后缀组成视图的路径,通过 Thymeleaf 对视图进行渲染,最终 == 转发 (转发方式跳转到 (所以就算对应页面是在 WEB-INF 下面也可以的))== 到视图所对应页面__

WEB-INF 目录: 是 Java 的 WEB 应用的安全目录。所谓安全就是客户端无法访问,只有服务端可以访问的目录。 页面放在 WEB-INF 目录下面,这样可以限制访问,提高安全性


# 三、@RequestMapping 注解

# 1、@RequestMapping 注解的功能

从注解名称上我们可以看到,@RequestMapping 注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。

SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求。

我们必须保证在所有的控制类里面的所有方法的 @RequestMapping 映射不会产生冲突,比如说一个类的方法有 @RequestMapping("/") 然后另外一个类的方法也有 @RequestMapping("/") , 产生冲突了,不知道去哪个方法处理这个请求

# 2、@RequestMapping 注解的位置

@RequestMapping 标识一个类:设置映射请求的请求路径的初始信息

@RequestMapping 标识一个方法:设置映射请求请求路径的具体信息

@Controller
@RequestMapping("/test")
public class RequestMappingController {

	//此时请求映射所映射的请求的请求路径为:/test/testRequestMapping
    @RequestMapping("/testRequestMapping")
    public String testRequestMapping(){
        return "success";
    }

}

所以需要这样:

image-20220207234540662

才可以和我们的注解匹配到,然后执行那个方法处理请求

先匹配到类上面的注解的 value, 然后再去匹配那个类里面的具体的注解的 value, 这种方式很大程度上解决了我们之前说的多个 @RequestMapping 注解发生冲突的问题 (用来给不同模块使用)

# 3、@RequestMapping 注解的 value 属性

@RequestMapping 注解的 value 属性通过请求的__请求地址__匹配请求映射

@RequestMapping 注解的 value 属性是一个__字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求__

@RequestMapping 注解的 value 属性必须设置,至少通过请求地址匹配请求映射

<a th:href="@{/testRequestMapping}">测试@RequestMapping的value属性-->/testRequestMapping</a><br>
<a th:href="@{/test}">测试@RequestMapping的value属性-->/test</a><br>
@RequestMapping(
        value = {"/testRequestMapping", "/test"}
)
public String testRequestMapping(){
    return "success";
}

# 4、@RequestMapping 注解的 method 属性

若是这个没填,就会是默认的也就是所有请求方法类型都接收

@RequestMapping 注解的 method 属性通过请求的请求方式(get 或 post)匹配请求映射

@RequestMapping 注解的 method 属性是一个 RequestMethod 类型的__数组,表示该请求映射能够匹配多种请求方式的请求__

若当前请求的请求地址满足请求映射的 value 属性,但是请求方式不满足 method 属性,则浏览器报错 405:Request method ‘POST’ not supported

注意 value 属性必须给,要是设置这些其他值,代表既要匹配上 value 也要匹配上这个 method (或是其他的) 才行,一个匹配不上都不行

<a th:href="@{/test}">测试@RequestMapping的value属性-->/test</a><br>
<form th:action="@{/test}" method="post">
    <input type="submit">
</form>
@RequestMapping(
        value = {"/testRequestMapping", "/test"},
        method = {RequestMethod.GET, RequestMethod.POST}
)
public String testRequestMapping(){
    return "success";
}

注:

1、对于处理指定请求方式的控制器方法,SpringMVC 中提供了 @RequestMapping 的派生注解

处理 get 请求的映射–>@GetMapping

处理 post 请求的映射–>@PostMapping

处理 put 请求的映射–>@PutMapping

处理 delete 请求的映射–>@DeleteMapping

2、常用的请求方式有 get,post,put,delete

但是目前浏览器只支持 get 和 post,若在 form 表单提交时,为 method 设置了其他请求方式的字符串(put 或 delete),则按照默认的请求方式 get 处理 (所以你就算写了什么 PUT 或者 DELETE 都会被表单变成 GET)

若要发送 put 和 delete 请求,则需要通过 spring 提供的过滤器 HiddenHttpMethodFilter,在 RESTful 部分会讲到

# 5、@RequestMapping 注解的 params 属性(了解)

@RequestMapping 注解的 params 属性通过请求的请求参数匹配请求映射

@RequestMapping 注解的 params 属性是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系

“param”:要求请求映射所匹配的请求必须携带 param 请求参数

“!param”:要求请求映射所匹配的请求必须不能携带 param 请求参数

“param=value”:要求请求映射所匹配的请求必须携带 param 请求参数且 param=value

“param!=value”:要求请求映射所匹配的请求必须携带 param 请求参数但是 param!=value

<a th:href="@{/test(username='admin',password=123456)">测试@RequestMapping的params属性-->/test</a><br>
@RequestMapping(
        value = {"/testRequestMapping", "/test"}
        ,method = {RequestMethod.GET, RequestMethod.POST}
        ,params = {"username","password!=123456"}
)
public String testRequestMapping(){
    return "success";
}

注:

若当前请求满足 @RequestMapping 注解的 value 和 method 属性,但是不满足 params 属性,此时页面回报错 400:Parameter conditions “username, password!=123456” not met for actual request parameters: username={admin}, password=

# 6、@RequestMapping 注解的 headers 属性(了解)

@RequestMapping 注解的 headers 属性通过请求的请求头信息匹配请求映射

@RequestMapping 注解的 headers 属性是一个字符串类型的数组,可以通过四种表达式设置请求头信息和请求映射的匹配关系

“header”:要求请求映射所匹配的请求必须携带 header 请求头信息

“!header”:要求请求映射所匹配的请求必须不能携带 header 请求头信息

“header=value”:要求请求映射所匹配的请求必须携带 header 请求头信息且 header=value

“header!=value”:要求请求映射所匹配的请求必须携带 header 请求头信息且 header!=value

若当前请求满足 @RequestMapping 注解的 value 和 method 属性,但是不满足 headers 属性,此时页面显示 404 错误,即资源未找到

# 7、SpringMVC 支持 ant 风格的路径

?:表示任意的单个字符

*:表示任意的 0 个或多个字符

**:表示任意的一层或多层目录

注意:在使用 ** 时,只能使用 /**/xxx 的方式,然后这么做就可以让:image-20220208004136814

也好使 (匹配的到)

# 8、SpringMVC 支持路径中的占位符(重点)⭐️

原始方式:/deleteUser?id=1

rest 方式:/deleteUser/1

SpringMVC 路径中的占位符常用于 RESTful 风格中,当请求路径中将某些数据通过路径的方式传输到服务器中,就可以在相应的 @RequestMapping 注解的 value 属性中通过占位符 {xxx} 表示传输的数据,在通过 @PathVariable 注解,将占位符所表示的数据赋值给控制器方法的形参

<a th:href="@{/testRest/1/admin}">测试路径中的占位符-->/testRest</a><br>
@RequestMapping("/testRest/{id}/{username}")
public String testRest(@PathVariable("id") String id, @PathVariable("username") String username){
    System.out.println("id:"+id+",username:"+username);
    return "success";
}
//最终输出的内容为-->id:1,username:admin

@PathVariable 注解让你设的那个 value 对应的占位符 (@RequestMapping 注解里面的) 自动赋值给之后的形参

如果不给那个占位符传值就会报错:

image-20220208005308104


# 四、SpringMVC 获取请求参数

# 1、通过 ServletAPI 获取

将 HttpServletRequest 作为控制器方法的形参,此时 HttpServletRequest 类型的参数表示封装了当前请求的请求报文的对象

DispatcherServlet (或者其他中间组件) 检测到我们这里的形参, 就会给我们传个实参,所以我们可以直接写个实参然后在里面用

@RequestMapping("/testParam")
public String testParam(HttpServletRequest request){
    String username = request.getParameter("username");
    String password = request.getParameter("password");
    System.out.println("username:"+username+",password:"+password);
    return "success";
}

如果 restful 方式就不能用这个了,因为传的只是值,根本就没有键名,所以这个行不通的

# 2、通过控制器方法的形参获取请求参数

在控制器方法的形参位置,设置和请求参数同名的形参,当浏览器发送请求,匹配到请求映射时,在 DispatcherServlet 中就会将请求参数赋值给相应的形参 (完全没必要用原生 servlet request 方式,这里 SpringMVC 已经帮我们封装好了)

<a th:href="@{/testParam(username='admin',password=123456)}">测试获取请求参数-->/testParam</a><br>
@RequestMapping("/testParam")
public String testParam(String username, String password){
    System.out.println("username:"+username+",password:"+password);
    return "success";
}

注:

若请求所传输的请求参数中有多个同名的请求参数,此时可以在控制器方法的形参中设置字符串数组或者字符串类型的形参接收此请求参数

若使用字符串数组类型的形参,此参数的数组中包含了每一个数据

若使用字符串类型的形参,此参数的值为每个数据中间使用逗号拼接的结果 (这我倒是没想到,因为原生 servlet request 方式 getParameter 只会获取第一个,这里是所有获取了并且逗号分开作为一整个字符串返回的)

# 3、@RequestParam

@RequestParam 是将__请求参数和控制器方法的形参创建映射关系__

@RequestParam 注解一共有三个属性:

  • value:指定为形参赋值的请求参数的参数名

  • required:设置是否必须传输此请求参数,默认值为 true

    • 若设置为 true 时,则当前请求必须传输 value 所指定的请求参数,若没有传输该请求参数,且没有设置 defaultValue 属性 (如果自己设置了个默认值在这种情况就不会报错了),则页面报错 400:Required String parameter ‘xxx’ is not present;
    • 若设置为 false,则当前请求不是必须传输 value 所指定的请求参数,若没有传输,则注解所标识的形参的值为 null
  • defaultValue:不管 required 属性值为 true 或 false,当 value 所指定的请求参数没有传输或传输的值为 "" 时,则使用默认值为形参赋值

这个用的比较多,再不确定传没传或者可能传一个空字符串时给他一个我们设置的默认值

# 4、@RequestHeader

@RequestHeader 是将请求__头信息和控制器方法的形参创建映射关系__

@RequestHeader 注解一共有三个属性:value、required、defaultValue,用法同 @RequestParam

image-20220208013224297

跟 @RequestParam 不一样 @RequestParam 计算不加直接就是那个形参名映射请求参数 (键名) 就行

你要是想要请求头的信息就必须是 @RequestHeader 了

# 5、@CookieValue

@CookieValue 是将 cookie 数据和控制器方法的形参创建映射关系

@CookieValue 注解一共有三个属性:value、required、defaultValue,用法同 @RequestParam

# 6、通过 POJO 获取请求参数

可以在控制器方法的形参位置设置一个实体类类型的形参,此时若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值

需要保证我们这个设置的 bean 类里面的属性名和请求参数里面的那些 name 值都一值就可以在 controller 方法里面直接放着歌 bean 类形参,springmvc (DispatcherServlet) 底层会帮我们传过来一个封装好的这个 bean 类的对象,然后这个对象里面所有的属性都会被对应的请求参数 name 传来的值被赋值,不过可能会出现乱码问题.

<form th:action="@{/testpojo}" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    性别:<input type="radio" name="sex" value=""><input type="radio" name="sex" value=""><br>
    年龄:<input type="text" name="age"><br>
    邮箱:<input type="text" name="email"><br>
    <input type="submit">
</form>
@RequestMapping("/testpojo")
public String testPOJO(User user){
    System.out.println(user);
    return "success";
} //注意在这里改编码是没什么用的,因为已经从servlet获取到了请求本身,要设置编码以防止乱码就只能在DispatcherServlet(servlet那)那里设置
//最终结果-->User{id=null, username='张三', password='123', age=23, sex='男', email='123@qq.com'}

image-20220208014902063

# 7、解决获取请求参数的乱码问题

解决获取请求参数的乱码问题,可以使用__SpringMVC 提供的编码过滤器 CharacterEncodingFilter (已经提供了,我们只需要在 web.xml 注册一下就行了,不用自己写)__,但是必须在 web.xml 中进行注册

过滤器比 servlet 更早执行,所以可以交给过滤器

(先是 servletContextListener (只监听我们的 servletContext 的创建和销毁所以和我们服务器本身开启和关闭一样只执行一次)-> 然后过滤器 (只要访问的请求地址满足过滤路径,都会触发过滤器所以用过滤器而不是监听器)-> 然后 servlet)

<!--配置springMVC的编码过滤器-->
<filter>
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param>
        <param-name>encoding</param-name>
        <param-value>UTF-8</param-value> // 别忘了这步!!!!!
    </init-param>
    <init-param>
        <param-name>forceResponseEncoding</param-name>
        <param-value>true</param-value> // 如果想要响应也是那个编码就需要这个,请求不需要设,源码里面的判断
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CharacterEncodingFilter</filter-name>
    <url-pattern>/*</url-pattern> //所有请求路径都要去(符合)过滤器(路径)
</filter-mapping>

image-20220208020830092

注:

SpringMVC 中处理编码的过滤器一定要配置到其他过滤器之前,否则无效


# 五、域对象共享数据

# 1、使用 ServletAPI 向 request 域对象共享数据

@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request){
    request.setAttribute("testScope", "hello,servletAPI");
    return "success";
}

# 2、使用 ModelAndView 向 request 域对象共享数据

@RequestMapping("/testModelAndView")
public ModelAndView testModelAndView(){
    /**
     * ModelAndView有Model和View的功能
     * Model主要用于向请求域共享数据
     * View主要用于设置视图,实现页面跳转
     */
    ModelAndView mav = new ModelAndView();
    //向请求域共享数据
    mav.addObject("testScope", "hello,ModelAndView");
    //设置视图,实现页面跳转
    mav.setViewName("success");   //这个就相当于是像我们之前那样方法直接返回那个页面的名字交给页面解析器加上前缀和后缀一样
    return mav;
}

方法必须将一个 ModelAndView 类的对象返回才可以让 DispatcherServlet 发现并解析

这种方法很重要,因为其他方法域对象共享数据不管怎么样其实内部都是会被 SpringMVC 封装到一个 ModelAndView 类的对象中

# 3、使用 Model 向 request 域对象共享数据

@RequestMapping("/testModel")
public String testModel(Model model){
    model.addAttribute("testScope", "hello,Model");
    return "success";
}

往 model 这个形参存的数据就是我们 (request) 域对象分享的数据

# 4、使用 map 向 request 域对象共享数据

@RequestMapping("/testMap")
public String testMap(Map<String, Object> map){
    map.put("testScope", "hello,Map");
    return "success";
}

往 map 这个形参存的数据就是我们 (request) 域对象分享的数据

# 5、使用 ModelMap 向 request 域对象共享数据

@RequestMapping("/testModelMap")
public String testModelMap(ModelMap modelMap){
    modelMap.addAttribute("testScope", "hello,ModelMap");
    return "success";
}

往 modelMap 这个形参存的数据就是我们 (request) 域对象分享的数据

# 6、Model、ModelMap、Map 的关系

Model、ModelMap、Map 类型的参数其实本质上都是 BindingAwareModelMap 类型的对象,所以底层操作都一样

public interface Model{}
public class ModelMap extends LinkedHashMap<String, Object> {} //LinkedHashMap实现了Map接口
public class ExtendedModelMap extends ModelMap implements Model {}
public class BindingAwareModelMap extends ExtendedModelMap {}

上面这些方式在底层都是 DispatcherServlet 调用我们那个方法然后按照我们要的参数给参数啊等等等,然后分析返回值,__不管怎么样最后都会__封装成一个 ModelandView 类的对象然后接着处理等等等然后解析模板 (前缀后缀等等等)

注意如果用原生 servlet 的话最后封装成的 ModelandView 类的对象的 view 属性是按照方法返回值赋值,但是用因为没用 model 所以 model 属性没值。其他方式都会往 model 属性存

# 7、向 session 域共享数据

@RequestMapping("/testSession")
public String testSession(HttpSession session){ // 注意这!直接形参接收session就行
    session.setAttribute("testSessionScope", "hello,session");
    return "success";
} // 虽然SpringMVC提供了让我们把数据存session的注解方式(其实就是先是共享到request域然后共享到session域中)但还是推荐使用原生

session 有钝化和活化 (之前 javaseIO 流有说过,钝化和活化就是 session 的序列化和反序列化), 所以服务器关闭之前的 session 还在

不过要是关闭了浏览器那么 cookie 默认是清除,那么 JSESSIONID 就消失了,匹配不上了 (虽然 session 可能还在,但已经无用了)

而 servletContext 则是服务器关闭就没了,服务器一开始才会有,所以 servletContext 可以共享数据,因为我们用的对象都是同一个 (每次服务器执行的时候)

# 8、向 application 域共享数据

@RequestMapping("/testApplication")
public String testApplication(HttpSession session){
	ServletContext application = session.getServletContext(); // 获取ServletContext有很多方式,这里就是用session获取
    application.setAttribute("testApplicationScope", "hello,application");
    return "success";
}

# 六、SpringMVC 的视图

SpringMVC 中的视图是 View 接口,视图的作用渲染数据,将模型 Model 中的数据展示给用户

SpringMVC 视图的种类很多,默认有转发视图和重定向视图

当工程引入 jstl 的依赖,转发视图会自动转换为 JstlView

若使用的视图技术为 Thymeleaf,在 SpringMVC 的配置文件中配置了 Thymeleaf 的视图解析器,由此视图解析器解析之后所得到的是 ThymeleafView

# 1、ThymeleafView

当__控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被 SpringMVC 配置文件中所配置的视图解析器解析__,视图名称拼接视图前缀和视图后缀所得到的最终路径,会__通过转发的方式实现跳转__

@RequestMapping("/testHello")
public String testHello(){
    return "hello";
}

# 2、转发视图

SpringMVC 中默认的转发视图是 InternalResourceView

SpringMVC 中创建转发视图的情况:

当控制器方法中所设置 (或者返回,反正最后都封装到 ModelAndView 类的对象的 view 属性里面) 的视图名称以 " forward: " 为前缀时创建 InternalResourceView 视图,此时的视图名称不会被 SpringMVC 配置文件中所配置的视图解析器解析,而是会将前缀 "forward:" 去掉,剩余部分作为最终路径通过转发的方式实现跳转 (forward)

例如 "forward:/",“forward:/employee”

@RequestMapping("/testForward")
public String testForward(){
    return "forward:/testHello";   // 这个就会去找当前 项目路径(application ontext)/testHello这个请求(可以重定向/转发到一个资源,也可以重定向/转发到到一个请求,这里就是相当于转发到另外一个请求叫做testHello然后看看Controller类里面有没有方法@RequestMapping注解的值匹配的上)
    
    //注意不会加上任何前缀和后缀,因为加上forward:就不会被SpringMVC配置文件中所配置的视图解析器解析所以就不会加
    
    //注意如果这里转发到一个具体页面(带有thymeleaf的)是行不通的,因为Html页面中有th标签,必须由Thymeleaf在服务器端解析后才能获取结果使用,而这种转发不是被pringMVC配置文件中所配置的视图解析器解析的,所以不行,只能转发到另外一个请求处理,那个请求处理可能就会是正常的给你资源的那个.(重定向更不行!!!因为是需要转发的!!比如说WEB-INF下的,只能通过转发让用户访问)
}

image-20220208141102285

这里其创建两个视图,一个是 InternalResourceView , 然后转发请求到上面的,产生了原本的 ThymeleafView 视图

# 3、重定向视图

SpringMVC 中默认的重定向视图是__RedirectView__

当控制器方法中所设置的视图名称以 " redirect: " 为前缀时,创建 RedirectView 视图,此时的视图名称不会被 SpringMVC 配置文件中所配置的视图解析器解析,而是会将前缀 "redirect:" 去掉,剩余部分作为最终路径通过重定向的方式实现跳转

一般都是用这个而不是上面的转发,因为那个转发也访问不了页面或者资源,用这个还可以改变地址

这个改变 url 一般是在当前的请求操作 (比如说删除,添加啥啥啥的等等等) 执行完给用户调准到别的请求 (然后这个请求可能做任何… 一般是展示页面,所以就正常的能使用 thymeleaf 解析器的)

重定向还防止用户重复提交表单问题

例如 "redirect:/",“redirect:/employee”

@RequestMapping("/testRedirect")
public String testRedirect(){
    return "redirect:/testHello";
}

注:

重定向视图在解析时,会先将 redirect: 前缀去掉,然后会判断剩余部分是否以 / 开头,若是则会自动拼接上下文路径

# 4、视图控制器 view-controller

当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用 view-controller 标签进行表示

<!--
	path:设置处理的请求地址
	view-name:设置请求地址所对应的视图名称
-->
<mvc:view-controller path="/testView" view-name="success"></mvc:view-controller>
//如果你只是想让请求请求到这个资源(没有其他操作)那就直接SpringMVC配置文件里面配置这个就行了,就不需要像我们之前一样创建一个类然后写个映射请求的那个处理请求的方法

注:

当 SpringMVC 中设置任何一个 view-controller 时,其他控制器中的请求映射将全部失效,不想这样的话此时需要在 SpringMVC 的核心配置文件中设置开启 mvc 注解驱动的标签: <mvc:annotation-driven />

这个 <mvc:annotation-driven /> 很重要!!!

image-20220208145934042


# 七、RESTful

# 1、RESTful 简介

REST:Representational State Transfer,表现层资源状态转移。

# a > 资源

资源是一种看待服务器的方式,即,将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念。因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西,可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解。与面向对象设计类似,资源是以名词为核心来组织的,首先关注的是名词。一个资源可以由一个或多个 URI 来标识。URI 既是资源的名称,也是资源在 Web 上的地址。对某个资源感兴趣的客户端应用,可以通过资源的 URI 与其进行交互。

# b > 资源的表述

资源的表述是一段对于资源在某个特定时刻的状态的描述。可以在客户端 - 服务器端之间转移(交换)。资源的表述可以有多种格式,例如 HTML/XML/JSON/ 纯文本 / 图片 / 视频 / 音频等等。资源的表述格式可以通过协商机制来确定。请求 - 响应方向的表述通常使用不同的格式。

# c > 状态转移

状态转移说的是:在客户端和服务器端之间转移(transfer)代表资源状态的表述。通过转移和操作资源的表述,来间接实现操作资源的目的。

核心:请求地址 URL 不变,根据请求方式的不同,对操作资源的方式进行区分

URI,统一资源标识符,就是用一个标识符,标识了客户端对该资源的各种操作。只要是对同一个资源的操作,请求地址都用同一个 URI只是用 HTTP 的不同请求方式来区分对该资源的不同具体操作

# 2、RESTful 的实现

具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。

它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。

REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。

操作 传统方式 REST 风格
查询操作 getUserById?id=1 user/1–>get 请求方式
保存操作 saveUser user–>post 请求方式
删除操作 deleteUser?id=1 user/1–>delete 请求方式
更新操作 updateUser user–>put 请求方式

然后根据你这个发送请求的信息等进行操作,比如说表单如果 Method 为 POST 请求路径就是 user, 然后我们之后在这个请求路径映射的注解标注的方法里判断什么方法,然后看出是 POST 然后就各种获取数据啊等等等

注意! user/1user 这两个不一样,前面那个传了参 (回顾之前), 我们用占位符来代表的,如果不传参数是匹配不上的!

# 3、HiddenHttpMethodFilter

由于浏览器只支持发送 get 和 post 方式的请求,那么该如何发送 put 和 delete 请求呢?

SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求

HiddenHttpMethodFilter 处理 put 和 delete 请求的条件:

  • a> 当前请求的请求方式必须为 post

  • b > 当前请求必须传输请求参数_method

满足以上条件,HiddenHttpMethodFilter 过滤器就会将当前请求的请求方式转换为请求参数_method 的值,因此请求参数_method 的值才是最终的请求方式

在 web.xml 中注册 HiddenHttpMethodFilter

<filter>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>HiddenHttpMethodFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>

注:

目前为止,SpringMVC 中提供了两个过滤器:CharacterEncodingFilter 和 HiddenHttpMethodFilter

在 web.xml 中注册时,必须先注册 CharacterEncodingFilter,再注册 HiddenHttpMethodFilter

原因:

  • 在 CharacterEncodingFilter 中通过 request.setCharacterEncoding (encoding) 方法设置字符集的

  • request.setCharacterEncoding (encoding) 方法要求前面不能有任何获取请求参数的操作

  • 而 HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作:

String paramValue = request.getParameter(this.methodParam);


请求方式被替换掉了,写个隐藏表单,里面写个name为_method,value为put或delete,然后表单外面的提交方式必须设置为post,就能被过滤器过滤到,通过这个方法把提交方式替换

就是将请求方式作为参数传给后端,过滤器会将参数指定的请求方式作为请求的真正方式 

![image-20220208155957707](https://raw.githubusercontent.com/HarryQu1229/image-host/main/notes-img/image-20220208155957707.png)



---

八、RESTful案例
===========

### 1、准备工作

和传统 CRUD 一样,实现对员工信息的增删改查。

*   搭建环境

*   准备实体类

```jav
package com.atguigu.mvc.bean;

public class Employee {

   private Integer id;
   private String lastName;

   private String email;
   //1 male, 0 female
   private Integer gender;
   
   public Integer getId() {
      return id;
   }

   public void setId(Integer id) {
      this.id = id;
   }

   public String getLastName() {
      return lastName;
   }

   public void setLastName(String lastName) {
      this.lastName = lastName;
   }

   public String getEmail() {
      return email;
   }

   public void setEmail(String email) {
      this.email = email;
   }

   public Integer getGender() {
      return gender;
   }

   public void setGender(Integer gender) {
      this.gender = gender;
   }

   public Employee(Integer id, String lastName, String email, Integer gender) {
      super();
      this.id = id;
      this.lastName = lastName;
      this.email = email;
      this.gender = gender;
   }

   public Employee() {
   }
}
  • 准备 dao 模拟数据

    package com.atguigu.mvc.dao;
    
    import java.util.Collection;
    import java.util.HashMap;
    import java.util.Map;
    
    import com.atguigu.mvc.bean.Employee;
    import org.springframework.stereotype.Repository;
    
    @Repository
    public class EmployeeDao {
    
       private static Map<Integer, Employee> employees = null;
       
       static{
          employees = new HashMap<Integer, Employee>();
    
          employees.put(1001, new Employee(1001, "E-AA", "aa@163.com", 1));
          employees.put(1002, new Employee(1002, "E-BB", "bb@163.com", 1));
          employees.put(1003, new Employee(1003, "E-CC", "cc@163.com", 0));
          employees.put(1004, new Employee(1004, "E-DD", "dd@163.com", 0));
          employees.put(1005, new Employee(1005, "E-EE", "ee@163.com", 1));
       }
       
       private static Integer initId = 1006;
       
       public void save(Employee employee){
          if(employee.getId() == null){
             employee.setId(initId++);
          }
          employees.put(employee.getId(), employee);
       }
       
       public Collection<Employee> getAll(){
          return employees.values();
       }
       
       public Employee get(Integer id){
          return employees.get(id);
       }
       
       public void delete(Integer id){
          employees.remove(id);
       }
    }

# 2、功能清单

功能 URL 地址 请求方式
访问首页√ / GET
查询全部数据√ /employee GET
删除√ /employee/2 DELETE
跳转到添加数据页面√ /toAdd GET
执行保存√ /employee POST
跳转到更新数据页面√ /employee/2 GET
执行更新√ /employee PUT

# 3、具体功能:访问首页

# a > 配置 view-controller
<mvc:view-controller path="/" view-name="index"/>
# b > 创建页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8" >
    <title>Title</title>
</head>
<body>
<h1>首页</h1>
<a th:href="@{/employee}">访问员工信息</a>
</body>
</html>

# 4、具体功能:查询所有员工数据

# a > 控制器方法
@RequestMapping(value = "/employee", method = RequestMethod.GET)
public String getEmployeeList(Model model){
    Collection<Employee> employeeList = employeeDao.getAll();
    model.addAttribute("employeeList", employeeList);     
    return "employee_list";
}

所以一般往作用域存的就是从数据库取出来的数据,当然也可以存其他的

然后看到底是什么操作来决定存在于什么域中,如果 request 就足够因为我们是针对每次查询来返回什么什么那就 request 作用域足够,不用存 session 啊什么什么的

注意这里我认为是前后端不分离方式,如果分离了直接打包为 json 存到 response 里面让前端接收且操作

# b > 创建 employee_list.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Employee Info</title>
    <script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
</head>
<body>

    <table border="1" cellpadding="0" cellspacing="0" style="text-align: center;" id="dataTable">
        <tr>
            <th colspan="5">Employee Info</th>
        </tr>
        <tr>
            <th>id</th>
            <th>lastName</th>
            <th>email</th>
            <th>gender</th>
            <th>options(<a th:href="@{/toAdd}">add</a>)</th>
        </tr>
        <tr th:each="employee : ${employeeList}">
            <td th:text="${employee.id}"></td>
            <td th:text="${employee.lastName}"></td>
            <td th:text="${employee.email}"></td>
            <td th:text="${employee.gender}"></td>
            <td>
                <a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${employee.id}}">delete</a>
                <a th:href="@{'/employee/'+${employee.id}}">update</a>
            </td>
        </tr>
    </table>
</body>
</html>

# 5、具体功能:删除

# a > 创建处理 delete 请求方式的表单
<!-- 作用:通过超链接控制表单的提交,将post请求转换为delete请求 -->
<form id="delete_form" method="post">
    <!-- HiddenHttpMethodFilter要求:必须传输_method请求参数,并且值为最终的请求方式 -->
    <input type="hidden" name="_method" value="delete"/>
</form>
# b > 删除超链接绑定点击事件

引入 vue.js

<script type="text/javascript" th:src="@{/static/js/vue.js}"></script>

删除超链接

<a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${employee.id}}">delete</a>

通过 vue 处理点击事件

<script type="text/javascript">
    var vue = new Vue({
        el:"#dataTable",
        methods:{
            //event表示当前事件
            deleteEmployee:function (event) {
                //通过id获取表单标签
                var delete_form = document.getElementById("delete_form");
                //将触发事件的超链接的href属性为表单的action属性赋值
                delete_form.action = event.target.href;
                //提交表单
                delete_form.submit();
                //阻止超链接的默认跳转行为
                event.preventDefault();
            }
        }
    });
</script>
# c > 控制器方法
@RequestMapping(value = "/employee/{id}", method = RequestMethod.DELETE)
public String deleteEmployee(@PathVariable("id") Integer id){
    employeeDao.delete(id);
    return "redirect:/employee";
}

image-20220208165543871

需要配置这个标签不然不行

配置了这个后:

  • 首先静态资源 (比如说 Vue.js) 会先被 SpringMVC 进行处理,也就是前端控制器的处理
  • 如果在前端控制器 (Controller) 找不到相对应的请求映射他就会交给默认的 servlet 处理 (也就是我们这里标签设置的), 如果默认的 servlet 能访问到资源那就访问资源,如果找不到相对应的资源,还是 404

如果不配置这个标签就也让 DispatcherServlet 来处理 (这是因为当初我们 web.xml 把这个 DsipatcherServlet 设为给所有请求路径开头为 "/" 的,也就是除了 jsp 文件所有的,包括静态资源 (jsp 文件特殊的,自己会有 servlet 来处理)) 然后控制器找映射对应的,找不到直接报错了,没有默认的 servlet 来处理 (找).

任何请求 DispatcherServlet (其实也就是 Controller 那些真正映射的) 如果找不到对应的,那么如果我们设置这个标签,那就会让默认的 servlet 来找对应的请求或者资源.

tomcat 服务器的 web.xml (不是我们项目的,继承关系,如果有冲突就近原则) 里面开启的默认 servlet (会被我们项目的 web.xml 里面配置的 SpringMVC 的 DispatcherServlet 覆盖,所以需要我们写那个标签让默认的 servlet 在我们的 DispatcherServlet 找不到时也用上):image-20220208172912691

SpringMVC 配置文件:

image-20220208173702196

如果不配置下面那个,所有请求都会被默认 servlet 处理,但是默认 servlet 处理不了,只能找 (处理) 静态资源

如果不配置上面,所有请求都会被 DispatcherServlet 处理,但是 DispatcherServlet 处理不了一些静态资源

# 6、具体功能:跳转到添加数据页面

# a > 配置 view-controller
<mvc:view-controller path="/toAdd" view-name="employee_add"></mvc:view-controller>
# b > 创建 employee_add.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Add Employee</title>
</head>
<body>

<form th:action="@{/employee}" method="post">
    lastName:<input type="text" name="lastName"><br>
    email:<input type="text" name="email"><br>
    gender:<input type="radio" name="gender" value="1">male
    <input type="radio" name="gender" value="0">female<br>
    <input type="submit" value="add"><br>
</form>

</body>
</html>

# 7、具体功能:执行保存

# a > 控制器方法
@RequestMapping(value = "/employee", method = RequestMethod.POST)
public String addEmployee(Employee employee){
    employeeDao.save(employee);
    return "redirect:/employee";
}

# 8、具体功能:跳转到更新数据页面

# a > 修改超链接
<a th:href="@{'/employee/'+${employee.id}}">update</a>
# b > 控制器方法
@RequestMapping(value = "/employee/{id}", method = RequestMethod.GET)
public String getEmployeeById(@PathVariable("id") Integer id, Model model){
    Employee employee = employeeDao.get(id);
    model.addAttribute("employee", employee);
    return "employee_update";
}
# c > 创建 employee_update.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Update Employee</title>
</head>
<body>

<form th:action="@{/employee}" method="post">
    <input type="hidden" name="_method" value="put">
    <input type="hidden" name="id" th:value="${employee.id}">
    lastName:<input type="text" name="lastName" th:value="${employee.lastName}"><br>
    email:<input type="text" name="email" th:value="${employee.email}"><br>
    <!--
        th:field="${employee.gender}"可用于单选框或复选框的回显
        若单选框的value和employee.gender的值一致,则添加checked="checked"属性
    -->
    gender:<input type="radio" name="gender" value="1" th:field="${employee.gender}">male
    <input type="radio" name="gender" value="0" th:field="${employee.gender}">female<br>
    <input type="submit" value="update"><br>
</form>

</body>
</html>

# 9、具体功能:执行更新

# a > 控制器方法
@RequestMapping(value = "/employee", method = RequestMethod.PUT)
public String updateEmployee(Employee employee){
    employeeDao.save(employee);
    return "redirect:/employee";
}

# 八、HttpMessageConverter

HttpMessageConverter,报文信息转换器,将请求报文转换为 Java 对象,或__将 Java 对象转换为响应报文__

HttpMessageConverter 提供了__两个注解__和__两个类型__:@RequestBody,@ResponseBody,RequestEntity,

ResponseEntity

注意页面显示的都是服务器 response 的响应体内容,不管是怎么请求的 -> 服务器接收然后可能转发可能重定向 -> 返回相应,然后响应携带响应体,而这个响应体就是显示到页面上的内容

所以原生的 response.getWriter ().print (“XXXX”) 就相当于是返回一个响应体为 XXXX 的响应然后页面显示的就是 XXXX

# 1、@RequestBody

@RequestBody 可以获取__请求体__,需要在控制器方法设置一个形参,使用 @RequestBody 进行标识,当前__请求的请求体就会为当前注解所标识的形参赋值__

<form th:action="@{/testRequestBody}" method="post">
    用户名:<input type="text" name="username"><br>
    密码:<input type="password" name="password"><br>
    <input type="submit">
</form>
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody String requestBody){ //需要是个String类型的,此时这个requestBody形参存的就是请求体
    System.out.println("requestBody:"+requestBody);
    return "success";
}

输出结果:

requestBody:username=admin&password=123456

别忘了请求体是 POST 才有,GET 没有请求体

# 2、RequestEntity

RequestEntity 封装__整个请求报文__的一种类型,需要在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过 getHeaders () 获取请求头信息,通过 getBody () 获取请求体信息

@RequestMapping("/testRequestEntity")
public String testRequestEntity(RequestEntity<String> requestEntity){ //这个requestEntity就是整个请求报文对象,可以调方法获取不同的
    System.out.println("requestHeader:"+requestEntity.getHeaders());
    System.out.println("requestBody:"+requestEntity.getBody());
    return "success";
}

输出结果:
requestHeader:[host:“localhost:8080”, connection:“keep-alive”, content-length:“27”, cache-control:“max-age=0”, sec-ch-ua:"" Not A;Brand";v=“99”, “Chromium”;v=“90”, “Google Chrome”;v=“90"”, sec-ch-ua-mobile:"?0", upgrade-insecure-requests:“1”, origin:“http://localhost:8080”, user-agent:“Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/90.0.4430.93 Safari/537.36”]
requestBody:username=admin&password=123

# 3、@ResponseBody⭐️

@ResponseBody 用于标识一个控制器方法,可以将该方法的_返回值_直接作为响应报文的响应体响应到浏览器

@RequestMapping("/testResponseBody")
@ResponseBody
public String testResponseBody(){
    return "success";
}

结果:浏览器页面显示 success, 而不是被 thymeleaf 模板解析 (作为视图名称) 加上前缀和后缀然后跳转到对应的页面

注意这里这个方法如果返回的是个对象就会直接报错,要不想这样,就需要把这个对象转换为 json 对象,参考以下⬇️

# 4、SpringMVC 处理 json

@ResponseBody 处理 json 的步骤:

a > 导入 jackson 的依赖

<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
    <version>2.12.1</version>
</dependency>

b> 在 SpringMVC 的核心配置文件中开启 mvc 的注解驱动,此时在 HandlerAdaptor 中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,可以将响应到浏览器的 Java 对象转换为 Json 格式的字符串

<mvc:annotation-driven /> //这就我们之前viewController时候用到,然后在处理静态资源开启默认servlet也用到了

c > 在处理器方法上使用 @ResponseBody 注解进行标识

这个很重要,平时我们都是用这个然后传个 json 对象 (String)

d > 将 Java 对象直接作为控制器方法的返回值返回,就会自动转换为 Json 格式的字符串

@RequestMapping("/testResponseUser")
@ResponseBody
public User testResponseUser(){
    return new User(1001,"admin","123456",23,"男");
}

浏览器的页面中展示的结果:

我们多做的不多,只是加了个 jackson 依赖,就好使了,其他步骤我们之前已经展示过

一般 java 对象转换为 json 会成为 json 对象

java 里面的 list 转换为 json 会成为 json 数组

# 5、SpringMVC 处理 ajax

a > 请求超链接:

<div id="app">
	<a th:href="@{/testAjax}" @click="testAjax">testAjax</a><br>
</div>

b > 通过 vue 和 axios 处理点击事件:

注意 ajax 本身就是页面不刷新与服务器进行交互, 也就是说我们服务器中不能使用转发和重定向到一个页面资源,只能响应浏览器数据 (也就是 @ResponseBody 注解的方法 (当然也有需要 @RequestMapping 来匹配请求地址), 然后方法里面返回一个字符串或者对象 (只要我们装了依赖然后那个注解 (看上面), 就会让 springmvc 底层帮给我们把对象类型的返回值转换成 json 对象 / 数据)

<script type="text/javascript" th:src="@{/static/js/vue.js}"></script>
<script type="text/javascript" th:src="@{/static/js/axios.min.js}"></script>
<script type="text/javascript">
    var vue = new Vue({
        el:"#app",
        methods:{
            testAjax:function (event) {
                axios({
                    method:"post",
                    url:event.target.href,
                    params:{
                        username:"admin",
                        password:"123456"
                    }
                }).then(function (response) {
                    alert(response.data);
                });
                event.preventDefault();
            }
        }
    });
</script>

c > 控制器方法:

@RequestMapping("/testAjax")
@ResponseBody
public String testAjax(String username, String password){
    System.out.println("username:"+username+",password:"+password);
    return "hello,ajax";
}	

# 6、@RestController 注解

@RestController 注解是 springMVC 提供的一个复合注解,标识在控制器的类上,就相当于

  • 为类添加了 @Controller 注解,
  • 并且为其中的每个方法添加了 @ResponseBody 注解

实在是因为之后 @ResponseBody 用了太多,instead of using this on every method, we can just use @RestController on 类 itself

# 7、ResponseEntity

ResponseEntity 用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文

这个的目的就是文件下载 (看以下!)


# 九、文件上传和下载

# 1、文件下载

使用 ResponseEntity 实现下载文件的功能

@RequestMapping("/testDown")
public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException {
    //获取ServletContext对象
    ServletContext servletContext = session.getServletContext();
    //获取服务器中文件的真实路径
    String realPath = servletContext.getRealPath("/static/img/1.jpg");
    //创建输入流
    InputStream is = new FileInputStream(realPath);
    //创建字节数组
    byte[] bytes = new byte[is.available()]; //is.available()获取is变量存的输入流的字节总数量
    //将流读到字节数组中
    is.read(bytes);  // 一般写到while循环里去读,这里直接把所有的放到字节数组里了,可能会很慢
    //创建HttpHeaders对象设置响应头信息
    MultiValueMap<String, String> headers = new HttpHeaders();
    //设置要下载方式以及下载文件的名字
    headers.add("Content-Disposition", "attachment;filename=1.jpg");
    //设置响应状态码
    HttpStatus statusCode = HttpStatus.OK;
    //创建ResponseEntity对象
    ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);   // 注意传的参数->读完流的数组,http响应头信息,响应状态码 构建一个responseEntity对象
    //关闭输入流
    is.close();
    return responseEntity;
}
  • servletContext.getRealPath 方法获取的是获取当前服务器的部署路径 (我们当前路径然后部署到 tomcat 服务器上的路径)
  • servletContext.getRealPath("/static/img/1.jpg") 指的就是那个字符串所对应的文件在当前服务器的路径

image-20220209144042011

那个我们项目才是真正部署到服务器的 war 包,所以在服务器运行的时候是从他那自己的路径来找资源而不是我们写项目在 idea 里面那些路径

所以上面这个的返回结果存的就是这个路径:

image-20220209145622975

意思就是你用 tomcat 访问 target 里的东西 路径也是根据这个来的


  • headers.add("Content-Disposition", "attachment;filename=1.jpg"); 这个方法定义了下载的文件的名称以及下载方式 (为 attachment), 有了这个在响应头里浏览器才知道是要下载,所以必须设置这个!

  • 这里返回的 responseEntity 对象就代表了整个响应报文 (相当于是自己定制一个响应报文返回给我们浏览器)

# 2、文件上传

文件上传要求 form 表单的请求方式必须为 post并且添加属性 enctype=“multipart/form-data”

image-20220209145933574

这么做当前表单的__所有的数据__不会再以 name=value&name=value 方式传递给服务器了

而是通过二进制的方式传给服务器,这样我门才可以接收到我们的文件

SpringMVC 中将上传的文件封装到 MultipartFile 对象中,通过此对象可以获取文件相关信息

上传步骤:

a > 添加依赖:

需要 commons.fileupload 依赖,上传文件必须要这个!

下载文件用不上这个

<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
    <groupId>commons-fileupload</groupId>
    <artifactId>commons-fileupload</artifactId>
    <version>1.3.1</version>
</dependency>

b > 在 SpringMVC 的配置文件中添加配置:

<!--必须通过文件解析器的解析才能将文件转换为MultipartFile对象-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"></bean>

这样子我们才可以在控制器方法中接收 MultipartFile 类型的对象 (形参)

注意这里就正常配置文件中注册 bean, 但是下面我们用的时候不需要给参数 @Autowired, 个人认为 springmvc 底层已经 @Autowired 或者 getBean 方式赋值然后传给我们的, 所以这里记得要配置这个 id 对应着 springmvc 底层不管哪里使用的反正那个属性就是 multipartResolver 所以我们 id 也必须是 multipartResolver 让他对应上好找到 (springmvc 就是通过 id 获取的)

springMVC 底层:

this.multipartResolver = context.getBean(MULTIPART_RESOLVER_BEAN_NAME, MultipartResolver.class);

c > 控制器方法:

@RequestMapping("/testUp")
public String testUp(MultipartFile photo, HttpSession session) throws IOException {
    
    //获取上传的文件的文件名
    String fileName = photo.getOriginalFilename(); //获取原来用户上传文件时的文件名
    //处理文件重名问题 (要是用户上传同名文件会覆盖之前上传的同名文件)
    String suffix = fileName.substring(fileName.lastIndexOf("."));
    fileName = UUID.randomUUID().toString() + suffix;
    
    //获取服务器中photo目录的路径
    ServletContext servletContext = session.getServletContext();
    String photoPath = servletContext.getRealPath("photo"); //我们想要存的路径
    File file = new File(photoPath); //用来判断存不存在这个路径,不存在建一个
    if(!file.exists()){
        file.mkdir();
    }
    String finalPath = photoPath + File.separator + fileName; //就是我们文件的上传路径(带上本身的文件名)
    //实现上传功能
    photo.transferTo(new File(finalPath));// 把里面内容都传进去我们上面写好的那个路径的文件里面去
    return "success"; //注意上面传的是个File对象才行
}

MultipartFile photo 这个形参其实就是 springmvc 将那个将 name 为 photo 的表单中的文件 (名字必须一致!) 给包装成了个 MultipartFile 类型的对象供我们使用,这个对象里面包含了各种方法可以获取到上传文件的各种信息以及我们可以做的操作等等等

文件上传的步骤也就是先配置好,然后先读再写就行

注意!!!最终上传的地方还是服务器里面的那个我们设的路径的文件里面,因为我们获取的 getRealPath 什么的都是获取服务器中的路径


# 十、拦截器

# 1、拦截器的配置

SpringMVC 中的拦截器用于拦截控制器方法的执行

也就是 DispatcherServlet 找到对应的请求映射的服务器的方法,然后调用控制器方法,在那个方法执行前和执行后可加拦截器做一些操作

SpringMVC 中的拦截器需要实现 HandlerInterceptor

SpringMVC 的拦截器必须在 SpringMVC 的配置文件中进行配置:

<mvc:interceptor>
<bean class="com.atguigu.interceptor.FirstInterceptor"></bean>
</mvc:interceptor>
或者
<mvc:interceptor>
<ref bean="firstInterceptor"></ref> //这个记得给那个我们写的拦截器类给加@Component注解让这个类注册, 并且在springmvc配置文件中开启自动扫描注解找的到那个我们写的拦截器类
</mvc:interceptor>
<!-- 以上两种配置方式都是 对DispatcherServlet所处理的(如果压根没有匹对的控制器方法那根本就不会有拦截器) 所有的请求进行拦截 -->
<mvc:interceptor>
    <mvc:mapping path="/**"/>  如果只有一个*代表只是那一层的会匹配上,两个*代表任何层
    <mvc:exclude-mapping path="/testRequestEntity"/>
    <ref bean="firstInterceptor"></ref>
</mvc:interceptor>
<!-- 
	以上配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求,通过mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求
-->

image-20220209160926022

注意写在配置文件中的 viewController 其实底部也是有跟我们呢 controller 里面写控制器方法的,都一样的,所以配置文件中写 viewController 也然后去那个对应的路径也会触发拦截器

注意那个 preHandler 拦截器方法,只有 return true 才会让我们想执行的那个控制器方法执行,不然不会放行

# 2、拦截器的三个抽象方法

SpringMVC 中的拦截器有三个抽象方法:

  • preHandle:控制器方法执行之前执行 preHandle (),其 boolean 类型的返回值表示是否拦截或放行,返回 true 为放行,即调用控制器方法;返回 false 表示拦截,即不调用控制器方法

  • postHandle:控制器方法执行之后执行 postHandle ()

  • afterComplation:分析控制器方法返回最终封装成 modelAndView 的那个对象用来处理完视图和模型数据,渲染视图完毕之后才执行 afterComplation ()

# 3、多个拦截器的执行顺序

a > 若每个拦截器的 preHandle () 都返回 true

此时多个拦截器的执行顺序和拦截器在 SpringMVC 的配置文件的配置顺序有关:

preHandle () 会按照配置的顺序执行,而 postHandle () 和 afterCompletion () 会按照配置的反序执行

这里,配置文件中,FirstInterceptor 先配置的,然后配置的 SecondInterceptorimage-20220209162700514

b > 若某个拦截器的 preHandle () 返回了 false

preHandle ()(返回 false 的那个) 和它之前的拦截器的 preHandle () 都会执行,所有拦截器的 postHandle () 都不执行,返回 false 的拦截器之前的拦截器的 afterComplation () 会执行


# 十一、异常处理器

# 1、基于配置的异常处理

SpringMVC 提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver

HandlerExceptionResolver 接口的实现类有:

  • DefaultHandlerExceptionResolver (SpringMVC 默认的异常处理器,就是我们写的控制器方法执行时出现异常这个默认就会帮我们重新生成一个 modelAndView 对象用来解析放到页面上) 和
  • SimpleMappingExceptionResolver (这个给我们用的就是出现了什么什么异常,我们给他设置一个新的视图名称 (然后就会解析这个加前缀后缀跳转到对应的页面,当然__这是我们没给任何前缀 (redirect:/forward:) 的情况__))

SpringMVC 提供了自定义的异常处理器 SimpleMappingExceptionResolver,使用方式:

<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <property name="exceptionMappings">
        <props>
        	<!--
        		properties的键表示处理器方法执行过程中出现的异常
        		properties的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面
        	-->
            <prop key="java.lang.ArithmeticException">error</prop>
        </props>
    </property>
    <!--
    	exceptionAttribute属性设置一个属性名,将出现的异常信息在请求域中进行共享
下面就是把这个异常信息存入请求作用域中,然后在请求作用域里面ex就是键名,异常信息就是指,然后我们可以显示用thymeleaf显示到页面
    -->
    <property name="exceptionAttribute" value="ex"></property>
</bean>

这样,在__任何__控制器方法中出现了上面设置的 java.lang.ArithmeticException 异常,就会跳转到我们设置的 error 页面 (看上面我们写的值)

# 2、基于注解的异常处理

//@ControllerAdvice将当前类标识为异常处理的组件(这个还省得我们写@Controller)
@ControllerAdvice
public class ExceptionController {

    //@ExceptionHandler用于设置所标识方法处理的异常
    @ExceptionHandler(ArithmeticException.class)
    //ex表示当前请求处理中出现的异常对象
    public String handleArithmeticException(Exception ex, Model model){
        model.addAttribute("ex", ex); //将异常保存到请求作用域,这样页面用thymeleaf可以获取到然后用方法(或者什么表达式什么的)展示到页面
        return "error";
    }

}

这个就是如果在__任何__控制器方法中出现了上面设置的 java.lang.ArithmeticException 异常,就会执行这个 @ExceptionHandler (ArithmeticException.class) 所注解的方法作为新的控制器方法,然后这时候我们就正常操作返回一个 String 代表视图名称,留着解析加前缀后缀然后跳转到对应页面

注意上方这个方法里面形参可接收一个 Exception 类型的参数 (对象), 也就代表了那个异常信息 (我们设的 ArithmeticException 异常报的信息)


# 十二、注解配置 SpringMVC

使用配置类和注解代替 web.xml 和 SpringMVC 配置文件的功能

# 1、创建初始化类,代替 web.xml

在 Servlet3.0 环境中,容器会在类路径中查找实现 javax.servlet.ServletContainerInitializer 接口的类,如果找到的话就用它来配置 Servlet 容器 (比如说 tomcat 服务器)。
Spring 提供了这个接口的实现,名为 SpringServletContainerInitializer,这个类反过来又会查找实现 WebApplicationInitializer 的类并将配置的任务交给它们来完成。Spring3.2 引入了一个便利的 WebApplicationInitializer 基础实现,名为 AbstractAnnotationConfigDispatcherServletInitializer,当我们的类扩展了 AbstractAnnotationConfigDispatcherServletInitializer 并将其部署到 Servlet3.0 容器的时候,容器会自动发现它,并用它来配置 Servlet 上下文。

public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * 指定spring的配置类
     * @return
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[]{SpringConfig.class}; //这个SpringConfig也是我们自己建的类,在这个SpringConfig里面我们可以配置原本在Spring配置文件的那些东西
    }

    /**
     * 指定SpringMVC的配置类
     * @return
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class}; //这个WebConfig也是我们自己建的类,在这个WebConfig里面我们可以配置原本在SpringMVC配置文件的那些东西
    }

    /**
     * 指定DispatcherServlet的映射规则,即url-pattern
     * @return
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"}; // 就跟之前web.xml里面那样配置一样,除了.jsp文件所有的项目路径下的请求都会跟被DispatcherServlet请求映射条件映射上,启动我们的DispatcherServlet
    }

    /**
     * 添加过滤器
     * @return
     */
    @Override
    protected Filter[] getServletFilters() {
        CharacterEncodingFilter encodingFilter = new CharacterEncodingFilter();
        encodingFilter.setEncoding("UTF-8");
        encodingFilter.setForceRequestEncoding(true);
        HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
        return new Filter[]{encodingFilter, hiddenHttpMethodFilter};
    }
}

上面那些方法都是返回的一些各种类型的数组,其实就代表了我们创建自己想要的配置项然后放到数组里面 return 出去交给 Spring 还是什么的处理

# 2、创建 SpringConfig 配置类,代替 spring 的配置文件

@Configuration
public class SpringConfig {
	//ssm整合之后,spring的配置信息写在此类中
}

# 3、创建 WebConfig 配置类,代替 SpringMVC 的配置文件

image-20220209172925295

@Configuration
//1. 扫描组件
@ComponentScan("com.atguigu.mvc.controller")
//5. 开启MVC注解驱动
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer { //注意这个接口为了提供方法让我们实现,这些方法就是用来配置一些不是bean对象的一些功能

    4. //使用默认的servlet处理静态资源
    @Override
    public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
        configurer.enable();
    }

    6. //配置文件上传解析器
    @Bean 注意这个@Bean!因为我们需要的就是这个类的对象被创建出来注册让SpringMVC底层使用
    public CommonsMultipartResolver multipartResolver(){
        return new CommonsMultipartResolver();
    }

    8. //配置拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        FirstInterceptor firstInterceptor = new FirstInterceptor(); //我们自己写的拦截器
        registry.addInterceptor(firstInterceptor).addPathPatterns("/**"); //记得在拦截器中/*不是代表所有,而是只是一层的所有,可能多层,需要/**
    }
    
    3. //配置视图控制
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index"); //index页面也是我们写的
    }
    
    7. //配置异常映射
        -可以像springmvc.xml之前那样配置bean在这里写个bean方式
        -也可以直接通过下面这个方法来实现
    @Override
    public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
        SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();   //别忘了 这个类本来就有
        Properties prop = new Properties();
        prop.setProperty("java.lang.ArithmeticException", "error");
        //设置异常映射
        exceptionResolver.setExceptionMappings(prop);
        //设置共享异常信息的键
        exceptionResolver.setExceptionAttribute("ex");
        resolvers.add(exceptionResolver);
    }

    2. //配置生成模板解析器
    @Bean
    public ITemplateResolver templateResolver() {
        WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
        // ServletContextTemplateResolver需要一个ServletContext作为构造参数,可通过WebApplicationContext 的方法获得
        ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(
                webApplicationContext.getServletContext());
        templateResolver.setPrefix("/WEB-INF/templates/");
        templateResolver.setSuffix(".html");
        templateResolver.setCharacterEncoding("UTF-8");
        templateResolver.setTemplateMode(TemplateMode.HTML);
        return templateResolver;
    }

    //生成模板引擎并为模板引擎注入模板解析器
    @Bean
    public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
        SpringTemplateEngine templateEngine = new SpringTemplateEngine();
        templateEngine.setTemplateResolver(templateResolver);
        return templateEngine;
    }

    //生成视图解析器并未解析器注入模板引擎
    @Bean
    public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
        ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
        viewResolver.setCharacterEncoding("UTF-8");
        viewResolver.setTemplateEngine(templateEngine);
        return viewResolver;
    }

}

这些方法里面的参数的类型会被解析然后自动装配对应的 bean (如果之前已经创建好的,如果没有就不会自动装配,找不着,报错)

@Bean 方法的返回值,注入到 IOC 容器,形参从 IOC 容器里取 Bean


# 十三、SpringMVC 执行流程

# 1、SpringMVC 常用组件

  • DispatcherServlet:前端控制器,不需要工程师开发,由框架提供

作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求

  • HandlerMapping:处理器映射器,不需要工程师开发,由框架提供

作用:根据请求的 url、method 等信息查找 Handler,即控制器方法

  • Handler(Controller):处理器,需要工程师开发

作用:在 DispatcherServlet 的控制下 Handler 对具体的用户请求进行处理

  • HandlerAdapter:处理器适配器,不需要工程师开发,由框架提供

作用:通过 HandlerAdapter 对处理器(控制器方法)进行执行

(HandlerMapping 来找对应的 handler 来执行,HandlerAdapter 来真正去执行那个)

处理器适配器是用来执行的,会进行方法形参的封装等操作

  • ViewResolver:视图解析器,不需要工程师开发,由框架提供

作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView

  • View:视图

作用:将模型数据通过页面展示给用户

# 2、DispatcherServlet 初始化过程

DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度。

image-20220209182959336

image-20220209183015089

# a > 初始化 WebApplicationContext

所在类:org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext initWebApplicationContext() {
    WebApplicationContext rootContext =
        WebApplicationContextUtils.getWebApplicationContext(getServletContext());
    WebApplicationContext wac = null;

    if (this.webApplicationContext != null) {
        // A context instance was injected at construction time -> use it
        wac = this.webApplicationContext;
        if (wac instanceof ConfigurableWebApplicationContext) {
            ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
            if (!cwac.isActive()) {
                // The context has not yet been refreshed -> provide services such as
                // setting the parent context, setting the application context id, etc
                if (cwac.getParent() == null) {
                    // The context instance was injected without an explicit parent -> set
                    // the root application context (if any; may be null) as the parent
                    cwac.setParent(rootContext);
                }
                configureAndRefreshWebApplicationContext(cwac);
            }
        }
    }
    if (wac == null) {
        // No context instance was injected at construction time -> see if one
        // has been registered in the servlet context. If one exists, it is assumed
        // that the parent context (if any) has already been set and that the
        // user has performed any initialization such as setting the context id
        wac = findWebApplicationContext();
    }
    if (wac == null) {
        // No context instance is defined for this servlet -> create a local one
        // 创建WebApplicationContext
        wac = createWebApplicationContext(rootContext);
    }

    if (!this.refreshEventReceived) {
        // Either the context is not a ConfigurableApplicationContext with refresh
        // support or the context injected at construction time had already been
        // refreshed -> trigger initial onRefresh manually here.
        synchronized (this.onRefreshMonitor) {
            // 刷新WebApplicationContext
            onRefresh(wac);
        }
    }

    if (this.publishContext) {
        // Publish the context as a servlet context attribute.
        // 将IOC容器在应用域共享
        String attrName = getServletContextAttributeName();
        getServletContext().setAttribute(attrName, wac);
    }

    return wac;
}
# b > 创建 WebApplicationContext

所在类:org.springframework.web.servlet.FrameworkServlet

protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
    Class<?> contextClass = getContextClass();
    if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
        throw new ApplicationContextException(
            "Fatal initialization error in servlet with name '" + getServletName() +
            "': custom WebApplicationContext class [" + contextClass.getName() +
            "] is not of type ConfigurableWebApplicationContext");
    }
    // 通过反射创建 IOC 容器对象
    ConfigurableWebApplicationContext wac =
        (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);

    wac.setEnvironment(getEnvironment());
    // 设置父容器
    wac.setParent(parent);
    String configLocation = getContextConfigLocation();
    if (configLocation != null) {
        wac.setConfigLocation(configLocation);
    }
    configureAndRefreshWebApplicationContext(wac);

    return wac;
}
# c>DispatcherServlet 初始化策略

FrameworkServlet 创建 WebApplicationContext 后,刷新容器,调用 onRefresh (wac),此方法在 DispatcherServlet 中进行了重写,调用了 initStrategies (context) 方法,初始化策略,即初始化 DispatcherServlet 的各个组件 (比如说 MultipartResolver,HandlerMappings 等等等 (看下面!))

所在类:org.springframework.web.servlet.DispatcherServlet

protected void initStrategies(ApplicationContext context) {
   initMultipartResolver(context);
   initLocaleResolver(context);
   initThemeResolver(context);
   initHandlerMappings(context);
   initHandlerAdapters(context);
   initHandlerExceptionResolvers(context);
   initRequestToViewNameTranslator(context);
   initViewResolvers(context);
   initFlashMapManager(context);
}

# 3、DispatcherServlet 调用组件处理请求

# a>processRequest()

FrameworkServlet 重写 HttpServlet 中的 service () 和 doXxx (),这些方法中调用了 processRequest (request, response)

所在类:org.springframework.web.servlet.FrameworkServlet

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

    long startTime = System.currentTimeMillis();
    Throwable failureCause = null;

    LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
    LocaleContext localeContext = buildLocaleContext(request);

    RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
    ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
    asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());

    initContextHolders(request, localeContext, requestAttributes);

    try {
		// 执行服务,doService()是一个抽象方法,在DispatcherServlet中进行了重写
        doService(request, response);
    }
    catch (ServletException | IOException ex) {
        failureCause = ex;
        throw ex;
    }
    catch (Throwable ex) {
        failureCause = ex;
        throw new NestedServletException("Request processing failed", ex);
    }

    finally {
        resetContextHolders(request, previousLocaleContext, previousAttributes);
        if (requestAttributes != null) {
            requestAttributes.requestCompleted();
        }
        logResult(request, response, failureCause, asyncManager);
        publishRequestHandledEvent(request, response, startTime, failureCause);
    }
}
# b>doService()

所在类:org.springframework.web.servlet.DispatcherServlet

@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
    logRequest(request);

    // Keep a snapshot of the request attributes in case of an include,
    // to be able to restore the original attributes after the include.
    Map<String, Object> attributesSnapshot = null;
    if (WebUtils.isIncludeRequest(request)) {
        attributesSnapshot = new HashMap<>();
        Enumeration<?> attrNames = request.getAttributeNames();
        while (attrNames.hasMoreElements()) {
            String attrName = (String) attrNames.nextElement();
            if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
                attributesSnapshot.put(attrName, request.getAttribute(attrName));
            }
        }
    }

    // Make framework objects available to handlers and view objects.
    request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
    request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
    request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
    request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());

    if (this.flashMapManager != null) {
        FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
        if (inputFlashMap != null) {
            request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
        }
        request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
        request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
    }

    RequestPath requestPath = null;
    if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) {
        requestPath = ServletRequestPathUtils.parseAndCache(request);
    }

    try {
        // 处理请求和响应
        doDispatch(request, response);
    }
    finally {
        if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
            // Restore the original attribute snapshot, in case of an include.
            if (attributesSnapshot != null) {
                restoreAttributesAfterInclude(request, attributesSnapshot);
            }
        }
        if (requestPath != null) {
            ServletRequestPathUtils.clearParsedRequestPath(request);
        }
    }
}
# c>doDispatch()

所在类:org.springframework.web.servlet.DispatcherServlet

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    HttpServletRequest processedRequest = request;
    HandlerExecutionChain mappedHandler = null;
    boolean multipartRequestParsed = false;

    WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;

        try {
            processedRequest = checkMultipart(request);
            multipartRequestParsed = (processedRequest != request);

            // Determine handler for the current request.
            /*
            	mappedHandler:调用链
                包含handler、interceptorList、interceptorIndex
            	handler:浏览器发送的请求所匹配的控制器方法
            	interceptorList:处理控制器方法的所有拦截器集合
            	interceptorIndex:拦截器索引,控制拦截器afterCompletion()的执行
            */
            mappedHandler = getHandler(processedRequest);
            if (mappedHandler == null) {
                noHandlerFound(processedRequest, response);
                return;
            }

            // Determine handler adapter for the current request.
           	// 通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法
            HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());

            // Process last-modified header, if supported by the handler.
            String method = request.getMethod();
            boolean isGet = "GET".equals(method);
            if (isGet || "HEAD".equals(method)) {
                long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
                if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
                    return;
                }
            }
			
            // 调用拦截器的preHandle()
            if (!mappedHandler.applyPreHandle(processedRequest, response)) {
                return;
            }

            // Actually invoke the handler.
            // 由处理器适配器调用具体的控制器方法,最终获得ModelAndView对象
            mv = ha.handle(processedRequest, response, mappedHandler.getHandler());

            if (asyncManager.isConcurrentHandlingStarted()) {
                return;
            }

            applyDefaultViewName(processedRequest, mv);
            // 调用拦截器的postHandle()
            mappedHandler.applyPostHandle(processedRequest, response, mv);
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            // As of 4.3, we're processing Errors thrown from handler methods as well,
            // making them available for @ExceptionHandler methods and other scenarios.
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        // 后续处理:处理模型数据和渲染视图
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    catch (Exception ex) {
        triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
    }
    catch (Throwable err) {
        triggerAfterCompletion(processedRequest, response, mappedHandler,
                               new NestedServletException("Handler processing failed", err));
    }
    finally {
        if (asyncManager.isConcurrentHandlingStarted()) {
            // Instead of postHandle and afterCompletion
            if (mappedHandler != null) {
                mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
            }
        }
        else {
            // Clean up any resources used by a multipart request.
            if (multipartRequestParsed) {
                cleanupMultipart(processedRequest);
            }
        }
    }
}
# d>processDispatchResult()
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
                                   @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
                                   @Nullable Exception exception) throws Exception {

    boolean errorView = false;

    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            logger.debug("ModelAndViewDefiningException encountered", exception);
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }

    // Did the handler return a view to render?
    if (mv != null && !mv.wasCleared()) {
        // 处理模型数据和渲染视图
        render(mv, request, response);
        if (errorView) {
            WebUtils.clearErrorRequestAttributes(request);
        }
    }
    else {
        if (logger.isTraceEnabled()) {
            logger.trace("No view rendering, null ModelAndView returned.");
        }
    }

    if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
        // Concurrent handling started during a forward
        return;
    }

    if (mappedHandler != null) {
        // Exception (if any) is already handled..
        // 调用拦截器的afterCompletion()
        mappedHandler.triggerAfterCompletion(request, response, null);
    }
}

# 4、SpringMVC 的执行流程

  1. 用户向服务器发送请求,请求被 SpringMVC 前端控制器 DispatcherServlet 捕获。

  2. DispatcherServlet 对请求 URL 进行解析,得到请求资源标识符(URI),判断请求 URI 对应的映射:

a) 不存在

i. 再判断是否配置了 mvc:default-servlet-handler

ii. 如果没配置,则控制台报映射查找不到,客户端展示 404 错误

iii. 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示 404 错误

b) 存在则执行下面的流程

  1. 根据该 URI,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及 Handler 对象对应的拦截器 (还有那个之前说道的拦截器索引)),最后以 HandlerExecutionChain 执行链对象的形式返回。

  2. DispatcherServlet 根据获得的 Handler,选择一个合适的 HandlerAdapter (这个 HandlerAdapter 也是有很多类型的)。

  3. 如果成功获得 HandlerAdapter,此时将开始执行拦截器的 preHandler (…) 方法【正向】

  4. 提取 Request 中的模型数据,填充 Handler 入参,开始执行 Handler(Controller) 方法,处理请求。在填充 Handler 的入参过程中,根据你的配置,Spring 将帮你做一些额外的工作:

​ a) HttpMessageConveter: 将请求消息(如 Json、xml 等数据)转换成一个对象,将对象转换为指定的响应信息

​ b) 数据转换:对请求消息进行数据转换。如 String 转换成 Integer、Double 等 (所以我们控制器方法参数类型可以是 Integer 这种的,不一定是 String)

​ c) 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等

​ d) 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到 BindingResult 或 Error 中

这个我们一般前端做,所以后端的数据验证没怎么讲

  1. Handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象。

  2. 此时将开始执行拦截器的 postHandle (…) 方法【逆向】。

  3. 根据返回的 ModelAndView(此时会判断是否存在异常:如果存在异常,则执行 HandlerExceptionResolver 进行异常处理)选择一个适合的 ViewResolver 进行视图解析,根据 Model 和 View,来渲染视图。

  4. 渲染视图完毕执行拦截器的 afterCompletion (…) 方法【逆向】。

  5. 将渲染结果返回给客户端。