Spring Web MVC 是⼀个 Web 框架,简称之为: Spring MVC,要想真正的理解什么是 Spring MVC,首先要搞清楚什么是 MVC
MVC 是 Model View Controller 的缩写,它是软件工程中的⼀种软件架构设计模式,它把软件系统分为模型、视图和控制器三个基本部分

MVC 是⼀种架构设计模式, 也⼀种思想, 而 Spring MVC 是对 MVC 思想的具体实现,Spring MVC 是⼀个实现了 MVC 模式的 Web 框架, Spring MVC 主要关注两个点:
其实, Spring MVC 我们在前面已经用过了, 在创建 Spring Boot 项目时,我们勾选的 Spring Web 框架其实就是 Spring MVC 框架:

Spring Boot 只是实现 Spring MVC 的其中⼀种方式,Spring Boot 可以添加很多依赖, 借助这些依赖实现不同的功能. Spring Boot 通过添加 Spring WebMVC 框架, 来实现web功能.
Spring在实现MVC时,也结合自身项目的特点,做了⼀些改变,相对而言,下⾯这个图或许更加合适⼀些。

学习 Spring MVC, 重点也就是学习如何通过浏览器和用户程序进行交互.
主要分以下三个方面:
Spring MVC 项目创建和 Spring Boot 创建项目相同,在创建的时候选择 Spring Web 就相当于创建了Spring MVC 的项⽬.
创建项目时, 勾选上 Spring Web 模块即可,如下图所示:

在 Spring MVC 中使用 @RequestMapping 来实现 URL 路由映射 ,也就是浏览器连接程序的作用
路由映射: 当用户访问⼀个 URL 时, 将用户的请求对应到程序中某个类的某个方法的过程就叫路由映射.
我们先来看看代码怎么写:
创建⼀个 UserController 类,实现用户通过浏览器和程序的交互,具体实现代码如下:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@RestController
public class UserController {
// 路由器规则注册
@RequestMapping("/sayHi")
public String sayHi(){
return "hello,Spring MVC";
}
}
注意:方法名和路径名称无需⼀致:
接下来访问: http://127.0.0.1:8080/sayHi, 就可以看到程序返回的数据了

@RequestMapping 是 Spring Web MVC 应用程序中最常被用到的注解之⼀,它是用来注册接口的路由映射的,表示服务收到请求时, 路径为 /sayHi 的请求就会调用 sayHi 这个方法的代码.
既然 @RequestMapping 已经可以达到我们的目的了, 我们为什么还要加 @RestController 呢?
我们把 @RestController 去掉, 再来访问⼀次:

可以看到, 程序报了404, 找不到该页面
一个项目中, 会有很多类, 每个类可能有很多的方法, Spring 程序怎么知道要执行哪个方法呢?
Spring 会对所有的类进行扫描, 如果类加了注解 @RestController, Spring 才会去看这个类里面的方法有没有加 @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 (类路径 + 方法路径)

@RequestMapping 的 URL 路径最前⾯加不加 / (斜杠)都可以, Spring程序启动时, 会进行判断, 如果前面没有加 / , Spring会拼接上⼀个 /(通常情况下, 我们加上 / )
@RequestMapping("user")
@RestController
public class UserController {
@RequestMapping("sayHi")
public String sayHi(){
return "hello,Spring MVC";
}
}
依然可以正确响应.

@RequestMapping("/user/m1")
@RestController
public class UserController {
@RequestMapping("/say/hi")
public String sayHi(){
return "hello,Spring MVC";
}
}
访问路径: http://127.0.0.1:8080/user/m1/say/hi

@RequestMapping 既支持 Get 请求, 又支持 Post 请求. 也支持其他的请求方式,那我们该如何指定 GET 或者 POST 类型呢?
我们可以显示的指定 @RequestMapping 来接收 POST 的情况,如下所示:
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
@RestController
public class UserController {
@RequestMapping(value = "/getRequest",method= RequestMethod.POST)
public String sayHi(){
return "get request...";
}
}
具体来说:
value = "/getRequest":这指定了请求的URL路径为/getRequest。也就是说,当有请求发送到/getRequest路径时,该方法将被调用来处理这个请求。method = RequestMethod.POST:这指定了请求的 HTTP 方法为 POST。也就是说,这个方法只会处理发送到/getRequest路径的 POST 请求。从上⾯的案例中, 也发现了⼀个新的问题, 就是我们测试后端方法时, 还需要去写前端代码. 这对我们来说, 是⼀件麻烦又痛苦的事情.
随着互联网的发展, 也随着项目难度的增加,后端开发工程师, 不要求也不需要掌握前端技能了.那后端开发工程师, 如何测试自己的程序呢? – 使用专业的接口测试工具 Postman



查询字符串就是请求的参数

表单提交的数据, 在 form 标签中加上 enctyped=“multipart/form-data” , 通常用于提交图片 / 文件。

3. x-www-form-urlencoded
form表单, 对应 Content-Type: application/x-www-from-urlencoded

4. raw
可以上传任意格式的文本,可以上传 text、json、xml、html 等

访问不同的路径, 就是发送不同的请求. 在发送请求时, 可能会带⼀些参数, 所以学习Spring的请求, 主要是学习如何传递参数到后端以及后端如何接收.
接收单个参数, 在 Spring MVC 中直接用方法中的参数就可以,比如以下代码:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/param")
public class ParamController {
@RequestMapping("/m1")
public String method1(String name){
return "接收到参数name:"+ name;
}
}
咱们使⽤浏览器发送请求并传参:http://127.0.0.1:8080/param/m1?name=spring

可以看到, 后端程序正确拿到了name参数的值,Spring MVC 会根据方法的参数名, 找到对应的参数, 赋值给方法

但如果参数不⼀致, 是获取不到参数的,比如请求 URL: http://127.0.0.1:8080/param/m1?name1=spring
响应结果:

注意事项:使用基本类型来接收参数时, 参数必须传(除boolean类型), 否则会报500错误,类型不匹配时, 会报400错误.
代码例子:
@RequestMapping("/m1/int")
public Object method1GetInt(int age){
return "接收到参数age:" + age;
}

通过 Fiddler 观察请求和响应, HTTP 响应状态码为 200, Content-Type 为 text/html


通过 Fiddler 观察请求和响应, HTTP 响应状态码为 500

尝试观察程序的错误日志并解决


通过 Fiddler 观察请求和响应, HTTP 响应状态码为 400

对于包装类型, 如果不传对应参数,Spring 接收到的数据则为null,所以企业开发中,对于参数可能为空的数据,建议使用包装类型
和接收单个参数⼀样, 直接使用方法的参数接收即可. 使用多个形参.
代码示例:
@RequestMapping("/m2")
public Object method2(String name, String password) {
return "接收到参数name:" + name + ", password:" + password;
}
使⽤浏览器发送请求并传参: http://127.0.0.1:8080/param/m2?name=zhangsan&password=123456

可以看到, 后端程序正确拿到了 name 和 password 参数的值

当有多个参数时,前后端进行参数匹配时,是以参数的名称进行匹配的,因此参数的位置是不影响后端获取参数的结果.
访问: http://127.0.0.1:8080/param/m2?password=123456&name=zhangsan 同样可以拿到正确的结果

如果参数比较多时, 方法声明就需要有很多形参. 并且后续每次新增⼀个参数, 也需要修改方法声明,我们不妨把这些参数封装为⼀个对象.
Spring MVC 也可以自动实现对象参数的赋值,比如 Person 对象:
**public class Person {
private int id;
private String name;
private String password;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public String toString() {
return "Person{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}**
传递对象代码实现:
@RequestMapping("/m3")
public Object method3(Person p){
return p.toString();
}
使⽤浏览器发送请求并传参: http://127.0.0.1:8080/param/m3?id=5&name=zhangsan&password=123456

可以看到, 后端程序正确拿到了Person对象⾥各个属性的值

Spring 会根据参数名称自动绑定到对象的各个属性上, 如果某个属性未传递, 则赋值为null,基本类型则赋值为默认初识值
某些特殊的情况下,前端传递的参数 key 和我们后端接收的 key 可以不⼀致,比如前端传递了⼀个 time 给后端,而后端是使⽤ createtime 字段来接收的,这样就会出现参数接收不到的情况,我们就可以使用 @RequestParam 来重命名前后端的参数值,以此避免这种情况
@RequestMapping("/m4")
public Object method_4(@RequestParam("time") String createtime) {
return "接收到参数createtime:" + createtime;
}
@RequestParam(“time”) String createtime 表示 createtime 参数会接收来自 HTTP 请求中名为 time 的参数的值,相当于将 createtime 重命名为了 time
使用浏览器发送请求并传参: http://127.0.0.1:8080/param/m4?time=2023-09-12

可以看到, Spring 可以正确的把浏览器传递的参数 time 绑定到了后端参数 caretetime 参数上
此时, 如果浏览器使用 createtime 进行参数传递呢?
访问URL: http://127.0.0.1:8080/param/m4?createtime=2023-09-12

错误日志信息为:

可以得出结论:
非必传参数设置:
如果我们的实际业务前端的参数是⼀非必传的参数, 针对上述问题, 如何解决呢?
先来了解下参数必传的原因, @RequestParam 注解实现如下:
@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"
}
可以看到 required 的默认值为true, 表示含义就是: 该注解修饰的参数默认为必传,既然如此, 我们可以通过设置 @RequestParam 中的 required=false 来避免不传递时报错,
具体实现如下:
@RequestMapping("/m4")
public Object method4(@RequestParam(value = "time", required = false) Stiring createtime) {
return "接收到参数createtime:" + createtime;
}
Spring MVC 可以自动绑定数组参数的赋值
@RequestMapping("/m5")
public String method5(String[] arrayParam) {
return Arrays.toString(arrayParam);
}
使用浏览器发送请求并传参:
数组参数:请求参数名与形参数组名称相同且请求参数为多个, 后端定义数组类型形参即可接收参数
这三种方式都可以
浏览器响应结果:

集合参数:和数组类似, 同⼀个请求参数名有为多个, 且需要使⽤ @RequestParam 绑定参数关系
代码如下:
**@RequestMapping("/m6")
public String method6(@RequestParam List<String> listParam){
return "size:"+listParam.size() + ",listParam:"+listParam;
}

JSON就有自己的格式和语法, 使用文本表示⼀个对象或数组的信息, JSON 是⼀个字符串,其格式非常类似于 JavaScript 对象字面量的格式
JSON的语法:
我们来看⼀段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 strength"]
},
{
"name": "Eternal Flame",
"age": 1000000,
"secretIdentity": "Unknown",
"powers": ["Immortality", "Heat Immunity", "Inferno", "Teleportation"]
}
]
}
也可以压缩表⽰:
{"squadName":"Super hero squad","homeTown":"Metro City","formed":2016,"secretBaseSuper tower","active":true,"members":[{"name":"M Man","age":29,"secretIdentity":"Dan Jukes","powers":["Radiation resistance","Turning tiny","Radiation blast"]},{"name":"Madameppercut","age":39,"secretIdentity":"Jane Wilson","powers":["Million ton punch","Damage resistance","Superhuman reflexes"]},{"name":"Eternal Flame","age":1000000,"secretIdentity":"Unknown","powers":["Immortality","Heat Immunity","Inferno","Teleportation","Interdimensional travel"]}]}
JSON的两种结构

以下都是合法的JSON数据:
{"name":"admin","age":18}
["hello", 3.1415, "json"]
[{"name":"admin","age":18},{"name":"root","age":16},{"name":"张三","age":20}]
JSON优点:
基于以上特点, JSON 在 Web 应用程序中被广泛使用
我们接收JSON对象, 需要使用 @RequestBody 注解(请求正文),这个注解作用在请求正文的数据绑定,请求参数必须在写在请求正⽂中
@RequestMapping(value = "/m7")
public Object method7(@RequestBody Person person) {
return person.toString();
}
RequestBody 注解应用在方法的参数 Person person 上,表示这个方法期望接收一个HTTP请求的主体,并将其转换为一个名为 person 的 Person 对象。
使用 Postman 来发送 json 请求参数:

可以看到, 后端正确接收了,通过 Fiddler 观察⼀下请求参数:

path variable: 路径变量,和字面表达的意思⼀样, 这个注解主要作用在请求 URL 路径上的数据绑定,默认传递参数写在 URL 上,SpringMVC 就可以获取到
@RequestMapping("/m8/{id}/{name}")
public String method8(@PathVariable Integer id, @PathVariable("name") String userName )
return "解析参数id:"+id+",name:"+userName;
}
使用浏览器发送请求:http://127.0.0.1:8080/param/m8/5/zhangsan
或者使⽤ Postman 发送请求:

可以看到, 后端正确获取到了URL中的参数,参数对应关系如下:

注意:
@RequestMapping("/m9")
public String getfile(@RequestPart("file") MultipartFile file) throws IOException
//获取⽂件名称
String fileName = file.getOriginalFilename();
//⽂件上传到指定路径
file.transferTo(new File("D:/temp/" + file.getOriginalFilename()));
return "接收到⽂件名称为: "+fileName;
}
MultipartFile是Spring框架中用于表示上传的类,它包含了文件的内容、名称、大小等信息。在这例子中,使用MultipartFile类型的参数file来接收上传的文件。
在这个例子中,文件将被保存到"D:/temp/"目录下,并使用原始文件名作为文件名。
使用 Postman 发送请求:

观察 D:/temp 路径下,文件是否上传成功:

HTTP 协议自身是属于 “无状态” 协议,即默认情况下 HTTP 协议的客户端和服务器之间的这次通信, 和下次通信之间没有直接的联系,但是实际开发中, 我们很多时候是需要知道请求之间的关联关系的.

上述图中的 “令牌” 通常就存储在 Cookie 字段中,此时在服务器这边就需要记录"令牌"信息, 以及令牌对应的用户信息, 这个就是 Session 机制所做的工作.
Session, 会话,也就是对话的意思

在计算机领域, 会话是⼀个客户与服务器之间的不中断的请求响应. 对客户的每个请求,服务器能够识别出请求来自于同⼀个客户.
当⼀个未知的客户⼾向 Web 应用程序发送第⼀个请求时就开始了⼀个会话.当客户明确结束会话或服务器在⼀个时限内没有接受到客户的任何请求时,会话就结束了.
服务器同⼀时刻收到的请求是很多的. 服务器需要清楚的区分每个请求是从属于哪个用户, 也就是属于哪个会话, 所以就需要在服务器这边记录每个会话以及与用户的信息的对应关系,Session 是服务器为了保存用户信息而创建的⼀个特殊的对象.

Session的本质就是⼀个 “哈希表”, 存储了⼀些键值对结构. Key 就是SessionID, Value 就是用户信息



注意:Session 默认是保存在内存中的. 如果重启服务器则 Session 数据就会丢失.
传统获取Cookie:
@RequestMapping("/m10")
public String method10(HttpServletRequest request,HttpServletResponse response){
// 获取所有 cookie 信息
Cookie[] cookies = request.getCookies();
//打印Cookie信息
StringBuilder builder = new StringBuilder();
if (cookies!=null){
for (Cookie ck:cookies) {
builder.append(ck.getName()+":"+ck.getValue());
}
}
return "Cookie信息:"+builder;
}



从这个例子中, 也可以看出 Cookie 是可以伪造的, 也就是不安全的, 所以使用 Cookie 时, 后端需要进行 Cookie 校验
简洁获取Cookie
@RequestMapping("/getCookie")
public String cookie(@CookieValue("bite") String bite) {
return "bite:" + bite;
}
@CookieValue(“bite”)表示从 HTTP 请求的 Cookie 中获取名为 “bite” 的 Cookie 的值,并将其转换为字符串类型(因为方法参数类型是String)传递给方法。
运行结果: http://127.0.0.1:8080/param/getCookie

Session 也是基于HttpServletRequest 来存储和获取的,Session 是服务器端的机制, 我们需要先存储, 才能再获取
@RequestMapping("/setSess")
public String setsess(HttpServletRequest request) {
// 获取Session对象
HttpSession session = request.getSession();
if (session != null) {
session.setAttribute("username", "java");
}
return "session 存储成功";
}
这个代码中看不到 SessionId 这样的概念. getSession 操作内部提取到请求中的 Cookie ⾥的 SessionId , 然后根据 SessionId 获取到对应的 Session 对象, Session 对象用HttpSession来描述

获取 Session 有两种方式:
HttpSession getSession(boolean create);
HttpSession getSession();
读取 Session 可以使用 HttpServletRequest
@RequestMapping("/getSess")
public String sess(HttpServletRequest request) {
// 如果 session 不存在, 不会⾃动创建
HttpSession session = request.getSession(false);
String username = null;
if (session != null && session.getAttribute("username") != null) {
username = (String) session.getAttribute("username");
}
return "username:" + username;
}
运行: http://127.0.0.1:8080/param/setSess

通过 Fiddler 观察 Http 请求和响应情况:

可以看到, Http响应中, 通过 Set-Cookie 告知客户端, 把 SessionID 存储在 Cookie 中,通过浏览器, 可以观察到运行结果:

获取Session: http://127.0.0.1:8080/param/getSess

通过 Fiddler 观察 Http 请求和响应

可以看到, Http请求时, 把SessionId通过Cookie传递到了服务器.
@RequestMapping("/getSess2")
public String sess2(@SessionAttribute(value = "username",required = false) String
return "username:"+username;
}
@SessionAttribute(value = “username”, required = false)表示从当前会话中获取名为 “username” 的属性,并将其作为参数传递给方法。
value = “username”:这是指定的Session属性的名称,即要从当前Session中获取的属性名。
required = false:这是一个标志,指示属性是否是必需的。如果为false,即使Session中没有该属性,Spring框架也不会抛出错误。如果为true,那么Session中没有该属性时会抛出异常。
运⾏结果: http://127.0.0.1:8080/param/getSess2

通过 Spring MVC 内置对象 HttpSession 来获取
@RequestMapping("/getSess3")
public String sess3(HttpSession session){
String username=(String)session.getAttribute("username");
return"username:"+username;
}
HttpSession session = request.getSession() 中 Session 不存在的话, 会自动进行创建
运行结果: http://127.0.0.1:8080/param/getSess3

@RequestMapping("/param10")
public String param10(HttpServletRequest request, HttpServletResponse response)
String userAgent = request.getHeader("User-Agent");
return name + ":"+userAgent;
}
运⾏结果: http://127.0.0.1:8080/param/getHeader

通过 Fiddler 观察, 获取的 User-Agent 是否正确

@RequestMapping("/header")
public String header(@RequestHeader("User-Agent") String userAgent) {
return "userAgent:"+userAgent;
}
@RequestHeader(“User-Agent”) 表示从 HTTP 请求的请求头中获取名为 User-Agent 的值,并将其转换为字符串类型(因为方法参数类型是String)传递给方法。
运行结果: http://127.0.0.1:8080/param/getHeader2
