1. 什么是 Spring Web MVC
Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从从⼀开始就包含在Spring框架中。它的 正式名称“SpringWebMVC”来⾃其源模块的名称(Spring-webmvc),但它通常被称为"Spring MVC".
什么是Servlet呢?
Servlet 是⼀种实现动态⻚⾯的技术.准确来讲Servlet是⼀套JavaWeb开发的规范,或者说是⼀套 Java Web开发的技术标准.只有规范并不能做任何事情,必须要有⼈去实现它.所谓实现Servlet规 范,就是真正编写代码去实现Servlet规范提到的各种功能,包括类、⽅法、属性等. Servlet 规范是开放的,除了Sun公司,其它公司也可以实现Servlet规范,⽬前常⻅的实现了 Servlet 规范的产品包括Tomcat、Weblogic、Jetty、Jboss、WebSphere等,它们都被称 为"Servlet 容器".Servlet 容器⽤来管理程序员编写的Servlet类
从上述定义我们可以得出一个信息:Spring Web MVC 是一个 Web 框架
以下简称为 Spring MVC
2. MVC 定义
MVC是ModelViewController的缩写,它是软件⼯程中的⼀种软件架构设计模式,它把软件系统分 为模型、视图和控制器三个基本部分
View(视图):指在应用程序中专门用来与浏览器进行交互,展示数据的资源
Model(模型):是应用程序的主体部分,用来处理程序中数据逻辑的部分
Controller(控制器):可以理解为一个分发器,用来决定对于视图发来的请求,需要用哪一个模型来处理,以及处理完后需要跳回到哪一个视图,即用来连接视图和模型
⽐如去饭店吃饭
客⼾进店之后,服务员来接待客⼾点餐,客⼾点完餐之后,把客⼾菜单交给前厅,前厅根据客⼾菜单 给后厨下达命令.后厨负责做饭,做完之后,再根据菜单告诉服务员,这是X号餐桌客⼈的饭. 在这个过程中
服务员就是View(视图),负责接待客⼾,帮助客⼾点餐,以及给顾客端饭
前厅就是Controller(控制器),根据⽤⼾的点餐情况,来选择给哪个后厨下达命令
后厨就是Model(模型),根据前厅的要求来完成客⼾的⽤餐需求
3. Spring MVC
MVC是⼀种架构设计模式,也是⼀种思想, ⽽SpringMVC是对MVC思想的具体实现.除此之外, Spring MVC还是⼀个Web框架.
总结来说,SpringMVC是⼀个实现了MVC模式的Web框架.
所以,SpringMVC主要关注有两个点:
1. MVC
2. Web 框架
前面创建 Spring Boot 项目时,勾选的 Spring Web 框架就是 Spring MVC 框架
Spring Boot 是实现 Spring MVC 的一种方式,Spring Boot 可以添加很多依赖,借助这些依赖实现不同功能,其通过添加 Spring Web MVC 框架来实现 Web 功能
而 Spring 在实现 MVC 时,也结合自身项目特点,做了一些改变,相对而言,用下面这个图来描述 Soring MVC 更加合适一些:
4. 使用 Spring MVC
使用 Spring MVC 就是通过浏览器和用户程序进行交互
主要分为以下三个方面:
1. 建立连接:将用户(浏览器)和 Java 程序连接起来,也就是访问一个地址能够调用到我们的 Spring 程序
2. 请求:用户请求的时候会带一些参数,在程序中要想办法获取参数,所以请求主要是获取参数的功能
3. 响应:执行了业务逻辑之后,要把程序执行的结果返回给用户,也就是响应
4.1 项目准备
Spring MVC 项目创建和 Spring Boot 创建项目相同,在创建的时候选择 Spring Web 就相当于创建了 Spring MVC 的项目
4.2 建立连接
在 Spring MVC 中使用 @RequestMapping 来实现 URL 路由映射,也就是浏览器连接程序的作用
创建一个 UserController 类,实现用户通过浏览器和程序的交互,代码如下:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@RequestMapping("/sayHi")
public String sayHi() {
return "Hello Spring MVC";
}
}
tip:方法名和路径名可以不同
接下来访问:http://127.0.0.1:8080/sayHi 就可以看到程序返回的数据了
4.2.1 @RequestMapping 注解介绍
@RequestMapping 是 Spring Web MVC 应用程序中最常被用到的注解之一,它是用来注册接口的路由映射的
表示服务收到请求时,路径为 /sayHi 的请求就会调用 sayHi 这个方法的代码
路由映射:当用户访问一个 URL 时,将用户的请求对应到程序中某个类的某个方法的过程叫做路由映射
其与 @RequestController 需要同时存在,若注释掉 @RequestController
一个项目中会有很多类,每个类中又有很多方法,当我们要访问 /sayHi 时,Spring 会对所有类进行扫描,只有类加了注解 @RequestController,Spring 才会去看这个类里面有没有 @RequestMapping("/sayHi"),如果有,就执行该方法;若是类没有加注解 @RequestController,Spring就不会进入该类,即使该类中有 @RequestMapping("/sayHi")
4.2.2 @RequestMapping 使用
@RequestMapping 既可以修饰类,也可以修饰方法,当修饰类和方法时,访问的地址是类路径 + 方法路径
@RequestMapping 标识一个类:设置映射请求的请求路径的初始信息
@RequestMapping 标识一个方法:设置映射请求路径的具体信息
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/sayHi")
public String sayHi() {
return "Hello Spring MVC";
}
}
访问地址:http://127.0.0.1:8080/user/sayHi
tip:
1. @RequestMapping 的 URL 路径最前面加不加 / 都可以,Spring 程序启动时会进行判断,如果前面没有加 /,Spring 会拼接上一个 /,通常情况下,建议加上 /
2. 注解没有先后顺序之分
4.2.3 @RequestMapping 是 GET 还是 POST 请求
GET 请求:
浏览器发送的请求类型都是 get,通过上面案例可知 @RequestMapping 支持 get 请求
通过 Fiddler 也可以观察到:
POST 请求:
通过 form 表单来构造请求:创建 test.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/sayHi" method="post">
<input type="submit" value="提交">
</form>
</body>
</html>
从运行结果可以看出:@RequestMapping 既支持 get 请求,又支持 post 请求,同理,也支持其他的请求方式
4.2.4 指定 GET/POST 方法类型
方法一:我们可以显式的指定 @RequestMapping 来接收 POST 的情况,如下:
方法二:使用 @GetMapping 或 @PostMapping 注解来设置
@PostMapping 使用方法同理
5. 使用 Postman 创建请求
界面介绍
6. 请求
6.1 传递单个参数
接收单个参数,在 Spring MVC 中直接用方法中的参数就可以,如下代码:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/param")
@RestController
public class ParamController {
@RequestMapping("/p1")
public String p1(String name) {
return "接收到参数,name:" + name;
}
}
使用 Postman 构建发送请求:
Spring MVC 根据方法的参数名,找到对应的参数,赋值给方法
如果参数不一致,是获取不到参数的,如下:
tip:
使用基本类型来接收参数时,参数必须传(除非是 boolean 类型),否则会报 500 错误
类型不匹配时,会报 400 错误
使用包装类型,如果不传参数,Spring 接收到的数据则为 null,因此在企业开发中,对于参数可能为空的数据,建议使用包装类型
6.2 传递多个参数
和接收单个参数一样,直接使用方法的参数接收即可,如下:
tip:当有多个参数时,前后端进行参数匹配时,是以参数的名称进行匹配的,因此参数的位置是不影响后端获取参数的结果
6.3 传递对象
当参数比较多时,方法声明就需要有很多形参,并且后续每新增一个参数,也需要修改方法声明
因此我们不妨将这些参数封装成一个对象
Spring MVC 也可以自动实现对象参数的赋值,如下:
创建一个 User 对象:
package com.example.sprignmvc_demo_20241021;
public class User {
private String name;
private int age;
private Integer gender;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public Integer getGender() {
return gender;
}
public void setGender(Integer gender) {
this.gender = gender;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
", gender=" + gender +
'}';
}
}
传递对象代码:
Spring 会根据参数名称自动绑定到对象的各个属性上,如果某个属性未传递,则赋值为 null(基本类型则赋值为默认初始值,如 int 被赋值为 0)
6.4 后端参数重命名(后端参数映射)
某些特殊情况下,前端传递的参数 key 和我们后端接收的 key 可以不一致,如:前端传递了一个 userName,而后端是使用 name 字段来接收的,这样就会出现参数接收不到的情况,如果出现这种情况,我们就可以使用 @RequestParam 重命名前后端的参数值,如下:
若此时前端使用 name 来传参:
查看 @RequestParam 的源码:
可以看到,required 的默认值为 true,表示的含义是:该注解修饰的参数默认为必传参数
既然有默认,就可以更改:
再次运行:
不报错了,但是接收不到前端传的 name 参数,这是因为使用 @RequestParam 进行参数重命名时,请求参数只能和 @RequestParam 声明的名称一致,才能进行参数绑定和赋值
6.5 传递数组
Spring MVC 可以自动绑定数组参数的赋值
像第二种方式,会自动进行分割,如下:
6.6 传递集合
这种传参方式和传数组是一样的,HTTP 默认将其封装成了一个数组,相当于是传递了一个数组,而 List 无法用来接收该数组
此时就需要使用 @RequestParam 来进行参数绑定,将数绑定成 List
6.7 传递 JSON 数据
6.7.1 JSON 概念
JSON:JavaScriptObjectNotation 【JavaScript 对象表⽰法】
JSON是⼀种轻量级的数据交互格式.它基于ECMAScript(欧洲计算机协会制定的js规范)的⼀个⼦集, 采⽤完全独⽴于编程语⾔的⽂本格式来存储和表⽰数据。--百度百科
简单来说:JSON就是⼀种数据格式,有⾃⼰的格式和语法,使⽤⽂本表⽰⼀个对象或数组的信息,因此 JSON本质是字符串. 主要负责在不同的语⾔中数据传递和交换.
6.7.2 JSON 和 JavaScript 的关系
没有关系,只是语法相似
6.7.3 JSON 语法
JSON 是一个字符串,其格式非常类似于 JavaScript 对象字面量的格式
看一段 JSON 数据:
{
"squadName": "Super hero squad",
"homeTown": "Metro City",
"formed": 2016,
"secretBase": "Super tower",
"active": true,
"members": [{
"name": "Molecule Man",
"age": 29,
"secretIdentity": "Dan Jukes",
"powers": ["Radiation resistance", "Turning tiny", "Radiation blast"]
},
{
"name": "Madame Uppercut",
"age": 39,
"secretIdentity": "Jane Wilson",
"powers": ["Million tonne punch", "Damage resistance", "Superhuman reflexes"]
},
{
"name": "Eternal Flame",
"age": 1000000,
"secretIdentity": "Unknown",
"powers": ["Immortality", "Heat Immunity", "Inferno", "Teleportation", "Interdimensional travel"]
}]
}
也可以压缩表示:(和上面数据一样,只不过上面数据进行了格式化,更易读)
{"squadName":"Super hero squad","homeTown":"Metro City","formed":2016,"secretBase":"Super tower","active":true,"members":[{"name":"Molecule Man","age":29,"secretIdentity":"Dan Jukes","powers":["Radiation resistance","Turning tiny","Radiation blast"]},{"name":"Madame Uppercut","age":39,"secretIdentity":"Jane Wilson","powers":["Million tonne punch","Damage resistance","Superhuman reflexes"]},{"name":"Eternal Flame","age":1000000,"secretIdentity":"Unknown","powers":["Immortality","Heat Immunity","Inferno","Teleportation","Interdimensional travel"]}]}
JSON 的语法:
数据在 键值对(Key / Value) 中
数据由 逗号 分隔
对象用 { } 表示
数组用 [ ] 表示
值可以为对象,也可以为数组,数组中可以包含多个对象
JSON 的两种结构:
对象:大括号 { } 保存的对象是一个无序的 键值对 集合,一个对象以 左括号 { 开始,右括号 } 结束,每个 键 后跟一个冒号 : ,键值对使用 逗号 分隔
数组:中括号 [ ] 保存的数组是 值(Value) 的有序集合,一个数组以 左中括号 [ 开始,右中括号 ] 结束,值之间使用 逗号 分隔
6.7.4 JSON 字符串和 Java 对象互转
JSON 本质上是一个字符串,通过文本来存储和描述数据
Spring MVC 框架也集成了 JSON 的转换工具,我们可以直接使用,来完成 JSON 字符串和 Java 对象的互转
本质上是 jackson-databind 提供的转换功能,Spring MVC 框架中已经把该工具包引入了进来,我们可以直接使用,若要脱离 Spring MVC 使用,需要引入相关依赖,如下:
<dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.13.5</version> </dependency>
JSON 的转换工具包有很多,jackson-databind 只是其中的一种
后端实现:
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
public class JsonTest {
public static void main(String[] args) throws JsonProcessingException {
ObjectMapper objectMapper = new ObjectMapper(); // 该类来自于 jackson-databind,用于处理 JSON
User user = new User();
user.setName("lisi");
user.setAge(18);
user.setGender(1); // 表示 男
// 对象转 JSON
String s = objectMapper.writeValueAsString(user);
System.out.println(s);
// JSON 转对象
User user1 = objectMapper.readValue(s, User.class);
System.out.println(user1);
}
}
使用 ObjectMapper 对象提供的两个方法,可以完成对象和 JSON 字符串的互转
writeValueAsString:把对象转为 JSON 字符串
readValue:把字符串转为对象
6.7.5 JSON 优点
简单易⽤:语法简单,易于理解和编写,可以快速地进⾏数据交换
跨平台⽀持: JSON可以被多种编程语⾔解析和⽣成,可以在不同的平台和语⾔之间进⾏数据交换和 传输
轻量级:相较于XML格式,JSON数据格式更加轻量级,传输数据时占⽤带宽较⼩,可以提⾼数据传输 速度
易于扩展: JSON的数据结构灵活,⽀持嵌套对象和数组等复杂的数据结构,便于扩展和使⽤
安全性:JSON数据格式是⼀种纯⽂本格式,不包含可执⾏代码,不会执⾏恶意代码,因此具有较⾼ 的安全性
6.8 传递 JSON 对象
接收 JSON 对象,需要使用 @RequestBody 注解
RequestBody:请求正文,这个注解作用在请求正文的数据绑定,请求参数必须写在请求正文中
后端实现:
@RequestMapping("/p8")
public String p8(@RequestBody User user) {
return "user:" + user;
}
tip:区分传递对象时使用 get 和 post 方式
区分传递对象与传递 JSON 对象
6.9 获取 URL 中参数(@PathVariable)
path variable:路径变量
这个注解主要作用在请求 URL 路径上的数据绑定
默认传递餐宿写在 URL 上,Spring MVC 就可以获取到
多个参数的情况:
tip:如果方法参数名称和需要绑定的 URL 中的变量名称一致时,可以简写,不用给 @PathVariable 的属性赋值,如上述例子中的 name 变量;反之则需要
6.10 传递文件(@RequestPart)
重命名:
6.11 获取 Cookie/Session
6.11.1 Cookie
HTTP 协议自身是属于 “无状态” 协议
无状态是指:默认情况下 HTTP 协议的客户端和服务器之间的这次通信和下次通信之间没有直接的联系
但是实际开发中,我们很多时候是需要知道请求之间的关联关系的
例如:登录网站成功后,第二次访问的时候,服务器就能知道该请求是否已经登录过了
上述途中的 “令牌” 通常就存储在 Cookie 字段中
⽐如去医院挂号
- 看病之前先挂号.挂号时候需要提供⾝份证号,同时得到了⼀张"就诊卡",这个就诊卡就相当于患 者的"令牌".
- 后续去各个科室进⾏检查,诊断,开药等操作,都不必再出⽰⾝份证了,只要凭就诊卡即可识别出当 前患者的⾝份.
- 看完病了之后,不想要就诊卡了,就可以注销这个卡.此时患者的⾝份和就诊卡的关联就销毁了.(类 似于⽹站的注销操作)
- ⼜来看病,可以办⼀张新的就诊卡,此时就得到了⼀个新的"令牌"
此时在服务器这边就需要记录 “令牌” 信息,以及令牌对应的用户信息,这个就是 Cookie机制所作的工作
6.11.2 Session
会话:就是对话的意思
在计算机领域,会话是一个客户与服务器之间的不中断的请求响应,对客户的每个请求,服务器能识别出请求来自于同一个客户
当一个位置的客户像 Web 应用程序发送第一个请求时,就开始了一个会话
当客户明确结束会话或服务器在一个时限内没有接收到客户的任何请求时,会话就结束了
⽐如我们打客服电话
每次打客服电话,是⼀个会话.挂断电话,会话就结束了
下次再打客服电话,⼜是⼀个新的会话.
如果我们⻓时间不说话,没有新的请求,会话也会结束.
服务器同一时刻收到的请求是很多的,服务器需要清楚的区分每个请求是属于哪个用户,也就是属于哪个会话,就需要在服务器这边记录每个会话以及与用户的信息的对应关系
Session 是服务器为了保存用户信息而创建的一个特殊的对象
Session 的本质就是一个 “哈希表”,存储了一些键值对结构,Key 就是 SessionID,Value 就是用户信息(用户信息可以根据需求灵活设计)
SessionID 是由服务器生成的一个 “唯一性字符串”,从 Session 机制的角度来看,这个唯一性字符串称为 “SessionID”,但是站在整个登录流程中看待,也可以把这个唯一性字符串称为 “token”
上述例子中的 令牌ID 就可以看作是 SessionID,只不过令牌除了 ID 之外,还会带有一些其他信息,比如时间、签名等
- 当用户登录的时候,服务器在 Session 中新增一个新记录,并把 SessionID 返回给客户端(通过 HTTP 响应中的 Set-Cookie 字段返回)
- 客户端后续再给服务器发送请求的时候,需要在请求中带上 SessionID(通过 HTTP 请求中的 Cookie 字段带上)
- 服务器收到请求之后,根据请求中的 SessionID 在 Session 信息中获取到对应的用户信息,再进行后续操作,找不到则重新创建 Session,并把 SessionID 返回
tip:Session 默认是保存在内存中的,如果重启服务器则 Session 数据就会丢失
6.11.3 Cookie 和 Session 的区别
- Cookie 是客户端保存用户信息的一种机制;Session 是服务器端保存用户信息的一种机制
- Cookie 和 Session 之间主要是通过 SessionID 关联起来的,SessionID 是 Cookie 和 Session 之间的桥梁
- Cookie 和 Session 经常会在一起配合使用,但不是必须配合
完全可以用 Cookie 来保存一些数据在客户端,这些数据不一定是用户身份信息,也不一定是 SessionID
Session 中的 SessionID 也不是非得通过 Cookie / Set-Ckkoie 传递,比如通过 URL 传递
6.11.4 获取 Cookie
1) 传统方法获取:
// 获取 Cookie
@RequestMapping("/getCookie")
public String getCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
// cookie 为空判断
// if (cookies == null) return "Cookie 为 null";
for (Cookie cookie : cookies) {
System.out.println(cookie.getName() + ":" + cookie.getValue());
}
return "Cookie 获取成功";
}
- Spring MVC是基于ServletAPI构建的原始Web框架,也是在Servlet的基础上实现的
- HttpServletRequest , HttpServletResponse 是Servlet提供的两个类,是Spring MVC⽅法的内置对象.需要时直接在⽅法中添加声明即可.
- HttpServletRequest 对象代表客⼾端的请求,当客⼾端通过HTTP协议访问服务器时,HTTP请 求头中的所有信息都封装在这个对象中,通过这个对象提供的⽅法,可以获得客⼾端请求的所有信 息.
- HttpServletResponse 对象代表服务器的响应.HTTP响应的信息都在这个对象中,⽐如向客⼾ 端发送的数据,响应头,状态码等.通过这个对象提供的⽅法,可以获得服务器响应的所有内容
- Spring MVC在这两个对象的基础上进⾏了封装,给我们提供更加简单的使⽤⽅法.
此时没有设置 Cookie,通过浏览器访问 http://127.0.0.1:8080/header/getCookie,得到 Cookie 为 null
在浏览其中设置 Cookie 的值(手动添加 Cookie)
再次访问:
2) 简洁方法获取:
@RequestMapping("/getCookie2")
public String getCookie2(@CookieValue("zhangsan") String value) {
return "从 Cookie 中获取信息:" + value;
}
运行结果:
两种方式对比:
第一种方法可以获取多次参数
第二种方法每获取一个参数都需要加一个注解
6.11.5 获取 Session
1) Session 存储和获取
Session 是服务器端的机制,我们需要先存储,才能获取
Session 也是基于 HttpServletRequest 来存储和获取的
2) Session 存储
@RequestMapping("/setSession")
public String setSession(HttpServletRequest request) {
// 先获取到 session 对象,若没有,则创建一个空 session
HttpSession session = request.getSession();
session.setAttribute("userName", "zhangsan");
session.setAttribute("age", 18);
return "设置 session 成功";
}
第一次运行上述程序,是没有 session 对象的,可以在 Fiddler 中观察到 Set-Cookie 生成 SessionID:
3) Session 读取(使用 HttpServletRequest)
@RequestMapping("/getSession")
public String getSession(HttpServletRequest request) {
HttpSession session = request.getSession();
// session 是类似 map 的结构
// 判空
if (session.getAttribute("userName") == null) return "session 为 null";
String userName = (String)session.getAttribute("userName");
System.out.println(session.getAttribute("age"));
return "从 session 中获取信息,userName:" + userName;
}
request.getSession() 共做了两件事:先从 Cookie 中拿到 SessionID,然后从 SessionID 中拿到 Session
设置完 session 后,再获取 session:
可以看到,Http 请求时,将 SessionID 通过 Cookie 传递到了服务器
4) 简洁获取①
@RequestMapping("/getSession2")
public String getSession2(HttpSession session) {
String userName = (String)session.getAttribute("userName");
System.out.println(session.getAttribute("age"));
return "从 session 中获取信息,userName:" + userName;
}
tip:Session 存储在服务器的内村上,服务重启时,Session 会丢失
先设置 session,再获取
5) 简洁获取②
@RequestMapping("/getSession3")
public String getSession3(@SessionAttribute String userName) {
return "从 session 中获取信息,userName:" + userName;
}
6.11.6 获取 Header
1) 传统方式:
获取 Header 也是从 HttpServletRequest 中获取
@RequestMapping("/getHeader")
public String getHeader(HttpServletRequest request) {
String userAgent = request.getHeader("User-Agent");
return "User-Agent:" + userAgent;
}
2) 简洁方式:
@RequestMapping("/getHeader2")
public String getHeader2(@RequestHeader("User-Agent") String userAgent) {
return "User-Agent:" + userAgent;
}