SpringMVC入门详解

目录

一、SpringMVC简介

1. 什么是MVC

2. 什么是SpringMVC

3. SpringMVC的特点

二、SpringMVC入门案例

1. 开发环境

2. 创建maven工程

3. 配置web.xml

4. 创建SpringMVC的配置文件

5. 创建请求控制器

6. 测试Demo工程

7. 总结

三、@RequestMapping注解

1. @RequestMapping注解的功能

2. @RequestMapping注解的位置

3. @RequestMapping注解的value属性

4. @RequestMapping注解的method属性

5. @RequestMapping注解的params属性

6. @RequestMapping注解的headers属性

7. SpringMVC支持ant风格的路径

8. SpringMVC支持路径中的占位符(重点)

四、SpringMVC获取请求参数

1. 通过ServletAPI获取

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

3. @RequestParam

4. @RequestHeader

5. @CookieValue

6. 通过POJO获取请求参数

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

五、域对象共享数据

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

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

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

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

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

6. Model、ModelMap、Map的关系

7. 向session域共享数据

8. 向application域共享数据

六、SpringMVC的视图

1. ThymeleafView

 2. 转发视图

3. 重定向视图

4. 视图控制器view-controller

七、RESTful

1. RESTful简介

2. RESTful的实现

3. HiddenHttpMethodFilter

4. CharacterEncodingFilter和HiddenHttpMethodFilter的配置顺序

八、RESTful案例

1. 功能清单

2. 准备工作

3. 功能实现

3.1. 访问首页

3.2. 查询所有员工信息

3.3. 删除功能

3.4. 添加功能

3.5. 修改功能

九、HttpMessageConverter

1. @RequestBody

2. RequestEntity

3. @ResponseBody

4. SpringMVC处理json

5. SpringMVC处理ajax

6. @RestController注解

7. ResponseEntity

十、文件上传和下载

1. 文件下载

2. 文件上传

十一、拦截器

1. 拦截器的配置

2. 拦截器的三个抽象方法

3. 多个拦截器的执行顺序

十二、异常处理器

1. 基于配置的异常处理

2. 基于注解的异常处理

十三、注解配置SpringMVC

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

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

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

4. 测试功能

十四、SpringMVC执行流程

1. SpringMVC常用组件

2. DispatcherServlet初始化过程

3. DispatcherServlet调用组件处理请求

4. SpringMVC的执行流程


一、SpringMVC简介

1. 什么是MVC

MVC全称Model View Controller,是一种软件架构的思想,将软件按照模型、视图、控制器来划分:
● M:Model(模型),指数据模型。用于存储数据以及处理用户请求的业务逻辑。在Web应用中,JavaBean对象,业务模型等都属于Model。
JavaBean分为两类:一类称为实体类Bean:专门存储业务数据的,如 User、Employee等,一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问
● V:View(视图),指工程中的html或jsp等页面,作用是与用户进行交互,展示模型中的数据。
● C:Controller(控制器),是应用程序中处理用户交互的部分。作用是接受视图提出的请求,将数据交给模型处理,并将处理后的结果交给视图显示。

MVC的工作流程: 用户通过视图层发送请求到服务器,在服务器中请求被Controller接收,Controller 调用相应的Model层处理请求,处理完毕将结果返回到Controller,Controller再根据请求处理的结果找到相应的View视图,渲染数据后最终响应给浏览器。
注:三层架构分为表述层(或表示层)、业务逻辑层、数据访问层,表述层表示前台页面和后台Servlet

2. 什么是SpringMVC

     SpringMVC是一个基于 MVC模式的轻量级Web框架,是Spring框架的一个模块,和Spring可以直接整合使用。SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案。在表述层框架历经 Strust、 WebWork、Strust2 等诸多产品的历代更迭之后,目前业界普遍选择了 SpringMVC 作为 Java EE 项目表述层开发的首选方案。SpringMVC代替了Servlet技术,它通过一套注解,让一个简单的Java类成为处理请求的控制器,而无须实现任何接口。

3. SpringMVC的特点

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

  对于原生Servlet,每个请求都要被我们自己定义的Servlet进行处理,对于SpringMVC,因为当前所有请求都要被前端控制器
DispatcherServlet来处理,所以处理请求过程中的一些步骤、一些公共的部分都会被它进行处理,我们只需要来接收当前SpringMVC前端控制器处理之后的数据就可以。例如在Servlet中获取请求参数用的是req.getParameter(),在SpringMVC中,不需要用HttpServletRequest对象来获取参数,因为获取请求参数的过程,已经在前端控制器中做过了(也就是说已经封装到了DispatcherServlet中),我们只需要在真正处理请求和响应的方法中,在形参的位置,写相对应的形参,就可以直接接收我们的请求参数。

二、SpringMVC入门案例

1. 开发环境

IDE:IntelliJ IDEA、构建工具:maven3.5.2、服务器:tomcat9
构建就是通过原材料去生产一个完整的工程的过程

2. 创建maven工程

2.1. 通过idea的模板创建

75bf7f5472f440edb64811e3df132ec7.png

说明:如果是手动创建的maven工程(没有勾选Create from archetype),需要自行添加web模块:首先要创建webapp目录,创建完webapp目录后,还需要添加一个web.xml,web.xml作为web工程的描述符,是web工程的一个入口配置文件,每当启动Tomcat服务器,首先要加载的文件就是web.xml,这个配置文件里面可以注册Servlet、过滤器、监听器。

2.2. 打包方式为war

2.3. 引入依赖

<!--当前web工程打成war包之后,这里所依赖的jar包在打成war包之后都会放到WEB-INF下面的lib中-->
    <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 -->
        <!-- provided:表示已被提供,被服务器提供。就是当前服务器中已经提供了Servlet的jar包,在这里把依赖范围设置成为provided之后,当我们把当前的web工程打成为war包之后,
        那这个Servlet的jar包就不会存在于当前工程打成war包之后的WEB-INF下面的lib中
        -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <version>3.1.0</version>
            <scope>provided</scope>
        </dependency>
        <!-- Spring5和Thymeleaf整合包,通过视图技术控制页面中所显示的内容-->
        <dependency>
            <groupId>org.thymeleaf</groupId>
            <artifactId>thymeleaf-spring5</artifactId>
            <version>3.0.12.RELEASE</version>
        </dependency>
    </dependencies>
<!--
scope是用来限制 dependency 的作用范围的,影响maven项目在各个生命周期时导入的package的状态,主要管理依赖的部署。
scope 的作用范围:
(1)compile:默认值,适用于所有阶段(表明该jar包在编译、运行以及测试中路径均可见),并且会随着项目一起发布。
(2)test:只在测试时使用,用于编译和运行测试代码,不会随项目发布。
(3)runtime:无需参与项目的编译,不过后期的测试和运行周期需要其参与,与 compile相比,跳过了编译。如 JDBC 驱动,适用运行和测试阶段。
(4)provided:编译和测试时有效,但是该依赖在运行时由服务器提供,并且打包时也不会被包含进去,如 servlet-api。
(5)system:类似 provided,需要显式提供包含依赖的jar,不会从 maven仓库下载,而是从本地文件系统获取,需要添加systemPath的属性来定义路径。
-->
注:由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其它靠传递性导入。
a31ae93aac084663869fadb4c1ad7a82.png

3. 配置web.xml

在资源webapp目录下的WEB-INF目录下的web.xml文件,这是webapp核心配置文件,并配置相关内容:注册SpringMVC的前端控制器DispatcherServlet
3.1. 默认配置方式
此配置作用下,SpringMVC的配置文件默认位于WEB-INF下,默认名称为<servlet-name>- servlet.xml,例如,以下配置方式所对应的SpringMVC的配置文件位于WEB-INF下,文件名为DispatcherServlet-servlet.xml。也就是说,如果不显式指定配置文件的位置,配置文件一定得有一个固定的路径和名称,按照默认的方式去加载。
<!-- 配置SpringMVC的前端控制器,对浏览器发送的请求统一进行处理 -->
<servlet>
   <servlet-name>DispatcherServlet</servlet-name>
   <servlet-class>org.springframework.web.servlet.DispatcherServlet</servletclass>
</servlet>
<servlet-mapping>
   <servlet-name>DispatcherServlet</servlet-name>
<!--
设置SpringMVC的前端控制器所能处理请求的请求路径,"/"所匹配的请求可以是/testServlet、/login.do或后缀为.html或.js或.css方式的请求路径,
但是"/"不能匹配.jsp请求路径的请求
-->
   <url-pattern>/</url-pattern>
</servlet-mapping>
3.2. 扩展配置方式(推荐)
可通过<init-param>标签设置SpringMVC配置文件的位置和名称( 显式上下文配置位置),通过<load-on-startup>标签设置SpringMVC前端控制器DispatcherServlet的初始化时间。
<!-- 配置SpringMVC的前端控制器,对浏览器发送的请求进行统一的处理,<servlet>和<servlet-mapping>这两个标签是来共同注册一个Servlet-->
<servlet>
  <servlet-name>DispatcherServlet</servlet-name>
  <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
  <!--初始化参数,指定SpringMVC配置文件的位置和名称-->
  <init-param>
    <!--显式上下文配置位置,参数名contextConfigLocation为固定值,需要有一个固定的值来让SpringMVC知道,
这是表示当前配置文件的位置和名称,所以这里使用的参数名,一定是由SpringMVC在它的前端控制器DispatcherServlet中已经定义好的-->
    <param-name>contextConfigLocation</param-name>
    <!-- 使用classpath:表示从类路径查找配置文件,也就是target中的classes目录下-->
    <param-value>classpath:SpringMVC.xml</param-value>
  </init-param>
<!--
 作为框架的核心组件,在启动过程中有大量的初始化操作要做,而这些操作放在第一次请求时才执行会严重影响访问速度
 因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时
 -->
    <load-on-startup>1</load-on-startup>
 </servlet>
<!--设置SpringMVC的核心控制器所能处理请求的请求路径,既然是对浏览器发送的请求进行统一的处理,<url-pattern>匹配路径就不能是固定、具体的路径-->
<servlet-mapping>
   <servlet-name>DispatcherServlet</servlet-name>
   <url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>

<!-- <url-pattern>标签中使用"/"和"/*"的区别:
"/"所匹配的请求可以是/testServlet、/login.do或后缀为.html或.js或.css方式的请求路径,但是"/"不能匹配.jsp请求路径的请求。
因为jsp本质就是一个Servlet,它是需要经过当前服务器中指定的Servlet(就是它自己)来进行处理,假设"/"能够匹配.jsp,
那么.jsp后缀路径的请求也会被SpringMVC的前端控制器来处理,就会把当前的jsp请求当做是一个普通的请求来处理,而不会找到它相对应的jsp页面。
因此,"/"可以避免在访问jsp页面时,该请求被DispatcherServlet处理,从而找不到相应的页面.
"/*"可以匹配所有请求包括.jsp请求。例如在使用过滤器时,若需要对所有请求进行过滤,就需要使用/*的写法
-->

我们都知道,web.xml中主要就是来注册Servlet、过滤器、监听器,那为什么使用SpringMVC的时候还需要配置web.xml呢?
因为SpringMVC是基于原生的Servlet,它为我们提供了一个封装处理请求过程的前端控制器DispatcherServlet,Servlet要想处理请求,必须在web.xml中注册。

可以这么理解,SpringMVC对Servlet进行了封装,其实本质就是一个Servlet,封装了Servlet之后就有了一个功能非常强大的前端控制器DispatcherServlet。原先浏览器发送的请求需要我们自己去写Servlet来处理,而对于SpringMVC,浏览器发送的请求都会被SpringMCV提供的前端控制器DispatcherServlet进行统一的处理。这个时候,它就会将请求和响应中很多的一些过程(执行步骤)进行封装,例如,获取请求参数、向域对象设置值、实现页面跳转、转发和重定向的时候,这个时候,DispatcherServlet就会对这些操作进行统一处理。

注册的原因:服务器上的每一项资源,都有一个路径与之对应,而浏览器不能直接访问一个类,因为并不是所有的类都能够接受请求和响应。JavaEE平台下的技术标准提供了Servlet,可以通过访问Servlet来实现,要想访问Servlet,必须要给它设置一个匹配路径,每当我们所访问的路径与我们当前所设置路径匹配的时候,那这个时候当前的请求就会被我们的Servlet来进行处理,所以我们要配置web.xml。

4. 创建SpringMVC的配置文件

编写SpringMVC核心配置文件springMVC.xml,该文件和Spring配置文件写法一样。

   <!--扫描组件-->
    <context:component-scan base-package="com.cgc.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">
                        <!--thymeleaf在进行解析的时候,必须要设置一个视图前缀和视图后缀-->
                        <!-- 视图前缀 -->
                        <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>
</beans>

<!--
视图是要被视图解析器解析的,把我们设置的视图名称加上前缀和后缀,最终就能跳转到我们指定的页面
每当我们实现页面跳转的时候,如果我们指定的视图名称符合条件的话,它就会被我们当前的视图解析器解析,找到相对应的页面,实现页面跳转
-->

说明:框架简单的说,其实就是配置文件+jar包,引入依赖之后,jar包有了,要想实现相对应功能,还需要去创建SpringMVC的配置文件

5. 创建请求控制器

由于前端控制器对浏览器发送的请求进行了统一的处理,但是具体的每个请求有不同的处理过程( 例如,请求参数不同、向域对象中设置的值也不同、最终的转发和重定向也不同),因此需要创建处理具体请求的类,即请求控制器,请求控制器中每一个处理请求的方法称为控制器方法。由于SpringMVC的控制器由一个POJO(普通的Java类)担任,因此需要通过@Controller注解将其标识为一个控制层组件,交给Spring的IoC容器管理,此时SpringMVC才能够识别控制器的存在。
@Controller
public class DemoController {


}

6. 测试Demo工程

6.1.  在请求控制器中创建处理请求的方法实现对首页的访问
与我们之前用的Servlet方式不一样,我们需要在类中创建方法,通过SpringMVC中的方式去匹配到这个方法,那这个方法就是处理请求的方法。一个控制器中可以有很多方法,怎么将请求和处理请求的方法进行匹配?使用@RequestMapping注解
@RequestMapping注解,处理请求和控制器方法之间的映射关系
@RequestMapping注解的value属性可以通过请求地址匹配请求
/**
 * @RequestMapping注解:处理请求和控制器方法之间的映射关系
 * 当客户端发送的请求和@RequestMapping注解的value值匹配的时候,就会执行当前注解所标识的方法
 * "/"表示匹配的是当前工程的应用上下文路径:localhost:8080/SpringMVC/
 * "/"--> 跳转到/WEB-INF/templates/index.html
 */
@RequestMapping("/")
public String index() {
  //返回视图名称,被视图解析器进行解析
  //每当实现页面跳转的时候,如果视图名称符合条件的话,它就会被当前的视图解析器解析, 找到相对应的页面,实现页面跳转
  return "index";
}
总结:想要处理一个请求,需要找到控制器,通过请求映射来匹配当前的请求。
6.2. 通过超链接跳转到指定页面
在主页index.html中设置超链接
<!DOCTYPE html>
<!--xmlns:th="http://www.thymeleaf.org" 是thymeleaf的命名空间,有了它页面中才能使用thymeleaf的语法-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
</head>
<body>
<h1>首页</h1>
<a th:href="@{/toTarget}">访问目标页面welcome.html</a><br/>
</body>
</html>
<!--当前页面是在WEB-INF下的,因为WEB-INF下的内容是受保护的,所以浏览器是不能直接访问的,通过重定向也是不能访问的,只能通过请求转发来进行访问-->

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

@RequestMapping("/toTarget")
public String toWelcome() {
   return "welcome";
}

关于绝对路径的说明:

以"/"开头的路径叫做绝对路径,而绝对路径又分为浏览器解析的和服务器解析的,浏览器解析的绝对路径会把"/"解析成:localhost:8080/
超链接中的绝对路径就是由浏览器解析的,这个时候的"/"表示的不是从应用上下文路径下访问,而是从项目的部署目录开始访问(Servlet容器:Tomcat的webapps目录下开始找,即从localhost:8080下访问)。

为什么要用thymeleaf呢?
<a href="/toTarget"> 直接这样写,浏览器所解析的绝对路径会少一个上下文路径(点击‘Edit Configurations’打开"Run/Debug Configurations",这个时候我们选择deployment,就会看到Application context),thymeleaf用@{}包起来的绝对路径,会自动添加上下文路径。使用thymeleaf,想要通过thymeleaf解析某个属性,就在属性前面加 th:,当检测到@{/toTarget}中使用的是绝对路径,就会自动帮助我们添加上下文路径

关于Application context
localhost:8080/projectName或者是localhost:8080就是我们平常所说的应用上下文,项目中的路径名都是相对于这个应用上下文来说的。
在idea下开发的时候,有时候我们可能需要让访问路径带上项目名,但是,idea默认是为每个项目单独配置tomcat的,要想让idea的应用上下文改变,只需将这个Application context改成你需要的项目名就行了。

7. 总结

客户端发送请求,若请求地址符合前端控制器<url-pattern>的匹配规则,该请求就会被前端控制器DispatcherServlet处理,前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器, 将请求地址和控制器中@RequestMapping注解的value属性值进行匹配,若匹配成功,该注解所标识的控制器方法就是处理请求的方法,处理请求的方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析,加上前缀和后缀组成视图的路径,通过Thymeleaf对视图进行渲染,最终转发到视图所对应页面。

三、@RequestMapping注解

1. @RequestMapping注解的功能

从注解名称上我们可以看到,@RequestMapping注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。SpringMVC 接收到指定的请求,就会去找在映射关系中对应的控制器方法来处理这个请求。

2. @RequestMapping注解的位置

@RequestMapping标识一个类:设置映射请求的请求路径的初始信息
@RequestMapping标识一个方法:设置映射请求的请求路径的具体信息
/**
 * @RequestMapping可以加在类上,给不同模块的控制器设置@RequestMapping,主要用于区分不同业务与层次的模块,实现路径识别与管理
 * 如果不加,有可能在不同的控制器中出现相同的请求映射,即有多个方法上面的@RequestMapping,配了相同的请求地址
 *
 * 说明:在所有的控制器中,@RequestMapping所能匹配到的请求地址必须是唯一的,也就是说相同的请求地址,一定只有一个@RequestMapping与之匹配 */
@Controller
//@RequestMapping("/requestMappingController")
public class RequestMappingController {

    /**
     *  RequestMappingController类(控制器)加上@RequestMapping("/requestMappingController")
     *  此时请求映射所映射的请求的请求路径为:/requestMappingController/testMethod
     */
    @RequestMapping("/testMethod")
    public String testRequestMapping(){
        return "success";
    }
}

3. @RequestMapping注解的value属性

●  @RequestMapping注解的value属性通过请求的请求地址匹配请求映射
●  @RequestMapping注解的value属性是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址
所对应的请求
●  @RequestMapping注解的value属性必须设置,即至少要通过请求地址匹配请求映射
<a th:href="@{/testValuePropertyForRequestMapping}">测试RequestMapping的value属性:"/testValuePropertyForRequestMapping"</a><br>
<a th:href="@{/testValue}">测试RequestMapping的value属性:"/testValue"</a><br>
/**
 * @RequestMapping注解的value属性 通过客户端请求的请求地址来匹配请求映射
 * 该请求映射能够匹配多个请求地址所对应的请求
 */
@RequestMapping(value = {"/testValuePropertyForRequestMapping", "/testValue"})
public String testValueProperty() {
   return "success";
}

4. @RequestMapping注解的method属性

@RequestMapping注解的method属性通过请求的请求方式(get或post)匹配请求映射
@RequestMapping注解的method属性是一个RequestMethod类型的数组,表示该请求映射能够匹配多种请求方式的请求。若当前请求的请求地址满足请求映射的value属性,但是请求方式不满足method属性,则浏览器报错 405:Request method 'GET' not supported或
Request method 'POST' not supported
<a th:href="@{/testMethodForRequestMapping}">测试RequestMapping的method属性:GET</a><br>
<form th:action="@{/testMethodForRequestMapping}" method="post">
    <input type="submit" value="测试RequestMapping的method属性:POST">
</form>
/**
 * @RequestMapping注解的method属性
 */
 @RequestMapping(value ="/testMethodForRequestMapping",method = {RequestMethod.GET})
 public String testMethodProperty() {
    return "success";
 }
说明:
4.1. 对于处理指定请求方式的控制器方法,SpringMVC中提供了@RequestMapping的派生注解
处理get请求的映射:@GetMapping             处理post请求的映射:@PostMapping
处理put请求的映射:@PutMapping            处理delete请求的映射:@DeleteMapping
4.2. 常用的请求方式有get,post,put,delete
但是目前浏览器只支持get和post,若在form表单提交时,为method设置了其它的请求方式(put或delete),是不生效的,只会按照默认的请求方式get处理。若要发送put和delete请求,则需要通过spring提供的过滤器HiddenHttpMethodFilter。

5. @RequestMapping注解的params属性

@RequestMapping注解的params属性通过请求的请求参数匹配请求映射
@RequestMapping注解的params属性是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系
"param":要求请求映射所匹配的请求必须携带param请求参数
"!param":要求请求映射所匹配的请求必须不能携带param请求参数
"param=value":要求请求映射所匹配的请求必须携带param请求参数,且param=value
"param!=value":要求请求映射所匹配的请求必须携带param请求参数,但是param!=value
<!--请求参数可以加在()中-->
<a th:href="@{/testParamsPropertyForRequestMapping(loginname='admin',password='123')}">测试RequestMapping注解的params属性 -->/testParamsPropertyForRequestMapping</a>
/**
 * @RequestMapping注解的params属性
 */
@RequestMapping(value = "/testParamsPropertyForRequestMapping", method = RequestMethod.GET,
            params = {"loginname", "password!=123456"})
public String testParams() {
   return "success";
}
说明:
若当前请求满足@RequestMapping注解的value和method属性,但是不满足params属性,此时页面会报错400:Parameter conditions "loginname, password!=123456" not met for actual request parameters: loginname={admin}, password={123456}

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错误,即资源未找到。
<a th:href="@{/testHeadersPropertyForRequestMapping}">测试RequestMapping注解的params属性 -->/testHeadersPropertyForRequestMapping</a><br>
 @RequestMapping(value = "/testHeadersPropertyForRequestMapping", method = RequestMethod.GET,
            headers = {"Host=localhost:8080"})
public String testHeaders() {
   return "success";
}

7. SpringMVC支持ant风格的路径

?:表示任意的单个字符
*:表示任意的0个或多个字符
**:表示任意的一层或多层目录
注意:在使用**时,只能使用/**/xxx的方式,即**前后是不能添加其它字符的,如/a**a/testAnt,如果加了,那么**的作用和*是一样的
<a th:href="@{/ant/testAnt}">测试@RequestMapping可以匹配ant风格的路径:使用?</a><br>
<a th:href="@{/antant/testAnt}">测试@RequestMapping可以匹配ant风格的路径:使用* </a><br>
<a th:href="@{/test/ant/testAnt}">测试@RequestMapping可以匹配ant风格的路径:使用** </a><br>
 /**
  * SpringMVC支持ant风格的路径
  * ?,表示任意的单个字符; *,表示任意的0个或多个字符, **,表示任意的一层或多层目录
  * @RequestMapping("/a?t/testAnt")
  * @RequestMapping("/a*t/testAnt")
  */
  @RequestMapping("/**/testAnt")
  public String testAnt() {
     return "success";
  }

8. SpringMVC支持路径中的占位符(重点)

原始方式:/deleteEmployee?id=1
rest方式:/deleteEmployee/1
SpringMVC路径中的占位符常用于RESTful风格中,当请求路径中将某些数据通过路径的方式传输到服务器中,就可以在相应@RequestMapping注解的value属性中通过占位符{xxx}表示传输的数据,在通过@PathVariable注解,将占位符所表示的数据赋值给控制器方法的形参。
<a th:href="@{/testRestPath/1/xiaoming}">测试@RequestMapping支持路径中的占位符:使用/testRestPath </a>
 /**
  * SpringMVC支持路径中的占位符,如果用的是占位符,在发送请求的时候,请求地址必须要和@RequestMapping()中的value进行匹配
  * 也就是说,@RequestMapping中有占位符,那么在请求地址中也必须有这一层
  * "/"在路径里面表示路径的分隔符,路径里面也是用"/"表示一层一层的目录
  */
 @RequestMapping("/testPath/{id}/{name}")
 public String testPath(@PathVariable("id") Integer id, @PathVariable("name") String name) 
 {
    System.out.println("id:" + id + "name:" + name);
    return "success";
 }

四、SpringMVC获取请求参数

1. 通过ServletAPI获取

将HttpServletRequest作为控制器方法的形参,此时HttpServletRequest类型的参数表示封装了当前请求的请求报文的对象
<a th:href="@{/testServletAPI(username='zhangsan',password=123)}">测试使用ServletAPI获取请求参数</a><br>
  /**
   * 这里可以直接使用HttpServletRequest对象
   * 控制器方法是DispatcherServlet底层中调用的,它可以根据当前控制器方法的形参,为它们注入不同的值
   * 当前形参是HttpServletRequest对象,它就可以将当前DispatcherServlet中所获得的表示当前请求request的对象赋值给形参request
   * 形参位置的request表示当前请求
   */
  @RequestMapping("/testServletAPI")
  public String testServletAPI(HttpServletRequest request){
     String username = request.getParameter("username");
     String password = request.getParameter("password");
     System.out.println("username:"+username+",password:"+password);
     return "success";
  }

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

在控制器方法的形参位置,设置和请求参数同名的形参,当浏览器发送请求,匹配到请求映射时,在 DispatcherServlet中就会将请求参数赋值给相应的形参。
<a th:href="@{/testParam(username='lisi',password=123456)}">测试使用控制器形参获取请求参数</a><br>
<form th:action="@{/testParam}" method="post">
    用户名:<input type="text" name="username"><br>
    密码:  <input type="password" name="password"><br>
    爱好:  <input type="checkbox" name="hobby" value="sing">sing
           <input type="checkbox" name="hobby" value="swim">swim
           <input type="checkbox" name="hobby" value="basketball">basketball<br>
           <input type="submit" value="测试使用控制器形参获取请求参数">
</form>
 /**
  *若请求参数中出现多个同名的请求参数(例如复选框勾选多个选项),在控制器方法相应的形参位置上声明字符串类型或字符串数组类型的参数来接收此请求参数
  *若使用字符串数组类型的形参,此参数的数组中包含了每一个数据
  *若使用字符串类型的形参,最终接收到的值为:同名的请求参数值之间使用逗号进行拼接,例如:"sing,swim,basketball"
  */
  @RequestMapping("/testParam")
  public String testParam(String username, String password,String hobby) {
      System.out.println("username:" + username + ",password:" + password + ",hobby:" + hobby);
      return "success";
  }

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

5. @CookieValue

@CookieValue是将cookie数据和控制器方法的形参创建映射关系
@CookieValue注解一共有三个属性:value、required、defaultValue,用法同@RequestParam
<a th:href="@{/testCreateSession}">创建session</a><br>
<form th:action="@{/testRequestParam}" method="get">
    用户名:<input type="text" name="username"><br>
    密码:  <input type="password" name="password"><br>
    爱好:  <input type="checkbox" name="hobby" value="sing">sing
    <input type="checkbox" name="hobby" value="swim">swim
    <input type="checkbox" name="hobby" value="basketball">basketball<br>
    <input type="submit" value="测试使用控制器形参获取请求参数:@RequestParam">
</form>
 /**
  * HttpSession是一种保存少量信息至服务器端的一种技术,第一请求时,服务器会创建HttpSession,
  * 我们可以在HttpSession对象中保存一些关于用户的状态信息,并将HttpSession的JSESSIONID以Cookie形式响应给浏览器,
  * 第二次请求,浏览器会携带之前的JSESSIONID的Cookie,发送给服务器,服务器根据JSESSIONID获取对应的HttpSession对象.
  */
 @RequestMapping("/testCreateSession")
 public String getSession(HttpServletRequest request){
     HttpSession session = request.getSession();
     return "success";
 }

 /**
  * 在此请求之前需要先通过上面请求映射为@RequestMapping("/testCreateSession")的控制器方法,创建HttpSession对象,
  * 并将HttpSession的JSESSIONID以Cookie形式响应给浏览器,这里才能获取到JSESSIONID
  */
 @RequestMapping("/testRequestParam")
 public String testReqParam(@RequestParam(value = "username",required = false,defaultValue = "admin") String user_name,
         String password, String hobby,
         @RequestHeader("Host") String host,
         @CookieValue("JSESSIONID") String JSESSIONID)
{
    System.out.println("username:" + user_name + ",password:" + password + ",hobby:" + hobby);
    System.out.println("host:" + host);
    System.out.println("JSESSIONID:" + JSESSIONID);
    return "success";
}

6. 通过POJO获取请求参数

可以在控制器方法的形参位置设置一个实体类类型的形参,此时若浏览器传输的请求参数的参数名和实体类中的属性名一致,那么请求参数就会为此属性赋值
<form th:action="@{/testBean}" 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" th:value="使用实体类接收请求参数">
</form>
  /**
   * 通过POJO获取请求参数
   */
   @RequestMapping("/testBean")
   public String testBean(User user) {
      System.out.println(user);
      return "success";
   }

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

解决获取请求参数的乱码问题,可以使用SpringMVC提供的编码过滤器CharacterEncodingFilter,但是必须在web.xml中进行注册

 <!--配置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>
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
     String encoding = this.getEncoding();
     if (encoding != null) {
          if (this.isForceRequestEncoding() || request.getCharacterEncoding() == null) {
               request.setCharacterEncoding(encoding);
          }

          if (this.isForceResponseEncoding()) {
               response.setCharacterEncoding(encoding);
           }
      }
      filterChain.doFilter(request, response);
 }

关于乱码问题

在设置请求对象的编码的时候有一个前提条件:如果在此之前,已经获取了某一个请求参数,那我们设置的编码是没有任何作用的。由于在DispatcherServlet中就会获取客户端的请求参数,所以,我们在控制器方法中设置编码是不起作用的,因此,要在DispatcherServlet获取请求参数之前来设置编码。那有什么组件比Servlet更早执行呢?答案是过滤器 。Web服务器中三大组件:监听器、过滤器、Servlet,加载时间最早的是监听器,其次是过滤器,最后才是Servlet。对于过滤器,我们设置了过滤路径,只要我们访问的请求地址满足过滤路径,都会被过滤器进行过滤。

五、域对象共享数据

1. 使用ServletAPIrequest域对象共享数据

<a th:href="@{/testRequestScope}">使用ServletAPI向request域对象共享数据</a><br/>
 @RequestMapping("/testRequestScope")
 public String testRequestScopeByServletAPI(HttpServletRequest request){
     request.setAttribute("testRequestScope","RequestScope");
     //这里就属于请求转发,转发到success.html页面
     return "success";
  }
在success.html中,获取request域对象中的值
<!--在html页面中,只有"th:"所对应的属性中的内容才会被thymeleaf解析
不能 <p>${testRequestScope}</p>这样写,这样只会把${testRequestScope}当成<p></p>标签中的文本来解析
-->
<p th:text="${testRequestScope}"></p>
<p th:text="${session.testSessionScope}"></p>
<p th:text="${application.testApplicationScope}"></p>

2. 使用ModelAndViewrequest域对象共享数据

<a th:href="@{/testModuleAndView}">使用ModelAndView向request域对象共享数据</a><br/>
 /**
  * 实现页面跳转,应该是由控制器方法来设置视图名称,视图名称由视图解析器解析之后,找到最终的页面
  * 所以,在控制器方法中,针对视图部分只需要设置一个视图名称即可
  */
  @RequestMapping("/testModuleAndView")
  public ModelAndView testModelAndView(){
     //ModelAndView有Model和View的功能,Model主要用于向请求域共享数据,View主要用于设置视图,实现页面跳转
     ModelAndView mav = new ModelAndView();
     //处理模型数据,即向请求域request共享数据
     mav.addObject("testRequestScope","ModelAndView");
     //设置视图,实现页面跳转
     mav.setViewName("success");
     //将封装了模型和视图的ModelAndView对象作为方法的返回值,返回给DispatcherServlet,DispatcherServlet去解析
     return mav;
  }

3.使用Modelrequest域对象共享数据

<a th:href="@{/testModel}">使用Model向request域对象共享数据</a><br/>
 @RequestMapping("/testModel")
 public String testModel(Model model) {
     model.addAttribute("testRequestScope", "Model");
     //都要在控制器方法中创建相对应的形参,而形参具有向request域中共享数据的功能
     System.out.println(model.getClass().getName());
     return "success";
 }

4. 使用maprequest域对象共享数据

<a th:href="@{/testMap}">使用Map向request域对象共享数据</a><br/>

  @RequestMapping("/testMap")
  public String testMap(Map<String,Object> map){
     map.put("testRequestSocpe","Map");
     //都要在控制器方法中创建相对应的形参,而形参具有向request域中共享数据的功能
     System.out.println(map.getClass().getName());
     return "success";
  }

5. 使用ModelMaprequest域对象共享数据

<a th:href="@{/testModelMap}">使用ModelMap向request域对象共享数据</a><br/>
public String testModelMap(ModelMap modelMap){
   modelMap.addAttribute("testRequestSocpe","ModelMap");
   //都要在控制器方法中创建相对应的形参,而形参具有向request域中共享数据的功能
   System.out.println(modelMap.getClass().getName());
   return "success";
}

6. ModelModelMapMap的关系

Model、ModelMap、Map类型的参数其实本质上都是BindingAwareModelMap类型的,都要在控制器方法中创建相对应的形参,而形参具有向request域中共享数据的功能。
public interface Model{}
public class ModelMap extends LinkedHashMap<String, Object> {}
public class ExtendedModelMap extends ModelMap implements Model {}
public class BindingAwareModelMap extends ExtendedModelMap {}

7. 向session域共享数据

<a th:href="@{/testSession}">使用ServletAPI向session域对象共享数据</a><br/>
 @RequestMapping("/testSession")
 public String testSession(HttpSession session){
   session.setAttribute("testSessionScope","HttpSession");
   return "success";
 }

8. 向application域共享数据

<a th:href="@{/testApplication}">使用ServletAPI向applications域对象共享数据</a><br/>
/**
 * 使用ServletAPI向application域共享数据
 */
 @RequestMapping("/testApplication")
 public String testApplication(HttpSession session){
     ServletContext servletContext = session.getServletContext();
     servletContext.setAttribute("testApplicationScope","ServletContext");
     return "success";
 }

六、SpringMVC的视图

SpringMVC中的视图是View接口,视图的作用渲染数据,将模型Model中的数据展示给用户,SpringMVC视图的种类很多,默认有转发视图InternalResourceView和重定向视图RedirectView。当工程引入jstl的依赖,转发视图会自动转换为JstlView。若使用的视图技术为Thymeleaf,在SpringMVC的配置文件中配置了Thymeleaf的视图解析器,由此视图解析器解析之后所得到的是ThymeleafView。

1. ThymeleafView

当控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被SpringMVC配置文件中所配置的视图解析器解析,视图名称拼接视图前缀和视图后缀所得到的最终路径,会通过转发的方式实现跳转。
@RequestMapping("/testThymeleafView")
public String testThymeleafView(){
   return "success";
}

e8afeb66ed374518a25fc55a6e73c851.png

 2. 转发视图

SpringMVC中默认的转发视图是InternalResourceView ,SpringMVC中创建转发视图的情况:
当控制器方法中所设置的视图名称以"forward:"为前缀时,创建InternalResourceView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"forward:"去掉,剩余部分作为最终路径通过转发的方式实现跳转
例如"forward:/","forward:/testThymeleafView"。
 @RequestMapping("/testInternalResourceView")
 public String testInternalResourceView(){
     return "forward:/testThymeleafView";
 }
b5cace3cc46049ccac3b15f51cfbb53e.png

有问题的写法:
 @RequestMapping("/testInternalResourceView")
 public String testInternalResourceView(){
     return "forward:/WEB-INF/templates/success.html";
 }

以上写法是错误的,我们在使用html页面时,一般会结合Thymeleaf处理后端发来的数据,因此,多数情况必不可少的会使用"th:"有关的标签。因为在html页面中,只有"th:"所对应的属性中的内容才会被Thymeleaf解析。当视图名称以"forward:"为前缀,生成的不是ThymeleafView视图,而是InternetResourceView视图。如果是直接转发到该页面,那就不能被配置的Thymeleaf视图解析器解析到,也不能生成ThymeleafView,而页面又使用了Thymeleaf,所以,"th:"所对应的属性中的内容不会被解析,也就不能访问到该页面了。

如果使用的是jsp页面,那么就不需要考虑有关Thymeleaf的视图解析器了,正常使用即可。

3. 重定向视图

SpringMVC中默认的重定向视图是RedirectView,当控制器方法中所设置的视图名称以"redirect:"为前缀时,创建RedirectView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"redirect:"去掉,剩余部分作为最终路径通过重定向的方式实现跳转。例如"redirect:/","redirect:/employee"。
@RequestMapping("/testRedirectView")
public String testRedirectView(){
   return "redirect:/testThymeleafView";
}

ce7b6fa81f78414c99c90154e0cefcc1.png

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

注意点与转发类似,但是重定向是两次请求,如果要重定向到WEB-INF目录下的页面,那就相当于从浏览器直接发送了一次访问WEB-INF目录的请求,而浏览器是无法直接访问WEB-INF目录下的页面的,所以只能通过重定向到其它方法,在由其它方法使用转发,经过ThymeleafView的视图解析器解析来到WEB-INF目录下。 

4. 视图控制器view-controller

当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称,不需要处理业务逻辑时,可以将处理器方法使用view-controller标签进行表示
 <!--视图控制器view-controller
     当控制器方法中,仅仅用来实现页面跳转,并不需要处理模型数据时,可以将控制器方法使用view-controller标签进行表示
     path:设置处理的请求地址,相当于@RequestMapping中的path
     view-name:设置请求地址所对应的视图名称
     例:以下标签用来代替TestController中的index()这个控制器方法
    -->
  <mvc:view-controller path="/" view-name="index"></mvc:view-controller>
@Controller
public class TestController {

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

}

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

七、RESTful

1. RESTful简介

REST: Representational State Transfer,表现层(前端的视图页面到后端的控制层)资源状态转移。
RESTful是一种风格,是一种软件架构的风格。
1.1 资源
资源是一种看待服务器的方式,即,将服务器看作是由很多离散的资源组成,每个资源是服务器上一个可命名的抽象概念。因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、 数据库中的一张表等等具体的东西,可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解。 部署到服务器中的各种内容都可以称之为资源。与面向对象设计类似,资源是以名词为核心来组织的,首先关注的是名词。一个资源可以由一个或多个URI来标识,URI既是资源的名称,也是资源在Web上的地址。对某个资源感兴趣的客户端应用,可以通过资源的URI与其进行交互。
1.2.  资源的表述
资源的表述是一段对于资源在某个特定时刻的状态的描述,可以在客户端-服务器端之间转移(交换)。资源的表述可以有多种格式,例如HTML/XML/JSON/纯文本/图片/视频/音频等等。资源的表述格式可以通过协商机制来确定。请求-响应方式的表述通常使用不同的格式。
1.3.  状态转移
状态:资源的表现形式,例如HTML/XML/JSON/纯文本/图片/视频/音频等等状态转移说的是:在客户端和服务器端之间转移(transfer)代表资源状态的表述。通过转移(浏览器发送请求路径去请求服务器中的资源)和操作资源的表述(请求路径),来间接实现操作资源的目的。
在客户端和服务器端之间转移:客户端发送请求到服务器,客户端请求的是什么,服务器就要给客户端响应什么,这个时候,就要将服务器中的资源转移到客户端

2. RESTful的实现

当我们操作同一个资源,例如,操作的资源为用户信息,可以对其添加、删除、修改,无论是添加用户信息、删除用户信息还是修改用户信息,我们所操作的资源都是用户信息,用了RESTful后,既然操作的资源相同,那请求路径就应该相同。请求路径相同,怎么表示操作这些资源的方式3?具体说,就是 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请求方式
总结:RESTful实现的本质就是相同的请求路径不同的请求方式来表示不同的操作。

3. HiddenHttpMethodFilter

由于浏览器只支持发送GET和POST方式的请求,那么该如何发送PUT和DELETE请求呢?
● SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将POST请求转换为 DELETE 或 PUT 请求,HiddenHttpMethodFilter 处理put和
delete请求的条件:
1)当前请求的请求方式必须为post
2)当前请求必须传输请求参数_method
满足以上条件, HiddenHttpMethodFilter 过滤器就会将当前请求的请求方式转换为请求参数 _method的值,因此请求参数_method的值才
是最终的请求方式,以下是 HiddenHttpMethodFilter中执行过滤的方法,其中会将当前请求的请求方式转换为请求参数 _method的值。
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    HttpServletRequest requestToUse = request;
    if ("POST".equals(request.getMethod()) && request.getAttribute("javax.servlet.error.exception") == null) {
       String paramValue = request.getParameter(this.methodParam);
       if (StringUtils.hasLength(paramValue)) {
           String method = paramValue.toUpperCase(Locale.ENGLISH);
           if (ALLOWED_METHODS.contains(method)) {
               requestToUse = new HiddenHttpMethodFilter.HttpMethodRequestWrapper(request, method);
            }
       }
    }

     filterChain.doFilter((ServletRequest)requestToUse, response);
}

● HiddenHttpMethodFilter过滤器需要在web.xml中注册

实现步骤:

1. 在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>

2. 编写控制器方法

/**
 * 使用RESTful模拟操作用户资源(增删改查)
 * /user       GET      查询所有用户信息
 * /user/1     GET      根据用户id查询用户信息
 * /user       POST     添加用户信息
 * /user/1     DELETE   删除用户信息
 * /user       PUT      修改用户信息
 *
 * 说明:RESTful是一种风格,REST风格提倡 URL地址使用统一的风格设计
 * 无论是增加、删除、修改、还是查询我们访问的都是用户资源,既然访问的资源相同,那么访问路径也应该相同
 * 根据请求方式的不同来实现对用户的不同操作
 */
@Controller
public class UserController {

    @RequestMapping(value = "/user",method = RequestMethod.GET)
    public String getAllUsers(){
        System.out.println("查询所有用户信息");
        return "success";
    }

    @RequestMapping(value = "/user/{id}",method = RequestMethod.GET)
    public String getUserById(@PathVariable("id") String id) {
        System.out.println("根据"+ id + "查询用户信息");
        return "success";
    }

    @RequestMapping(value = "/user",method = RequestMethod.POST)
    public String addUser(String userName,String password){
        System.out.println("添加用户信息为:"+userName+","+password);
        return "success";
    }

    @RequestMapping(value = "/user",method = RequestMethod.PUT)
    public String updateUser(String userName,String password){
        System.out.println("修改用户信息为:"+userName+","+password);
        return "success";
    }
}

3. 配置页面请求

<a th:href="@{/user}">查询所有用户信息</a><br>
<a th:href="@{/user/1}">根据id查询用户信息</a><br>
<form th:action="@{/user}" method="post">
    用户名:<input type="text" name="userName"><br>
    密码:  <input type="password" name="password"><br>
    <input type="submit" value="添加">
</form>
<!--由于浏览器只支持发送get和post方式的请求,那么该如何发送put和delete请求呢?
SpringMVC提供了HiddenHttpMethodFilter帮助我们将POST请求转换为DELETE或PUT请求
要想发送put和delete请求,要满足两个条件:
1.请求方式为post 2.必须传输一个请求参数,参数名为_method,参数值是请求方式
-->
<form th:action="@{/user}" method="post">
    <input type="hidden" name="_method" value="PUT">
    用户名:<input type="text" name="userName"><br>
    密码:  <input type="password" name="password"><br>
    <input type="submit" value="修改">
</form>

4. CharacterEncodingFilter和HiddenHttpMethodFilter的配置顺序

当有多个过滤器的时候,过滤器的执行顺序由 <filter-mapping>的顺序决定,先配置的先执行。目前为止,SpringMVC中提供了两个过滤器:CharacterEncodingFilter和HiddenHttpMethodFilter。在web.xml中注册时,必须先注册CharacterEncodingFilter,再注册HiddenHttpMethodFilter。原因:设置编码是有前提条件的,在CharacterEncodingFilter中是通过 request.setCharacterEncoding(encoding) 方法设置字符集的,而request.setCharacterEncoding(encoding) 方法要求前面不能有任何获取请求参数的操作,如果有获取请求参数的操作,编码设置就没有效果了,而HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作:String paramValue = request.getParameter(this.methodParam);

<!--配置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>
 
    <!--配置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>

八、RESTful案例

1. 功能清单

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

2. 准备工作

和传统 CRUD 一样,实现对员工信息的增删改查。
● 搭建环境
● 准备实体类
package bean;

public class Employee {

    private Integer id;

    private String lastName;

    private String email;

    //1 male, 0 female
    private Integer gender;
    
    public Employee() {
    }

    public Employee(Integer id, String lastName, String email, Integer gender) {
        super();
        this.id = id;
        this.lastName = lastName;
        this.email = email;
        this.gender = 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;
    }

}
● 准备持久层(dao) 模拟数据
package com.cgc.mvc.dao;

import bean.Employee;

import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Repository
public class EmployeeDao {

    private static Map<Integer, Employee> employees;

    private static Integer initId = 107;

    static{
        employees = new HashMap<>();
        employees.put(101,new Employee(101, "张先生", "zhang@163.com", 1));
        employees.put(102,new Employee(102, "王女士", "wang@163.com", 0));
        employees.put(103,new Employee(103, "李先生", "li@163.com", 1));
        employees.put(104,new Employee(104, "赵先生", "zhao@163.com", 1));
        employees.put(105,new Employee(105, "孙女士", "sun@163.com", 0));
        employees.put(106,new Employee(106, "李女士", "li@163.com", 0));
    }

    public Collection<Employee> getAll(){
        return employees.values();
    }

    public Employee get(Integer id){
        return employees.get(id);
    }
    public void delete(Integer id){
        employees.remove(id);
    }

    public void save(Employee employee){
       if(employee.getId() == null){
           employee.setId(initId++);
       }
        employees.put(employee.getId(),employee);
    }

}

3. 功能实现

3.1. 访问首页
● 配置view-controller,开启MVC注解驱动
<!--配置视图控制器-->
<mvc:view-controller path="/" view-name="index"></mvc:view-controller>
<!--开启mvc注解驱动-->
<mvc:annotation-driven/>

● 创建页面,在templates下创建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="@{/employee}">查询员工信息</a>
</body>
</html>
3.2. 查询所有员工信息

● 控制器方法

/**
 * 注解+扫描的方式,当前注解所标识的类才会被作为组件进行管理
 */

@Controller
public class EmployController {

     @Autowired
     private EmployeeDao employeeDao;

     @RequestMapping(value = "/employee",method = RequestMethod.GET)
     public String getAllEmployees(Model model){
          Collection<Employee> employeeList = employeeDao.getAll();
          model.addAttribute("employeeList",employeeList);
          return "employees";
     }
}

● 创建employees.html页面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>员工信息</title>
    <!--引入vue.js-->
    <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>name</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.name}"></td>
        <td th:text="${employee.email}"></td>
        <td th:text="${employee.gender}"></td>
        <td>
            <!--<a @click="deleteEmployee" th:href="@{/employee/}+${employee.id}">delete</a>-->
            <!--两种方式都可以-->
            <a @click="deleteEmployee" th:href="@{'/employee/'+${employee.id}}">delete</a>
            <a th:href="@{'/employee/'+${employee.id}}">update</a>
        </td>
    </tr>
</table>
   <!-- 创建处理delete请求方式的表单,作用:通过超链接控制表单的提交,将post请求转换为delete请求 -->
    <form id="delete_form" method="post">
        <!-- HiddenHttpMethodFilter要求:必须传输_method请求参数,并且值为最终的请求方式 -->
        <input type="hidden" name="_method" value="delete"/>
    </form>

<!--超链接控制表单提交,可以使用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>

</body>
</html>
<!--
 删除用的是超链接,但是超链接不能发送delete请求,发送delete请求需要满足的条件:
 1.请求方式post   2.传输请求参数:_method
 所以,通过vue,点击超链接时,去控制一个表单的提交,进而可以发送delete请求
-->
3.3. 删除功能

删除某员工信息,需要使用超链接时,可以使用vue来阻止超链接跳转并且控制表单的提交,从而实现删除功能。通过超链接的点击事件【vue】来提交表单【post+隐藏域put/delete】,然后请求被filter拦截,再被前端控制器解析,解析过后来到Controller进行匹配。

① 删除超链接,绑定点击事件

● 在webapp文件夹下创建static目录和js子目录,用于存放静态资源,放入vue.js文件,并在index.html中引入vue.js

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

● 删除超链接

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

● 创建处理delete请求方式的表单

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

● 通过vue处理点击事件

!--超链接控制表单提交,可以使用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>

● 在SpringMVC.xml中,增加配置项:开放对静态资源的访问

 <!--开放对静态资源的访问
    工作流程:1.请求在被处理的过程中,请求会先被SpringMVC的前端控制器DispatcherServlet来处理
            2.如果在控制器中找不到对应的请求映射,就会交给默认的Servlet来处理,如果默认的Servlet能找到相对应的资源,就访问资源
             如果找不到相对应的资源就会404
    -->
<mvc:default-servlet-handler/>

<!--开启mvc注解驱动-->
<mvc:annotation-driven/>

说明:加上<mvc:default-servlet-handler />表示如果当前浏览器发送的请求DispatcherServlet处理不了就会交给DefaultServlet来处理,同时需要配置 <mvc:annotation-driven/>,如果不配置,当前所有的请求都将被默认的DefaultServlet处理。

SpringMVC.xml中,如果没有<mvc:default-servlet-handler/>这个配置项,进行删除的时候会报405。

在Console(控制台)中,发现vue.js找不到

原因:通过请求路径 http://localhost:8080/SpringMVC/employee 查询所有员工信息,返回employees.html视图页面,由于在employees.html视图页面中引入了vue.js,浏览器会再次发送请求获取vue.js,而在SpringMVC工程中,由于配置了前端控制器DispatcherServlet,并且设置匹配的请求路径为"/",会拦截除JSP之外的所有请求,因此,访问静态资源vue.js,也会由SpringMVC的前端控制器DispatcherServlet来处理,而静态资源是不能交给SpringMVC的前端控制器DispatcherServlet来处理的(控制器中没有静态资源的请求映射),需要由默认的Servlet:DefaultServlet来处理静态资源。DispatcherServlet处理请求的方式是根据请求地址去控制器中找到相对应的请求映射,而控制器中是没有写访问静态资源的请求映射的,所以找不到。

关于在SpringMVC.xml中配置<mvc:default-servlet-handler />的一些说明

在SpringMVC.xml中配置<mvc:default-servlet-handler />后,会在SpringMVC上下文中定义一个org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler,它会像一个检查员,对进入DispatcherServlet的URL进行筛查,如果发现是静态资源的请求,就将该请求转由Web应用服务器默认的Servlet处理,如果不是静态资源的请求,才由DispatcherServlet继续处理。

一般Web应用服务器默认的Servlet名称是"default",因此DefaultServletHttpRequestHandler可以找到它。如果你所用的Web应用服务器的默认Servlet名称不是"default",则需要通过default-servlet-name属性显示指定:

<mvc:default-servlet-handler default-servlet-name="所使用的Web服务器默认使用的Servlet名称" />

② 控制器方法  

 @RequestMapping(value = "/employee/{id}",method = RequestMethod.DELETE)
 public String deleteEmployee(@PathVariable("id") Integer id){
    employeeDao.delete(id);
    return "redirect:/employee";
 }
3.4. 添加功能

① 跳转到添加数据页面

● 配置view-controller
<mvc:view-controller path="/toAdd" view-name="employee_add"/>
在templates下创建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>

② 执行保存

@RequestMapping(value = "/employee",method = RequestMethod.POST)
public String addEmployee(Employee employee){
   employeeDao.save(employee);
   return "redirect:/employee";
}
3.5. 修改功能

① 跳转到更新数据页面

● 修改超链接

 <a th:href="@{'/employee/'+${employee.id}}">update</a>

● 控制器方法

@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";
}

● 在templates下创建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>

② 执行更新

@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。
HttpMessageConverter中的Http指的就是http协议,里面包含了请求报文和响应报文。请求报文是从浏览器发送到服务器,响应报文是由服务器响应给浏览器。
报文可以简单的理解为我们前端页面传入后端代码的一些键值对,例如在登录案例中,前端页面就需要传入浏览器中用户输入的username和password,而这些信息内容在不同的请求中处于不同的位置,但是无论是get还是post请求亦或是其它类型请求,它们把信息转化成请求键值对的格式是都是一样的:key=value&key=value...

1. @RequestBody

@RequestBody可以获取请求体,作用是将请求报文中的请求体转换为java数据, 需要在控制器方法设置一个形参,使用@RequestBody 进行标识,当前请求的请求体就会为当前注解所标识的形参赋值。
<form th:action="@{/testRequestBody}" method="post">
   用户名:<input type="text" name="username"><br>
   密码:<input type="password" name="password"><br>
   <input type="submit" value="测试@RequestMapping">
</form>
/**
 * 使用@RequestBody注解实现将请求报文转化为java对象(这里转化为String类型对象)
 */
@RequestMapping(value = "/testRequestBody")
 public String testRequestBody(@RequestBody String requestBody){ //请求体是字符串
    System.out.println("requestBody:"+requestBody);
    return "success";
 }
输出结果: requestBody:username=admin&password=123

2. RequestEntity

RequestEntity 封装请求报文的一种类型,需要在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过getHeaders() 获取请求头信息,通过 getBody() 获取请求体信息。
<form th:action="@{/testRequestEntity}" method="post">
  用户名:<input type="text" name="username"><br>
  密码:<input type="password" name="password"><br>
        <input type="submit" value="测试testRequestEntity">
</form>
@RequestMapping(value = "/testRequestEntity")
public String testRequestEnty(RequestEntity<String> requestEntity){
   //当前requestEntity表示整个请求报文的信息,泛型为String,表示以字符串的方式获取请求报文
   System.out.println("请求头:"+requestEntity.getHeaders());
   System.out.println("请求体:"+requestEntity.getBody());
   return "success";
}

注:RequestEntity封装的是整个请求报文。

输出结果:

请求头:[host:"localhost:8080", connection:"keep-alive", content-length:"27", cache-control:"max-age=0", sec-ch-ua:""Google Chrome";v="113", "Chromium";v="113", "Not-A.Brand";v="24"", sec-ch-ua-mobile:"?0", sec-ch-ua-platform:""Windows"", 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/113.0.0.0 Safari/537.36", accept:"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7", sec-fetch-site:"same-origin", sec-fetch-mode:"navigate", sec-fetch-user:"?1", sec-fetch-dest:"document", referer:"http://localhost:8080/SpringMVC/", accept-encoding:"gzip, deflate, br", accept-language:"zh-CN,zh;q=0.9", Content-Type:"application/x-www-form-urlencoded;charset=UTF-8"]
请求体:username=admin&password=123

概要说明:下面有关response,我们在学习完JavaWeb之后,我们可以清楚的感受到request的主要作用其实就是获取客户端请求的信息,有些信息是程序员需要看的,但是有的信息是计算机需要看的。程序员需要处理的就是类似于登录案例的username和password请求信息,而像浏览器版本信息之类的请求信息以及被高度封装的计算机处理,所以我们要处理的request信息并不多。但是,我们响应给浏览器的方式有很多,内容也十分复杂。比如,我们可以通过response的getwriter方法获取字符输出流将信息反馈输出到前端页面,这是一种响应,这种方法我们在学习Javaweb阶段一般不是响应页面显示时使用的,而是和异步请求AJAX(以及json)联合使用。另外一种响应就是response的重定向setRedirect以及request的转发。所以终点在于我们根据前台信息的逻辑判断反馈给浏览器的各种服务,response这块也就自然的变成了这部分学习的重点。

3. @ResponseBody

@ResponseBody 用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器
<a th:href="@{/testResponse}">通过ServletAPI的Response对象响应浏览器数据</a><br/>
<a th:href="@{/testResponseBody}">通过@ResponseBody响应浏览器数据</a><br/>
/**
 * 在通过控制器处理请求时,要想对浏览器进行响应,有几种方式:
 * 1.通过实现页面跳转(转发、重定向),响应给浏览器一个完整的页面
 * 2.通过response.getWriter()获取一个PrintWriter对象,再通过其中的write()或print()响应浏览器
 */
@RequestMapping("/testResponse")
public void testResponseBody(HttpServletResponse response) throws IOException {
   //操作响应体,    将print()中的内容:"测试Response",直接作为响应报文的响应体响应到浏览器
   response.getWriter().print("测试Response");
}

/**  
 *  也就是说加了@ResponseBody注解,当前方法的返回值就是要响应到浏览器的数据
 *  不加@ResponseBody, return "success", sucesss会作为视图名称,被视图解析器解析
 *  加了@ResponseBody, return "success", sucesss表示的不再是视图名称,而是当前响应的响应体
 */
@RequestMapping("/testResponseBody")
@ResponseBody
public String testResponseBody(){
  return "success";
}

4. SpringMVC处理json

1)导入jackson 的依赖
<dependency>
  <groupId>com.fasterxml.jackson.core</groupId>
  <artifactId>jackson-databind</artifactId>
  <version>2.12.1</version>
</dependency>

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

<mvc:annotation-driven />

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

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

/**
 * 服务器是不能直接响应浏览器对象的,因为浏览器并不能识别我们响应的java对象,只能接收文本(浏览器不知道服务器用的是什么语言)
 * 而http协议就是将浏览器端和服务器端做一个统一的规范,和浏览器的种类、编程语言的种类无关,所以,要按照请求报文和响应报文的格式发送请求和响应才行
 *
 * 当前既要保留Student对象中的各个数据,还要让它能够正常的响应到浏览器,怎么做?转换为json
 * 这里要将控制器方法的返回值转换为json格式的,json是一种数据交互格式,区分json对象和数组:最外层是{}是json对象,最外层是[]是json数组
 */
@RequestMapping("/testResponseStudent")
@ResponseBody
public Student testResponseStudent(){
   return new Student(1,"admin",20,"1001");
}
浏览器的页面中展示的结果 :{"id":1,"name":"admin","age":20,"number":"1001"}

注:json是一种数据交互格式,xml也是一种数据交互格式。数据交互格式现在用的最多的是json,xml用的较多的是作为配置文件。

补充 什么是 JSON ?
  • JSON 指的是 JavaScript 对象表示法(JavaScript Object Notation)
  • JSON 是轻量级的文本数据交换格式
  • JSON 独立于语言:JSON 使用 Javascript语法来描述数据对象,但是 JSON 仍然独立于语言和平台。JSON 解析器和 JSON 库支持许多不同的编程语言。C、Python、C++、Java、PHP、Go等编程语言都支持 JSON。
  • JSON 具有自我描述性,更易理解

5. SpringMVC处理ajax

1)请求超链接

 <!--ajax在页面不发生跳转的情况下(不刷新)与服务器进行交互-->
 <div id="app">
    <a th:href="@{/testAjax}" @click="testAjax">SpringMVC处理Ajax</a><br>
 </div>

2)通过vueaxios处理点击事件


    <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({
            //与DOM的id进行绑定,相互关联
            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>
3)控制器方法
/**
 * ajax:页面不刷新与服务器进行交互,所以在服务器中不能用转发和重定向
 * 只能使用响应浏览器数据
 */
 @RequestMapping("/testAjax")
 @ResponseBody
 public String testAjax(String username, String password){
     System.out.println("username:"+username+",password:"+password);
     return "hello,ajax";
 }

补充:

● AJAX = Asynchronous JavaScript and XML(异步的 JavaScript 和 XML)。

● AJAX 不是新的编程语言,而是一种使用现有标准的新方法。

● AJAX 最大的优点是在不重新加载整个页面的情况下,可以与服务器交换数据并更新部分网页内容。

● AJAX 不需要任何浏览器插件,但需要用户允许 JavaScript 在浏览器上执行。

● XMLHttpRequest 只是实现 Ajax 的一种方式

6. @RestController注解

@RestController 注解是S pringMVC 提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了 @ResponseBody 注解。

7. ResponseEntity

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

十、文件上传和下载

1. 文件下载

使用 ResponseEntity 实现下载文件的功能
/**
 * 文件上传和下载本质上是一个文件复制的过程
 */
@Controller
public class FileUpLoadAndDownLoadController {

    @RequestMapping("/testDownload")
    public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws
            IOException {
        //获取ServletContext对象
        ServletContext servletContext = session.getServletContext();
        //获取服务器中文件的真实路径
        String realPath = servletContext.getRealPath("/static/img/1.jpg");
        System.out.println(realPath);
        //创建输入流
        InputStream is = new FileInputStream(realPath);
        //创建字节数组, is.available():获取输入流所对应的文件所有的字节
        byte[] bytes = new byte[is.available()];
        //将输入流所对应的文件中的所有字节读到字节数组中
        is.read(bytes);
        //创建HttpHeaders对象设置响应头信息
        MultiValueMap<String, String> headers = new HttpHeaders();
        //设置要下载方式以及下载文件的名字, attachment:以附件的方式来下载文件  filename:为下载的文件设置的默认的名字
        //只有filename可以改,其它都是固定的
        headers.add("Content-Disposition", "attachment;filename=1.jpg");
        //设置响应状态码
        HttpStatus statusCode = HttpStatus.OK;
        //创建ResponseEntity对象, ResponseEntity可以自定义一个响应报文响应浏览器
        //bytes存放了当前要下载的文件中所有的字节,也就是响应体
        ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes, headers, statusCode);
        //关闭输入流
        is.close();
        return responseEntity;
    }

}

2. 文件上传

文件上传要求 form 表单的请求方式必须为 post ,并且添加属性 enctype="multipart/form-data" ,SpringMVC中将上传的文件封装到 MultipartFile 对象中,通过此对象可以获取文件相关信息
上传步骤:
 
1) 添加依赖:
<dependency>
  <groupId>commons-fileupload</groupId>
  <artifactId>commons-fileupload</artifactId>
  <version>1.3.1</version>
</dependency>
2) 在 SpringMVC 的配置文件中添加配置
<!--文件上传解析器,必须通过文件解析器的解析才能将文件转换为MultipartFile对象-->
<bean id="multipartResolver"
        class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
</bean>
3) 控制器方法
    /**
     * SpringMVC把当前上传的文件封装成了MultipartFile对象
     */
    @RequestMapping("/testUp")
    public String testUp(MultipartFile photo, HttpSession session) throws
            IOException {
        //获取上传的文件的文件名
        String fileName = photo.getOriginalFilename();
        //处理上传相同文件重名问题
        String hzName = fileName.substring(fileName.lastIndexOf("."));
        fileName = UUID.randomUUID().toString() + hzName;
        //通过ServletContext获取服务器中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";
    }

十一、拦截器

1. 拦截器的配置

SpringMVC中的拦截器用于拦截控制器方法的执行,作用于控制器执行的前后
SpringMVC中的拦截器需要实现HandlerInterceptor

只是实现HandlerInterceptor接口,SpringMVC并不会将它识别为一个拦截器,SpringMVC的拦截器必须在SpringMVC的配置文件中进行配置

方式1:
public class MyFirstInterceptor implements HandlerInterceptor {

    /**
     * 控制器方法执行之前执行, 对控制器方法进行拦截。返回值:false表示拦截,true表示放行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor的preHandle方法已执行");
        return true;
    }

    /**
     * 控制器方法执行之后执行
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor的postHandle方法已执行");
    }

    /**
     * 处理完视图和模型数据,渲染视图完毕之后执行
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor的afterCompletion方法已执行");
    }
}
<!--配置拦截器-->
<mvc:interceptors>
  <!--通过<bean>标签声明,表示某一个类型的对象就是一个拦截器-->
  <bean class="com.cgc.mvc.interceptor.MyFirstInterceptor "/>
</mvc:interceptors>

方式2:

@Component
public class MyFirstInterceptor implements HandlerInterceptor {

    /**
     * 控制器方法执行之前执行, 对控制器方法进行拦截。返回值:false表示拦截,true表示放行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor的preHandle方法已执行");
        return true;
    }

    /**
     * 控制器方法执行之后执行
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor的postHandle方法已执行");
    }

    /**
     * 处理完视图和模型数据,渲染视图完毕之后执行
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor的afterCompletion方法已执行");
    }
}
<!--配置拦截器-->
<mvc:interceptors>-->
<!-- 通过ref标签, 需要在xml中通过<bean>标签声明MyInterceptor的bean,或者在MyInterceptor类上加注解-->
 <ref bean="myFirstInterceptor "></ref>-->
</mvc:interceptors>

以上两种配置方式都是对DispatcherServlet所处理的所有的请求进行拦截,无法设置拦截规则

方式3:

@Component
public class MyFirstInterceptor implements HandlerInterceptor {

    /**
     * 控制器方法执行之前执行, 对控制器方法进行拦截。返回值:false表示拦截,true表示放行
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        System.out.println("MyInterceptor的preHandle方法已执行");
        return true;
    }

    /**
     * 控制器方法执行之后执行
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        System.out.println("MyInterceptor的postHandle方法已执行");
    }

    /**
     * 处理完视图和模型数据,渲染视图完毕之后执行
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        System.out.println("MyInterceptor的afterCompletion方法已执行");
    }
}
<!--配置拦截器-->
<mvc:interceptors>
   <mvc:interceptor>
   <!--设置拦截的路径:"/*"只表示访问上下文路径下面的一层目录  "/**"拦截所有 -->
   <mvc:mapping path="/**"/>
   <!--要排除的请求映射-->
   <mvc:exclude-mapping path="/"/>
   <!--指定拦截器-->
   <ref bean="myFirstInterceptor"></ref>
</mvc:interceptor>
</mvc:interceptors>
<!--以上配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求,通过 
 mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求-->

2. 拦截器的三个抽象方法

SpringMVC中的拦截器有三个抽象方法:
preHandle:控制器方法执行之前执行preHandle(),其boolean类型的返回值表示是否拦截或放行,返回true为放行,即调用控制器方法;返回false表示拦截,即不调用控制器方法;
postHandle:控制器方法执行之后执行postHandle();
afterComplation:处理完视图和模型数据,渲染视图完毕之后执行afterComplation();
以下为DispatcherServlet中的部分源码,SpringMVC的拦截器中的三个方法就是在这里执行:
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 = 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.
				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);
				}
			}
		}
}

	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) {
            //执行拦截器的afterComplation()
			// Exception (if any) is already handled..
			mappedHandler.triggerAfterCompletion(request, response, null);
		}
	}

3. 多个拦截器的执行顺序

3.1. 若每个拦截器的preHandle()都返回true,此时多个拦截器的执行顺序和拦截器在SpringMVC配置文件中配置的先后顺序有关(先配置哪个拦截器,哪个拦截器就在前): preHandle()会按照配置的顺序执行,而postHandle()和afterComplation()会按照配置的反序执行
<!--配置拦截器-->
<mvc:interceptors>
    <mvc:interceptor>
       <mvc:mapping path="/**"/>
       <!--要排除的请求映射-->
       <mvc:exclude-mapping path="/"/>
       <!--指定拦截器-->
       <ref bean="myFirstInterceptor"></ref>
    </mvc:interceptor>
    <mvc:interceptor>
        <mvc:mapping path="/**"/>
        <mvc:exclude-mapping path="/"/>
        <ref bean="mySecondInterceptor"></ref>
    </mvc:interceptor>
</mvc:interceptors>
8ad1f04c881744689123ec37d415486b.png

3.2. 若某个拦截器的preHandle()返回了false,那这个拦截器的preHandle()和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,preHandle()返回false的拦截器之前的拦截器的afterComplation()会执行

09290be5385f4b3fbb7721eb4a5ed5bf.png

十二、异常处理器

1. 基于配置的异常处理

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

SpringMVC中的异常处理器就是在控制器方法执行的过程中,如果出现了异常,就为它返回一个新的ModelAndView,跳转到指定页面。

HandlerExceptionResolver接口的实现类有:DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver

DefaultHandlerExceptionResolver是SpringMVC默认使用的异常处理器,也就是说,在使用过程中出现的一些异常,SpringMVC都已经帮我们处理过了,而DefaultHandlerExceptionResolver中有一个doResolveException()方法。doResolveException()的作用就是:如果在控制器方法执行过程中出现了指定的异常,它就可以代替原来方法要返回的ModelAndView,返回一个新的ModelAndView,跳转到指定的页面,为我们显示异常信息。而ModelAndView就是用来处理模型数据和渲染视图的,所以有了ModelAndView就可以跳转到指定的页面了

protected ModelAndView doResolveException(
			HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {

		try {
			if (ex instanceof HttpRequestMethodNotSupportedException) {
				return handleHttpRequestMethodNotSupported(
						(HttpRequestMethodNotSupportedException) ex, request, response, handler);
			}
			else if (ex instanceof HttpMediaTypeNotSupportedException) {
				return handleHttpMediaTypeNotSupported(
						(HttpMediaTypeNotSupportedException) ex, request, response, handler);
			}
			else if (ex instanceof HttpMediaTypeNotAcceptableException) {
				return handleHttpMediaTypeNotAcceptable(
						(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
			}
			else if (ex instanceof MissingPathVariableException) {
				return handleMissingPathVariable(
						(MissingPathVariableException) ex, request, response, handler);
			}
			else if (ex instanceof MissingServletRequestParameterException) {
				return handleMissingServletRequestParameter(
						(MissingServletRequestParameterException) ex, request, response, handler);
			}
			else if (ex instanceof ServletRequestBindingException) {
				return handleServletRequestBindingException(
						(ServletRequestBindingException) ex, request, response, handler);
			}
			else if (ex instanceof ConversionNotSupportedException) {
				return handleConversionNotSupported(
						(ConversionNotSupportedException) ex, request, response, handler);
			}
			else if (ex instanceof TypeMismatchException) {
				return handleTypeMismatch(
						(TypeMismatchException) ex, request, response, handler);
			}
			else if (ex instanceof HttpMessageNotReadableException) {
				return handleHttpMessageNotReadable(
						(HttpMessageNotReadableException) ex, request, response, handler);
			}
			else if (ex instanceof HttpMessageNotWritableException) {
				return handleHttpMessageNotWritable(
						(HttpMessageNotWritableException) ex, request, response, handler);
			}
			else if (ex instanceof MethodArgumentNotValidException) {
				return handleMethodArgumentNotValidException(
						(MethodArgumentNotValidException) ex, request, response, handler);
			}
			else if (ex instanceof MissingServletRequestPartException) {
				return handleMissingServletRequestPartException(
						(MissingServletRequestPartException) ex, request, response, handler);
			}
			else if (ex instanceof BindException) {
				return handleBindException((BindException) ex, request, response, handler);
			}
			else if (ex instanceof NoHandlerFoundException) {
				return handleNoHandlerFoundException(
						(NoHandlerFoundException) ex, request, response, handler);
			}
			else if (ex instanceof AsyncRequestTimeoutException) {
				return handleAsyncRequestTimeoutException(
						(AsyncRequestTimeoutException) ex, request, response, handler);
			}
		}
		catch (Exception handlerEx) {
			if (logger.isWarnEnabled()) {
				logger.warn("Failure while trying to resolve exception [" + ex.getClass().getName() + "]", handlerEx);
			}
		}
		return null;
}

SpringMVC提供了自定义的异常处理器SimpleMappingExceptionResolver,让我们自定义异常处理。例如,如果当前控制器方法在执行的过程中出现了某些异常,我们就可以给它指定一个视图进行跳转。使用方式:

<!--配置异常处理-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
    <!--exceptionMappings是Properties类型的-->
    <property name="exceptionMappings">
         <!--赋值-->
         <props>
           <!--Properties的键表示控制器方法执行过程中出现的异常
             Properties的值表示若出现指定异常时,设置一个新的视图名称,跳转到指定页面
            -->
            <prop key="java.lang.ArithmeticException">error</prop>
          </props>
     </property>
     <!--exceptionAttribute属性,用于设置将出现的异常信息在请求域中进行共享的键,
       异常信息默认是存储到当前的请求域中的,value就是存储到请求域中的异常信息的键
     -->
     <property name="exceptionAttribute" value="ex"/>
 </bean>

2. 基于注解的异常处理

/**
 * @ControllerAdvice注解,是用@Component来进行标识,所以@ControllerAdvice注解是@Component的一个扩展注解
 * 所以具有将类标识为组件的功能
 *
 * @ControllerAdvice将当前类标识为异常处理的组件
 */
@ControllerAdvice
public class ExcepHandler {
    
    /**
     * @ExceptionHandler注解中可以添加参数,参数是某个异常类的class,代表这个方法专门处理该类异常,
     * 如下此时注解的参数是ArithmeticException.class,NullPointerException.class,表示只有方法抛出ArithmeticException或者NullPointerException时,才会调用该方法
     * 这个时候就会将@ExceptionHandler注解所标识的方法作为当前新的控制器方法来执行
     */
    @ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class})
    public String testException(Exception ex, Model model) {
        model.addAttribute("ex", ex);
        return "error";
    }

}
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
出现异常
<p th:text="${ex}"></p>
</body>
</html>

十三、注解配置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上下文。
 
/**
 * web工程的初始化类,用来代替web.xml
 */
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {

    /**
     * 指定Spring的配置类
     */
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return new Class[0];
    }

    /**
     * 指定SpringMVC的配置类
     */
    @Override
    protected Class<?>[] getServletConfigClasses() {
        return new Class[]{WebConfig.class};
    }

    /**
     * 指定DispatcherServlet的映射规则,即url-pattern
     */
    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }

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

}

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

/**
 *
 * SSM整合之后,Spring的配置信息写在此类中
 */
@Configuration
public class SpringConfig {

}

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

/**
 * 可以在一个类上通过注解@Configuration将它标识为配置类,这个时候它就可以代替Spring的配置文件
 * 代替SpringMVC的配置文件,之前在SpringMVC的配置文件中配置的内容有:
 * 1.扫描组件          2.视图解析器         3.view-controller       4.default-servlet-handler
 * 5.mvc注解驱动       6.文件上传解析器     7.异常处理               8.拦截器
 *
 * @Configuration将当前类标识为一个配置类
 * @ComponentScan扫描组件
 * @EnableWebMvc mvc注解驱动
 */

@Configuration
@ComponentScan("com.cgc.mvc")
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

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

    /**
     * 配置拦截器
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        MyInterceptor myInterceptor = new MyInterceptor();
        registry.addInterceptor(myInterceptor).addPathPatterns("/**");
    }

    /**
     *  配置视图控制 view-controller
     */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
    }

    /**
     * 配置文件上传解析器
     */
    @Bean
    public MultipartResolver multipartResolver() {
        CommonsMultipartResolver commonsMultipartResolver = new CommonsMultipartResolver();
        return commonsMultipartResolver;
    }

    /**
     * 配置异常处理器
     */
    @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);
    }

    /**
     * 配置生成模板解析器
     */
    @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;
    }

    /**
     * 生成模板引擎并为模板引擎注入模板解析器
     * templateEngine()方法是由Spring调用的,Spring在调用的时候会在IOC容器中找ITemplateResolver类型的bean,来为方法的参数赋值,相当于属性注入
     * 所以,当前方法所使用的参数,必须符合自动装配的规则,也就是说,方法所能够使用的参数必须是当前SpringIOC容器中所拥有的bean,并且IOC容器中的bean能为此参数赋值
     */
    @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;
    }

}

4. 测试功能

@Controller
public class TestController {

    @RequestMapping("/")
    public String toIndex() {
        return "index";
    }

}

十四、SpringMVC执行流程

1. SpringMVC常用组件

● DispatcherServlet: 前端控制器 ,不需要工程师开发,由框架提供。作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求。
● HandlerMapping:处理器映射器 ,不需要工程师开发,由框架提供。作用:根据请求的url method 等信息查找 Handler ,即控制器方法。简言之,就是找控制器方法。
● Handler: 处理器 ,需要工程师开发。作用:在DispatcherServlet 的控制下 Handler 对具体的用户请求进行处理。
● HandlerAdapter: 处理器适配器 ,不需要工程师开发,由框架提供 。作用:通过HandlerAdapter 对处理器(控制器方法)进行执行。
简言之,就是执行控制器方法。
● ViewResolver: 视图解析器 ,不需要工程师开发,由框架提供。作用:进行视图解析,得到相应的视图,例如:ThymeleafView InternalResourceView 、 RedirectView。
View 视图 。作用:将模型数据通过页面展示给用户。

2. DispatcherServlet初始化过程

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

初始化WebApplicationContext

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

@Override
protected final void initServletBean() throws ServletException {
	getServletContext().log("Initializing Spring " + getClass().getSimpleName() + " '" + getServletName() + "'");
	if (logger.isInfoEnabled()) {
		logger.info("Initializing Servlet '" + getServletName() + "'");
	}
	long startTime = System.currentTimeMillis();

	try {
        //对SpringMVC的IOC容器进行初始化
		this.webApplicationContext = initWebApplicationContext();
		initFrameworkServlet();
	}
	catch (ServletException | RuntimeException ex) {
		logger.error("Context initialization failed", ex);
		throw ex;
	}

	if (logger.isDebugEnabled()) {
		String value = this.enableLoggingRequestDetails ?
				"shown which may lead to unsafe logging of potentially sensitive data" :
				"masked to prevent unsafe logging of potentially sensitive data";
		logger.debug("enableLoggingRequestDetails='" + this.enableLoggingRequestDetails +
				"': request parameters and headers will be " + value);
	}

	if (logger.isInfoEnabled()) {
		logger.info("Completed initialization in " + (System.currentTimeMillis() - startTime) + " ms");
	}
}

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;
	}
● 创建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");
		}
        //
		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;
}
说明:在SSM整合的过程中,如果SpringMVC和Spring需要整合,就要分开两个配置文件:一个Spring的配置文件、一个SpringMVC的配置文件,创建的IOC容器就会有两个:一个是SpringMVC的IOC容器,一个是Spring的IOC容器,SpringMVC的IOC容器是子容器,Spring的IOC容器是父容器。如果不需要整合,SpringMVC和Spring共用一个配置文件即可
DispatcherServlet初始化策略
FrameworkServlet 创建 WebApplicationContext 后,刷新容器,调用 onRefresh(wac) ,此方法在DispatcherServlet中进行了重写,调用了 initStrategies(context) 方法,初始化策略,即初始化DispatcherServlet的各个组件
所在类: 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调用组件处理请求

● processRequest()

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

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

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

	HttpMethod httpMethod = HttpMethod.resolve(request.getMethod());
	if (httpMethod == HttpMethod.PATCH || httpMethod == null) {
		processRequest(request, response);
	}
	else {
		super.service(request, response);
	}
}

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);
	}
}
doService()
所在类: org.springframework.web.servlet.DispatcherServlet
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);
		}
	}
}
● 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);
			}
		}
	}
}
● 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 对应的映射:
● 不存在,再判断是否配置了 <mvc:default-servlet-handler />:默认的Servlet,处理静态资源。
如果没配置,则控制台报映射查找不到,客户端展示 404 错误
控制台:
16:27:00.926 [http-nio-8080-exec-9] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC/demo", parameters={}
16:27:00.926 [http-nio-8080-exec-9] WARN org.springframework.web.servlet.PageNotFound - No mapping for GET /SpringMVC/demo
16:27:00.927 [http-nio-8080-exec-9] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 404 NOT_FOUND

客户端:

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

控制台:

16:32:31.104 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.DispatcherServlet - GET "/SpringMVC/demo", parameters={}
16:32:31.112 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.handler.SimpleUrlHandlerMapping - Mapped to org.springframework.web.servlet.resource.DefaultServletHttpRequestHandler@1e297064
16:32:31.119 [http-nio-8080-exec-4] DEBUG org.springframework.web.servlet.DispatcherServlet - Completed 404 NOT_FOUND

客戶端:

● 存在,则执行下面的流程

3) 根据该 URI ,调用 HandlerMapping 获得该 Handler 配置的所有相关的对象(包括 Handler 对象以及Handler对象对应的拦截器),最后以 HandlerExecutionChain 执行链对象的形式返回。
4) DispatcherServlet 根据获得的 Handler ,选择一个合适的 HandlerAdapter
5) 如果成功获得 HandlerAdapter ,此时将开始执行拦截器的 preHandler(…) 方法。
6) 提取 Request 中的模型数据,填充 Handler 入参,开始执行 Handler Controller) 方法,处理请求。 在填充Handler 的入参过程中,根据你的配置, Spring 将帮你做一些额外的工作:
 ① HttpMessageConveter: 将请求消息(如 Json xml 等数据)转换成一个对象,将对象转换为指定的响应信息
 ②数据转换:对请求消息进行数据转换。如String转换成 Integer Double
 ③ 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
 ④ 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult Error
 7) Handler 执行完成后,向 DispatcherServlet 返回一个 ModelAndView 对象。
8) 此时将开始执行拦截器的 postHandle(...) 方法。
9) 根据返回的 ModelAndView (此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的 ViewResolver 进行视图解析,根据 Model和View ,来渲染视图。
10) 渲染视图完毕执行拦截器的 afterCompletion(…) 方法。
11) 将渲染结果返回给客户端。

  • 3
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值