一天急速通关SpringMVC
一天急速通关SpringMVC
- 0 文章介绍
- 1 介绍
- 1.1 MVC架构与三层架构
- 1.2 Spring MVC介绍
- 1.3 入门程序
- 2 请求的映射
- 3 请求数据的接收
- 3.1 @RequestParam接收
- 3.2 POJO/JavaBean接收
- 3.3 RequestHeader和CookieValue接收
- 4 请求数据的传递
- 5 视图
- 5.1 视图的理解
- 5.2 请求转发和响应重定向的使用
- 5.3 特定资源映射和静态资源映射
- 6 技巧
- 6.1 RESTful编程风格
- 6.2 HTTP消息转换器
- 6.3 文件上传与下载
- 6.4 异常处理器
- 6.5 拦截器
- 7 全注解开发
0 文章介绍
在倍速观看动力节点杜老师的Spring6MVC
教程之后,根据视频内容以及课程笔记进行实践,经过自己的理解并总结后形成这篇学习笔记。文章总共分为六个章节,包括了原教程的十四个章节的大部分知识,学习本文的前置知识需要:JavaSE
,JDBC
,MySQL
,XML
,MyBatis
,Spring6
,Ajax
,Thymeleaf
。本文所提供的信息和内容仅供参考,作者和发布者不保证其准确性和完整性。
1 介绍
1.1 MVC架构与三层架构
MVC
架构本质是一种用户界面设计模式,主要用于分离用户界面的展示、数据管理和用户交互逻辑,将应用分为三个核心组件:
- 模型(Model):应用程序的数据结构和业务逻辑,负责与数据库、文件系统和其他数据源进行交互,获取和处理数据。
- 视图(View):用户界面的展示部分,负责向用户呈现数据并接收用户的输入数据,不涉及业务逻辑。
- 控制器(Controller):充当模型和视图之间的中介,处理用户输入并调用模型和视图去完成用户的需求。
理解: 前端浏览器发送请求给Web
服务器,Web
服务器中的Controller
接收到用户的请求,Controller
负责将前端提交的数据进行封装,然后Controller
调用Model
来处理业务,当Model
处理完业务后会返回处理之后的数据给Controller
,Controller
再调用View
来完成数据的展示,最终将结果响应给浏览器,浏览器进行渲染展示页面。
三层架构(Three-Tier Architecture
)是一种分层架构,为了实现高内聚低耦合,提高系统的可维护性和可扩展性,将应用分为三个逻辑层:
- 表示层(
Presentation Layer
):负责用户界面的展示,接收用户输入并展示处理结果。 - 业务逻辑层(
Business Logic Layer
):处理业务规则,封装业务逻辑,组合数据访问层的基本功能,形成复杂的业务逻辑功能。 - 数据访问层(
Data Access Layer
):负责与数据库进行交互,执行数据的增删改查操作。
理解: 关注点和MVC
不一样,关注点在于系统逻辑分离。
MVC
架构在实际应用中,模型部分可能会包含复杂的业务逻辑和数据访问操作,为了更好地组织和解耦代码,将 MVC
中的模型层进一步细分为三层架构中的业务逻辑层(Service
)和数据访问层(DAO
),三层架构中的表示层负责接收用户请求并展示处理结果,其实就对应MVC
架构的视图层(View
)和控制层(Controller
)。
理解:MVC
架构本质上是一种用户界面设计模式,核心目标是分离用户界面的展示、数据和交互逻辑,本身并不涉及应用程序的底层架构设计,如数据存储、业务逻辑处理等,而这些通常是通过其他设计模式或架构(如三层架构、微服务架构等)来实现。通过结合两者,MVC
架构专注于用户界面的设计和交互,而三层架构则提供了更清晰的系统逻辑分离和模块化设计,使得应用程序更加易于维护和扩展。
1.2 Spring MVC介绍
SpringMVC
是一个实现了MVC
架构模式的Web
框架,底层基于Servlet
实现,之前学习JavaWeb
的时候学了Servlet
的一些操作,现在这个框架能够更方便的实现这些操作,同时还支持Spring6
的IoC
与AOP
,具体的点有:
- 通过
DispatcherServlet
作为入口控制器,负责接收请求和分发请求。Servlet
需要手动编写Servlet
程序。 - 表单提交时可以通过简单的操作就能自动将表单数据绑定到相应的
JavaBean
对象中。Servlet
需要手动获取。 - 通过
IoC
容器管理对象,只需要在配置文件中进行相应的配置即可获取实例对象。Servlet
需要手动创建。 - 提供了拦截器、异常处理器等统一处理请求的机制,并且可以灵活地配置这些处理器。
Servlet
需要手动过滤器、异常处理器等。 - 提供了多种视图模板,如
JSP
、Freemarker
、Velocity
等,并且支持国际化、主题等特性。Servlet
需要手动处理视图层。
总结:简化了很多操作的同时提供了更多的功能和扩展性。
1.3 入门程序
0 创建Maven Java Web
项目 -> 1 配置pom.xml
依赖 -> 2 配置web.xml
-> 3 配置springmvc-servlet.xml
-> 4 编写Java
代码与视图。
-
环境准备
-
IntelliJ IDEA
:2024.1.7 -
Navicat for MySQL
:17.1.2 -
MySQL
:8.0.26 -
JDK
:17.0.2 -
Maven
:3.9.1
-
-
设置
pom.xml
的打包方式以及添加依赖,打包方式设置为war
。<groupId>com.cut</groupId> <artifactId>spring-mvc-demo</artifactId> <version>1.0-SNAPSHOT</version> <!-- 打包方式设置为war方式 --> <packaging>war</packaging> <dependencies> <!-- Spring MVC依赖 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>6.1.14</version> </dependency> <!--日志框架Logback依赖--> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.5.3</version> </dependency> <!--Servlet依赖--> <dependency> <groupId>jakarta.servlet</groupId> <artifactId>jakarta.servlet-api</artifactId> <version>6.0.0</version> <scope>provided</scope> </dependency> <!--Spring6和Thymeleaf整合依赖--> <dependency> <groupId>org.thymeleaf</groupId> <artifactId>thymeleaf-spring6</artifactId> <version>3.1.2.RELEASE</version> </dependency> </dependencies>
-
配置
web.xml
文件,主要配置SpringMVC
的前端控制器DispatcherServlet
,让除了JSP
的所有请求都走这个控制器。<?xml version="1.0" encoding="UTF-8"?> <web-app xmlns="https://jakarta.ee/xml/ns/jakartaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="https://jakarta.ee/xml/ns/jakartaee https://jakarta.ee/xml/ns/jakartaee/web-app_6_0.xsd" version="6.0"> <!--SpringMVC提供的前端控制器--> <servlet> <servlet-name>springmvc</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <!--手动设置springmvc配置文件的路径及名字--> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <!--为了提高用户的第一次访问效率,建议在web服务器启动时初始化前端控制器--> <load-on-startup>6</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springmvc</servlet-name> <!-- /* 表示任何一个请求都交给DispatcherServlet来处理 --> <!-- / 表示当请求不是xx.jsp的时候,DispatcherServlet来负责处理本次请求--> <url-pattern>/</url-pattern> </servlet-mapping> </web-app>
-
配置
springmvc-servlet.xml,SpringMVC
框架的配置文件,名字默认为:<servlet-name>-servlet.xml
,默认存放的位置是WEB-INF
根目录。如果
web.xml
中DispatcherServlet
的servlet-name
为springmvc
,并且WEB-INF
根目录下有springmvc-servlet.xml
(短横线前面为servlet-name
)的配置文件,就不用设置contextConfigLocation
,自动加载,如果没有,则需要手动加载,这边在resources
目录下也创建了springmvc.xml
,内容一样,手动加载这个文件,没有使用默认路径下默认名称的springmvc-servlet.xml
,作为演示。<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd"> <!--组件扫描, 扫描注解--> <context:component-scan base-package="com.cut.springmvc"/> <!--视图解析器--> <bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver"> <property name="characterEncoding" value="UTF-8"/> <!--如果配置多个视图解析器,它来决定优先使用哪个视图解析器,它的值越小优先级越高--> <property name="order" value="1"/> <property name="templateEngine"> <bean class="org.thymeleaf.spring6.SpringTemplateEngine"> <property name="templateResolver"> <bean class="org.thymeleaf.spring6.templateresolver.SpringResourceTemplateResolver"> <property name="prefix" value="/WEB-INF/templates/"/> <property name="suffix" value=".html"/> <!--设置模板类型,例如:HTML,TEXT,JAVASCRIPT,CSS等--> <property name="templateMode" value="HTML"/> <property name="characterEncoding" value="UTF-8"/> </bean> </property> </bean> </property> </bean> </beans>
-
编写控制器
com.cut.springmvc.controller.IndexController.java
package com.cut.springmvc.controller; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; /** * @author tang * @version 1.0.0 * @since 1.0.0 */ @Controller public class IndexController { @RequestMapping("/") public String toIndex() { return "index"; } @RequestMapping("/hello") public String toHello() { // 返回逻辑视图名称(决定跳转到哪个页面) return "hello"; } }
-
编写视图
WEB-INF/templates/index.html,WEB-INF/templates/hello.html
<!DOCTYPE html> <!--指定 th 命名空间,让 Thymeleaf 标准表达式可以被解析和执行--> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> <!-- Thymeleaf检测到以 / 开始,表示绝对路径,自动会将webapp的上下文路径加上去 --> <!-- 最终的效果是:href="/springmvc/hello" --> <a th:href="@{/hello}">点击进入hello页面</a> </body> </html>
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Hello</title> </head> <body> <h1>Hello Spring MVC</h1> </body> </html>
-
配置
Tomcat
并访问即可。
2 请求的映射
在SpringMVC
中,@RequestMapping
注解是控制器映射注解,用于将请求映射到相应的处理方法上,即将指定URL
的请求绑定到一个特定的方法或类上,从而实现对请求的处理和响应,可以用在类或者方法上。
-
类属性和方法属性结合使用
同一个
webapp
中,RequestMapping
必须具有唯一性,即value
不能重复,同一个Controller
或者不同Controller
中不能出现相同的映射。@Controller public class UserController { @RequestMapping("/detail") public String toDetail(){ return "detail"; } }
@Controller public class ProductController { @RequestMapping("/detail") public String toDetail(){ return "detail"; } }
这种情况应该要加上前缀,区分两个
detail
映射,如何加,两种方式,一是方法上直接加前缀。@RequestMapping("/user/detail") public String toDetail(){ return "/user/detail"; }
@RequestMapping("/product/detail") public String toDetail(){ return "/product/detail"; }
二是在类上面加。
@Controller @RequestMapping("/user") public class UserController { @RequestMapping("/detail") public String toDetail(){ return "/user/detail"; } }
@Controller @RequestMapping("/product") public class ProductController { @RequestMapping("/detail") public String toDetail(){ return "/product/detail"; } }
-
RequestMapping
的value
属性value
属性是请求路径,通过该请求路径与对应的控制器的方法绑定,是一个字符串数组,可以映射多个路径。还支持模糊匹配路径,称之为Ant
风格?
,代表任意一个字符*
,代表0
到N
个任意字符**
,代表0
到N
个任意字符,路径中可以有分隔符 /,只能出现在路径的末尾。
请求路径中还可以使用占位符,配合
@PathVariable
注解接收请求传递的参数@RequestMapping(value="/testRESTful/{id}/{username}/{age}") public String testRESTful( @PathVariable("id") int id, @PathVariable("username") String username, @PathVariable("age") int age){ System.out.println(id + "," + username + "," + age); return "testRESTful"; }
<!--测试RequestMapping注解的value属性支持占位符--> <a th:href="@{/testRESTful/1/zhangsan/20}">测试value属性使用占位符</a>
之前学习的请求路径应该是下面这样的
http://localhost:8080/springmvc/login?username=admin&password=123&age=20
上面代码的请求路径是
RESTful
风格的,在现代的开发中使用较多,因此请求路径变为http://localhost:8080/springmvc/login/admin/123/20
-
RequestMapping
的method
属性这个属性用于限定请求方式,如果请求方式和
method
值不一致会出现405
错误,下面演示了要求前端必须得是POST
请求访问/login
路径。两种方式,一种是RequestMapping
,另一种直接使用PostMapping
,其他请求方式用法一致。//@RequestMapping(value="/login", method = RequestMethod.POST) @PostMapping("/login") public String testMethod(){ return "testMethod"; }
请求方式主要有下面的九种:
GET
:URL
中或请求头中传递参数,获取资源,只允许读取数据,不影响数据的状态和功能。POST
:通过表单等方式提交请求体提交资源。PUT
:通过请求体发送,更新指定的资源上所有可编辑内容。DELETE
:将要被删除的资源标识符放在URL
中或请求体中用于删除指定的资源。HEAD
:类似GET
命令,但是所有返回的信息都是头部信息,不能包含数据体。PATCH
:当被请求的资源是可被更改的资源时,每次更新一部分。OPTIONS
:请求获得服务器支持的请求方法类型,以及支持的请求头标志。TRACE
:服务器响应输出客户端的HTTP
请求,主要用于调试和测试。CONNECT
:建立网络连接,通常用于加密SSL/TLS
连接。
注意:超链接发送的请求是
get
请求,form
表单只能提交get
和post
请求,put
、delete
、head
请求可以使用发送ajax
请求的方式来实现,form
表单的method
如果不是get
或者post
,按照get
请求处理。比较常用的
get
和post
请求的区别:-
get
请求适合从服务器端获取数据,只能发送普通字符串,且长度有限制,请求的数据会回显到地址栏,支持缓存,如果存在缓存,就不会再发送到服务器,如果要避免,建议请求路径后加时间戳。 -
post
请求适合向服务器端传递数据,可以发送任何数据,数据在请求体中,不会回显,不支持缓存,每次请求都走服务器。
-
RequestMapping
的params
属性用于限定请求传递的方式必须和设置的一致,否则发生
400
错误,对于@RequestMapping(value="/login", params={})
,匹配规则如下:params={**"username"**, "password"}
,必须包含username
和password
params={**"!username"**, "password"}
,不能包含username
,但必须包含password
params={**"username=admin"**, "password"}
,必须包含值是admin
的username
参数,必须包含password
params={**"username!=admin"**, "password"}
,不能包含值是admin
的username
参数,必须包含password
-
RequestMapping
的headers
属性和
params
原理相同,用法也相同。请求头信息后端要求的不一致,则出现404
错误 -
状态码
400 Bad Request
:请求无效,请求格式错误或缺少必要的参数。404 Not Found
:请求的资源未找到。405 Method Not Allowed
:请求方法不被允许。
3 请求数据的接收
请求的数据接收主要用到@RequestParam
和POJO/JavaBean
来进行接收,还有RequestHeader
,@CookieValue
用于接收请求头和Cookie
。
3.1 @RequestParam接收
对于@RequestParam
来说,主要有三个属性vaue
,name
,required
,defaultValue
,其中name
和value
本质一样。
@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RequestParam {
@AliasFor("name")
String value() default "";
@AliasFor("value")
String name() default "";
boolean required() default true;
String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}
注册页面和注册成功页面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>用户注册</title>
</head>
<body>
<div class="container">
<h3>用户注册</h3>
<hr>
<form th:action="@{/success}" method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" required>
<label for="password">密码:</label>
<input type="password" id="password" name="password" required>
<label>性别:</label>
<label><input type="radio" name="sex" value="1"> 男</label>
<label><input type="radio" name="sex" value="0"> 女</label>
<label>爱好:</label>
<label><input type="checkbox" name="hobby" value="smoke"> 抽烟</label>
<label><input type="checkbox" name="hobby" value="drink"> 喝酒</label>
<label><input type="checkbox" name="hobby" value="perm"> 烫头</label>
<label for="intro">简介:</label>
<textarea id="intro" rows="10" cols="60" name="intro"></textarea>
<input type="submit" value="注册">
</form>
</div>
</body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Register Success</title>
</head>
<body>
<h1>注册成功!</h1>
</body>
</html>
RegisterController
package com.cut.springmvc.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
/**
* @author tang
* @version 1.0.0
* @since 1.0.0
*/
@Controller
public class RegisterController {
@RequestMapping("/success")
public String toSuccess(
@RequestParam(value = "username", required = true)
String username,
@RequestParam(value = "password", required = false)
String password,
@RequestParam(value = "sex")
String sex,
@RequestParam(value = "hobby")
String[] hobby,
@RequestParam(name = "intro")
String intro,
@RequestParam(value = "otherParam", required = false, defaultValue = "default value")
String otherParam
) {
return "success";
}
@RequestMapping("/register")
public String toRegister(
) {
return "register";
}
}
关于几个属性的说明:
- 混用
name
和value
或者省略,都可以成功接收参数。 required
默认情况为true
,表示请求参数必需,如果缺少会抛出异常。设置为false
如果请求中缺少则方法的参数值为null
。defaultValue
设置形参的默认值,当没有提供对应的请求参数或者请求参数的**值是空字符串""**的时候,方法的形参会采用默认值。
注意,@RequestParam
可以省略的,请求参数和方法形参的名字相同,则可以省略,如果形参有请求参数没有的,为null
。
3.2 POJO/JavaBean接收
当提交的数据非常多时,方法的形参个数会非常多,能直接使用POJO/JavaBean
来接收吗,可以的,但是有个要求,实体类得含有对应请求参数名一样的set
方法,属性名可以不一致,但是set
方法得一致。
private Long ID;
public void setId(Long id) {
this.ID = id;
}
// 请求参数中有id
// 那么实体类这样也是可以的,只要setId对应id即可,属性ID名称没关系
@PostMapping("/register")
public String register(User user){
System.out.println(user);
return "success";
}
3.3 RequestHeader和CookieValue接收
@GetMapping("/register")
public String register(
User user,
@RequestHeader(value="Referer", required = false, defaultValue = "")
String referer,
@CookieValue(value="id", required = false, defaultValue = "1")
String id){
return "success";
}
4 请求数据的传递
之前在学习Servlet
的时候,通过setAttribute + getAttribute
方法来完成在域中数据的传递和共享。回顾Servlet
的三个域对象:
域对象 | 作用范围 | 生命周期 | 使用场景 |
---|---|---|---|
应用域ServletContext | 全局范围 | Web 应用启动到停止 | 全局变量、配置信息 |
会话域HttpSession | 会话范围 | 用户会话开始到结束 | 用户登录信息、购物车 |
请求域HttpServletRequest | 请求范围 | 请求开始到结束 | 请求参数、临时数据 |
-
请求域对象中共享数据方式有四种,前三者本质都是一样的
BindingAwareModelMap
。-
Model
接口,model.addAttribute(key,value)
,@RequestMapping("/testModel") public String testModel(Model model){ model.addAttribute("testRequestScope", "在SpringMVC中使用Model接口实现request域数据共享"); return "view"; }
-
Map
接口,map.put(key,value)
@RequestMapping("/testMap") public String testMap(Map<String, Object> map){ map.put("testRequestScope", "在SpringMVC中使用Map接口实现request域数据共享"); return "view"; }
-
ModelMap
接口,modelMap.addAttribute(key,value)
,直接添加对应类型的形参使用即可。@RequestMapping("/testModelMap") public String testModelMap(ModelMap modelMap){ modelMap.addAttribute("testRequestScope", "在SpringMVC中使用ModelMap实现request域数据共享"); return "view"; }
-
ModelAndView
接口这个类的实例封装了
Model
和View,
也就是说这个类既封装业务处理之后的数据,也体现了跳转到哪个视图。不是添加形参,而是在方法里new
。@RequestMapping("/testModelAndView") public ModelAndView testModelAndView(){ ModelAndView modelAndView = new ModelAndView(); // 绑定数据 modelAndView.addObject("testRequestScope", "在SpringMVC中使用ModelAndView实现request域数据共享"); // 绑定视图 modelAndView.setViewName("view"); return modelAndView; }
-
-
会话域对象中共享数据方式
直接在
Controller
类上使用@SessionAttributes
注解,标注了当key
是x
或者y
时,数据将被存储到会话session
中,如果没有SessionAttributes
注解,默认存储到request
域中。@Controller @SessionAttributes(value = {"x", "y"}) public class SessionScopeTestController { @RequestMapping("/testSessionScope2") public String testSessionAttributes(ModelMap modelMap){ // 向session域中存储数据 modelMap.addAttribute("x", "我是埃克斯"); modelMap.addAttribute("y", "我是歪"); return "view"; } }
-
应用域对象中共享数据方式
直接使用
Servlet API
即可@Controller public class ApplicationScopeTestController { @RequestMapping("/testApplicationScope") public String testApplicationScope(HttpServletRequest request){ // 获取ServletContext对象 ServletContext application = request.getServletContext(); // 向应用域中存储数据 application.setAttribute("applicationScope", "我是应用域当中的一条数据"); return "view"; } }
5 视图
5.1 视图的理解
视图(View
)是MVC
架构中的表示层组件,负责将模型(Model
)中的数据呈现给用户,并处理用户交互,之前在 springmvc.xml
配置的,就是Thymeleaf
视图解析器ThymeleafViewResolver
。
<bean id="thymeleafViewResolver" class="org.thymeleaf.spring6.view.ThymeleafViewResolver">
...
</bean>
除了Thymeleaf
以外,还有其他的视图:
InternalResourceView
:内部资源视图(Spring MVC
框架内置的,专门为JSP模板语法
准备的)RedirectView
:重定向视图(Spring MVC
框架内置的,用来完成重定向效果)ThymeleafView
:Thymeleaf
视图(第三方的,为Thymeleaf模板语法
准备的)FreeMarkerView
:FreeMarker
视图(第三方的,为FreeMarker模板语法
准备的)VelocityView
:Velocity
视图(第三方的,为Velocity模板语法
准备的)PDFView
:PDF
视图(第三方的,专门用来生成pdf
文件视图)ExcelView
:Excel
视图(第三方的,专门用来生成excel
文件视图)
SpringMVC
如何实现视图机制的呢,主要有下面几个核心接口
- 负责中央调度的前端控制器
DispatcherServlet
,核心方法为doDispatch
。 - 将逻辑视图名转换为物理视图名的视图解析器
ViewResolver
,核心方法为resolveViewName
,返回视图实现类对象。 - 将模板语言转换成
HTML
代码的视图View
,核心代码为render
。 - 将视图解析器注册到
web
容器中的视图解析器注册器ViewResolverRegistry
,按order
顺序放入到List
集合中。
理解: 浏览器发送请求给we
b服务器,DispatcherServlet
接收到请求并根据请求路径分发到对应的Controller
并调用,Controller
返回逻辑视图名给DispatcherServlet
,DispatcherServlet
再调用ThymeleafViewResolver
的resolveViewName
方法,将逻辑视图名转换为物理视图名,并获取ThymeleafView
对象,最后DispatcherServlet
调用ThymeleafView
的render
方法,render
方法将模板语言转换为HTML
代码,响应给浏览器,完成最终的渲染。
5.2 请求转发和响应重定向的使用
关于请求转发和响应重定向在SpringMVC
中的使用,回顾之前Servlet
中的使用
request.getRequestDispatcher("/index").forward(request, response);
response.sendRedirect("/webapproot/index");
两个的区别:
- 转发是一次请求,服务器端内部的行为,地址栏不变化,不能实现跨域,但是可以访问
WEB-INF
下受保护的目录。 - 重定向是两次请求,既可以完成服务端内部资源跳转也可以完成跨域跳转,地址栏会发生变化,无法访问
WEB-INF
下受保护的目录。注意第一次发送请求,服务端会将重定向的地址返回给浏览器,浏览器再对这个地址发送请求。
Spring MVC
中控制器的返回值解析有下面三种情况:
-
返回值以
"forward:"
开头,Spring MVC
会执行请求转发到指定的路径(/path
)。@RequestMapping("/a") public String toA(){ return "forward:/b"; }
一共创建了两个视图对象,转发源:
InternalResourceView
,转发目的地:ThymeleafView
-
返回值以
"redirect:"
开头,Spring MVC
会执行HTTP
重定向到指定的路径(/path
)。@RequestMapping("/a") public String toA(){ return "redirect:/b"; }
一共创建了两个视图对象,重定向源:
RedirectView
,重定向目的地:ThymeleafView
-
返回值是除了前两种情况的字符串,
Spring MVC
会将其视为视图名,通过视图解析器来解析对应的视图资源。
5.3 特定资源映射和静态资源映射
<mvc:view-controller>
配置用于将某个请求映射到特定的视图上,即指定某一个 URL
请求到一个视图资源的映射,使得这个视图资源可以被访问。当Controller
中只是简单返回视图名的话,可以直接使用这个配置,例如首页之类的资源映射。
<mvc:view-controller path="/" view-name="index" />
<!-- 使用了上面的需要再手动开启SpringMVC的注解,不然会发生404错误 -->
<mvc:annotation-driven/>
关于静态资源的映射,比较简单的方式就是:
<!-- 开启注解驱动 -->
<mvc:annotation-driven />
<!-- 配置静态资源处理,请求路径是"/static/"开始的,都会去"/static/"目录下找该资源。 -->
<mvc:resources mapping="/static/**" location="/static/" />
也可以使用默认Servlet
处理,直接开启默认Servlet
处理静态资源功能,同一个请求路径,先走DispatcherServlet
,如果找不到则走默认的Servlet
:
<!-- 开启注解驱动 -->
<mvc:annotation-driven />
<!--开启默认Servlet处理-->
<mvc:default-servlet-handler>
6 技巧
主要包括了RESTful
编程风格,HTTP
消息转换器,文件上传下载,异常处理器,拦截器等方面的知识。
6.1 RESTful编程风格
RESTful
(Representational State Transfer
,表述性状态转移)是WEB
服务接口的一种设计风格,定义了一组约束条件和规范用于让WEB
服务接口更加简洁、易于理解、易于扩展、安全可靠。其中:
- 表述性(
Representational
)是:URI
+ 请求方式。 - 状态(
State
)是:服务器端的数据。 - 转移(
Transfer
)是:变化。 - 表述性状态转移是指:通过URI + 请求方式来控制服务器端数据的变化。
传统的 URL | RESTful URL |
---|---|
GET /getUserById?id=1 | GET /user/1 |
GET /getAllUser | GET /user |
POST /addUser | POST /user |
POST /modifyUser | PUT /user |
GET /deleteUserById?id=1 | DELETE /user/1 |
如何通过表单来提交这些请求呢,之前说过表单只能提交get
或者post
请求,这里就得需要针对请求做一个处理,例如put
请求:
<!--修改用户-->
<hr>
<form th:action="@{/api/user}" method="post">
<!--隐藏域的方式提交 _method=put -->
<input type="hidden" name="_method" value="put">
用户名:<input type="text" name="username"><br>
<input type="submit" th:value="修改">
</form>
表面上是post
请求,服务端接收到后通过HiddenHttpMethodFilter
判断是否为post
请求,如果是post
请求,调用request.getParameter(this.methodParam)
。this.methodParam
的值是"_method"
,对应名称为"_method"
的input
标签,只有请求方式是put/delete/patch
的时候会创建HttpMethodRequestWrapper
对象将method
从post
变成put/delete/patch
。
<!--隐藏的HTTP请求方式过滤器-->
<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>
注意:在web.xml
文件中,应该先配置CharacterEncodingFilter
,然后再配置HiddenHttpMethodFilter
,因为HiddenHttpMethodFilter
使用了request.getParameter()
,避免乱码问题。
6.2 HTTP消息转换器
HTTP消息转换器HttpMessageConverter
的主要作用是完成HTTP协议
与Java程序中的对象
之间的互相转换,主要学习下面几个注解和类的使用:
-
@ResponseBody
:将控制器方法的返回值直接写入HTTP
响应体中。@Controller public class MyController { // StringHttpMessageConverter @GetMapping("/hello") @ResponseBody public String hello() { return "Hello, World!"; } //MappingJackson2HttpMessageConverter @PostMapping("/user") @ResponseBody public User createUser() { // 处理用户数据 return new User(); // 返回 JSON 格式的用户数据 } }
这里只要有
@ResponseBody
注解,那么返回的就不是视图的逻辑名称了,而是响应体,上面演示了两个消息转换器,一个是StringHttpMessageConverter
,另一个是MappingJackson2HttpMessageConverter
将POJO
对象转换成JSON
格式的字符串,响应给前端,使用MappingJackson2HttpMessageConverter
得添加依赖<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.17.0</version> </dependency>
并在配置文件中开启注解
<mvc:annotation-driven/>
如果这个
Controller
类中全是响应体,那么可以在类上使用RestController
,就不用每个方法都添加@ResponseBody
@RestController public class MyController { // StringHttpMessageConverter @GetMapping("/hello") public String hello() { return "Hello, World!"; } //MappingJackson2HttpMessageConverter @PostMapping("/user") public User createUser() { // 处理用户数据 return new User(); // 返回 JSON 格式的用户数据 } }
-
@RequestBody
:将HTTP
请求体中的数据绑定到控制器方法的参数上。@RestController public class MyController { // FormHttpMessageConverter将请求体转换成user对象 @PostMapping("/user") public String hello(User user) { System.out.println(user) return "success"; } // FormHttpMessageConverter将请求体转换成user字符串 @PostMapping("/userStr") public User createUser(@RequestBody String userStr) { System.out.println("请求体:" + userStr); return "success"; } }
那还有一种组合呢
// FormHttpMessageConverter将请求体的JSON字符串转换成POJO对象 @PostMapping("/user") public User createUser(@RequestBody User user) { System.out.println("请求体:" + userStr); return "success"; }
总结:
User user:Form
表单请求体用POJO
接收@RequestBody User user:Json
请求体转换为POJO
类@RequestBody String user:Form
表单请求体转换为字符串
-
RequestEntity
:用于封装HTTP
响应的完整信息,包括状态码、响应头和响应体。直接在形参中添加即可使用。@RequestMapping("/send") @ResponseBody public String send(RequestEntity<User> requestEntity){ System.out.println("请求方式:" + requestEntity.getMethod()); System.out.println("请求URL:" + requestEntity.getUrl()); HttpHeaders headers = requestEntity.getHeaders(); System.out.println("请求的内容类型:" + headers.getContentType()); System.out.println("请求头:" + headers); User user = requestEntity.getBody(); System.out.println(user); System.out.println(user.getUsername()); System.out.println(user.getPassword()); return "success"; }
-
RequestEntity
:用于封装HTTP
请求的完整信息,包括请求方法、请求头和请求体,将返回值类型变为对应的ResponseEntity
对象即可,有啥用呢:前端提交一个id
,后端根据id进行查询,如果返回null
,请在前端显示404
错误。如果返回不是null
,则输出返回的user
。@Controller public class UserController { @GetMapping("/users/{id}") public ResponseEntity<User> getUserById(@PathVariable Long id) { User user = userService.getUserById(id); if (user == null) { return ResponseEntity.status(HttpStatus.NOT_FOUND).body(null); } else { return ResponseEntity.ok(user); } } }
6.3 文件上传与下载
-
文件的上传
html
文件上传表单,enctype="multipart/form-data"
<!--文件上传表单--> <form th:action="@{/file/up}" method="post" enctype="multipart/form-data"> 文件:<input type="file" name="fileName"/><br> <input type="submit" value="上传"> </form>
前端控制器配置
multipart-config
<!--前端控制器--> <servlet> <servlet-name>dispatcherServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:springmvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> <multipart-config> <!--设置单个支持最大文件的大小--> <max-file-size>102400</max-file-size> <!--设置整个表单所有文件上传的最大值--> <max-request-size>102400</max-request-size> <!--设置最小上传文件大小--> <file-size-threshold>0</file-size-threshold> </multipart-config> </servlet> <servlet-mapping> <servlet-name>dispatcherServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>
Controller
,主要就是文件的写入操作,注意@RequestParam("fileName") MultipartFile multipartFile
的理解@RequestMapping(value = "/file/up", method = RequestMethod.POST) public String fileUp(@RequestParam("fileName") MultipartFile multipartFile, HttpServletRequest request) throws IOException { String name = multipartFile.getName(); System.out.println(name); // 获取文件名 String originalFilename = multipartFile.getOriginalFilename(); System.out.println(originalFilename); // 将文件存储到服务器中 // 获取输入流 InputStream in = multipartFile.getInputStream(); // 获取上传之后的存放目录 File file = new File(request.getServletContext().getRealPath("/upload")); // 如果服务器目录不存在则新建 if(!file.exists()){ file.mkdirs(); } // 开始写 //BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file.getAbsolutePath() + "/" + originalFilename)); // 可以采用UUID来生成文件名,防止服务器上传文件时产生覆盖 BufferedOutputStream out = new BufferedOutputStream(new FileOutputStream(file.getAbsolutePath() + "/" + UUID.randomUUID().toString() + originalFilename.substring(originalFilename.lastIndexOf(".")))); byte[] bytes = new byte[1024 * 100]; int readCount = 0; while((readCount = in.read(bytes)) != -1){ out.write(bytes,0,readCount); } // 刷新缓冲流 out.flush(); // 关闭流 in.close(); out.close(); return "ok"; }
-
文件的下载即可
<!--文件下载--> <a th:href="@{/download}">文件下载</a>
注意返回类型
@GetMapping("/download") public ResponseEntity<byte[]> downloadFile(HttpServletResponse response, HttpServletRequest request) throws IOException { File file = new File(request.getServletContext().getRealPath("/upload") + "/1.jpeg"); // 创建响应头对象 HttpHeaders headers = new HttpHeaders(); // 设置响应内容类型 headers.setContentType(MediaType.APPLICATION_OCTET_STREAM); // 设置下载文件的名称 headers.setContentDispositionFormData("attachment", file.getName()); // 下载文件 ResponseEntity<byte[]> entity = new ResponseEntity<byte[]>(Files.readAllBytes(file.toPath()), headers, HttpStatus.OK); return entity; }
6.4 异常处理器
Spring MVC
中,异常处理是一个非常重要的功能,用于捕获和处理控制器方法中抛出的异常,从而向客户端返回友好的错误信息。实际就是当异常发生了,决定返回什么的操作。
主要有两个异常处理的方式,一个是方法级别的@ExceptionHandler
,一个是类级别的@ControllerAdvice
,这里结合之前学习的ResponseEntity
使用。
@ExceptionHandler
,方法上捕获特定异常或者所有异常。
@RestController
public class MyController {
@GetMapping("/test")
public String test() {
throw new IllegalArgumentException("测试异常");
}
// 捕获特定异常
@ExceptionHandler(IllegalArgumentException.class)
public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("发生非法参数异常:" + ex.getMessage());
}
// 捕获所有异常
@ExceptionHandler(Exception.class)
public ResponseEntity<String> handleException(Exception ex) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("发生未知错误:" + ex.getMessage());
}
}
-
@ControllerAdvice
,统一管理异常,Advice
通知,之前AOP
的术语之一应该就明白什么意思了,面向切面编程,业务Controller
就只关注核心业务。@ControllerAdvice public class GlobalExceptionHandler { // 捕获特定异常 @ExceptionHandler(IllegalArgumentException.class) public ResponseEntity<String> handleIllegalArgumentException(IllegalArgumentException ex) { return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("发生非法参数异常:" + ex.getMessage()); } // 捕获所有异常 @ExceptionHandler(Exception.class) public ResponseEntity<String> handleException(Exception ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("发生未知错误:" + ex.getMessage()); } }
6.5 拦截器
Spring MVC
的拦截器作用是在请求到达控制器之前或之后进行拦截,可以对请求和响应进行一些特定的处理。下面是拦截器在整个请求到返回过程中的运行位置。
可以看到有三个核心方法,preHandle,postHandle,afterCompletion,Interceptor
是后于Filer
的执行的。
客户端发请求,Servlet
容器接收,然后执行Filters
的doFilter
方法,之后进入Spring
,执行Interceptors
的preHandle
后进入Controller
并返回ModelAndView
,之后可以执行Interceptors
的postHandle
,再进行视图解析,渲染,完成后执行Interceptors
的afterCompletion
方法,最后返回响应到客户端并完成Filters
链。
在SpringMVC
中配置拦截器的方式
<mvc:interceptors>
<bean class="com.powernode.springmvc.interceptors.MyInterceptor"/>
</mvc:interceptors>
<!-- 下面这种需要配置包扫描并使用 @Component 注解进行标注 -->
<mvc:interceptors>
<ref bean="myInterceptor"/>
</mvc:interceptors>
这种简单的配置会拦截所有请求,如何自定义呢,下面表示除 /test
请求路径之外,剩下的路径全部拦截。
<mvc:interceptors>
<mvc:interceptor>
<!--拦截所有路径-->
<mvc:mapping path="/**"/>
<!--除 /test 路径之外-->
<mvc:exclude-mapping path="/test"/>
<!--拦截器-->
<ref bean="myInterceptor"/>
</mvc:interceptor>
</mvc:interceptors>
如果有多个拦截器,他们的三个方法的执行顺序呢
<mvc:interceptors>
<ref bean="interceptor1"/>
<ref bean="interceptor2"/>
</mvc:interceptors>
preHandles
顺序执行,postHandle
逆序执行,afterCompletion
逆序执行
Interceptor1.preHandle() // 请求阶段,按顺序执行
Interceptor2.preHandle() // 请求阶段,按顺序执行
Controller处理请求 // 请求到达Controller
Interceptor2.postHandle() // 返回阶段,逆序执行
Interceptor1.postHandle() // 返回阶段,逆序执行
视图解析与渲染 // 视图解析器渲染页面
Interceptor2.afterCompletion() // 完成阶段,逆序执行
Interceptor1.afterCompletion() // 完成阶段,逆序执行
注意: 只要有一个拦截器preHandle
返回false,任何postHandle
都不执行。但返回false
的拦截器的前面(不包括当前)的拦截器按照逆序执行afterCompletion
。
Interceptor1.preHandle() // 请求阶段,按顺序执行
Interceptor2.preHandle() // 请求阶段,按顺序执行,返回false,阻止后续处理
Interceptor1.afterCompletion() // 完成阶段,逆序执行
7 全注解开发
其实就web.xml
和springmvc.xml
怎么通过类配置
web.xml
的配置文件需要继承AbstractAnnotationConfigDispatcherServletInitializer
import jakarta.servlet.Filter;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.filter.HiddenHttpMethodFilter;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[0];
}
// 指定Spring MVC配置类
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class[]{SpringmvcConfig.class};
}
// 指定DispatcherServlet的映射路径
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
// 配置过滤器,characterEncodingFilter先于hiddenHttpMethodFilter
@Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter("UTF-8");
characterEncodingFilter.setForceRequestEncoding(true);
characterEncodingFilter.setForceResponseEncoding(true);
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
return new Filter[]{characterEncodingFilter, hiddenHttpMethodFilter};
}
}
springmvc.xml
配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.*;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
// 指定该类是一个配置类,可以当配置文件使用
@Configuration
// 指定扫描的包路径
@ComponentScan("com.example.controller")
// 开启Spring MVC注解支持
@EnableWebMvc
public class SpringmvcConfig implements WebMvcConfigurer {
// 配置视图解析器
@Bean
public ThymeleafViewResolver getViewResolver(SpringTemplateEngine springTemplateEngine) {
ThymeleafViewResolver resolver = new ThymeleafViewResolver();
resolver.setTemplateEngine(springTemplateEngine);
resolver.setCharacterEncoding("UTF-8");
resolver.setOrder(1);
return resolver;
}
@Bean
public SpringTemplateEngine templateEngine(ITemplateResolver iTemplateResolver) {
SpringTemplateEngine templateEngine = new SpringTemplateEngine();
templateEngine.setTemplateResolver(iTemplateResolver);
return templateEngine;
}
@Bean
public ITemplateResolver templateResolver(ApplicationContext applicationContext) {
SpringResourceTemplateResolver resolver = new SpringResourceTemplateResolver();
resolver.setApplicationContext(applicationContext);
resolver.setPrefix("/WEB-INF/thymeleaf/");
resolver.setSuffix(".html");
resolver.setTemplateMode(TemplateMode.HTML);
resolver.setCharacterEncoding("UTF-8");
resolver.setCacheable(false);//开发时关闭缓存,改动即可生效
return resolver;
}
// 配置静态资源处理
@Override
public void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
// view-controller特殊映射
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/test").setViewName("test");
}
// 配置拦截器,这里的MyInterceptor需要自己写
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**").excludePathPatterns("/test");
}
// 配置异常处理器
@Override
public void configureHandlerExceptionResolvers(List<HandlerExceptionResolver> resolvers) {
SimpleMappingExceptionResolver exceptionResolver = new SimpleMappingExceptionResolver();
Properties properties = new Properties();
properties.setProperty("java.lang.Exception", "error");
exceptionResolver.setExceptionMappings(properties);
exceptionResolver.setExceptionAttribute("ex");
resolvers.add(exceptionResolver);
}
}