SpringMVC从入门到精通
文章目录
本文章是基于尚硅谷的 SpringMVC讲解视频,加入了自己的一些见解,文中有出错的地方还请指正,码字不易,支持一下吧!
概念引入
人们习惯将JavaEE分为经典的三层架构:
-
表现层(web层)
整个表现层负责接收客户端请求并响应结果给客户端(Http请求)
具体来说,表现层=view视图层+controller控制层。controller层主要负责接收请求,转发请求给service层、跳转视图页面
servlet–>struts(过滤器)–>springmvc
-
业务层(service层)
负责业务逻辑处理,和项目需求息息相关(比如转账业务)。
spring–>springboot(全注解)
主要涉及逻辑:异常处理 参数处理 声明式事务AOP
-
持久层(DAO层)
和数据库交互,对数据表进行增删改查操作
jdbc–>jdbcTemplate–>DBUtils–>Mybatis–>spring Data JPA
主流:SSM–>潮流:SSS(spring全家桶)
那MVC又是什么呢?
MVC是一种软件架构的思想,将软件按照模型、视图、控制器来划分
M:Model,模型层,指工程中的JavaBean,作用是处理数据
JavaBean分为两类:
- 一类称为实体类Bean:专门存储业务数据的,如 Student、User 等
- 一类称为业务处理 Bean:指 Service 或 Dao 对象,专门用于处理业务逻辑和数据访问。
V:View,视图层,指工程中的html或jsp等页面,作用是与用户进行交互,展示数据
C:Controller,控制层,指工程中的servlet,作用是接收请求和响应浏览器
MVC的工作流程: 用户通过视图层发送请求到服务器,在服务器中请求被Controller接收,Controller调用相应的Model层处理请求,处理完毕将结果返回到Controller,Controller再根据请求处理的结果找到相应的View视图,渲染数据后最终响应给浏览器
而springMVC就是对MVC思想的一个很好的体现产品。
SpringMVC 是 Spring 为表述层开发提供的一整套完备的解决方案。在表述层框架历经 Strust、WebWork、Strust2 等诸多产品的历代更迭之后,目前业界普遍选择了 SpringMVC 作为 Java EE 项目表述层开发的首选方案。
注:三层架构分为表述层(或表示层)、业务逻辑层、数据访问层,表述层表示前台页面和后台servlet
SpringMVC的特点:
- Spring 家族原生产品,与 IOC 容器等基础设施无缝对接
- 基于原生的Servlet,通过了功能强大的前端控制器DispatcherServlet,对请求和响应进行统一处理
- 表述层各细分领域需要解决的问题全方位覆盖,提供全面解决方案
- 代码清新简洁,大幅度提升开发效率
- 内部组件化程度高,可插拔式组件即插即用,想要什么功能配置相应组件即可
- 性能卓著,尤其适合现代大型、超大型互联网项目要求
Spring都能干什么呢?
- 接受请求
- 跳转页面
- 会话控制
- 过滤拦截
- 异步交互
- 文件上传
- 文件下载
- 数据校验
- 类型转换
入门案例
之前: request请求到servlet,一个项目中有很多servlet,不同serlvet处理不同的事情。这就导致当我们的项目需求越来越大的时候,会有超级多的servlet,及其不易维护和管理。
但是所有的servlet却都进行同样的两件事:请求和转发。
因此我们需要一个中央控制器,由他接收全部的请求,然后根据具体的请求类型分发给不同的页面进行响应。而springMVC就是这样实现的,SpringMVC全局只需要一个servlet。SpringMVC的最根本的思想就是分发思想。
开发环境
这里我所用的开发环境是:
- IDE:idea 2021.1.3
- 构建工具:maven 3.8.4
- tomcat:9.0.56
- spring:5.3.15
- 模板引擎:thymeleaf
创建项目进行配置
-
创建一个web项目,这里我是使用maven构建工具创建的,所需要的jar包依赖如下:
<dependencies> <!--测试包--> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <!-- SpringMVC --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.3.15</version> </dependency> <!-- 日志 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <!-- ServletAPI --> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.1.0</version> <scope>provided</scope> </dependency> <!-- Spring5和Thymeleaf整合包 --> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring5</artifactId> <version>3.0.12.RELEASE</version> </dependency> <!--快速封装JavaBean--> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency> </dependencies>
注:由于 Maven 的传递性,我们不必将所有需要的包全部配置依赖,而是配置最顶端的依赖,其他靠传递性导入。
-
导入依赖之后,配置
web.xml
文件<!DOCTYPE web-app PUBLIC "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN" "http://java.sun.com/dtd/web-app_2_3.dtd" > <web-app> <!-- 配置servlet:前端控制器,类名为DispatcherServlet,本身就是Servlet,继承自HttpServlet 客户端浏览器所有的请求,都让前端控制器接收 请求分发给不同的程序 --> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <!-- url-pattern 配置浏览器的请求地址 / 所匹配的请求可以是/login或.html或.js或.css方式的请求路径 但是/不能匹配.jsp请求路径的请求 *.do *.action 浏览器的地址栏,最后面是.do的请求全部接受 --> <url-pattern>/*</url-pattern> </servlet-mapping> </web-app>
-
我们知道
DispatcherServlet
是做请求转发的,根据不同的请求调用不同的控制器,因此我们还需要一系列的控制器。创建一个包:com.soberw.controller
,在包内创建一个类SayHelloController
作为控制器:package com.soberw.controller; /** * 控制器:控制页面跳转,调用service层等 * @author soberw * @Classname SayHelloController * @Description * @Date 2022-03-01 9:28 */ public class SayHelloController { public String sayHello(){ System.out.println("Hello world..."); return null; } }
那么控制器有了,如何与中央控制器产生联系呢?
我们需要将其放在IOC容器中,因此需要通过
@Controller注解
将其标识为一个控制层组件,交给Spring的IOC容器管理,此时SpringMVC才能够识别控制器的存在在方法上添加
@RequestMapping
注解:/** * 方法:浏览器请求,调用方法sayHello * 注解:@RequestMapping * 注解的属性 value=浏览器的请求地址 */ @RequestMapping(value = "/sayHello") public String sayHello(){ System.out.println("Hello world..."); return null; }
-
创建一个配置文件
springMVC.xml
,引入context命名空间,配置包扫描;因为我使用的是thymeleaf渲染的页面,因此还需要配置
thymeleaf
:<!--配置包扫描--> <context:component-scan base-package="com.soberw.controller"/> <!-- 配置Thymeleaf视图解析器 --> <bean id="viewResolver" class="org.thymeleaf.spring5.view.ThymeleafViewResolver"> <!--设置视图解析器优先级,越小优先级越高--> <property name="order" value="1"/> <!--设置解析编码--> <property name="characterEncoding" value="UTF-8"/> <!--设置模板 内部bean注入--> <property name="templateEngine"> <bean class="org.thymeleaf.spring5.SpringTemplateEngine"> <property name="templateResolver"> <bean class="org.thymeleaf.spring5.templateresolver.SpringResourceTemplateResolver"> <!-- 视图前缀 --> <property name="prefix" value="/WEB-INF/pages/"/> <!-- 视图后缀 --> <property name="suffix" value=".html"/> <!--模板模型--> <property name="templateMode" value="HTML5"/> <!--编码--> <property name="characterEncoding" value="UTF-8" /> </bean> </property> </bean> </property> </bean>
而如果你使用的是
jsp
渲染页面时,则需要这样配置:<!-- 配置jsp视图解析器 --> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/pages/"/> <property name="suffix" value=".jsp"/> </bean>
-
配置完之后,如何读取呢?我们在学习Spring的时候,都是通过
ClassPathXMLApplicationContext
类去读取配置文件的,但是现在我们写的是页面请求,如何实现呢?我们需要在web.xml中给控制器指定初始化参数:
<servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!-- 通过初始化参数指定SpringMVC配置文件的位置和名称 --> <init-param> <!-- contextConfigLocation为固定值 --> <param-name>contextConfigLocation</param-name> <!-- 使用classpath:表示从类路径查找配置文件,例如maven工程中的src/main/resources --> <param-value>classpath:springMVC.xml</param-value> </init-param> <!-- 作为框架的核心组件,在启动过程中有大量的初始化操作要做 而这些操作放在第一次请求时才执行会严重影响访问速度 因此需要通过此标签将启动控制DispatcherServlet的初始化时间提前到服务器启动时 --> <load-on-startup>1</load-on-startup> </servlet>
-
在方法上添加
@RequestMapping
注解,配置请求映射:/** * 方法:浏览器请求,调用方法sayHello * 注解:@RequestMapping * 注解的属性 value=浏览器的请求地址 */ @RequestMapping(value = "/sayHello") public String sayHello(){ System.out.println("Hello world..."); //返回视图名称 //由thymeleaf处理,拼接上前后缀后,进行转发 // 例如: 访问 "/sayHello" --> 响应到 "/WEB-INF/pages/index.html" 页面 return "index"; }
-
在指定的前缀目录下创建一个index.html页面,作为响应页面
注意:这里的
前缀目录
指的就是在进行thymeleaf配置时指定的目标前缀:在页面头引入thymeleaf命名空间:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h1>Hello world!!!!</h1> </body> </html>
测试运行
配置tomcat服务器:
然后直接运行tomcat服务器:
注意:这里报404错误并不意味我们做错了,我们要先了解tomcat运行的原理,他隐藏掉我们WEB-INF下面的资源文件,注意此时我的index.html就在此路径下,而tomcat启动时会首先找根目录下面的index.jsp或者index.html页面加载,但是因为我们的根目录下是空的,tomcat找不到,因此显示404错误
但这并不影响我们进行测试:
在地址栏后面加上/sayHello
,敲回车键进行跳转:
再返回控制台查看:
也正常打印出来了,证明我们在进行资源请求时,确实被此控制器接收到了,并进行了处理,而且也最终完成了资源的跳转。这就说明我们的配置是成功的!
进一步查看控制台打印信息:
这就是我们引入日志依赖的好处,可以帮助我们更好的了解程序 的执行流程以及错误处理。
总结:
浏览器发送请求,若请求地址符合前端控制器的url-pattern,该请求就会被前端控制器DispatcherServlet处理。前端控制器会读取SpringMVC的核心配置文件,通过扫描组件找到控制器,将请求地址和控制器中@RequestMapping注解的value属性值进行匹配,若匹配成功,该注解所标识的控制器方法就是处理请求的方法。处理请求的方法需要返回一个字符串类型的视图名称,该视图名称会被视图解析器解析,加上前缀和后缀组成视图的路径,通过Thymeleaf对视图进行渲染,最终转发到视图所对应页面
优化配置
在上面的配置中,其实是存在一定的问题的,这个问题其实是spring的历史版本的遗留问题。
在spring3.x框架中,有一套处理器适配器,处理器映射器
到了spring4.x及以上版本后,又换了一套新的,这两套其实所实现的功能都一样,但是性能却不一样,显然4.x以上的性能更好一些。
但是spring框架并不会在运行时自动选择最优的,而是沿用3.x的版本,当然是考虑到了系统兼容性的问题。因此我们需要进行配置,让spring自动进行最优的处理器和映射器。
在springMVC.xml配置文件中进行配置,首先添加mvc命名空间:
<!--
处理静态资源,例如html、js、css、jpg
若只设置该标签,则只能访问静态资源,其他请求则无法访问
此时必须设置<mvc:annotation-driven/>解决问题
-->
<mvc:default-servlet-handler/>
<!-- 开启mvc注解驱动 -->
<mvc:annotation-driven>
<mvc:message-converters>
<!-- 处理响应中文内容乱码 -->
<bean class="org.springframework.http.converter.StringHttpMessageConverter">
<!--默认字符集-->
<property name="defaultCharset" value="UTF-8" />
<!--支持的文本类型-->
<property name="supportedMediaTypes">
<list>
<value>text/html</value>
<value>application/json</value>
</list>
</property>
</bean>
</mvc:message-converters>
</mvc:annotation-driven>
@RequestMapping 注解
从注解名称上我们可以看到,@RequestMapping注解的作用就是将请求和处理请求的控制器方法关联起来,建立映射关系。
SpringMVC 接收到指定的请求,就会来找到在映射关系中对应的控制器方法来处理这个请求。
问题引入
新建一个项目springmvc02_requestMappingAndRequest
,并按照上面的完整配置过程搭建环境,注意最后要开启注解驱动。
在上面的入门案例中,我们使用@RequestMapping注解映射了一个请求访问地址,那么就产生了一个问题:如果控制器中有多个方法对应同一个请求,那么控制器会作何处理呢?
下面演示:
创建两个控制器类TestController1
和TestController2
,并分别声明一个方法,但都加入一个相同的映射地址"/test"
:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname TestController1
* @Description
* @Date 2022-03-01 18:02
*/
@Controller
public class TestController1 {
@RequestMapping("/test")
public String test1(){
return "index";
}
}
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname TestController2
* @Description
* @Date 2022-03-01 18:02
*/
@Controller
public class TestController2 {
@RequestMapping("/test")
public String test2(){
return "index";
}
}
启动tomcat服务器运行,根本不用访问资源,直接报错了:
因此我们 需要注意:在开发中每一个请求所对应的地址一定要是唯一的,不能出现模棱两可的情况。
那么在实际开发中,我们难免会遇到相同的资源请求,比如我的商品模块和订单模块都有查看详情的功能,我们在命名的时候自然就会使用相同的地址方便处理。但是直接使用就会报错。
如何解决?
@RequestMapping注解的位置
查看注解的源码:
@RequestMapping标识一个类:设置映射请求的请求路径的初始信息
@RequestMapping标识一个方法:设置映射请求请求路径的具体信息
举例:
创建一个IndexController
作为Index.html的访问,使得服务器运行时默认打开此页面:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname IndexController
* @Description
* @Date 2022-03-01 19:26
*/
@Controller
public class IndexController {
@RequestMapping("/")
public String index(){
return "index";
}
}
创建一个类:RequestMappingController
并声明一个方法,在类以及方法上分别添加注解以及映射地址:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname RequestMappingController
* @Description
* @Date 2022-03-01 19:19
*/
@Controller
@RequestMapping("/hello")
public class RequestMappingController {
@RequestMapping("/success")
public String test(){
//响应到success.html页面
return "success";
}
}
对应的,创建一个success.html页面作为响应页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>跳转成功!!</h1>
</body>
</html>
在index.html页面上添加一个超链接,实现请求跳转:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
<a th:href="@{/success}">点击跳转到success页面</a>
</body>
</html>
运行tomcat服务器:
和之前的案例相同的请求方式,为什么这里就报错了
返回index页面打开检查:
可以看出,我们直接请求/success,其实浏览器会自动帮我们拼接上前缀http://localhost:8080
上面我介绍过,@RequestMapping注解作用在类上时,表示的是请求路径的初始信息,也就是头信息 ,因此,这里我们需要填写的正确的请求路径是:/hello/success
:
这也解决了上面的问题,我们在进行相同的资源请求时,可以根据不同的业务功能,在类上添加注解,设置不同的初始信息,既不违背唯一性原则,也能正确进行跳转。
@RequestMapping注解的属性
我们发现其实可供填写的属性还是很多的,这里就比较常用的四个属性进行讲解:
value属性
-
@RequestMapping注解的value属性通过请求的请求地址匹配请求映射
-
@RequestMapping注解的value属性是一个字符串类型的数组,表示该请求映射能够匹配多个请求地址所对应的请求
-
@RequestMapping注解的value属性必须设置,至少通过请求地址匹配请求映射
我们之前所使用的就是value属性,但是都是单个请求地址的情况,其实他也支持传入一个数组,意味着能够匹配多个地址对应的请求:
创建一个类ValueController测试:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname ValueController
* @Description
* @Date 2022-03-01 20:42
*/
@Controller
public class ValueController {
@RequestMapping(value = {"/succ", "/success"})
public String test() {
return "success";
}
}
添加两个超链接对应两个地址:
测试:
method属性
-
@RequestMapping注解的method属性==通过请求的请求方式(get或post)==匹配请求映射
-
@RequestMapping注解的method属性是一个RequestMethod类型的数组,表示该请求映射能够匹配多种请求方式的请求
-
若当前请求的请求地址满足请求映射的value属性,但是请求方式不满足method属性,则浏览器报错405:Request method ‘POST’ not supported
此method属性对应的就是我们前端form表单提交数据的方式,是get还是post。若设置此值,则在请求时,除了要保证请求的地址正确,请求的方式也要正确,不然回报405错误。
需要注意一点:当我们没有指定请求方式时,所有的请求都可以接收,下面就证明一下,还沿用上面的例子,在index.html中 添加一个超链接和一个form表单,分别代表get和post请求
<body>
<h1>首页</h1>
<a th:href="@{/hello/success}">测试注解的位置</a><br>
<a th:href="@{/succ}">测试value属性--> /succ</a><br>
<a th:href="@{/success}">测试value属性--> /success</a><br>
<a th:href="@{/succ}">测试method属性--> get请求</a><br>
<form th:action="@{/succ}" method="post">
<input type="submit" value="测试method属性--> post请求">
</form>
</body>
这是默认的情况,会接收所有的请求并做出响应。
但是当我们给method属性赋值后,那就不行了,就只能接收我们设置的请求方式发送的请求:
例如我给我得测试类的方法上添加method属性:
测试运行:
注意:注解的method属性是一个RequestMethod类型的数组,表示该请求映射能够匹配多种请求方式的请求,与value属性相似
另外:对于处理指定请求方式的控制器方法,SpringMVC中提供了==@RequestMapping的派生注解==
-
处理get请求的映射–>
@GetMapping
-
处理post请求的映射–>
@PostMapping
-
处理put请求的映射–>
@PutMapping
-
处理delete请求的映射–>
@DeleteMapping
在使用时只需要提供value地址值即可,会自动绑定上对应的请求方式。
但是要知道,form表单只能发送get和post请求,那么我们如何接收其他方式呢?比如delete,Spring给我们提供了专门的处理方法,后面我们会说到。
params属性(了解)
-
@RequestMapping注解的params属性通过请求的请求参数匹配请求映射
-
@RequestMapping注解的params属性是一个字符串类型的数组,可以通过四种表达式设置请求参数和请求映射的匹配关系
-
"param":要求请求映射所匹配的请求必须携带param请求参数
-
"!param":要求请求映射所匹配的请求必须不能携带param请求参数
-
"param=value":要求请求映射所匹配的请求必须携带param请求参数且param=value
-
"param!=value":要求请求映射所匹配的请求必须携带param请求参数但是param!=value
-
简单来说,就是设置此属性后,会给传入的参数添加筛选条件,只接受符合的情况,如果不符合就会报错:
声明一个方法,并指定请求路径以及参数属性,如上,这里可以指定四种情况,这里我就以其中两种做说明:
//此注解的意思是,匹配地址为/testMethod的,且请求参数中必须包含username和password但是username不能为admin
@RequestMapping(value = "/testMethod", params = {"username!=admin", "password"})
public String methodTest() {
return "success";
}
在index.html中添加超链接:
<a th:href="@{/testMethod}">测试params属性-->不传参数</a><br>
<a th:href="@{/testMethod(username='admin')}">测试params属性-->只传一个</a><br>
<a th:href="@{/testMethod(username='admin',password=1)}">测试params属性-->都传入,但是其中一个不符合条件</a><br>
<a th:href="@{/testMethod(usernmae='wang',password=111)}">测试params属性-->都符合</a><br>
运行:
-
测试不传参数:
-
测试只传一个:
-
测试都传入,但是其中一个不满足条件:
-
测试按要求传入:
-
总结
- params属性数组对应的条件缺一不可,都需要满足
- 若当前请求满足@RequestMapping注解的value和method属性,但是不满足params属性,此时页面回报错400
headers属性(了解)
-
@RequestMapping注解的headers属性通过请求的请求头信息匹配请求映射
-
@RequestMapping注解的headers属性是一个字符串类型的数组,可以通过四种表达式设置请求头信息和请求映射的匹配关系
-
"header":要求请求映射所匹配的请求必须携带header请求头信息
-
"!header":要求请求映射所匹配的请求必须不能携带header请求头信息
-
"header=value":要求请求映射所匹配的请求必须携带header请求头信息且header=value
-
"header!=value":要求请求映射所匹配的请求必须携带header请求头信息且header!=value
-
若当前请求满足@RequestMapping注解的value和method属性,但是不满足headers属性,此时页面显示404错误,即资源未找到
同上面的设置方法一样,此属性对应的是发送的请求头信息即request headers,例如我点击跳转链接,打开浏览器控制台,就能显示我们此时所发送的头信息:
其中包含了许多的头信息,而因为我们做的是请求跳转,因此可以去设置请求头信息,添加条件,
设置的方式与设置params属性一样,包含四种情况。
总结:
- @RequestMapping设置的属性越多,就需要满足越多的条件,但是所表示的也就越精确。
- 如果发送的请求地址没有与任何@RequestMapping的value属性值匹配,报404
- 如果请求方式不匹配,报405
- 如果请求参数不支持,报400
- 如果请求头信息错误,报404
value属性的模糊匹配
另外,针对于value属性值的设置,还有一些说明:
他是支持模糊匹配的,包括以下几种方式:
-
?:表示任意的单个字符
-
*:表示任意的0个或多个字符
-
**:表示任意的一层或多层目录
注意:在使用 ** 表示目录时,只能使用 /**/xxx 的方式
在使用任意符号?或者 * 时,不能输入 ? 以及 / 符号
举例说明:
@RequestMapping("/a?/ant1")
public String valueAntTest1(){
return "success";
}
@RequestMapping("/a*/ant2")
public String valueAntTest2(){
return "success";
}
@RequestMapping("/**/ant3")
public String valueAntTest3(){
return "success";
}
在index.html中添加跳转链接:
<a th:href="@{/as/ant1}">测试value属性的模糊匹配-->?</a><br>
<a th:href="@{/asss/ant2}">测试value属性的模糊匹配-->*</a><br>
<a th:href="@{/ant3}">测试value属性的模糊匹配-->**</a><br>
-
测试?匹配
-
测试 * 匹配
-
测试 ** 匹配
获取请求参数
通过@RequestMapping注解我们可以对请求做出响应处理,那么请求必然少不了伴随着请求参数,因为我们很多的业务功能就是依赖于参数去处理的,那么如何接收呢?
准备工作:
新建一个页面用于进行测试test_param.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>测试请求参数</title>
</head>
<body>
<h1>测试请求参数页面</h1>
</body>
</html>
生成一个 方法跳转到此页面:
@RequestMapping("/test_param")
public String param(){
return "test_param";
}
在index.html首页中添加链接放访问:
<a th:href="@{/test_param}">点击跳转到test_param.html页面</a><br>
新建一个控制器类ParamController
,用于测试。
import org.springframework.stereotype.Controller;
/**
* @author soberw
* @Classname ParamController
* @Description
* @Date 2022-03-02 16:36
*/
@Controller
@RequestMapping("/param")
public class ParamController {
}
接下来我们就在test_param.html中测试获取参数。
通过原生ServletAPI获取请求参数
原生的获取参数的方式,就是通过当前请求对象HTTPServletRequest
的getParamter()
方法去获取对应的参数的,在springmvc中,我们可以直接写入形参,他会自动传递实参给我们:
//SpringMVC会自动将当前请求传递给形参,request表示的就是当前请求
@RequestMapping("/testServletAPI")
public String testServletAPI(HttpServletRequest request){
String username = request.getParameter("username");
String password = request.getParameter("password");
System.out.println("username = " + username);
System.out.println("password = " + password);
return "success";
}
在页面中添加超链接测试跳转:
<a th:href="@{/param/testServletAPI(username='admin',password=123456)}">测试使用servletAPI获取请求参数</a>
测试:
此时返回控制台:
成功获取。
但是我们一般不会用此种方式去获取参数,而是使用SpringMVC给我们提供的更方便的方式。
通过控制器方法的形参获取请求参数
上面说到,SpringMVC可以自动帮我们给形参赋值,因此我们不必去获取HTTPServletRequest对象了,直接在形参列表中写上要获取的参数即可,注意:参数名必须与传入的参数名一致!
@RequestMapping("/testParam")
public String testParam(String username,String password) {
System.out.println("username = " + username);
System.out.println("password = " + password);
return "success";
}
在页面中添加跳转链接:
<a th:href="@{/param/testParam(username='admin',password=123456)}">测试使用控制器形参获取请求参数</a><br>
点击跳转成功,返回控制台查看:
是不是就简单了许多,但是也引出了一个问题:
当我们的参数名重复时,如何获取呢?
何谓重复,比如我们在提交表单时,表单里定义复选框时会存在大量的同名参数,此时怎么做呢?
在页面中添加一个form表单:
<!--这里为了方便看到提交结果,使用get请求-->
<form th:action="@{/param/testParamRepeat1}" method="get">
用户名:<input type="text" name="username"><br>
密码:<input type="text" name="password"><br>
课程:<input type="checkbox" name="course" value="java">java
<input type="checkbox" name="course" value="php">php
<input type="checkbox" name="course" value="c++">c++
<br>
<input type="submit" value="测试通过使用控制器获取重复的参数">
</form>
这里我们有两种获取方式,因为是多个同名参数,因此可以使用数组去接收,但是也可以用单个String类型去获取:
-
使用数组获取:
@RequestMapping("/testParamRepeat1") public String testParamRepeat1(String username, String password,String[] course) { System.out.println("username = " + username); System.out.println("password = " + password); System.out.println("course = " + Arrays.toString(course)); return "success"; }
填写数据,点击提交:
跳转成功。查看控制台:
-
使用字符串获取:
要知道,在原生的ServletAPI中,如果要获取多个参数的情况是要使用
getParameterValues()
方法去获取,如果还是使用的普通方法,则只能获取到第一个,但是在SpringMVC中,则有所不同:@RequestMapping("/testParamRepeat2") public String testParamRepeat2(String username, String password,String course) { System.out.println("username = " + username); System.out.println("password = " + password); System.out.println("course = " + course); return "success"; }
修改表单请求地址为当前地址:
测试:
重点查看控制台:
可以看出,我们同样可以使用字符串去接收,SpringMVC会帮我们拼接成字符串,多个之间用逗号隔开,方便我们进行处理。
注:
若请求所传输的请求参数中有多个同名的请求参数,此时可以在控制器方法的形参中设置字符串数组或者字符串类型的形参接收此请求参数
若使用字符串数组类型的形参,此参数的数组中包含了每一个数据
若使用字符串类型的形参,此参数的值为每个数据中间使用逗号拼接的结果
@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所指定的请求参数没有传输或传输的值为""时,则使用默认值为形参赋值
声明一个方法,用于测试请求跳转;
@RequestMapping("/testRequestParam")
public String testRequestParam(@RequestParam("username") String name, String password) {
System.out.println("username = " + name);
System.out.println("password = " + password);
return "success";
}
在页面上新建一个超链接,并拼接两个参数:
<a th:href="@{/param/testRequestParam(username='wang',password=123)}">测试使用@RequestParam注解映射请求参数</a><br>
测试:
点击跳转:
此时可以发现我发送的请求参数与控制器方法形参并不一样,前往控制台查看:
一样获取到了。
但是注意,此时我将请求参数中指明映射关系的参数删除后:
会报错,因为此注解映射的参数默认是必须的。
解决方法有两种:
- 给请求参数中添加username参数
- 给注解添加required属性为false
- 添加defaultValue属性
添加参数不必多说,下面在@RequestParam注解上添加required属性为false:
再次运行:
另外,@RequestParam还有一个重要的属性:defaultValue属性,默认参数值
即当value所指定的请求参数没有传输或传输的值为""时,则使用默认值为形参赋值,且不受required属性的影响。
此时我们再请求,当我不传输username参数时:
一样成功,而此时获取到的实际上是我们设置的默认值:
或者我传输为空字符串:
同样正常打印。
注:
我们在开发中,使用最多的属性是
defaultValue
有了此属性,我们在给请求参数设置默认值后,就不必再对接收的参数进行判断了
比如空值判断或者!=null判断,因为已经用默认值给形参赋值了,减少了我们很多的工作量。
请求参数是JavaBean对象
这里所说的请求参数是JavaBean对象,并不是说直接将一个java对象作为参数进行传递,而是说当传递的参数正好和一个JavaBean对象的字段一一对应时,我们可以直接使用一个对象来接收请求参数,这种获取方式也是很常用的,下面演示:
创建一个User
类,定义属性:
package com.soberw.springmvc.pojo;
import lombok.Data;
/**
* @author soberw
* @Classname User
* @Description
* @Date 2022-03-02 20:47
*/
@Data
public class User {
private String username;
private String password;
private int age;
private double money;
}
创建一个控制器方法,对应的形参为User对象:
@RequestMapping("/testUser")
public String testUser(User user){
System.out.println("user = " + user);
return "success";
}
在页面上添加一个表单,一一对应着User对象的字段值,注意请求参数值一定与字段名一样,这里就算设置映射关系也不行!
<form th:action="@{/param/testUser}" method="get" style="border: 1px solid black">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
年龄:<input type="text" name="age"><br>
工资:<input type="text" name="money"><br>
<input type="submit" value="测试传递JavaBean对象">
</form>
填写数据并提交:
发现,是可以获取到的,且一一对应,对我们来说相当方便。
请求参数是JavaBean对象的包装类型(了解)
包装类型,简单理解就是在类外面再包一层,我们通过外部的对象给内部对象赋值。
因此,声明一个User的包装类UserVo
:
package com.soberw.springmvc.pojo;
import lombok.Data;
/**
* @author soberw
* @Classname UserVo
* @Description
* @Date 2022-03-02 21:06
*/
@Data
public class UserVo {
private User user;
}
添加方法:
@RequestMapping("/testUserVo")
public String testUserVo(UserVo userVo){
System.out.println("userVo = " + userVo);
return "success";
}
在页面上添加一个表单,==注意此时我设置的参数名!==是包装类的属性点上具体的属性值:
<form th:action="@{/param/testUserVo}" method="get" style="border: 1px solid black">
用户名:<input type="text" name="user.username"><br>
密码:<input type="password" name="user.password"><br>
年龄:<input type="text" name="user.age"><br>
工资:<input type="text" name="user.money"><br>
<input type="submit" value="测试传递JavaBean对象的包装类型">
</form>
相当于外面又包了一层,可以看出来,这样非常麻烦,因此不常用,了解即可。
请求参数是JavaBean对象的集合类型(了解)
既然可以直接将同名参数作为集合接收,也可以接收对象,那么可不可以将参数作为对象的集合接收呢?
当然可以,只不过这样比较麻烦,这里以List集合为例,其他类同 :
新建一个类Users存放User集合:
package com.soberw.springmvc.pojo;
import lombok.Data;
import java.util.List;
/**
* @author soberw
* @Classname Users
* @Description
* @Date 2022-03-03 10:11
*/
@Data
public class Users {
private List<User> userList;
}
创建一个方法用于接收:
@RequestMapping("/testUserList")
public String testUserList(Users users){
System.out.println("users = " + users);
return "success";
}
在页面中添加一个表单,是不是已经感觉到了麻烦:
<form th:action="@{/param/testUserList}" method="get" style="border: 1px solid black">
用户名:<input type="text" name="userList[0].username"><br>
密码:<input type="password" name="userList[0].password"><br>
年龄:<input type="text" name="userList[0].age"><br>
工资:<input type="text" name="userList[0].money"><br>
<hr>
用户名:<input type="text" name="userList[1].username"><br>
密码:<input type="password" name="userList[1].password"><br>
年龄:<input type="text" name="userList[1].age"><br>
工资:<input type="text" name="userList[1].money"><br>
<input type="submit" value="测试传递JavaBean对象的集合类型">
</form>
此时输出的正是一个List集合,但是此做法相当麻烦,因此不建议使用。
@RequestHeader注解
上面的@RequestParam注解我们知道是与请求参数建立映射关系,那么见名知意,@RequestHeader
注解自然是与请求头建立联系。
要知道请求头信息也是通过键值对的形式来传输的,因此我们只需要使用此注解指定键对应的参数,就能获取到对应的值信息。
举例:
新建一个方法:
@RequestMapping("/testRequestHeader")
public String testRequestHeader(@RequestHeader("host") String host){
System.out.println("host = " + host);
return "success";
}
在页面生成超链接:
<a th:href="@{/param/testRequestHeader}">测试获取请求头信息</a><br>
启动服务器:
注:
@RequestHeader是将请求头信息和控制器方法的形参创建映射关系
@RequestHeader注解一共有三个属性:value、required、defaultValue,用法同@RequestParam
与@RequestParam不同的是,此注解在不使用的时候,默认是不获取请求头信息的
@CookieValue注解
见名知意,@CookieValue注解自然是跟我们的Cookie建立映射的。我们如果想要获取Cookie信息,就可以使用此注解。
@CookieValue是将cookie数据和控制器方法的形参创建映射关系
@CookieValue注解一共有三个属性:value、required、defaultValue,用法同@RequestParam
这里不再过多讲解。
解决请求参数的乱码问题
在上面的请求中,我都没有使用中文参数,那是因为,如果直接传输中文,是会出现乱码问题的。
另外,我所使用的请求都是get请求,而get请求一般是没有乱码的,当然如果使用Tomact服务器时,get请求也是可能出现乱码的。
比如,我就以上面的表单为例,现在为修改请求为post,当我传输的参数包含中文时:
下面就解决这个问题:
首先说,一般get请求是不会出现乱码的,但是因为tomcat服务器自身的原因,导致我们在发送get请求时也可能出现乱码的情况,如何解决呢?
找到tomcat的配置文件,在tomcat根目录下的conf文件夹里,server.xm文件,找到这一行:
添加这个属性,设置为UTF-8,我是这里加过了,所以之前没有出现过乱码。
URIEncoding="UTF-8"
但是post请求的乱码依然无法解决,还需要我们添加配置。
我们知道,在原生servlet中,我们可以在获取请求参数的前面
,通过HTTPServletRequest的setCharacterEncoding()
方法来修改编码解决乱码问题。
但是在这里需要注意:
因为我们是使用的SpringMVC框架,在此框架中只有一个Servlet–>DispatcherServlet,由他接收所有的请求,因此当你在之后的方法里再设置,已经不行了,因为这些参数已经都被中央控制器接收过了,我们在控制器方法里再获取,实际上是 在向中央控制器获取,而中央控制器又是由SpringMVC管理的,因此想要解决乱码问题,我们需要在中央控制器拿到参数之前,就设置编码,这样中央控制器拿到的就不是乱码了。
那么有什么是可以在中央控制器执行之前执行的呢?要知道我们为了提高响应效率,将中央控制器的初始化时间提前到了服务器启动时。
我们 服务器有三大组件:监听器,过滤器,servlet。
最先执行的就是监听器,其次是过滤器,最后是servlet。而监听器监听的是servlet的创建和销毁的,一般只会执行一次。而过滤器在设置请求路径后,每次请求只要满足过滤条件就能被拦截下来,因此
我们只需要设置过滤器就行了。而过滤器SpringMVC也已经给我们提供好了,我们只需要进行简单配置即可。
在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>
注:
SpringMVC中处理编码的过滤器一定要配置到其他过滤器之前,否则无效
在运行测试一下:
此时已经不会再乱码了。
域对象共享数据
我们知道一共有四种域对象:
- page:页面域,只在当前页面有效(基本不用)
request
:请求域,只在一次请求有效session
:会话域,只在一次会话有效,即浏览器开启到浏览器关闭applicationContext
:只在整个应用有效,即服务器开启到服务器关闭
实际中我们使用最多的就是request和session这俩中域对象。
而在这两个域对象中,request又是使用比较多的,因为为了保证数据的时效性,比如查询操作,要确保每次查询到的数据都是最新的,因此在选择的时候,要选既能实现功能的、存放的范围又尽量小的,所以request很常用。
新建一个工程项目springmvc03-request
,相关配置同上,注意配置过滤器,目录结构如下:
新建一个index.html 页面,作为首页,以及一个success.html页面作为响应跳转页面。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>首页</h1>
</body>
</html>
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>跳转成功!!</h1>
</body>
</html>
新建一个IndexController控制类,设置为程序启动跳转到index.html:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname IndexController
* @Description
* @Date 2022-03-04 16:20
*/
@Controller
public class IndexController {
@RequestMapping("/")
public String index(){
return "index";
}
}
启动tomcat服务器,显示此页面,说明配置成功:
新建一个控制类ScopeController
来完成我们的工作。
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname ScopeController
* @Description
* @Date 2022-03-04 16:43
*/
@Controller
@RequestMapping("/scope")
public class ScopeController {}
通过原生ServletAPI向request域对象共享数据
我们既然可以通过原生的ServletAPI获取请求的数据,自然也可以向域对象共享数据:
//使用servletAPI向request域对象共享数据
@RequestMapping("/testRequestByServletAPI")
public String testRequestByServletAPI(HttpServletRequest request){
request.setAttribute("testRequestScope","使用servletAPI向request域对象共享数据");
return "success";
}
既然是向域对象共享数据,那么就把侧重点放在success.html页面上:
首先在index上创建一个超链接跳转到success.html页面上:
<a th:href="@{/scope/testRequestByServletAPI}">测试通过原生ServletAPI向request域对象共享数据</a><br>
在success.html页面上添加标签,接收数据:
<p th:text="${testRequestScope}"></p>
运行:
获取成功。
而SpringMVC也给我们提供了好几种的共享方式,下面一一介绍。
使用ModelAndView向request域对象共享数据
见名知意,主要可以完成两个功能:
-
Model主要用于向请求域共享数据
-
View主要用于设置视图,实现页面跳转
是SpringMVC里面一个及其重要的组件。下面简单演示用法:
首先要想使ModelAndView被我们的中央控制器DispatchServlet所解析,就必须其对象作为方法返回值提交给中央控制器。
因此声明方式时有所不同:
//使用ModelAndView向request域对象共享数据
@RequestMapping("/testRequestByModelAndView")
public ModelAndView testRequestByModelAndView(){
ModelAndView mav= new ModelAndView();
//处理模型数据,即向请求域request共享数据
mav.addObject("testRequestScope","使用ModelAndView向request域对象共享数据");
//设置视图名称,实现视图跳转
mav.setViewName("success");
return mav;
}
对应的,在index.html页面设置超连接即可,因为我这里设置的建名称是一样的:
<a th:href="@{/scope/testRequestByModelAndView}">测试通过ModelAndView向request域对象共享数据</a><br>
使用Model向request域对象共享数据
创建一个方法:
//使用Model向request域对象共享数据
@RequestMapping("/testRequestByModel")
public String testRequestByModel(Model model){
model.addAttribute("testRequestScope","使用Model向request域对象共享数据");
return "success";
}
创建超链接:
<a th:href="@{/scope/testRequestByModel}">测试通过Model向request域对象共享数据</a><br>
通过Map集合向request域对象共享数据
我们只需要给控制器方法传递一个Map类型的形参,然后向里面添加数据即可。
//使用map向request域对象共享数据
@RequestMapping("/testRequestByMap")
public String testRequestByMap(Map<String, Object> map){
map.put("testRequestScope","使用Map向request域对象共享数据");
return "success";
}
<a th:href="@{/scope/testRequestByMap}">测试通过Map向request域对象共享数据</a><br>
通过ModelMap向request域对象共享数据
//使用ModelMap向request域对象共享数据
@RequestMapping("/testRequestByModelMap")
public String testRequestByModelMap(ModelMap modelMap){
modelMap.addAttribute("testRequestScope","使用ModelMap向request域对象共享数据");
return "success";
}
<a th:href="@{/scope/testRequestByModelMap}">测试通过ModelMap向request域对象共享数据</a><br>
Model、ModelMap、Map的关系
通过上面我们发现,这三个的基本步骤都是一致的,因此,我们可以推断出他们三者之间肯定是存在一定的联系的,接下来就验证一下:
我在三个对应的方法中都打印一下他们的类型:
依次点击三个链接:
查看打印信息:
他们最终都是通过相同的一个类BindingAwareModelMap
来实现的。
我们依次查看这三个API源码:
Model是一个接口,且没有父接口或者父类,说明他是SpringMVC中操作数据模型的一个顶层接口了。
Map不用多说,是我们java原生的jdk。
ModelMap,查看源码结构:
首先,他继承自LinkedHashmap,而LinkedHashmap又是Map接口的实现类,因此ModelMap可以看做是Map接口的实现类。
此时就可以发现,BindingAwareModelMap,实际上即实现了Model接口,又继承了ModelMap类,而ModelMap又是Map的实现类,因此BindingAwareModelMap才可以实例化Map的对象。
public interface Model{}
public class ModelMap extends LinkedHashMap<String, Object> {}
public class ExtendedModelMap extends ModelMap implements Model {}
public class BindingAwareModelMap extends ExtendedModelMap {}
向session域对象共享数据
与request域对象一样,有两种方式可以向session域对象共享数据,即通过servletAPI或者通过SpringMVC给我们提供的方式。如果是使用SpringMVC方式,需要使用到@SessionAttribute注解,原理是当我们向request域共享数据时,使用此注解可以让此数据同时在session域里也共享一份,那么就相当于session域和request域中都保存着相同的数据,因此不太好用。我更推荐使用ServletAPI原生的实现方式,下面演示:
创建一个方法:
//使用原生ServletAPI向session域对象共享数据
@RequestMapping("/testSession")
public String testSession(HttpServletRequest request){
HttpSession session = request.getSession();
session.setAttribute("testSessionScope","使用原生ServletAPI向session域对象共享数据");
return "success";
}
在页面添加超链接完成跳转,并在跳转页面接收:
<a th:href="@{/scope/testSession}">测试通过原生ServletAPI向session域对象共享数据</a><br>
<p th:text="${session.testSessionScope}"></p>
向application域对象共享数据
同session域一样,推荐使用servletAPI的方式:
//使用原生ServletAPI向application域对象共享数据
@RequestMapping("/testApplication")
public String testApplication(HttpServletRequest request){
ServletContext servletContext = request.getServletContext();
servletContext.setAttribute("testApplicationScope","使用原生ServletAPI向application域对象共享数据");
return "success";
}
<a th:href="@{/scope/testApplication}">测试通过原生ServletAPI向application域对象共享数据</a><br>
<p th:text="${application.testApplicationScope}"></p>
SpringMVC的视图
SpringMVC中的视图使用的都是View
接口,视图的作用是渲染数据,将模型Model中的数据展示给用户。
SpringMVC视图的种类很多,默认有转发视图InternalResourceView和重定向视图RedirectView.
当工程引入jstl的依赖,转发视图会自动转换为JstlView
.
若使用的视图技术为Thymeleaf,在SpringMVC的配置文件中配置了Thymeleaf的视图解析器,由此视图解析器解析之后所得到的是ThymeleafView
.
注:
需要注意的是,引入视图解析器 例如Thymeleaf后,并不代表所有的视图请求都会被Thymeleaf所解析。
只有在视图名称
没有任何前缀
的情况下才会被Thymeleaf所解析,就比如我们上面的所有的情况都是没有前缀的,那有前缀的情况下面介绍。
新建一个控制器类ViewController
:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname ViewController
* @Description
* @Date 2022-03-05 10:57
*/
@Controller
@RequestMapping("/view")
public class ViewController {
}
ThymeleafView
当控制器方法中所设置的视图名称没有任何前缀时,此时的视图名称会被SpringMVC配置文件中所配置的视图解析器解析,视图名称拼接视图前缀和视图后缀所得到的最终路径,会通过转发的方式实现跳转。
实际上,我们之前所有的例子都是通过Thymeleaf解析的,这里再演示一下:
//通过Thymeleaf渲染
@RequestMapping("/testThymeleafView")
public String testThymeleafView(){
return "success";
}
<a th:href="@{/view/testThymeleafView}">测试Thymeleaf渲染</a><br>
在中央控制器DispatcherServlet中设置断点:
说明我们此时得到的是ThymeleafView:
转发视图
虽然配置了Thymeleaf视图解析器使得我们在进行视图转发时很是方便,但是也存在一定的问题。
比如当我在接收请求时,在处理完成后,我想将处理结果转发给另一个控制器去操作,而不是直接转给页面去渲染,就比如:
//通过Thymeleaf渲染
@RequestMapping("/testThymeleafView")
public String testThymeleafView(){
return "success";
}
@RequestMapping("/testForward")
public String testForward(){
return "/testThymeleafView";
}
当我访问 /testForward
时,我想再内部转发给/testThymeleafView
进行处理,由他跳转到最终页面,但是如果我们这样写,运行时:
<a th:href="@{/view/testForward}">测试通过/testThymeleaf跳转到success页面</a><br>
错误显而易见,这是因为我们配置了视图解析器,而在视图名称没有任何前缀
的情况下就会导致解析器将我们的请求解析了,拼接上我们设置的视图前缀和后缀,而拼接后的地址自然是不存在的,因此会报错。
而解决方法也很简单,加上视图前缀就可以了。
注意,试图前缀也不是乱加的,SpringMVC中默认的转发视图是InternalResourceView
SpringMVC中创建转发视图的情况:
当控制器方法中所设置的视图名称以"forward:"
为前缀时,创建InternalResourceView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"forward:"去掉,剩余部分作为最终路径通过转发的方式实现跳转
例如"forward:/employee"
因此在返回结果上加上"forward:/"后:
而完成这一操作实际上内部经历了两步,而最终得到的视图对象是InternalResourceView
重定向视图
重定向也是开发中比较常用的一种 操作,SpringMVC中默认的重定向视图是RedirectView
当控制器方法中所设置的视图名称以"redirect:"
为前缀时,创建RedirectView视图,此时的视图名称不会被SpringMVC配置文件中所配置的视图解析器解析,而是会将前缀"redirect:"去掉,剩余部分作为最终路径通过重定向的方式实现跳转
例如"redirect:/employee"
@RequestMapping("/testRedirect")
public String testRedirect(){
return "redirect:/view/testThymeleafView";
}
<a th:href="@{/view/testRedirect}">测试通过重定向跳转到success页面</a><br>
注:
重定向视图在解析时,会先将redirect:前缀去掉,然后会判断剩余部分是否以/开头,若是则会自动拼接上下文路径
单看结果感觉和转发一样,但是很是不同,首先,重定向最终的视图对象是:
而转发和重定向也是有很大区别的:
转发和重定向的区别:
- 转发是两次请求,在浏览器请求一次,然后服务器内部进行了转发,最后再返回给浏览器,因此地址栏不会发生变化,还是浏览器请求的地址
重定向也是两次请求,是浏览器发生了两次请求,第一次是发送给servlet,第二次是发送给重定向后的地址,因此地址栏显示的是重定向后的地址- 转发可以获取到request请求域中的数据,重定向不能
- 转发可以访问到WEB-INF下的资源,重定向不可以
- 转发不能跨域,重定向可以跨域,即转发只能访问服务器内部的资源,而重定向可以访问任何资源,比如重定向到百度
视图控制器view-controller
当控制器方法中,仅仅用来实现页面跳转,即只需要设置视图名称时,可以将处理器方法使用view-controller标签进行表示。
就比如我们在设置首页时,所做的就是在控制器方法中进行的,那么现在就可以换成使用此标签完成:
<!--
path:设置处理的请求地址
view-name:设置请求地址所对应的视图名称
-->
<mvc:view-controller path="/" view-name="index"></mvc:view-controller>
注释掉控制方法:
运行:
也能成功跳转。
注:
当SpringMVC中设置任何一个view-controller时,其他控制器中的请求映射将全部失效,此时需要在SpringMVC的核心配置文件中设置开启mvc注解驱动的标签:
<mvc:annotation-driven />
而我在最开始优化配置的时候已经配置过了,因此可以正常访问。
RESTful风格
REST
:Representational State Transfer,表现层资源状态转移。
-
资源
资源是一种看待服务器的方式,即,将服务器看作是由很多离散的资源组成。每个资源是服务器上一个可命名的抽象概念。因为资源是一个抽象的概念,所以它不仅仅能代表服务器文件系统中的一个文件、数据库中的一张表等等具体的东西,可以将资源设计的要多抽象有多抽象,只要想象力允许而且客户端应用开发者能够理解。与面向对象设计类似,资源是以
名词
为核心来组织的,首先关注的是名词。一个资源可以由一个或多个URI来标识。URI既是资源的名称,也是资源在Web上的地址。对某个资源感兴趣的客户端应用,可以通过资源的URI与其进行交互。 -
资源的表述
资源的表述是一段对于资源在某个特定时刻的状态的描述。可以在客户端-服务器端之间转移(交换)。资源的表述可以有多种格式,例如HTML/XML/JSON/纯文本/图片/视频/音频等等。资源的表述格式可以通过协商机制来确定。请求-响应方向的表述通常使用不同的格式。
-
状态转移
状态转移说的是:在客户端和服务器端之间转移(transfer)代表资源状态的表述。通过转移和操作资源的表述,来间接实现操作资源的目的。
注:
需要注意的是,RESTful只是一种资源定位以及资源操作的风格。不是标椎也不是协议,只是一种风格。基于这种风格设计的语言可以更加简洁,更有层次。
RESTful的实现
具体说,就是 HTTP 协议里面,四个表示操作方式的动词:GET、POST、PUT、DELETE。
它们分别对应四种基本操作:GET 用来获取资源,POST 用来新建资源,PUT 用来更新资源,DELETE 用来删除资源。
REST 风格提倡 URL 地址使用统一的风格设计,从前到后各个单词使用斜杠分开,不使用问号键值对方式携带请求参数,而是将要发送给服务器的数据作为 URL 地址的一部分,以保证整体风格的一致性。
操作 | 传统方式 | REST风格 |
---|---|---|
查询操作 | getUserById?id=1 | user/1–>get请求方式 |
保存操作 | saveUser | user–>post请求方式 |
删除操作 | deleteUser?id=1 | user/1–>delete请求方式 |
更新操作 | updateUser | user–>put请求方式 |
通过对比就可以发现,相比于原始的请求风格,RESTful更有优势:
-
安全
使用问号键对的方式给服务器传递数据太明显,容易被人利用来对系统进行破坏。使用REST风格携带的数据不再需要明显的暴露数据的名称。
-
风格统一
URL地址整体格式统一,从前到后始终都是用斜杠划分各个内容部分,用简单一致的格式表达语义。
-
简洁、优雅
过去增删改查操作需要设计4个不同的URL,现在一个就够了。
那么在SpringMVC中如何实现这个风格呢?
对于处理指定请求方式的控制器方法,SpringMVC中提供了@RequestMapping的派生注解
:
-
处理get请求的映射–>
@GetMapping
-
处理post请求的映射–>
@PostMapping
-
处理put请求的映射–>
@PutMapping
-
处理delete请求的映射–>
@DeleteMapping
但是目前浏览器只支持get和post,若在form表单提交时,为method设置了其他请求方式的字符串(put或delete),则按照默认的请求方式get处理。
路径占位符
因为RESTful风格的基本实现就是将参数风格统一化,即将参数用单斜杠追加在URL后面。但是这就导致SpringMVC误将参数也当成地址去跳转。如何解决?
SpringMVC支持路径中的占位符。
SpringMVC路径中的占位符常用于RESTful风格中,当请求路径中将某些数据通过路径的方式
传输到服务器中,就可以在相应的@RequestMapping注解的value属性中通过占位符{xxx}
表示传输的数据,在通过@PathVariable
注解,将占位符所表示的数据赋值给控制器方法的形参
新建一个控制类以及方法:
package com.soberw.springmvc.controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname RESTfulController
* @Description
* @Date 2022-03-07 10:36
*/
@Controller
@RequestMapping("/rest")
public class RESTfulController {
//测试占位符
@RequestMapping("/testRest/{username}/{password}")
public String testRest(
@PathVariable("username") String username,
@PathVariable("password") String password){
System.out.println("username = " + username);
System.out.println("password = " + password);
return "success";
}
}
<a th:href="@{/rest/testRest/wang/123456}">测试占位符</a><br>
成功获取到了。
模拟请求
有了这个前置知识点,下面简单模拟一下使用RESTful操作用户资源。
模拟以下几个功能:
请求地址 | 请求方式 | 实现功能 |
---|---|---|
/user | get | 查询所有用户信息 |
/user/1 | get | 根据用户id查询信息 |
/user | post | 添加用户信息 |
/user/1 | delete | 删除指定用户信息 |
/user | put | 修改用户信息 |
模拟get和post请求
因为get和post请求可以直接通过表单提交,因此实现起来就比较简单:
-
get请求模拟查询所有用户信息
新建一个控制器方法:
@GetMapping(value ="/user") public String getAllUser(){ System.out.println("查询所有用户信息"); return "success"; }
<a th:href="@{/rest/user}">测试查询所有用数据</a><br>
-
get请求模拟查询指定id的用户信息
@GetMapping("/user/{id}") public String getUserById(@PathVariable String id) { System.out.println("查询到id为" + id + "的用户信息"); return "success"; }
<a th:href="@{/rest/user/001}">测试查询指定id用户的数据</a><br>
-
post请求模拟添加用户信息
目前支持发送post请求的主要有两种:form表单以及Ajax异步请求
这里通过表单测试:
@PostMapping("/user") public String addUser(String username,String password) { System.out.printf("添加的用户名为:%s,密码为:%s。\n",username,password); return "success"; }
<form th:action="@{/rest/user}" method="post" style="border: 1px"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="添加"> </form>
配置请求方法过滤器
对于上面两种方式,实现起来比较简单,但是put和delete方式呢?由于浏览器只支持发送get和post方式的请求,那么该如何发送put和delete请求呢?
SpringMVC 提供了 HiddenHttpMethodFilter 帮助我们将 POST 请求转换为 DELETE 或 PUT 请求
HiddenHttpMethodFilter 处理put和delete请求的条件:
-
当前请求的请求方式必须为
post
-
当前请求必须传输请求参数
_method
满足以上条件,HiddenHttpMethodFilter 过滤器就会将当前请求的请求方式转换为请求参数_method的值,因此请求参数_method的值才是最终的请求方式
在web.xml中注册HiddenHttpMethodFilter
<!--配置请求方式过滤器-->
<filter>
<filter-name>HiddenHttpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>HiddenHttpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
注:
目前为止,SpringMVC中提供了两个过滤器:
CharacterEncodingFilter
和HiddenHttpMethodFilter
在web.xml中注册时,必须先注册CharacterEncodingFilter,再注册HiddenHttpMethodFilter
原因:
在 CharacterEncodingFilter 中通过 request.setCharacterEncoding(encoding) 方法设置字符集的
request.setCharacterEncoding(encoding) 方法要求前面不能有任何获取请求参数的操作
而 HiddenHttpMethodFilter 恰恰有一个获取请求方式的操作:
String paramValue = request.getParameter(this.methodParam);
模拟put和delete请求
-
put请求模拟更新用户信息操作
@PutMapping("/user") public String updateUser(String id, String username, String password) { System.out.printf("修改id为:%s的用户的信息为username:%s,password:%s。%n", id, username, password); return "success"; }
<form th:action="@{/rest/user}" method="post" style="border: 1px"> <!--注意一定要带上此参数--> <input type="hidden" name="_method" value="put"> <input type="hidden" name="id" value="001"> 用户名:<input type="text" name="username"><br> 密码:<input type="password" name="password"><br> <input type="submit" value="修改"> </form>
-
delete请求模拟删除用户
实际上我们删除操作一般都是使用超链接形式,但是最终还是使用表单提交,通过js控制。
这里不再具体实现。
RESTful案例
和传统的CRUD一样,实现对员工信息的增删改查。主要实现一下功能:
功能 | URL 地址 | 请求方式 |
---|---|---|
访问首页√ | / | GET |
查询全部数据√ | /employee | GET |
删除√ | /employee/2 | DELETE |
跳转到添加数据页面√ | /toAdd | GET |
执行保存√ | /employee | POST |
跳转到更新数据页面√ | /employee/2 | GET |
执行更新√ | /employee | PUT |
准备工作
-
环境配置
新建一个工程项目
springmvc05-restful-demo
,并按照之前的操作进行配置。 -
配置数据库
-
新建一个数据库restfuldb,并新建一个表employee,结构如下:
-
简单插入几条数据
-
导入相关的依赖
<!-- https://mvnrepository.com/artifact/org.springframework/spring-jdbc --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>5.3.15</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-orm --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-orm</artifactId> <version>5.3.15</version> </dependency> <!-- https://mvnrepository.com/artifact/org.springframework/spring-tx --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>5.3.15</version> </dependency> <!-- https://mvnrepository.com/artifact/mysql/mysql-connector-java --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency> <!-- https://mvnrepository.com/artifact/com.alibaba/druid --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.22</version> </dependency>
-
创建一个外部
properties
属性文件存放数据库相关信息prop.driverClassName=com.mysql.cj.jdbc.Driver prop.url=jdbc:mysql://localhost:3306/restfuldb?useUnicode=true&characterEncodeing=UTF-8&useSSL=false&serverTimezone=GMT prop.username=root prop.password=123456
-
在spring配置文件中引入配置文件,并配置数据库连接池,这里我使用的是阿里的德鲁伊连接池
<!--引入外部文件--> <context:property-placeholder location="classpath:jdbc.properties" /> <!--配置数据库连接池--> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="driverClassName" value="${prop.driverClassName}"/> <property name="url" value="${prop.url}"/> <property name="username" value="${prop.username}"/> <property name="password" value="${prop.password}"/> </bean> <!--创建JdbcTemplate对象--> <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"> <!--set注入dataSource--> <property name="dataSource" ref="dataSource"></property> </bean>
-
-
新建一个实体pojo类,对应数据库的表employee:
package com.soberw.springmvc.pojo; import lombok.Data; /** * @author soberw * @Classname Employee * @Description * @Date 2022-03-07 19:41 */ @Data public class Employee { private Integer id; private String lastName; private String email; private Integer gender; }
-
新建一个dao层接口EmpDAO,对应着增删改查操作:
package com.soberw.springmvc.dao; import com.soberw.springmvc.pojo.Employee; import java.util.List; /** * @author soberw * @Classname EmpDAO * @Description * @Date 2022-03-07 19:53 */ @Repository public interface EmpDAO { /** * 获取所有员工信息 */ List<Employee> getAllEmployees(); /** * 通过id获取指定员工信息 */ Employee getEmployeeById(Integer id); /** * 添加员工信息 */ void addEmployee(Employee employee); /** * 更新员工信息 */ void updateEmployee(Employee employee); /** * 删除指定员工信息 */ void deleteEmployee(Integer id); }
-
编写EmpDAO实现类
EmpDAOImpl
,注入JdbcTemplate对象,用于进行数据库操作package com.soberw.springmvc.dao; import com.soberw.springmvc.pojo.Employee; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.jdbc.core.BeanPropertyRowMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Repository; import java.util.List; /** * @author soberw * @Classname EmpDAOImpl * @Description * @Date 2022-03-07 20:09 */ @Repository public class EmpDAOImpl implements EmpDAO { @Autowired private JdbcTemplate jdbcTemplate; @Override public List<Employee> getAllEmployees() { String sql = "select * from employee"; return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(Employee.class)); } @Override public Employee getEmployeeById(Integer id) { String sql = "select * from employee where id = ?"; return jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(Employee.class), id); } @Override public void addEmployee(Employee employee) { String sql = "insert into employee values (0,?,?,?)"; jdbcTemplate.update(sql, employee.getLastName(), employee.getEmail(), employee.getGender()); } @Override public void updateEmployee(Employee employee) { String sql = "update employee set lastname = ?,email = ?,gender = ? where id = ?"; jdbcTemplate.update(sql, employee.getLastName(), employee.getEmail(), employee.getGender(), employee.getId()); } @Override public void deleteEmployee(Integer id) { String sql = "delete from employee where id = ?"; jdbcTemplate.update(sql, id); } }
-
创建service层接口
EmployeeService
,对应着四种操作package com.soberw.springmvc.service; import com.soberw.springmvc.pojo.Employee; import org.springframework.stereotype.Service; import java.util.List; /** * @author soberw * @Classname EmployeeService * @Description * @Date 2022-03-07 20:46 */ @Service public interface EmployeeService { List<Employee> getAllEmployees(); Employee getEmployeeById(Integer id); void addEmployee(Employee employee); void updateEmployee(Employee employee); void deleteEmployee(Integer id); }
-
编写实现类
EmployeeServiceImpl
,并注入EmpDAO对象:package com.soberw.springmvc.service; import com.soberw.springmvc.dao.EmpDAO; import com.soberw.springmvc.pojo.Employee; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; /** * @author soberw * @Classname EmployeeServiceImpl * @Description * @Date 2022-03-07 20:49 */ @Service public class EmployeeServiceImpl implements EmployeeService{ @Autowired private EmpDAO empDAO; @Override public List<Employee> getAllEmployees() { return empDAO.getAllEmployees(); } @Override public Employee getEmployeeById(Integer id) { return empDAO.getEmployeeById(id); } @Override public void addEmployee(Employee employee) { empDAO.addEmployee(employee); } @Override public void updateEmployee(Employee employee) { empDAO.updateEmployee(employee); } @Override public void deleteEmployee(Integer id) { empDAO.deleteEmployee(id); } }
访问首页功能
新建一个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>
配置view-controller
<mvc:view-controller path="/" view-name="index"/>
创建一个控制器类EmployeeController
,注入EmployeeService
对象
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Controller;
/**
* @author soberw
* @Classname EmployeeController
* @Description
* @Date 2022-03-07 20:01
*/
@Controller
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
}
实现列表功能
添加控制类方法,获取所有员工信息:
@GetMapping("/employee")
public String getAllEmployees(Model model){
List<Employee> employees = employeeService.getAllEmployees();
model.addAttribute("employeeList",employees);
return "employee_list";
}
对应的,新建并编写employee_list.html页面,注意这里因为后面要用到vue.js,这里先引入,方便后续操作,对应的其他操作一会儿补全:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script 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">员工信息表</th></tr>
<tr>
<th>员工号</th>
<th>姓名</th>
<th>邮箱</th>
<th>年龄</th>
<th>操作</th>
</tr>
<tr th:each="e : ${employeeList}">
<td th:text="${e.id}"></td>
<td th:text="${e.lastName}"></td>
<td th:text="${e.email}"></td>
<td th:if="${e.gender==1}">男</td>
<td th:if="${e.gender==0}">女</td>
<td>
<a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${e.id}}">删除</a>
<a th:href="@{'/employee/'+${e.id}}">编辑</a>
</td>
</tr>
</table>
</body>
</html>
测试一下:
实现删除功能
删除对应的是delete请求,之前模拟的时候没有演示,是因为稍微有点麻烦,这里详细演示一下:
-
首先需要引入vue.js,方便操作
-
上面已经写了删除的超链接:
<a class="deleteA" @click="deleteEmployee" th:href="@{'/employee/'+${e.id}}">删除</a>
-
又因为delete请求必须借助与post请求来完成,因此我们还需要一个form表单
<!-- 作用:通过超链接控制表单的提交,将post请求转换为delete请求 --> <form id="delete_form" method="post"> <!-- HiddenHttpMethodFilter要求:必须传输_method请求参数,并且值为最终的请求方式 --> <input type="hidden" name="_method" value="delete"/> </form>
-
通过vue处理点击事件:
<script> let vue = new Vue({ el: "#dataTable", methods: { //event表示当前事件 deleteEmployee: function (event) { //通过id获取表单标签 let delete_form = document.getElementById("delete_form"); //将触发事件的超链接的href属性为表单的action属性赋值 delete_form.action = event.target.href; //提交表单 delete_form.submit(); //阻止超链接的默认跳转行为 event.preventDefault(); } } }); </script>
-
添加一个 控制器方法:
@DeleteMapping("/employee/{id}") public String deleteEmployee(@PathVariable("id") Integer id){ employeeService.deleteEmployee(id); //重定向,重新渲染页面 return "redirect:/employee"; }
-
测试:
实现添加功能
-
在页面中新建一个添加按钮,跳转到添加数据页面:
-
在配置文件中配置view-controller
<mvc:view-controller path="/add" view-name="employee_add"/>
-
创建一个页面employee_add.html用于添加数据
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <form th:action="@{/employee}" method="post"> 姓名:<input type="text" name="lastName"><br> 邮箱:<input type="text" name="email"><br> 性别:<input type="radio" name="gender" value="1">男 <input type="radio" name="gender" value="0">女<br> <input type="submit" value="提交"> </form> </body> </html>
-
在控制器中添加方法执行保存
@PostMapping("/employee") public String addEmployee(Employee employee){ employeeService.addEmployee(employee); return "redirect:/employee"; }
-
测试一下:
实现修改功能
最后还剩一个修改功能的实现,修改需要分为两步走:
- 将待修改数据填充在表单中,供用户修改
- 将修改后的数据返回数据库
首先,编辑修改按钮的超链接,使其通过当前id值获取到当前id值对应的员工信息:
<a th:href="@{'/employee/'+${e.id}}">编辑</a>
添加控制器方法,获取指定id对应的用户信息:
@GetMapping("/employee/{id}")
public String getEmployeeById(@PathVariable("id") Integer id,Model model){
Employee e = employeeService.getEmployeeById(id);
model.addAttribute("employee",e);
return "employee_update";
}
对应的,需要一个employee_update.html页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</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}">
姓名:<input type="text" name="lastName" th:value="${employee.lastName}"><br>
邮箱:<input type="text" name="email" th:value="${employee.email}"><br>
<!--
th:field="${employee.gender}"可用于单选框或复选框的回显
若单选框的value和employee.gender的值一致,则添加checked="checked"属性
-->
性别:<input type="radio" name="gender" value="1" th:field="${employee.gender}">男
<input type="radio" name="gender" value="0" th:field="${employee.gender}">女<br>
<input type="submit" value="更新"><br>
</form>
</body>
</html>
添加一个控制器方法:
@PutMapping("/employee")
public String updateEmployee(Employee employee){
employeeService.updateEmployee(employee);
return "redirect:/employee";
}
测试一下:
修改数据
提交,修改成功
HttpMessageConverter
HttpMessageConverter,报文信息转换器,将请求报文转换为Java对象,或将Java对象转换为响应报文
HttpMessageConverter提供了两个注解和两个类型:
-
@RequestBody,
@ResponseBody
-
RequestEntity,
ResponseEntity
实际上,我们使用比较多的还是@ResponseBody以及ResponseEntity,因为通过上面的方式,我们已经可以很方便的获取请求信息并进行转换了,因此我们常常使用响应转换,来将Java对象转换为响应报文,目前流行的就是转换为JSON格式,下面介绍。
新建工程springmvc06-HttpMessageConverter
,并按上面进行配置,新建index.html作为首页,新建success.html作为跳转页面,创建一个类作为控制器方法。
@RequestBody
@RequestBody可以获取请求体,需要在控制器方法设置一个形参,使用@RequestBody进行标识,当前请求的请求体就会为当前注解所标识的形参赋值
@RequestMapping("/testRequestBody")
public String testRequestBody(@RequestBody String requestBody){
System.out.println("requestBody = " + requestBody);
return "success";
}
<form th:action="@{/testRequestBody}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="测试@RequestBody获取请求体">
</form>
RequestEntity
RequestEntity封装请求报文的一种类型,需要在控制器方法的形参中设置该类型的形参,当前请求的请求报文就会赋值给该形参,可以通过getHeaders()获取请求头信息,通过getBody()获取请求体信息
@RequestMapping("/testRequestEntity")
public String testRequestEntity(RequestEntity<String> requestEntity){
System.out.println("请求头信息:" + requestEntity.getHeaders());
System.out.println("请求体信息:" + requestEntity.getBody());
return "success";
}
<form th:action="@{/testRequestEntity}" method="post">
用户名:<input type="text" name="username"><br>
密码:<input type="password" name="password"><br>
<input type="submit" value="测试@RequestEntity获取请求体">
</form>
这里的请求头信息对应的是浏览器发送的全部请求头信息,因此比较长。
原生ServletAPI响应浏览器数据
利用原生的ServletAPI方式响应浏览器数据,同上面的获取请求参数一样,需要我们传递HttpServletResponse对象参数。
@RequestMapping("/testResponseByServletAPI")
public void testResponseByServletAPI(HttpServletResponse response) throws IOException {
response.getWriter().println("hello,response");
}
<a th:href="@{/testResponseByServletAPI}">通过原生ServletAPI响应浏览器数据</a><br>
而SpringMVC同样给我们封装了功能强大的API,通过他们我们可以很方便的响应数据到浏览器。
@ResponseBody注解
@ResponseBody是SpringMVC给我们提供的一个功能非常强大的注解。
@ResponseBody用于标识一个控制器方法,可以将该方法的返回值直接作为响应报文的响应体响应到浏览器。
通过这个注解我们可以处理并返回JSON类型的数据,并可实现Ajax异步请求。
基本实现
先测试一下基本功能实现:
@RequestMapping("/testResponseBody")
@ResponseBody
public String testResponseBody(){
//加上@ResponseBody注解后,返回的就不是视图名称了,而是响应到浏览器的数据值
return "success";
}
<a th:href="@{/testResponseBody}">通过testResponseBody响应浏览器数据</a><br>
注意这里返回的是success值,而非success.html页面,是因为加上@ResponseBody注解后,返回的就不是视图名称了,而是响应到浏览器的数据值,即==@ResponseBody可以将方法的返回值直接作为响应报文的响应体响应到浏览器==
问题引入
既然@ResponseBody可以将方法的返回值直接作为响应报文返回给浏览器,那么他是否可以将 JavaBean对象返回给浏览器呢?
首先定义一个JavaBean对象User
,用以作为数据传输:
package com.soberw.springmvc.bean;
import lombok.Data;
/**
* @author soberw
* @Classname User
* @Description
* @Date 2022-03-08 16:27
*/
@Data
@AllArgsConstructor
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String sex;
}
编写控制类方法:
@RequestMapping("/testResponseUser")
@ResponseBody
public User testResponseUser(){
return new User(1001,"张三","123456",18,"男");
}
<a th:href="@{/testResponseUser}">通过testResponseUser响应JavaBean对象给浏览器</a><br>
直接报错406,而错误的原因就是因为浏览器无法解析服务端返回的内容,也就是说我们返回的JavaBean对象浏览器无法解析。
如何解决?
可以转化为JSON对象然后进行传输,JSON是一种数据传输格式,类似于XML但是却比XML更加简洁,采用的是键值对的形式传输的。
而SpringMVC对JSON天然支持,这就方便多了。我们可以通过将JavaBean对象转换为JSON对象,然后再响应给浏览器处理。目前比较流行的请求响应方式就是通过Ajax发送异步请求以JSON类型传递给后台进行处理,再通过JSON格式响应给浏览器。而通过@ResponseBody注解,可以很方便的完成。
注:
JSON可不是说只能通过Ajax异步请求发送,他只是实现传输JSON的一种主流方式。
这里先演示如何发送JSON格式对象,再演示如何通过Ajax发送JSON对象。
处理JSON
首先需要引人JSON的jar包,jackson-databind
:
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.1</version>
</dependency>
此时不需要做任何操作,直接重新运行项目:
已经可以成功显示了,就是这么神奇,这都是因为SpringMVC内部已经帮我们转换好了,天然支持JSON。
而完成上面的操作还需要一个大前提,就是开启mvc注解驱动:
<mvc:annotation-driven />
注:
- 在SpringMVC的核心配置文件中开启mvc的注解驱动,此时在HandlerAdaptor中会自动装配一个消息转换器:MappingJackson2HttpMessageConverter,可以将响应到浏览器的Java对象转换为Json格式的字符串
- 其次就是需要在处理器方法上使用@ResponseBody注解进行标识,此时将Java对象直接作为控制器方法的返回值返回,就会自动转换为Json格式的字符串。
处理Ajax
实际中使用较多的,尤其是前后端分离,还是通过Ajax发送异步请求,因为我们不太可能直接将JSON数据返回浏览器,我们肯定会再处理。而SpringMVC也提供了跟方便的处理Ajax的方式,还是使用@ResponseBody注解。
简单模拟一下,通过Ajax服务器向服务器请求数据并返回,然后再渲染到页面上,这里不在使用原生js+Ajax的方式了,直接使用目前最流行的vue+axios来操作。
-
先创建一个控制器方法,用户返回数据:
@RequestMapping("/testAxios") @ResponseBody public List<User> testAxios(String username, String password) { if ("admin".equals(username) && "123456".equals(password)) { List<User> userList = new ArrayList<>(); userList.add(new User(1001, "张三", "123456", 18, "男")); userList.add(new User(1002, "李四", "123456", 19, "女")); userList.add(new User(1003, "王五", "123456", 19, "男")); userList.add(new User(1004, "马六", "123456", 20, "女")); userList.add(new User(1005, "赵七", "123456", 22, "男")); return userList; } else { return null; } }
-
在页面中定义一个div标签,用户发送和渲染数据:
<div id="app"> <a th:href="@{/testAxios}" @click="getData">通过ResponseBody注解处理Ajax请求并返回数据</a> <table id="dataTable" v-show="users!=''" border="1"> <tr> <th>编号</th> <th>姓名</th> <th>年龄</th> <th>性别</th> </tr> <tr align="center" v-for="user in users"> <td>{{user.id}}</td> <td>{{user.username}}</td> <td>{{user.age}}</td> <td>{{user.sex}}</td> </tr> </table> </div>
-
编写js代码
let vue = new Vue({ el: "#app", data: { users: "" }, methods: { getData: function (event) { axios({ method: "post", url: event.target.href, params: { username: "admin", password: "123456" } }).then(function (response) { vue.users = response.data; }).catch(function (reason) { console.log(reason); }); event.preventDefault(); } } });
-
测试一下:
已经成功获取到了数据,并渲染在了页面上。
@RestController注解
@RestController
注解是springMVC提供的一个复合注解,标识在控制器的类上,就相当于为类添加了@Controller注解,并且为其中的每个方法添加了@ResponseBody注解。有了这个注解,我们就不在需要把控制类中的每个方法上再添加@ResponseBody注解了。
ResponseEntity
ResponseEntity用于控制器方法的返回值类型,该控制器方法的返回值就是响应到浏览器的响应报文。实际中我们经常使用此类型实现文件的下载功能。这里放到下一章节介绍。
文件的上传和下载
文件的上传与下载,其实就是对文件的复制操作,通常是将文件转换为字节流进行操作。
文件下载
通过使用使用ResponseEntity实现下载文件的功能,通过这个对象我们可以实现在浏览器下载我们服务器上面的很多种格式的文件,这里以下载一个图片为例,演示一下配置文件下载的流程是怎样的:
-
首先我们需要一张图片,这里我随便找了一张,放在
WEB-INF/static
下面的img
目录下,命名为1.png
-
编写控制器类方法用来实现
@RequestMapping("/testDown") public ResponseEntity<byte[]> testResponseEntity(HttpSession session) throws IOException { //图片的路径 String imgPath = "/static/img/1.png"; //获取ServletContext对象 ServletContext servletContext = session.getServletContext(); //获取服务器 中文件的真实路径 String realPath = servletContext.getRealPath(imgPath); //创建输入流读取文件 InputStream is = new FileInputStream(realPath); //创建字节数组用来存储读取的字节流,数组的长度是当前输入流的对应的所有字节数 byte[] bytes = new byte[is.available()]; //将流读到字节数组中 is.read(bytes); //创建HttpHeaders对象设置响应头信息,实际上就是一个Map集合 MultiValueMap<String, String> headers = new HttpHeaders(); String filename = UUID.randomUUID().toString() + ".png"; //设置要下载的方式以及下载的文件名字,为固定搭配 headers.add("Content-Disposition", "attachment;filename=" + filename); //设置响应状态码 HttpStatus status = HttpStatus.OK; //创建ResponseEntity对象 ResponseEntity<byte[]> responseEntity = new ResponseEntity<>(bytes,headers,status); //关闭输入流 is.close(); return responseEntity; }
-
在页面添加下载链接
<a th:href="@{/testDown}">点击下载图片</a>
文件上传
文件上传要求form表单的请求方式必须为post
,并且添加属性enctype="multipart/form-data"
,设置此属性后,表单就不会再是通过键值对的方式将参数传递给服务器了,而是通过二进制的方式传递的。
先创建一个form表单,用于上传文件:
<form th:action="@{/testUp}" method="post" enctype="multipart/form-data">
头像:<input type="file" name="photo"><br>
<input type="submit" value="提交">
</form>
SpringMVC中将上传的文件封装到MultipartFile对象中,通过此对象可以获取文件相关信息。
在编写控制器方法之前,还需要我们进行一些配置。
- 添加上传所需的依赖:
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
-
在SpringMVC配置文件中添加配置,配置文件上传解析器,将上传的文件封装为
MultipartFile
对象:<!-- 必须通过文件解析器的解析才能将文件转为MultipartFile对象 注意,此处必须设置id值且必须为multipartResolver,以便于让SpringMVC找到 --> <bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"/>
注:
在配置文件解析器时,必须指定id值,且id值必须为multipartResolver,以便于让SpringMVC找到并使用
如果不指定则会报500错误,在使用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; //获取服务器 中photo目录的路径 ServletContext servletContext = session.getServletContext(); String realPath = servletContext.getRealPath("photo"); File file = new File(realPath); //判断realPath所对应的路径是否存在 if (!file.exists()) { //如果不存在则创建一个目录 file.mkdirs(); } String finalPath = realPath + File.separator + filename; //实现上传功能 photo.transferTo(new File(finalPath)); return "success"; }
需要考虑到上传文件的重名问题。
-
测试一下:
选择一张图片
查看服务器目录:
正常显示。
拦截器
见名知意,就是对请求进行拦截处理。这往往会让人和过滤器混淆,但实际上他们二者还是有很大区别的:
- 首先过滤器是servlet中的,任何框架都可使用过滤器技术;而拦截器是SpringMVC所独有的。
- 过滤器是作用在servlet前面的,即作用在Http请求与Servlet之间,例如前面设置的编码过滤器以及请求方式过滤器,必须设置在DispatchServlet之前才有作用。通过设置过滤路径,可以精准的对请求信息进行过滤,比如实现过滤低俗文字、编码处理等操作;而拦截器是作用在servlet后面的,但是又在controller控制层之前,实现的是对控制器controller中的方法进行拦截,常用的功能有日志记录、权限检查、性能监控等。
- 拦截器是基于java的反射机制,利用的是aop的思想;而过滤器是基于函数的回调。
- 拦截器只能对action请求起作用,而过滤器则可以对几乎所有的请求起作用。
- 拦截器可以访问action上下文、值栈里的对象,而过滤器不能访问。
- 在action的生命周期中,拦截器可以多次被调用,而过滤器只能在容器初始化时被调用一次。
- 拦截器可以获取IOC容器中的各个bean,而过滤器就不行,这点很重要,在拦截器里注入一个service,可以调用业务逻辑。
简而言之,拦截器就是用于拦截控制器方法的执行。
基础配置
在开始使用拦截器之前,需要进行一些配置,这里我先创建一个新的工程来演示,先做一些通用配置,如上一样,添加首页。
新建一个控制器类,并创建一个方法,返回跳转成功页面:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname TestController
* @Description
* @Date 2022-03-10 16:06
*/
@Controller
public class TestController {
@RequestMapping("/testInterceptor")
public String testInterceptor(){
return "success";
}
}
在页面添加跳转链接:
<a th:href="@{/testInterceptor}">测试拦截器</a>
创建拦截器
创建一个拦截器类FirstInterceptor
,要想实现拦截功能,必须要继承HandlerInterceptorAdapter
类,或者实现接口HandlerInterceptor
,这里推荐使用实现接口的方式。
HandlerInterceptor
接口有三个默认方法,因为是默认方法,因此不强制让我们重写,可根据实际需要选择方法重写,源码如下:
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, @Nullable Exception ex) throws Exception {
}
}
-
preHandle方法:控制器方法执行之前执行,其Boolean类型的返回值表示是否拦截或者放行,返回true则放行,即调用控制器方法;返回false则表示拦截,则不调用控制器方法
-
postHandle方法:控制器方法执行之后执行的方法
-
afterCompletion方法:处理完视图和模型数据,渲染视图完毕之后执行
光实现接口是不行的,需要将这个类交给SpringMVC进行统一管理,因此就需要我们在SpringMVC配置文件中进行配置,注意这里有三种配置方式:
-
方式一:
<!--配置拦截器 方式一:--> <mvc:interceptors> <bean class="com.soberw.springmvc.interceptor.FirstInterceptor"/> </mvc:interceptors>
-
方式二:
<!-- 配置拦截器 方式二: 在拦截器类上添加@Component注解,表示放入IOC容器 --> <mvc:interceptors> <ref bean="firstInterceptor"/> </mvc:interceptors>
-
方式三:
<!--配置拦截器 方式三:可配置多个拦截器--> <mvc:interceptors> <mvc:interceptor> <!-- mapping属性: 若配置路径为 / 则路径为 localhost:8080/ 的路径,即我们配置的index页面 若配置为 /* 则对应上下文路径中的一层目录中的所有请求路径 若配置为 /** 则对应所有目录中的所有请求路径 --> <mvc:mapping path="/**"/> <!--要过滤的路径,即不被拦截的路径--> <mvc:exclude-mapping path="/testInterceptor"/> <!--对应的拦截器类--> <ref bean="firstInterceptor"/> </mvc:interceptor> </mvc:interceptors>
前两种方式都是对DispatcherServlet所处理的所有的请求进行拦截,即不管我们发送什么请求,只要经过DispatchServlet,就会被拦截到
最后一个配置方式可以通过ref或bean标签设置拦截器,通过mvc:mapping设置需要拦截的请求,通过mvc:exclude-mapping设置需要排除的请求,即不需要拦截的请求,因此会拦截的更加精准
这里我使用方式三配置,接下来测试一下,重写接口方法:
package com.soberw.springmvc.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author soberw
* @Classname FirstInterceptor
* @Description
* @Date 2022-03-10 16:16
*/
@Component
public class FirstInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("第一个拦截器的preHandle方法执行了...");
//设置为false,则拦截后不放行
return false;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("第一个拦截器的postHandle方法执行了...");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("第一个拦截器的afterCompletion方法执行了...");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
运行一下,注意我这里preHandle方法返回值为false,即为拦截不放行:
空白页面,说明index页面访问不到
打开控制台发现,是被拦截到了,且因为我们设置的不放行。下面改成放行再试试:
可以显示首页了:
控制台也正常显示了。
另外,如果配置了拦截路径,一定要注意路径的准确性:
例如,在配置过滤器拦截路径时,我们使用的是/*
过滤的所有servlet请求,而在拦截器中就不一样,例如我在控制器设置请求路径映射为/**/testInterceptor
,即模糊匹配,可以包含多级目录:
将拦截器拦截路径设置为/*
,测试一下:
点击链接,跳转成功,返回控制台查看:
成功拦截了,但是如果我发送的是多级目录:
一样跳转了,但是却不被拦截到:
是因为/*
只能拦截单级目录下的请求路径,如果想要拦截所有,则需要设置为/**
下面设置为/**
再试一下:
直接测试多级目录:
算是超多级了:
跳转成功,查看控制台;
成功拦截。
多个拦截器
拦截器是可以创建多个的,就像过滤器一样,不同的拦截器执行不同的功能。
那么我们知道,配置编码过滤器时一定要配置在请求方式过滤器前面,是为了保证执行顺序。
那如果配置了多个拦截器,他们的执行顺序又是怎样的?
下面再创建两个拦截器类SecondInterceptor
和 ThirdInterceptor
:
package com.soberw.springmvc.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author soberw
* @Classname SecondInterceptor
* @Description
* @Date 2022-03-10 19:22
*/
@Component
public class SecondInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("第二个拦截器的preHandle方法执行了...");
//设置为false,则拦截后不放行
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("第二个拦截器的postHandle方法执行了...");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("第二个拦截器的afterCompletion方法执行了...");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
package com.soberw.springmvc.interceptor;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @author soberw
* @Classname ThirdInterceptor
* @Description
* @Date 2022-03-10 19:22
*/
@Component
public class ThirdInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
System.out.println("第三个拦截器的preHandle方法执行了...");
//设置为false,则拦截后不放行
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
System.out.println("第三个拦截器的postHandle方法执行了...");
HandlerInterceptor.super.postHandle(request, response, handler, modelAndView);
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
System.out.println("第三个拦截器的afterCompletion方法执行了...");
HandlerInterceptor.super.afterCompletion(request, response, handler, ex);
}
}
接下来就开始配置SpringMVC配置文件了,注意我这里配置多个拦截器的先后顺序:
<!--配置多个拦截器-->
<mvc:interceptors>
<!-- first>>>second>>>third -->
<ref bean="firstInterceptor"/>
<ref bean="secondInterceptor"/>
<ref bean="thirdInterceptor"/>
</mvc:interceptors>
注意顺序,此顺序只与配置文件中的配置顺序有关。
这是都放行的情况,如果我某个拦截器不放行了,又会是怎样的情况呢?
在第二个拦截器中设置为不放行:
运行:
得出结论:
若每个拦截器的preHandle()都返回true。此时多个拦截器的执行顺序和拦截器在SpringMVC的配置文件的配置顺序有关:
- preHandle()会按照配置的顺序执行
- 而postHandle()和afterComplation()会按照配置的反序执行
若某个拦截器的preHandle()返回了false
preHandle()返回false和它之前的拦截器的preHandle()都会执行,postHandle()都不执行,返回false的拦截器之前的拦截器的afterComplation()会执行
异常处理器
SpringMVC提供了一个处理控制器方法执行过程中所出现的异常的接口:HandlerExceptionResolver
他的主要的实现类有DefaultHandlerExceptionResolver和SimpleMappingExceptionResolver
其中DefaultHandlerExceptionResolver
是SpringMVC默认的异常处理器,看似陌生,其实无时无刻不在看到他,比如页面显示405错误或者500等等,都是因为有了异常处理的作用。
除了默认的异常处理类,还提供了一个自定义的异常处理类SimpleMappingExceptionResolver
,通过这个类我们可以配置自定义的异常。
基于配置文件
在开始使用之前,同样需要在配置文件中进行配置:
<!--配置异常处理类-->
<bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<!--
配置异常处理映射:
对应的是property集合,
其中键是异常的类型,值是将要跳转到的页面视图名称
-->
<property name="exceptionMappings">
<props>
<prop key="java.lang.ArithmeticException">error</prop>
</props>
</property>
<!-- exceptionAttribute属性设置一个属性名,将出现的异常信息在请求域中进行共享-->
<property name="exceptionAttribute" value="ex"/>
</bean>
相对应的,创建一个error.html页面:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>你出错了!!</h1>
<p th:text="${ex}"></p>
</body>
</html>
编写控制类方法:并制造一个与配置相匹配的异常,我这里配置的是数学运算异常:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname TestController
* @Description
* @Date 2022-03-10 20:50
*/
@Controller
public class TestController {
@RequestMapping("/testEx")
public String testEx() {
int i = 10 / 0;
return "success";
}
}
测试运行:
<a th:href="@{/testEx}" >测试异常</a>
发现异常成功被捕获了,并跳转到了对应的error页面,打印了异常信息在页面。
基于注解
如果觉得配置文件方式太麻烦,也可以使用注解的 方式来实现,这里所使用的注解是:
@ControllerAdvice
:作用在类上,将当前的类标识为异常处理的组件@ExceptionHandler
:作用在方法上,value属性值为设置所标识方法处理的异常
下面实现,创建一个增强控制类用作处理异常:
package com.soberw.springmvc.controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
/**
* @author soberw
* @Classname AnnoExController
* @Description
* @Date 2022-03-10 21:21
*/
@ControllerAdvice
public class AnnoExController {
//形参位置的exception对象就是出现的异常
@ExceptionHandler(value = {ArithmeticException.class, NullPointerException.class})
public String testAnnoEx(Exception ex, Model model) {
model.addAttribute("ex",ex);
return "error";
}
}
全注解方式配置SpringMVC
我们上面的处理实现都是基于配置文件的,但是同Spring一样,在SpringMVC中,我们也可以使用配置类和注解代替web.xml和SpringMVC配置文件的功能。
新建一个项目用来实现。
代替web.xml
当我们将项目运行时,首先程序会加载的配置文件就是web.xml,所以首先我们要新建一个类代替web.xml。
在Servlet3.0环境中,容器会在类路径中查找实现javax.servlet.ServletContainerInitializer接口的类,如果找到的话就用它来配置Servlet容器即tomcat服务器。
Spring提供了这个接口的实现,名为SpringServletContainerInitializer
,这个类反过来又会查找实现WebApplicationInitializer
的类并将配置的任务交给它们来完成。Spring3.2引入了一个便利的WebApplicationInitializer基础实现,名为AbstractAnnotationConfigDispatcherServletInitializer
,当我们的类扩展了AbstractAnnotationConfigDispatcherServletInitializer并将其部署到Servlet3.0容器的时候,容器会自动发现它,并用它来配置Servlet上下文。
因此最终我们只需要创建一个类并继承于AbstractAnnotationConfigDispatcherServletInitializer
类就可以了。
创建WebInit类作为初始化类:
package com.soberw.springmvc.config;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
import javax.servlet.Filter;
/**
* @author soberw
* @Classname WebInit
* @Description web.xml 对应的配置类
* @Date 2022-03-11 15:46
*/
public class WebInit extends AbstractAnnotationConfigDispatcherServletInitializer {
/**
* 指定spring的配置类
* @return
*/
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[]{SpringConfig.class};
}
/**
* 指定SpringMVC的配置类
* @return
*/
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{WebConfig.class};
}
/**
* 指定DispatchServlet的映射规则,即url-pattern
* @return
*/
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
/**
* 注册配置过滤器
* @return
*/
@Override
protected Filter[] getServletFilters() {
//配置编码过滤器
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
//设置请求编码
characterEncodingFilter.setEncoding("utf-8");
//设置响应编码
characterEncodingFilter.setForceEncoding(true);
//配置请求方式过滤器
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
return new Filter[]{characterEncodingFilter,hiddenHttpMethodFilter};
}
}
实际上,其内部还提供了很多方法可供我们重写,实际中根据需要添加即可。
代替spring的配置文件
通过上面我们发现,在指定配置类的时候,都是返回的数组形式,因此我们的配置类可以有多个。
因为我们整合了SpringMVC所以这里就不需要在配置了,记得要加上配置注解将类标识为配置类
package com.soberw.springmvc.config;
import org.springframework.context.annotation.Configuration;
/**
* @author soberw
* @Classname SpringConfig
* @Description 代替spring的配置文件
* @Date 2022-03-11 15:54
*/
@Configuration
public class SpringConfig {
//ssm整合之后,spring的配置信息写在此类中
}
代替SpringMVC配置文件
重点在于如何配置SpringMVC的配置文件,我这里主要将其分为7个部分:
- 扫描组件
- 视图解析器
- view-controller
- default-servlet-handler
- mvc注解驱动
- 文件上传解析器
- 异常处理
- 拦截器
因为SpringMVC的组件是可插拔式的,就是用的时候加上,不用的时候可以不加,因此我这里先将一些必要的配置完成:
package com.soberw.springmvc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.context.ContextLoader;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.thymeleaf.spring5.SpringTemplateEngine;
import org.thymeleaf.spring5.view.ThymeleafViewResolver;
import org.thymeleaf.templatemode.TemplateMode;
import org.thymeleaf.templateresolver.ITemplateResolver;
import org.thymeleaf.templateresolver.ServletContextTemplateResolver;
/**
* @author soberw
* @Classname WebConfig
* @Date 2022-03-11 15:56
* @Description SpringMVC配置文件对应的配置类
* 1、扫描组件 2、视图解析器 3、view-controller 4、default-servlet-handler
* 5、mvc注解驱动 6、文件上传解析器 7、异常处理 8、拦截器
*/
@Configuration
//1、扫描组件
@ComponentScan(basePackages = "com.soberw.springmvc")
//5、开启MVC注解驱动
@EnableWebMvc
public class WebConfig {
//2、视图解析器
//2.1 配置生成模板解析器
@Bean
public ITemplateResolver templateResolver() {
WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext();
// ServletContextTemplateResolver需要一个ServletContext作为构造参数,可通过WebApplicationContext 的方法获得
ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver(
webApplicationContext.getServletContext());
templateResolver.setPrefix("/WEB-INF/pages/");
templateResolver.setSuffix(".html");
templateResolver.setCharacterEncoding("UTF-8");
templateResolver.setTemplateMode(TemplateMode.HTML);
return templateResolver;
}
//2.2 生成模板引擎并为模板引擎注入模板解析器
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
return templateEngine;
}
//2.3 生成视图解析器并未解析器注入模板引擎
@Bean
public ViewResolver viewResolver(SpringTemplateEngine templateEngine) {
ThymeleafViewResolver viewResolver = new ThymeleafViewResolver();
viewResolver.setCharacterEncoding("UTF-8");
viewResolver.setTemplateEngine(templateEngine);
return viewResolver;
}
}
接下来就可以测试运型了,创建配置类TestController
进行测试:
package com.soberw.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* @author soberw
* @Classname TestController
* @Description
* @Date 2022-03-12 9:22
*/
@Controller
public class TestController {
@RequestMapping("/")
public String index(){
return "index";
}
}
成功运行,说明配置是没有问题的。
进一步配置SpringMVC
上面只是完成了基本的配置。下面进行一些组件的配置,在配置组件前,需要让配置类实现WebMvcConfigurer
接口,并重写相应的方法:
我们想要实现哪个功能,就重写哪个方法即可。
-
配置view-controller
//3、配置view-controller @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/hello").setViewName("hello"); }
创建一个hello.HTML进行测试:
-
配置默认servlet驱动
// 4、配置默认的servlet可用 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); }
-
配置文件上传解析器,因为在配置文件里他就是一个bean对象 ,因此这里直接定义方法即可:
//6、文件上传解析器 @Bean public MultipartResolver multipartResolver(){ return new CommonsMultipartResolver(); }
-
配置异常处理
//7、配置异常处理 @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver(); Properties props = new Properties(); props.setProperty("java.lang.ArithmeticException","error"); exceptionResolver.setExceptionMappings(props); //在请求域中的共享异常信息的键 exceptionResolver.setExceptionAttribute("exception"); resolvers.add(exceptionResolver); }
-
配置拦截器:
//8、配置拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //拦截器类 FirstInterceptor firstInterceptor = new FirstInterceptor(); registry.addInterceptor(firstInterceptor); }
-
完整的配置代码如下:
package com.soberw.springmvc.config; import com.soberw.springmvc.interceptor.FirstInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.multipart.MultipartResolver; import org.springframework.web.multipart.commons.CommonsMultipartResolver; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ViewResolver; import org.springframework.web.servlet.config.annotation.*; import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver; import org.thymeleaf.spring5.SpringTemplateEngine; import org.thymeleaf.spring5.view.ThymeleafViewResolver; import org.thymeleaf.templatemode.TemplateMode; import org.thymeleaf.templateresolver.ITemplateResolver; import org.thymeleaf.templateresolver.ServletContextTemplateResolver; import java.util.List; import java.util.Properties; /** * @author soberw * @Classname WebConfig * @Date 2022-03-11 15:56 * @Description SpringMVC配置文件对应的配置类 * 1、扫描组件 2、视图解析器 3、view-controller 4、default-servlet-handler * 5、mvc注解驱动 6、文件上传解析器 7、异常处理 8、拦截器 */ @Configuration //1、扫描组件 @ComponentScan(basePackages = "com.soberw.springmvc") //5、开启MVC注解驱动 @EnableWebMvc //实现WebMvcConfigurer接口并实现方法,配置组件 public class WebConfig implements WebMvcConfigurer { //2、视图解析器 //2.1 配置生成模板解析器 @Bean public ITemplateResolver templateResolver() { WebApplicationContext webApplicationContext = ContextLoader.getCurrentWebApplicationContext(); // ServletContextTemplateResolver需要一个ServletContext作为构造参数,可通过WebApplicationContext 的方法获得 ServletContextTemplateResolver templateResolver = new ServletContextTemplateResolver( webApplicationContext.getServletContext()); templateResolver.setPrefix("/WEB-INF/pages/"); templateResolver.setSuffix(".html"); templateResolver.setCharacterEncoding("UTF-8"); templateResolver.setTemplateMode(TemplateMode.HTML); return templateResolver; } //2.2 生成模板引擎并为模板引擎注入模板解析器 @Bean public SpringTemplateEngine templateEngine(ITemplateResolver templateResolver) { SpringTemplateEngine templateEngine = new SpringTemplateEngine(); templateEngine.setTemplateResolver(templateResolver); return templateEngine; } //2.3 生成视图解析器并未解析器注入模板引擎 @Bean public ViewResolver viewResolver(SpringTemplateEngine templateEngine) { ThymeleafViewResolver viewResolver = new ThymeleafViewResolver(); viewResolver.setCharacterEncoding("UTF-8"); viewResolver.setTemplateEngine(templateEngine); return viewResolver; } //3、配置view-controller @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/hello").setViewName("hello"); } // 4、配置默认的servlet可用 @Override public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) { configurer.enable(); } //6、文件上传解析器 @Bean public MultipartResolver multipartResolver(){ return new CommonsMultipartResolver(); } //7、配置异常处理 @Override public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) { SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver(); Properties props = new Properties(); props.setProperty("java.lang.ArithmeticException","error"); exceptionResolver.setExceptionMappings(props); //在请求域中的共享异常信息的键 exceptionResolver.setExceptionAttribute("exception"); resolvers.add(exceptionResolver); } //8、配置拦截器 @Override public void addInterceptors(InterceptorRegistry registry) { //拦截器类 FirstInterceptor firstInterceptor = new FirstInterceptor(); registry.addInterceptor(firstInterceptor); } }
当然了除了这些,SpringMVC还提供了一些其他的插件共我们使用过,我们只需要在配置类里面去重写对应的方法即可。
SpringMVC执行流程
到这里,有关SpringMVC的主要内容已经讲解完成了,最后我们在回过头来,看一下SpringMVC的执行流程。
SpringMVC常用组件
- DispatcherServlet:前端控制器,不需要工程师开发,由框架提供
作用:统一处理请求和响应,整个流程控制的中心,由它调用其它组件处理用户的请求
- HandlerMapping:处理器映射器,不需要工程师开发,由框架提供
作用:根据请求的url、method等信息查找Handler,即控制器方法
- Handler:处理器,需要工程师开发
作用:在DispatcherServlet的控制下Handler对具体的用户请求进行处理
- HandlerAdapter:处理器适配器,不需要工程师开发,由框架提供
作用:通过HandlerAdapter对处理器(控制器方法)进行执行
- ViewResolver:视图解析器,不需要工程师开发,由框架提供
作用:进行视图解析,得到相应的视图,例如:ThymeleafView、InternalResourceView、RedirectView
- View:视图
作用:将模型数据通过页面展示给用户
DispatcherServlet初始化过程
DispatcherServlet 本质上是一个 Servlet,所以天然的遵循 Servlet 的生命周期。所以宏观上是 Servlet 生命周期来进行调度。
初始化WebApplicationContext
所在类:org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext initWebApplicationContext() {
WebApplicationContext rootContext =
WebApplicationContextUtils.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
if (this.webApplicationContext != null) {
// A context instance was injected at construction time -> use it
wac = this.webApplicationContext;
if (wac instanceof ConfigurableWebApplicationContext) {
ConfigurableWebApplicationContext cwac = (ConfigurableWebApplicationContext) wac;
if (!cwac.isActive()) {
// The context has not yet been refreshed -> provide services such as
// setting the parent context, setting the application context id, etc
if (cwac.getParent() == null) {
// The context instance was injected without an explicit parent -> set
// the root application context (if any; may be null) as the parent
cwac.setParent(rootContext);
}
configureAndRefreshWebApplicationContext(cwac);
}
}
}
if (wac == null) {
// No context instance was injected at construction time -> see if one
// has been registered in the servlet context. If one exists, it is assumed
// that the parent context (if any) has already been set and that the
// user has performed any initialization such as setting the context id
wac = findWebApplicationContext();
}
if (wac == null) {
// No context instance is defined for this servlet -> create a local one
// 创建WebApplicationContext
wac = createWebApplicationContext(rootContext);
}
if (!this.refreshEventReceived) {
// Either the context is not a ConfigurableApplicationContext with refresh
// support or the context injected at construction time had already been
// refreshed -> trigger initial onRefresh manually here.
synchronized (this.onRefreshMonitor) {
// 刷新WebApplicationContext
onRefresh(wac);
}
}
if (this.publishContext) {
// Publish the context as a servlet context attribute.
// 将IOC容器在应用域共享
String attrName = getServletContextAttributeName();
getServletContext().setAttribute(attrName, wac);
}
return wac;
}
创建WebApplicationContext
所在类:org.springframework.web.servlet.FrameworkServlet
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
Class<?> contextClass = getContextClass();
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException(
"Fatal initialization error in servlet with name '" + getServletName() +
"': custom WebApplicationContext class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
// 通过反射创建 IOC 容器对象
ConfigurableWebApplicationContext wac =
(ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
wac.setEnvironment(getEnvironment());
// 设置父容器
wac.setParent(parent);
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
configureAndRefreshWebApplicationContext(wac);
return wac;
}
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);
}
DispatcherServlet调用组件处理请求
a>processRequest()
FrameworkServlet重写HttpServlet中的service()和doXxx(),这些方法中调用了processRequest(request, response)
所在类:org.springframework.web.servlet.FrameworkServlet
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
// 执行服务,doService()是一个抽象方法,在DispatcherServlet中进行了重写
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
b>doService()
所在类:org.springframework.web.servlet.DispatcherServlet
@Override
protected void doService(HttpServletRequest request, HttpServletResponse response) throws Exception {
logRequest(request);
// Keep a snapshot of the request attributes in case of an include,
// to be able to restore the original attributes after the include.
Map<String, Object> attributesSnapshot = null;
if (WebUtils.isIncludeRequest(request)) {
attributesSnapshot = new HashMap<>();
Enumeration<?> attrNames = request.getAttributeNames();
while (attrNames.hasMoreElements()) {
String attrName = (String) attrNames.nextElement();
if (this.cleanupAfterInclude || attrName.startsWith(DEFAULT_STRATEGIES_PREFIX)) {
attributesSnapshot.put(attrName, request.getAttribute(attrName));
}
}
}
// Make framework objects available to handlers and view objects.
request.setAttribute(WEB_APPLICATION_CONTEXT_ATTRIBUTE, getWebApplicationContext());
request.setAttribute(LOCALE_RESOLVER_ATTRIBUTE, this.localeResolver);
request.setAttribute(THEME_RESOLVER_ATTRIBUTE, this.themeResolver);
request.setAttribute(THEME_SOURCE_ATTRIBUTE, getThemeSource());
if (this.flashMapManager != null) {
FlashMap inputFlashMap = this.flashMapManager.retrieveAndUpdate(request, response);
if (inputFlashMap != null) {
request.setAttribute(INPUT_FLASH_MAP_ATTRIBUTE, Collections.unmodifiableMap(inputFlashMap));
}
request.setAttribute(OUTPUT_FLASH_MAP_ATTRIBUTE, new FlashMap());
request.setAttribute(FLASH_MAP_MANAGER_ATTRIBUTE, this.flashMapManager);
}
RequestPath requestPath = null;
if (this.parseRequestPath && !ServletRequestPathUtils.hasParsedRequestPath(request)) {
requestPath = ServletRequestPathUtils.parseAndCache(request);
}
try {
// 处理请求和响应
doDispatch(request, response);
}
finally {
if (!WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Restore the original attribute snapshot, in case of an include.
if (attributesSnapshot != null) {
restoreAttributesAfterInclude(request, attributesSnapshot);
}
}
if (requestPath != null) {
ServletRequestPathUtils.clearParsedRequestPath(request);
}
}
}
c>doDispatch()
所在类:org.springframework.web.servlet.DispatcherServlet
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// Determine handler for the current request.
/*
mappedHandler:调用链
包含handler、interceptorList、interceptorIndex
handler:浏览器发送的请求所匹配的控制器方法
interceptorList:处理控制器方法的所有拦截器集合
interceptorIndex:拦截器索引,控制拦截器afterCompletion()的执行
*/
mappedHandler = getHandler(processedRequest);
if (mappedHandler == null) {
noHandlerFound(processedRequest, response);
return;
}
// Determine handler adapter for the current request.
// 通过控制器方法创建相应的处理器适配器,调用所对应的控制器方法
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
// Process last-modified header, if supported by the handler.
String method = request.getMethod();
boolean isGet = "GET".equals(method);
if (isGet || "HEAD".equals(method)) {
long lastModified = ha.getLastModified(request, mappedHandler.getHandler());
if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) {
return;
}
}
// 调用拦截器的preHandle()
if (!mappedHandler.applyPreHandle(processedRequest, response)) {
return;
}
// Actually invoke the handler.
// 由处理器适配器调用具体的控制器方法,最终获得ModelAndView对象
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
// 调用拦截器的postHandle()
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
// 后续处理:处理模型数据和渲染视图
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
}
d>processDispatchResult()
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
// 处理模型数据和渲染视图
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
// 调用拦截器的afterCompletion()
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
SpringMVC的执行流程
-
用户向服务器发送请求,请求被SpringMVC 前端控制器 DispatcherServlet捕获。
-
DispatcherServlet对请求URL进行解析,得到请求资源标识符(URI),判断请求URI对应的映射:
a) 不存在
i. 再判断是否配置了mvc:default-servlet-handler
ii. 如果没配置,则控制台报映射查找不到,客户端展示404错误
iii. 如果有配置,则访问目标资源(一般为静态资源,如:JS,CSS,HTML),找不到客户端也会展示404错误
b) 存在则执行下面的流程
-
根据该URI,调用HandlerMapping获得该Handler配置的所有相关的对象(包括Handler对象以及Handler对象对应的拦截器),最后以HandlerExecutionChain执行链对象的形式返回。
-
DispatcherServlet 根据获得的Handler,选择一个合适的HandlerAdapter。
-
如果成功获得HandlerAdapter,此时将开始执行拦截器的preHandler(…)方法【正向】
-
提取Request中的模型数据,填充Handler入参,开始执行Handler(Controller)方法,处理请求。在填充Handler的入参过程中,根据你的配置,Spring将帮你做一些额外的工作:
a) HttpMessageConveter: 将请求消息(如Json、xml等数据)转换成一个对象,将对象转换为指定的响应信息
b) 数据转换:对请求消息进行数据转换。如String转换成Integer、Double等
c) 数据格式化:对请求消息进行数据格式化。 如将字符串转换成格式化数字或格式化日期等
d) 数据验证: 验证数据的有效性(长度、格式等),验证结果存储到BindingResult或Error中
-
Handler执行完成后,向DispatcherServlet 返回一个ModelAndView对象。
-
此时将开始执行拦截器的postHandle(…)方法【逆向】。
-
根据返回的ModelAndView(此时会判断是否存在异常:如果存在异常,则执行HandlerExceptionResolver进行异常处理)选择一个适合的ViewResolver进行视图解析,根据Model和View,来渲染视图。
-
渲染视图完毕执行拦截器的afterCompletion(…)方法【逆向】。
-
将渲染结果返回给客户端。
码字不易,三连支持一下吧!