本节⽬标
- 学习常⻅的Spring Web MVC注解
- 掌握使⽤SpringMVC来完成基础的功能开发
- 了解MVC和三层架构的设计模式
- 掌握企业开发中的⼀些命名规范
一、 什么是 Spring Web MVC?
Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,从⼀开始就包含在 Spring 框架中。它的正式名称"Spring Web MVC"来⾃其源模块的名称(Spring-webmvc),但它通常被称为"Spring MVC".
什么是Servlet呢?
Servlet 是⼀种实现动态⻚⾯的技术. 准确来讲Servlet是⼀套 Java Web 开发的规范 ,或者说是⼀套 Java Web 开发的技术标准. 只有规范并不能做任何事情,必须要有⼈去实现它. 所谓实现 Servlet 规范,就是真正编写代码去实现 Servlet 规范提到的各种功能,包括类、⽅法、属性等.
Servlet 规范是开放的,除了 Sun 公司,其它公司也可以实现 Servlet 规范,⽬前常⻅的实现了Servlet 规范的产品包括 Tomcat、Weblogic、Jetty、Jboss、WebSphere 等,它们都被称
为"Servlet 容器". Servlet 容器⽤来管理程序员编写的 Servlet 类.
从上述定义我们可以得出⼀个信息: Spring Web MVC 是⼀个 Web 框架. 下⾯简称之为: Spring MVC
然⽽要真正的理解什么是 Spring MVC?我们⾸先要搞清楚什么是 MVC?
1.MVC 定义
MVC 是 Model View Controller 的缩写,它是软件⼯程中的⼀种软件架构设计模式,它把软件系统分为模型、视图和控制器三个基本部分
- View(视图) 指在应⽤程序中专⻔⽤来与浏览器进⾏交互,展⽰数据的资源.
- Model(模型) 是应⽤程序的主体部分,⽤来处理程序中数据逻辑的部分.
- Controller(控制器)可以理解为⼀个分发器,⽤来决定对于视图发来的请求,需要⽤哪⼀个模型来处理,以及处理完后需要跳回到哪⼀个视图。即⽤来连接视图和模型
⽐如去饭店吃饭
客⼾进店之后, 服务员来接待客⼾点餐, 客⼾点完餐之后, 把客⼾菜单交给前厅, 前厅根据客⼾菜单给后厨下达命令. 后厨负责做饭, 做完之后, 再根据菜单告诉服务员, 这是X号餐桌客⼈的饭.
在这个过程中
- 服务员就是View(视图), 负责接待客⼾, 帮助客⼾点餐, 以及给顾客端饭
- 前厅就是Controller(控制器), 根据⽤⼾的点餐情况, 来选择给哪个后厨下达命令.
- 后厨就是Model(模型), 根据前厅的要求来完成客⼾的⽤餐需求
⽐如去公司⾯试
我们到了公司之后, HR会给我们安排会议室, 根据候选⼈去通知不同的部⻔来安排⾯试, ⾯试结束后, 由HR来告诉⾯试结果
在这个过程中
- HR就是View(视图), 负责接待候选⼈, 并告知候选⼈⾯试结果
- 不同的部⻔, 就是Controller(控制器), HR根据候选⼈来选择对应的部⻔来进⾏⾯试
- ⾯试官, 就是Model层, 来处理⾯试这个事情.
2.什么是Spring MVC ?
MVC 是⼀种架构设计模式, 也是⼀种思想, ⽽ Spring MVC 是对 MVC 思想的具体实现. 除此之外, Spring MVC还是⼀个Web框架.
总结来说,Spring MVC 是⼀个实现了 MVC 模式的 Web 框架.
所以, Spring MVC主要关注有两个点:
- MVC
- Web框架
Spring MVC 全称是 Spring Web MVC
其实, Spring MVC 我们在前⾯已经⽤过了, 在创建 Spring Boot 项⽬时,我们勾选的 Spring Web 框架其实就是 Spring MVC 框架:
可以看到,Spring Web的介绍是:
Build web, including RESTful, applications using Spring MVC. Uses Apache Tomcat as the default embedded container.
这时候可能大家就懵了, 前⾯创建的不是SpringBoot项⽬吗? 怎么⼜变成了Spring MVC项⽬? 他们之间到底有着什么样的关系?
SpringBoot是2014年发布的, Spring 是2004年发布的, 在2014年发布之前, 就不能⽤Spring实现MVC架构吗? 显然不是了.
Spring Boot 只是实现Spring MVC的其中⼀种⽅式⽽已.
Spring Boot 可以添加很多依赖, 借助这些依赖实现不同的功能.
Spring Boot 通过添加Spring Web MVC框架, 来实现web功能.
⽐如: 厨房可以⽤来做饭, 但真实实现做饭功能的是⽕以及各种做饭相关的⻝材和⼯具.
厨房就好⽐是SpringBoot, 厨房可以装柜⼦, 实现收纳功能, 装燃⽓灶等, 实现做饭功能.
做饭这个事, 就是MVC, 在⼏千年前, 有⽕有⻝材就可以实现做饭.
不过Spring在实现MVC时, 也结合⾃⾝项⽬的特点, 做了⼀些改变, 相对⽽⾔, 下⾯这个图或许更加合适⼀些.
不过核⼼没变
⽐如上⾯的例⼦中, 去饭店吃饭. ⼀些饭店是前厅来负责接待客⼾, 帮助客⼾点餐, 也就是Controller来负责接收⽤⼾的请求.
去公司⾯试, 直接由⾯试官来接待候选⼈, 省去了HR中间的交接过程.
二、学习Spring MVC
既然是 Web 框架, 那么当⽤⼾在浏览器中输⼊了 url 之后,我们的 Spring MVC 项⽬就可以感知到⽤⼾的请求, 并给予响应.
咱们学习Spring MVC, 重点也就是学习如何通过浏览器和⽤⼾程序进⾏交互.
主要分以下三个⽅⾯:
- 建⽴连接:将⽤⼾(浏览器)和 Java 程序连接起来,也就是访问⼀个地址能够调⽤到我们的 Spring 程序。
- 请求: ⽤⼾请求的时候会带⼀些参数,在程序中要想办法获取到参数, 所以请求这块主要是 获取参数的功能.
- 响应: 执⾏了业务逻辑之后,要把程序执⾏的结果返回给⽤⼾, 也就是响应.
⽐如⽤⼾去银⾏存款
- 建⽴连接: 去柜台
- 请求: 带着银⾏卡, ⾝份证去存款
- 响应: 银⾏返回⼀张存折.
对于 Spring MVC 来说,掌握了以上 3 个功能就相当于掌握了 Spring MVC.
1. 项⽬准备
Spring MVC 项⽬创建和 Spring Boot 创建项⽬相同,在创建的时候选择 Spring Web 就相当于创建了Spring MVC 的项⽬.
Spring MVC 使⽤ Spring Boot 的⽅式创建
创建项⽬时, 勾选上 Spring Web 模块即可,如下图所⽰:
2.建⽴连接
在 Spring MVC 中使⽤ @RequestMapping 来实现 URL 路由映射 ,也就是浏览器连接程序的作⽤我们先来看看代码怎么写
创建⼀个 UserController 类,实现⽤⼾通过浏览器和程序的交互,具体实现代码如下:
java
package com.springboot.demo;
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";
}
}
接下来访问: http://127.0.0.1:8080/sayHi, 就可以看到程序返回的数据了
(1)@RequestMapping 注解介绍
@RequestMapping 是 Spring Web MVC 应⽤程序中最常被⽤到的注解之⼀,它是⽤来注册接⼝的
路由映射的.
表⽰服务收到请求时, 路径为 /sayHi 的请求就会调⽤ sayHi 这个⽅法的代码.
路由映射: 当用户访问⼀个 URL 时, 将用户的请求对应到程序中某个类的某个⽅法的过程就叫路由映射.
既然 @RequestMapping 已经可以达到我们的⽬的了, 我们为什么还要加 @RestController呢?
我们把 @RestController 去掉, 再来访问⼀次:
可以看到, 程序报了404, 找不到该⻚⾯.
这就是 @RestController 起到的作⽤.
⼀个项⽬中, 会有很多类, 每个类可能有很多的⽅法, Spring程序怎么知道要执⾏哪个⽅法呢?
Spring会对所有的类进⾏扫描, 如果类加了注解@RestController, Spring才会去看这个类⾥⾯的⽅法有没有加 @RequestMapping 这个注解, 当然他的作⽤不⽌这⼀点, 咱们先⽤, 后⾯再详细讲
(2)@RequestMapping 使⽤
@RequestMapping 既可修饰类,也可以修饰⽅法 ,当修饰类和⽅法时,访问的地址是类路径 + ⽅法路径.
资源路径=类路径+方法路径
- @RequestMapping标识⼀个类:设置映射请求的请求路径的初始信息
- @RequestMapping标识⼀个⽅法:设置映射请求请求路径的具体信息
java
package com.springboot.demo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
@RequestMapping("/sayHi")
public String sayHi(){
return "hello,Spring";
}
}
访问地址: http://127.0.0.1:8080/user/sayHi
注意:
- @RequestMapping 的URL 路径最前⾯加不加 / (斜杠)都可以, Spring程序启动时, 会进⾏判断, 如果前⾯没有加 / , Spring会拼接上⼀个 /,但通常情况下, 我们加上 /
java
@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路径也可以是多层路径, 最终访问时, 依然是 类路径 + ⽅法路径
java
@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
建议都加上类路径,原因
- 可以方便路径定位,避免发生错误,即使方法名相同,只要类名不相同,路径也不相同
- 提高代码可读性
(3)@RequestMapping 是 GET 还是 POST 请求?
我们来测试⼀下就知道结论了
- GET请求:
- 浏览器发送的请求类型都是get, 通过以上案例, 可以看出来 @RequestMapping ⽀持get请求.
- POST 请求:
- 我们通过form表单来构造请求,创建test.html, html代码:
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>
前端代码放在static⽬录下, 访问⽅式为: http://127.0.0.1:8080/test.html
如果有多层⽬录, 访问链接从static⽬录开始写
如上图, 访问链接为: http://127.0.0.1:8080/html/test.html
从运⾏结果可以看出: @RequestMapping 既⽀持Get请求, ⼜⽀持Post请求. 同理, 也⽀持其他的请求⽅式
那如何指定GET或者POST类型呢?
指定GET/POST⽅法类型
方法1,指定方式
java
package com.springboot.demo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/user")
@RestController
public class UserController {
//既支持get,又支持post请求
@RequestMapping("/sayHi")
public String sayHi(){
return "hi,Spring";
}
//只支持get请求
@RequestMapping(value = "/sayHello",method = RequestMethod.GET)
public String sayHello(){
return "hello,Spring";
}
//只支持POST请求
@RequestMapping(value = "/sayBye",method = RequestMethod.POST)
public String sayBye(){
return "Bye,Spring";
}
}
方法2,直接修改注解

注:

3.Postman介绍
从上⾯的案例中, 也发现了⼀个新的问题, 就是我们测试后端⽅法时, 还需要去写前端代码. 这对我们来说, 是⼀件⿇烦⼜痛苦的事情.
随着互联⽹的发展, 也随着项⽬难度的增加, 企业也按照开发的功能, 把⼈员拆分成了不同的团队. 界⾯ 显⽰交给"前端开发⼯程师", 业务逻辑的实现交给 "后端开发⼯程师".
后端开发⼯程师, 不要求也不需要掌握前端技能了.
那后端开发⼯程师, 如何测试⾃⼰的程序呢? -- 使⽤专业的接⼝测试⼯具
咱们使⽤的是Postman, 接下来我们来学习Postman的使⽤.
(1)下载安装postman
下载链接: https://www.postman.com/downloads/
安装后会提⽰有版本升级, 点击 Dismiss 即可(⽆需升级)
(2)创建请求

(3) 传参介绍
- 普通传参, 也就是通过查询字符串来传参
学习HTTP环节时, 我们通过URL来访问互联⽹上的某⼀个资源
URL的格式如下:
其中, 查询字符串就是请求的参数
2.form-data(完整表⽰为: multipart/form-data)
表单提交的数据, 在 form 标签中加上 enctyped="multipart/form-data" , 通常⽤于提交图⽚/⽂件. 对应 Content-Type: multipart/form-data
3.x-www-form-urlencoded
form表单, 对应 Content-Type: application/x-www-from-urlencode
4. raw
可以上传任意格式的⽂本,可以上传text、json、xml、html等
4. 请求
访问不同的路径, 就是发送不同的请求. 在发送请求时, 可能会带⼀些参数, 所以学习Spring的请求, 主要是学习如何传递参数到后端以及后端如何接收.
传递参数, 咱们主要是使⽤浏览器和Postman来模拟.
后端开发⼈员⽆需过度关注如何传递参数, 了解即可, 实际开发中以Postman测试为主.
⽐如餐厅的厨师, 不关注⽤⼾是在店⾥下单, 还是外卖平台下单, 或者⼩程序下单, 只需要知道如何接收订单, 根据订单做出对应的菜肴就可以了.
(1)传递单个参数
接收单个参数, 在 Spring MVC 中直接⽤⽅法中的参数就可以,⽐如以下代码:
java
package com.springboot.demo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/request")
@RestController
public class RequestController {
@RequestMapping("/r1")
public String r1(String s){
return "接收参数为:"+s;
}
}
咱们使⽤浏览器发送请求并传参
http://127.0.0.1:8080/param/m1?name=spring
可以看到, 后端程序正确拿到了name参数的值.
Spring MVC 会根据⽅法的参数名, 找到对应的参数, 赋值给⽅法
如果参数不⼀致, 是获取不到参数的.
注意事项
使⽤基本类型来接收参数时, 参数必须传(除boolean类型), 否则会报500错误
类型不匹配时, 会报400错误.
java
@RequestMapping("/m1/int")
public Object method1GetInt(int age){
return "接收到参数age:" + age;
}
a. 正常传递参数
http://127.0.0.1:8080/param/m1/int?age=1
通过Fiddler观察请求和响应, HTTP响应状态码为200, Content-Type 为 text/htm
b. 不传递age参数
http://127.0.0.1:8080/param/m1/int
通过Fiddler观察请求和响应, HTTP响应状态码为500

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

⼀般看⽇志堆栈信息的⾸⾏, 报错信息显⽰:
"int类型的参数'age',虽然为可选的, 但由于被声明为基本类型⽽不能转换为空值. 考虑将其声明为对应基本类型的包装类型."
按照错误信息解决错误即可
最开始学习时, 会遇到各种各样的问题, 我们要养成看错误⽇志的习惯, 根据错误⽇志来解决问题.
c. 传递参数类型不匹配
http://127.0.0.1:8080/param/m1/int?age=abc
通过Fiddler观察请求和响应, HTTP响应状态码为400

对于包装类型, 如果不传对应参数,Spring 接收到的数据则为null
所以企业开发中,对于参数可能为空的数据,建议使⽤包装类型
(2) 传递多个参数
如何接收多个参数呢?
和接收单个参数⼀样, 直接使⽤⽅法的参数接收即可. 使⽤多个形参.
java
@RequestMapping("/r4")
public String r4(Integer age,String name){
return "接收到参数:"+age+name;
}
使⽤浏览器发送请求并传参: http://127.0.0.1:8080/param/r4?age=12\&name=zhangsan
可以看到, 后端程序正确拿到了name和age参数的值
当有多个参数时,前后端进⾏参数匹配时,是以参数的名称进⾏匹配的,因此参数的位置是不影响后端获取参数的结果.
⽐如访问: http://127.0.0.1:8080/param/r4?name=zhangsan\&age=12 同样可以拿到正确的结果
(3) 传递对象
如果参数⽐较多时, ⽅法声明就需要有很多形参. 并且后续每次新增⼀个参数, 也需要修改⽅法声明.
我们不妨把这些参数封装为⼀个对象.
Spring MVC 也可以⾃动实现对象参数的赋值,⽐如UserInfo 对象:
java
package com.springboot.demo;
public class UserInfo {
public String name;
public int age;
public String gender;
public UserInfo(String name, int age, String gender) {
this.name = name;
this.age = age;
this.gender = 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 String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
@Override
public String toString() {
return "UseInfo{" +
"name='" + name + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
'}';
}
}
传递对象代码实现:
java
@RequestMapping("/r13")
public String r13(UserInfo userInfo){
return userInfo.toString();
}
使⽤浏览器发送请求并传参:
http://127.0.0.1:8080/param/r13?name=zhangsan\&age=12\&gender=man
可以看到, 后端程序正确拿到了UserInfo对象⾥各个属性的值
Spring 会根据参数名称⾃动绑定到对象的各个属性上, 如果某个属性未传递, 则赋值为null(基本类型则赋值为默认初识值, ⽐如int类型的属性, 会被赋值为0)
(4) 后端参数重命名(后端参数映射)
某些特殊的情况下,前端传递的参数 key 和我们后端接收的 key 可以不⼀致,⽐如前端传递了⼀个 time 给后端,⽽后端是使⽤ createtime 字段来接收的,这样就会出现参数接收不到的情况,如果出现 这种情况,我们就可以使⽤ @RequestParam 来重命名前后端的参数值.
具体⽰例如下,后端实现代码:
java
@RequestMapping("/r6")
public Object r6(@RequestParam("time") String createtime) {
return "接收到参数createtime:" + createtime;
}
使⽤浏览器发送请求并传参:http://127.0.0.1:8080/request/r6?time=2023-09-12
可以看到, Spring可以正确的把浏览器传递的参数time绑定到了后端参数caretetime参数上
此时, 如果浏览器使⽤createtime进⾏参数传递呢?
访问URL: http://127.0.0.1:8080/param/r6?createtime=2023-09-12
浏览器响应结果
错误⽇志信息为:
控制台打印⽇志显⽰: 请求参数 'time' 不存在
可以得出结论:
- 使⽤ @RequestParam 进⾏参数重命名时, 请求参数只能和 @RequestParam 声明的名称⼀致, 才能进⾏参数绑定和赋值.
- 使⽤ @RequestParam 进⾏参数重命名时, 参数就变成了必传参数.
⾮必传参数设置
如果我们的实际业务前端的参数是⼀个⾮必传的参数, 针对上述问题, 如何解决呢?
先来了解下参数必传的原因, 我们查看 @RequestParam 注解的实现细节就可以发现端倪,注解 实现如下:

可以看到 required 的默认值为true, 表⽰含义就是: 该注解修饰的参数默认为必传
既然如此, 我们可以通过设置 @RequestParam 中的 required=false 来避免不传递时报错,
具体实现如下:
java
@RequestMapping("/r7")
public Object r7(@RequestParam(value = "time", required = false) String createtime) {
return "接收到参数createtime:" + createtime;
}

可以看到, 添加required=false之后, time前⾯也加了key, 变成了 value = "time"
注解属性赋值时, 没有指明key的话, 默认为value属性.
如果需要有多个属性进⾏赋值时, 需要写上key
(5)传递数组
Spring MVC 可以⾃动绑定数组参数的赋值
后端实现代码
java
@RequestMapping("/r5")
public String r5(int[] arr){
return Arrays.toString(arr);
}
使⽤浏览器发送请求并传参:
数组参数:请求参数名与形参数组名称相同且请求参数为多个, 后端定义数组类型形参即可接收参数
使⽤Postman来发送请求的响应结果:http://127.0.0.1:8080/param/r5?arr=1&arr=2

可以看到后端对数组参数进⾏了正确的接收和响应.
(6) 传递集合
集合参数:和数组类似, 同⼀个请求参数名有为多个, 且需要使⽤ @RequestParam 绑定参数关系
默认情况下,请求中参数名相同的多个值,是封装到数组. 如果要封装到集合,要使@RequestParam 绑定参数关系
请求⽅式和数组类似:
java
@RequestMapping("/m6")
public String method6(@RequestParam List<String> listParam){
return "size:"+listParam.size() + ",listParam:"+listParam;
}
浏览器传参:
⽅式⼀:http://127.0.0.1:8080/request/m6?listParam=zhangsan&listParam=lisi&listParam=wangwuhttp://127.0.0.1:8080/param/r5?arr=1\&arr=2arr=3[http://127.0.0.1:8080/request/m6?listParam=zhangsan\&listParam=lisi\&listParam=wangwu](http://127.0.0.1:8080/request/m6?listParam=zhangsan&listParam=lisi&listParam=wangwu "http://127.0.0.1:8080/request/m6?listParam=zhangsan&listParam=lisi&listParam=wangwu")
⽅式⼆:http://127.0.0.1:8080/param/r5?arr=1%2c2%2c3
%2c 是逗号的转义编码, 解码后的url为:http://127.0.0.1:8080/param/r5?arr=1,2,3
后端接收代码:
(7)传递JSON数据
a.JSON概念
JSON:JavaScript Object Notation 【JavaScript 对象表⽰法】
JSON是⼀种轻量级的数据交互格式. 它基于 ECMAScript (欧洲计算机协会制定的js规范)的⼀个⼦集, 采⽤完全独⽴于编程语⾔的⽂本格式来存储和表⽰数据。--百度百科
简单来说:JSON就是⼀种数据格式, 有⾃⼰的格式和语法, 使⽤⽂本表⽰⼀个对象或数组的信息, 因此 JSON本质是字符串. 主要负责在不同的语⾔中数据传递和交换.
类似于:
- 国际通⽤语⾔-英语
- 中国56个⺠族不同地区的通⽤语⾔-普通话
有⾃⼰的语法, 其他语⾔也认识.
b.JSON与Javascript的关系
没有关系, 只是语法相似, js开发者能更快的上⼿⽽已, 但是他的语法本⾝⽐较简单, 所以也很好学
JSON语法
JSON 是⼀个字符串,其格式⾮常类似于 JavaScript 对象字⾯量的格式
我们先来看⼀段JSON数据
java
{
"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"]
}]
}
也可以压缩表⽰:
java
{"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) 中
- 数据由逗号 , 分隔
- 对象⽤ {} 表⽰
- 数组⽤ [] 表⽰
- 值可以为对象, 也可以为数组, 数组中可以包含多个对象
c.JSON的两种结构
- 对象: ⼤括号 {} 保存的对象是⼀个⽆序的 键值对 集合. ⼀个对象以左括号 { 开始, 右括号 } 结束。每个"键"后跟⼀个冒号 : ,键值对使⽤逗号 , 分隔
- 数组: 中括号 [] 保存的数组是值(value)的有序集合. ⼀个数组以左中括号 [ 开始, 右中括 号 ] 结束,值之间使⽤逗号 , 分隔。
所以, 以下都是合法的JSON数据
java
{"name":"admin","age":18}
["hello", 3.1415, "json"]
[{"name":"admin","age":18},{"name":"root","age":16},{"name":"张三","age":20}]
可以使⽤在线JSON格式化⼯具来进⾏校验和书写: 在线JSON校验格式化工具(Be JSON)
d. JSON字符串和Java对象互转
JSON本质上是⼀个字符串, 通过⽂本来存储和描述数据
Spring MVC框架也集成了JSON的转换⼯具, 我们可以直接使⽤, 来完成JSON字符串和Java对象的互转
本质上是jackson-databind提供的功能, Spring MVC框架中已经把该⼯具包引⼊了进来, 咱们直接使⽤即可, 如果脱离Spring MVC使⽤, 需要引⼊相关依赖
java
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.13.5</version>
</dependency>
JSON的转换⼯具包有很多, jackson-databind只是其中的⼀种.
javascript
void Jason() throws JsonProcessingException {
ObjectMapper objectMapper=new ObjectMapper();
UserInfo userInfo=new UserInfo("zhangsan",12,"man");
String s=objectMapper.writeValueAsString(userInfo);
System.out.println(s);
}
@Test
void Jason2() throws JsonProcessingException {
ObjectMapper objectMapper=new ObjectMapper();
String s= "{\"name\":\"zhangsan\",\"age\":12,\"gender\":\"man\"}";
UserInfo userInfo=objectMapper.readValue(s,UserInfo.class);
System.out.println(userInfo);
}
使⽤ObjectMapper 对象提供的两个⽅法, 可以完成对象和JSON字符串的互转
- writeValueAsString: 把对象转为JSON字符串
- readValue: 把字符串转为对象
e.JSON优点
- 简单易⽤: 语法简单,易于理解和编写,可以快速地进⾏数据交换
- 跨平台⽀持: JSON可以被多种编程语⾔解析和⽣成, 可以在不同的平台和语⾔之间进⾏数据交换和传输
- 轻量级: 相较于XML格式, JSON数据格式更加轻量级, 传输数据时占⽤带宽较⼩, 可以提⾼数据传输速度
- 易于扩展: JSON的数据结构灵活,⽀持嵌套对象和数组等复杂的数据结构,便于扩展和使⽤
- 安全性: JSON数据格式是⼀种纯⽂本格式,不包含可执⾏代码, 不会执⾏恶意代码,因此具有较⾼的安全性
基于以上特点, JSON在Web应⽤程序中被⼴泛使⽤, 如前后端数据交互、API接⼝数据传输等.
f.传递JSON对象
接收JSON对象, 需要使⽤ @RequestBody 注解
RequestBody: 请求正⽂,意思是这个注解作⽤在请求正⽂的数据绑定,请求参数必须在写在请求正⽂中
后端实现:
java
@RequestMapping("/m7")
public String method7(@RequestBody UserInfo userInfo){
return userInfo.toString();
}
使⽤Postman来发送json请求参数:{"name":"zhangsan","age":12,"gender":"man"}
可以看到, 后端正确接收了
尝试去除掉 @RequestBody 试试
请求响应结果如下:
后端未能成功给Person对象赋值.
(8) 获取URL中参数@PathVariable
path variable: 路径变量
和字⾯表达的意思⼀样, 这个注解主要作⽤在请求URL路径上的数据绑定
默认传递参数写在URL上,SpringMVC就可以获取到
后端实现代码:
java
//从URL中获取参数
@RequestMapping("/article/{id}/{articleId}")
public String r10(@PathVariable Integer articleId,@PathVariable("id") Integer idtype){
return "获取文章:"+articleId+" "+idtype;
}
使⽤Postman发送请求: http://127.0.0.01:8080/param/article/123/456
可以看到, 后端正确获取到了URL中的参数
多参数对应关系如下:
- 如果⽅法参数名称和需要绑定的URL中的变量名称⼀致时, 可以简写, 不⽤给@PathVariable的属性赋值, 如上述例⼦中的articleId变量
- 如果⽅法参数名称和需要绑定的URL中的变量名称不⼀致时, 需要@PathVariable的属性value赋值,如上述例⼦中的id变量.
(9)上传⽂件@RequestPart
后端代码实现:
java
@RequestMapping("/m9")
public String m9(@RequestPart("file11") MultipartFile file) throws IOException {
System.out.println(file.getOriginalFilename());//获取⽂件名称
file.transferTo(new File("D:\\" + file.getOriginalFilename()));//⽂件上传到指定路径
return "文件上传成功";
}
使⽤Postman发送请求:
观察 D: 路径下, ⽂件是否上传成功
参数可以放在哪里?
1.URL资源路径 2.URL查询字符串 3.header 4.请求正文body
- 路径参数:URL中除去查询字符串
- 请求参数:URL的查询字符串和请求正文
(10)获取Cookie/Session
a.理解Cookie
HTTP 协议⾃⾝是属于 "⽆状态" 协议.
"⽆状态" 的含义指的是:
默认情况下 HTTP 协议的客⼾端和服务器之间的这次通信, 和下次通信之间没有直接的联系.(可以理解为无记忆的)
但是实际开发中, 我们很多时候是需要知道请求之间的关联关系的.
例如登陆⽹站成功后, 第⼆次访问的时候服务器就能知道该请求是否是已经登陆过了.
上述图中的 "令牌" 通常就存储在 Cookie 字段中.
⽐如去医院挂号
- 看病之前先挂号. 挂号时候需要提供⾝份证号, 同时得到了⼀张 "就诊卡", 这个就诊卡就相当于患者的 "令牌".
- 后续去各个科室进⾏检查, 诊断, 开药等操作, 都不必再出⽰⾝份证了, 只要凭就诊卡即可识别出当前患者的⾝份.
- 看完病了之后, 不想要就诊卡了, 就可以注销这个卡. 此时患者的⾝份和就诊卡的关联就销毁了. (类似于⽹站的注销操作)
- ⼜来看病, 可以办⼀张新的就诊卡, 此时就得到了⼀个新的 "令牌"
此时在服务器这边就需要记录"令牌"信息, 以及令牌对应的⽤⼾信息, 这个就是 Session 机制所做的⼯作.
b.理解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返回
Session 默认是保存在内存中的. 如果重启服务器则 Session 数据就会丢失.
c.Cookie 和 Session 的区别
- Cookie 是客户端保存⽤⼾信息的⼀种机制. Session 是服务器端保存⽤⼾信息的⼀种机制.
- Cookie 和 Session之间主要是通过 SessionId 关联起来的, SessionId 是 Cookie 和 Session 之间的桥梁
- Cookie 和 Session 经常会在⼀起配合使⽤. 但是不是必须配合.
- 完全可以⽤ Cookie 来保存⼀些数据在客⼾端. 这些数据不⼀定是⽤户⾝份信息, 也不⼀定是 SessionId
- Session 中的sessionId 也不需要⾮得通过 Cookie/Set-Cookie 传递, ⽐如通过URL传递.
d.获取Cookie
传统获取Cookie
java
@RequestMapping("/getCookie1")
public String getCookie1(HttpServletRequest request, HttpServletResponse response) {
Cookie[] cookies = request.getCookies();
// if(cookies==null){
// return "cookie为空" ;
// }
//todo cookie可能为空,要进行空指针处理
for(Cookie cookie : cookies) {
System.out.println(cookie.getName()+":"+cookie.getValue());
}
return "获取cookie成功" ;
}
Spring MVC是基于 Servlet API 构建的原始 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是可以伪造的, 也就是不安全的, 所以使⽤Cookie时, 后端需要进⾏Cookie校验
简洁获取Cookie
也有更简洁的⽅式获取Cookie
java
@RequestMapping("/getCookie")
public String cookie(@CookieValue("bite") String bite) {
return "bite:" + bite;
}
运⾏结果:http://127.0.0.1:8080/header/getCookie2
e.获取Session
Session 存储和获取
Session是服务器端的机制, 我们需要先存储, 才能再获取
Session 也是基于HttpServletRequest 来存储和获取
Session存储
java
@RequestMapping("setSession")
public String setSession(HttpServletRequest request) {
HttpSession session = request.getSession();
session.setAttribute("userName", "zhangsan");
session.setAttribute("age", 13);
return "set session succeed:";
}
这个代码中看不到 SessionId 这样的概念的. getSession 操作内部提取到请求中的Cookie ⾥的SessionId, 然后根据SessionId获取到对应的Session 对象, Session 对象⽤HttpSession来描述获取Session有两种⽅式
- HttpSession getSession(boolean create) : 参数如果为 true, 则当不存在会话时新建会话; 参数如果为 false, 则当不存在会话时返回 null
- HttpSession getSession(): 和getSession(true) 含义⼀样, 默认值为true.
- void setAttribute(String name, Object value): 使⽤指定的名称绑定⼀个对象到该 session 会话
Session读取
读取 Session 可以使⽤ HttpServletRequest
java
@RequestMapping("getSession")
public String getSession(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession(false);// 如果 session 不存在, 不会⾃动创建
String userName=null;
if(session != null&&session.getAttribute("userName")!=null) {
userName = (String)session.getAttribute("userName");
}
return "从session中获取信息,userName:"+userName;
}
Object getAttribute(String name): 返回在该 session 会话中具有指定名称的对象,如果没有指定名称的对象,则返回 null.
运⾏
先设置Session: http://127.0.0.1:8080/header/setSession
通过Fiddler观察Http请求和响应情况:

可以看到, Http响应中, 通过Set-Cookie告知客⼾端, 把SessionID存储在Cookie中
获取Session: http://127.0.0.1:8080/header/getSession
通过Fiddler观察Http请求和响应

可以看到, Http请求时, 把SessionId通过Cookie传递到了服务器.
简洁获取 Session(1)
java
@RequestMapping("getSession2")
public String getSession2(@SessionAttribute(value = "userName",required = false) String userName) {
return "从session中获取信息,userName:"+userName;
}
运⾏结果: http://127.0.0.1:8080/header/getSession2
简洁获取 Session(2)
通过Spring MVC内置对象HttpSession 来获取
java
@RequestMapping("getSession3")
public String getSession3(HttpSession session) {
String userName = (String) session.getAttribute("userName");
return "从session中获取信息,userName:"+userName;
}
运⾏结果:http://127.0.0.1:8080/header/getSession3
(11) 获取Header
传统获取 header
获取Header也是从 HttpServletRequest 中获取
java
@RequestMapping("getUserAgen")
public String getUserAgent(HttpServletRequest request) {
String header = request.getHeader("User-Agent");
return "获取信息header:"+header;
}
使⽤HttpServletRequest 提供的getHeader⽅法来获取, 参数对应HTTP请求报头的"Key"
运⾏结果: http://127.0.0.1:8080/header/getUserAgen
观察获取的User-Agent是否正确:

简洁获取 Header
java
@RequestMapping("getUserAgent")
public String getUserAgent2(@RequestHeader("getUserAgent") String header) {
return "获取信息header:"+header;
}
@RequestHeader注解的参数值为HTTP请求报头中的"Key"
运⾏结果: http://127.0.0.1:8080/param/getHeader2
5. 响应
在我们前⾯的代码例⼦中,都已经设置了响应数据, Http响应结果可以是数据, 也可以是静态⻚⾯,也可以针对响应设置状态码, Header信息等.
(1)返回静态⻚⾯
创建前端⻚⾯ index.html(注意路径)
html代码如下:
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Index⻚⾯</title>
</head>
<body>
Hello,Spring MVC,我是Index⻚⾯.
</body>
</html>
后台代码:
java
@RestController
public class IndexController {
@RequestMapping("/index")
public Object index(){
//返回index.html
return "/index.html";
}
}
运⾏结果:http://127.0.0.1:8080/index
结果却发现, ⻚⾯未正确返回, http响应把 "/index.html" 当做了http响应正⽂的数据
那Spring MVC如何才能识别出来 index.html 是⼀个静态⻚⾯, 并进⾏返回呢?
我们需要把 @RestController 改为 @Controller
正确代码如下:
java
@Controller
public class IndexController {
@RequestMapping("/index")
public Object index(){
return "/index.html";
}
}
再次运⾏: http://127.0.0.1:8080/index
发现⻚⾯正确展⽰了
@RestController 和 @Controller 有着什么样的关联和区别呢?
咱们前⾯讲了MVC模式, 后端会返回视图, 这是早期时的概念
随着互联⽹的发展, ⽬前项⽬开发流⾏"前后端分离"模式, Java主要是⽤来做后端项⽬的开发, 所以也就不再处理前端相关的内容了
MVC的概念也逐渐发⽣了变化, View不再返回视图, ⽽是返回显⽰视图时需要的数据.
所以前⾯使⽤的 @RestController 其实是返回的数据.
@RestController = @Controller + @ResponseBody
@Controller : 定义⼀个控制器, Spring 框架启动时加载, 把这个对象交给Spring管理.
@ResponseBody : 定义返回的数据格式为⾮视图, 返回⼀个 text/html 信息
@RestController 源码:
java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.stereotype;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
@AliasFor(
annotation = Component.class
)
String value() default "";
}
如果想返回视图的话, 只需要把 @ResponseBody 去掉就可以了, 也就是 @Controller
(2)返回数据@ResponseBody
我们上⾯讲到, @ResponseBody 表⽰返回数据.
java
@Controller
@ResponseBody
public class IndexController {
@RequestMapping("/index")
public Object index(){
return "/index.html";
}
}
加上 @ResponseBody 注解, 该⽅法就会把 "/index.html" 当做⼀个数据返回给前端.
运⾏:http://127.0.0.1:8080/index
@ResponseBody 既是类注解, ⼜是⽅法注解

如果作⽤在类上, 表⽰该类的所有⽅法, 返回的都是数据, 如果作⽤在⽅法上, 表⽰该⽅法返回的是数据.
也就是说: 在类上添加 @ResponseBody 就相当于在所有的⽅法上添加了 @ResponseBody 注解.
同样, 如果类上有 @RestController 注解时:表⽰所有的⽅法上添加了 @ResponseBody 注解, 也就是当前类下所有的⽅法返回值做为响应数据
如果⼀个类的⽅法⾥, 既有返回数据的, ⼜有返回⻚⾯的, 就把 @ResponseBody 注解添加到对应的⽅法上即可.
java
@Controller
public class IndexController {
@RequestMapping("/index")
public Object index(){
return "/index.html";
}
@RequestMapping("/returnData")
@ResponseBody
public String returnData(){
return "该⽅法返回数据";
}
}
多个注解时, 没有先后顺序, 先写哪个都可以
运⾏程序, 浏览器响应结果如下: http://127.0.0.1:8080/returnData
如果去掉 @ResponseBody 注解, 程序会报404错误.
程序会认为需要返回的是视图, 根据内容去查找⽂件, 但是查询不到, 路径不存在, 报404
(3)返回HTML代码⽚段
后端返回数据时, 如果数据中有HTML代码, 也会被浏览器解析
java
@ResponseBody
@RequestMapping("/returnHTML")
public String returnHTML(){
return "<h1>hhhhh</h1>";
}
运⾏程序, 浏览器响应结果如下: http://127.0.0.1:8080/response/returnHTML
观察响应结果, Content-Type 为 text/html
响应中的 Content-Type 常⻅取值有以下⼏种:
- text/html : body 数据格式是 HTML
- text/css : body 数据格式是 CSS
- application/javascript : body 数据格式是 JavaScript
- application/json : body 数据格式是 JSON
如果请求的是js⽂件, Spring MVC会⾃动设置Content-Type为 application/javascript
如果请求的是css⽂件, Spring MVC会⾃动设置Content-Type为 text/css
java
@RequestMapping("/index2")
public Object index2(){
return "/a.js";
}
@RequestMapping("/index3")
public Object index3(){
return "/b.css";
}

(4)返回JSON
Spring MVC 也可以返回JSON
后端⽅法返回结果为对象
java
@ResponseBody
@RequestMapping("/returnJson")
public UserInfo returnJson(){
UserInfo userInfo=new UserInfo("zhangsan",12,"man");
return userInfo;
}
运⾏程序, 浏览器响应结果如下: http://127.0.0.1:8080/response/returnJson
观察响应结果, Content-Type 为 application/json

(5) 设置状态码
Spring MVC会根据我们⽅法的返回结果⾃动设置响应状态码, 程序员也可以⼿动指定状态码
通过Spring MVC的内置对象HttpServletResponse 提供的⽅法来进⾏设置
java
@ResponseBody
@RequestMapping("/setStatus")
public String setStatus(HttpServletResponse response){
UserInfo userInfo=new UserInfo("zhangsan",12,"man");
response.setStatus(500);
return userInfo.toString();
}
运⾏程序, 浏览器响应结果如下: http://127.0.0.1:8080/response/setStatus
状态码不影响⻚⾯的展⽰
(6)设置Header (了解)
Http响应报头也会向客⼾端传递⼀些附加信息, ⽐如服务程序的名称,请求的资源已移动到新地址等, 如: Content-Type, Local等.
这些信息通过 @RequestMapping 注解的属性来实现
先来看 @RequestMapping 的源码:
java
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.web.bind.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.aot.hint.annotation.Reflective;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
@Reflective({ControllerMappingReflectiveProcessor.class})
public @interface RequestMapping {
String name() default "";
@AliasFor("path")
String[] value() default {};
@AliasFor("value")
String[] path() default {};
RequestMethod[] method() default {};
String[] params() default {};
String[] headers() default {};
String[] consumes() default {};
String[] produces() default {};
}
-
value: 指定映射的URL
-
method: 指定请求的method类型, 如GET, POST等
-
consumes: 指定处理请求(request)的提交内容类型(Content-Type),例如application/json,
text/html;
-
produces: 指定返回的内容类型,还可以同时设置返回值的字符编码
-
Params: 指定request中必须包含某些参数值时,才让该⽅法处理
-
headers: 指定request中必须包含某些指定的header值,才能让该⽅法处理请求
了解即可, 更多说明参考 Request Mapping :: Spring Framework
设置Content-Type
我们通过设置 produces属性的值, 设置响应的报头Content-Type
java
@ResponseBody
@RequestMapping(value = "/setHeader",produces = "application/json")
public String setHeader(){
return "{\"name\":\"zhangsan\",\"age\":12}";
}
运⾏程序, 浏览器响应结果如下: http://127.0.0.1:8080/response/setHeader
如果不设置produces , ⽅法返回结果为String时, Spring MVC默认返回类型, 是text/html.
设置返回类型时, 也可以同步设置响应编码
java
@RequestMapping(value = "/returnJson2",produces = "application/json;charset=utf-8")
@ResponseBody
public String returnJson2() {
return "{\"success\":true}";
}
设置其他Header
设置其他Header的话, 需要使⽤Spring MVC的内置对象HttpServletResponse 提供的⽅法来进⾏设置 http://127.0.0.1:8080/response/setHeader2
void setHeader(String name, String value) 设置⼀个带有给定的名称和值的 header. 如果 name已经存在, 则覆盖旧的值.
运⾏程序, 浏览器响应结果如下: http://127.0.0.1:8080/response/setHeader2
