Spring Web MVC入门
本节目标
-
学习常见的Spring Web MVC注解
-
掌握使用SpringMVC来完成基础的功能开发
-
了解MVC和三层架构的设计模式
-
掌握企业开发中的一些命名规范
1. 什么是 Spring Web MVC?
官方对于 Spring MVC 的描述是这样的:
Spring Web MVC is the original web framework built on the Servlet API and has been included in the Spring Framework from the very beginning. The formal name, "Spring Web MVC", comes from the name of its source module (spring-webmvc)
引用来自:https://docs.spring.io/spring-framework/reference/web/webmvc.html
翻译为中文:
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
1.1 MVC 定义
MVC 是 Model View Controller 的缩写,它是软件工程中的一种软件架构设计模式,它把软件系统分 为模型、视图和控制器三个基本部分

• View(视图) 指在应用程序中专门用来与浏览器进行交互,展示数据的资源
• Model(模型) 是应用程序的主体部分,用来处理程序中数据逻辑的部分.
**• Controller(控制器)**可以理解为一个分发器,用来决定对于视图发来的请求,需要用哪一个模型 来处理,以及处理完后需要跳回到哪一个视图。即用来连接视图和模型
比如去饭店吃饭
客户进店之后, 服务员来接待客户点餐, 客户点完餐之后, 把客户菜单交给前厅, 前厅根据客户菜单 给后厨下达命令. 后厨负责做饭, 做完之后, 再根据菜单告诉服务员, 这是X号餐桌客人的饭
在这个过程中
服务员就是View(视图), 负责接待客户, 帮助客户点餐, 以及给顾客端饭
前厅就是Controller(控制器), 根据用户的点餐情况, 来选择给哪个后厨下达命令.
后厨就是Model(模型), 根据前厅的要求来完成客户的用餐需求
1.2 什么是Spring MVC ?
MVC 是一种架构设计模式, 也是一种思想, 而 Spring MVC 是对 MVC 思想的具体实现. 除此之外, Spring MVC还是一个Web框架.
总结来说,Spring MVC 是一个实现了 MVC 模式的 Web 框架
所以, Spring MVC主要关注有两个点:
1. MVC
2. Web框架
Spring MVC 全称是 Spring Web MVC
其实, Spring MVC 我们在前面已经用过了, 在创建 Spring Boot 项目时,我们勾选的 Spring Web 框架 其实就是 Spring MVC 框架:

这时候可能有人就懵了, 前面创建的不是SpringBoot项目吗? 怎么又变成了Spring MVC项目? 他们 之间到底有着什么样的关系?
Spring Boot 只是实现Spring MVC的其中一种方式而已.
Spring Boot 可以添加很多依赖, 借助这些依赖实现不同的功能. Spring Boot 通过添加Spring Web MVC框架, 来实现web功能.
比如: 厨房可以用来做饭, 但真实实现做饭功能的是火以及各种做饭相关的食材和工具
不过Spring在实现MVC时, 也结合自身项目的特点, 做了一些改变, 相对而言, 下面这个图或许更加合适 一些.
2. 学习Spring MVC
既然是 Web 框架, 那么当用户在浏览器中输入了 url 之后,我们的 Spring MVC 项目就可以感知到用户的请求, 并给予响应
咱们学习Spring MVC, 重点也就是学习如何通过浏览器和用户程序进行交互
主要分以下三个方面:
1.**建立连接:**将用户(浏览器)和 Java 程序连接起来,也就是访问一个地址能够调用到我们的 Spring程序
-
**请求:**用户请求的时候会带一些参数,在程序中要想办法获取到参数, 所以请求这块主要是 获取参数 的功能
-
**响应:**执行了业务逻辑之后,要把程序执行的结果返回给用户, 也就是响应.
掌握了以上 3 个功能就相当于掌握了 Spring MVC.
2.1 项目准备
Spring MVC 项目创建和 Spring Boot 创建项目相同,在创建的时候选择 Spring Web 就相当于创建了 Spring MVC 的项目
Spring MVC 使用 Spring Boot 的方式创建
2.2 建立连接
在 Spring MVC 中使用 @RequestMapping 来实现 URL 路由映射 ,也就是浏览器连接程序的作用

接下来访问: http://127.0.0.1:8080/cmr 就可以看到程序返回的数据了
2.2.1 @RequestMapping 注解介绍
@RequestMapping 是 Spring Web MVC 应用程序中最常被用到的注解之一,它是用来注册接口的 路由映射的.
路由映射: 当用户访问一个 URL 时, 将用户的请求对应到程序中某个类的某个方法的过程就叫路由映射.
2.2.2 @RequestMapping 使用
@RequestMapping 既可修饰类,也可以修饰方法,当修饰类和方法时,访问的地址是类路径 + 方法路径.
@RequestMapping标识一个类:设置映射请求的请求路径的初始信息
@RequestMapping标识一个方法:设置映射请求请求路径的具体信息

访问地址http://127.0.0.1:8080/rmc/cmr

注意:
@RequestMapping 的URL 路径最前面加不加 / (斜杠)都可以, Spring程序启动时, 会进行判断, 如果 前面没有加 / , Spring会拼接上一个/
2.2.3 @RequestMapping 是 GET 还是 POST 请求
我们来测试一下就知道结论了
GET请求:
浏览器发送的请求类型都是get, 通过以上案例, 可以看出来 @RequestMapping 支持get请求
POST 请求:
我们通过form表单来构造请求:
创建test.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


如上图, 访问链接为: http://127.0.0.1:8080/html/test.html
从运行结果可以看出: @RequestMapping 既支持Get请求, 又支持Post请求. 同理, 也支持其他的请 求方式.
那如何指定GET或者POST类型呢?
指定GET/POST方法类型
我们可以显示的指定@RequestMapping 来接收POST的情况,如下所示

2.3 Postman介绍
从上面的案例中, 也发现了一个新的问题, 就是我们测试后端方法时, 还需要去写前端代码. 这对我们来说, 是一件麻烦又痛苦的事情
随着互联网的发展, 也随着项目难度的增加, 企业也按照开发的功能, 把人员拆分成了不同的团队. 界面 显示交给"前端开发工程师", 业务逻辑的实现交给 "后端开发工程师".
后端开发工程师, 不要求也不需要掌握前端技能了.
那后端开发工程师, 如何测试自己的程序呢? -- 使用专业的接口测试工具
咱们课堂中使用的是Postman, 接下来我们来学习Postman的使用.
2.3.1 下载安装postman
下载链接:https://www.postman.com/downloads/
2.3.3 传参介绍
1. 普通传参, 也就是通过查询字符串来传参

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

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-urlencoded

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

2.4 请求
访问不同的路径, 就是发送不同的请求. 在发送请求时, 可能会带一些参数, 所以学习Spring的请求, 主要学习如何传递参数到后端以及后端如何接收
传递参数, 咱们主要是使用浏览器和Postman来模拟
后端开发人员无需过度关注如何传递参数, 了解即可, 实际开发中以Postman测试为主
2.4.1 传递单个参数
接收单个参数, 在 Spring MVC 中直接用方法中的参数就可以,比如以下代码:


如果参数不一致, 是获取不到参数的
比如请求URLhttp://127.0.0.1:8080/rmc/cmr?str1=haha

注意事项
使用基本类型来接收参数时, 参数必须传(除boolean类型), 否则会报500错误
类型不匹配时, 会报400错误
String 是 Object 的子类(所有类都继承自 Object),所以:
方法返回值是 Object 时,可以返回 String 类型的对象(向上转型,Java 自动完成)
@RequestMapping("/m1/int")
public Object method1GetInt(int age){
return "接收到参数age:" + age;
}
通过Fiddler观察请求和响应, HTTP响应状态码为200, Content-Type 为 text/html

2. 不传递age参数
浏览器响应情况

通过Fiddler观察HTTP响应状态码为500
尝试观察程序的错误日志, 并解决

一般看日志堆栈信息的首行, 报错信息显示
int类型的参数'age',虽然为可选的, 但由于被声明为基本类型而不能转换为空值. 考虑将其声明为对应基本类型的包装类型
按照错误信息解决错误即可
最开始学习时, 会遇到各种各样的问题, 我们要养成看错误日志的习惯, 根据错误日志来解决问题
最开始可能会看不懂, 或者比较懵, 要耐下心来, 慢慢看, 后面课堂中也会教大家更多看日志的思路.
3. 传递参数类型不匹配
通过Fiddler观察请求和响应, HTTP响应状态码为400

对于包装类型, 如果不传对应参数,Spring 接收到的数据则为null
所以企业开发中,对于参数可能为空的数据,建议使用包装类型
2.4.2 传递多个参数
如何接收多个参数呢?
和接收单个参数一样, 直接使用方法的参数接收即可. 使用多个形参.


当有多个参数时,前后端进行参数匹配时,是以参数的名称进行匹配的,因此参数的位置是不影响后端获取参数的结果.
2.4.3 传递对象
如果参数比较多时, 方法声明就需要有很多形参. 并且后续每次新增一个参数, 也需要修改方法声明.
我们不妨把这些参数封装为一个对象
Spring MVC 也可以自动实现对象参数的赋值,比如 Person 对象
package com.bite.springbootdemo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RequestMapping("/rmc")
@RestController
public class Ha {
// @RequestMapping(value = "/cmr")
//方法路径
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 "Ha{" +
"id=" + id +
", name='" + name + '\'' +
", password='" + password + '\'' +
'}';
}
}
传递对象代码实现
@RequestMapping(value = "/cmr")
public Object method3(Ha ha){
return ha.toString();
}

Spring 会根据参数名称自动绑定到对象的各个属性上, 如果某个属性未传递, 则赋值为null(基本类型则 赋值为默认初识值, 比如int类型的属性, 会被赋值为0)
2.4.4 后端参数重命名(后端参数映射)
些特殊的情况下,前端传递的参数 key 和我们后端接收的 key 可以不一致,比如前端传递了一个 time 给后端,而后端是使用 createtime 字段来接收的,这样就会出现参数接收不到的情况,如果出现 这种情况,我们就可以使用 @RequestParam 来重命名前后端的参数值
具体示例如下,后端实现代码:
@RequestMapping("/m4")
public Object method_4(@RequestParam("time") String createtime) {
return "接收到参数createtime:" + createtime;
}
可以看到, Spring可以正确的把浏览器传递的参数time绑定到了后端参数caretetime参数上

可以得出结论:
-
使用 @RequestParam 进行参数重命名时, 请求参数只能和 @RequestParam 声明的名称一 致, 才能进行参数绑定和赋值.
-
使用 @RequestParam 进行参数重命名时, 参数就变成了必传参数
非必传参数设置
如果我们的实际业务前端的参数是一个非必传的参数, 针对上述问题, 如何解决呢?
先来了解下参数必传的原因, 我们查看 @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 来避免不传递时报错, 具体实现如下

可以看到, 添加required=false之后, time前面也加了key, 变成了 value = "time"
注解属性赋值时, 没有指明key的话, 默认为value属性
如果需要有多个属性进行赋值时, 需要写上key
2.4.5 传递数组
Spring MVC 可以自动绑定数组参数的赋值
后端实现代码:
@RequestMapping("/m5")
public String method5(String[] arrayParam) {
return Arrays.toString(arrayParam);
}
使用浏览器发送请求并传参
数组参数:请求参数名与形参数组名称相同且请求参数为多个, 后端定义数组类型形参即可接收参数
http://127.0.0.1:8080/param/m5?arrayParam=zhangsan\&arrayParam=lisi\&arrayParam=wangwu
或者使用 http://127.0.0.1:8080/param/m6?listParam=zhangsan%2Clisi%2Cwangwu
或者使用 http://127.0.0.1:8080/param/m5?arrayParam=zhangsan,lisi,wangwu
浏览器响应结果:

可以看到后端对数组参数进行了正确的接收和响应
或者使用Postman来发送请求

2.4.6 传递集合
集合参数:和数组类似, 同一个请求参数名有为多个, 且需要使用 @RequestParam 绑定参数关系
浏览器传参:
方式一: http://127.0.0.1:8080/param/m6?listParam=zhangsan\&listParam=lisi\&listParam=wangwu 方式二: http://127.0.0.1:8080/param/m6?listParam=zhangsan%2Clisi%2Cwangwu
%2c是逗号的转义编码,解码后的url为:http://127.0.0.1:8080/param/m6? listParam=zhangsan,lisi,wangwu
后端接收代码:

Postman传参测试:

2.4.7 传递JSON数据
JSON概念
JSON:JavaScript Object Notation 【JavaScript 对象表示法】
JSON是一种轻量级的数据交互格式. 它基于 ECMAScript (欧洲计算机协会制定的js规范)的一个子集, 采用完全独立于编程语言的文本格式来存储和表示数据。--百度百科
简单来说:JSON就是一种数据格式, 有自己的格式和语法, 使用文本表示一个对象或数组的信息, 因此 JSON本质是字符串
JSON与Javascript的关系
没有关系, 只是语法相似, js开发者能更快的上手而已, 但是他的语法本身比较简单, 所以也很好学
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)的有序集合. 一个数组以左中括号 [ 开始, 右中括 号 ] 结束,值之间使用逗号 , 分隔.

所以, 以下都是合法的JSON数据
{"name":"admin","age":18}
["hello", 3.1415, "json"]
[{"name":"admin","age":18},{"name":"root","age":16},{"name":"张三","age":20}]
可以使用在线JSON格式化工具来进行校验和书写:
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只是其中的一种
public class JSONUtils {
private static ObjectMapper objectMapper = new ObjectMapper();
public static void main(String[] args) throws JsonProcessingException {
Person person = new Person();
person.setId(5);
person.setName("zhangsan");
person.setPassword("123456");
//对象转为JSON字符串
String jsonStr = objectMapper.writeValueAsString(person);
System.out.println("JSON字符串为:"+jsonStr);
//JSON字符串转为对象
Person p = objectMapper.readValue(jsonStr,Person.class);
System.out.println("转换的对象
id:"+p.getId()+",name:"+p.getName()+",password:"+p.getPassword());
}
}
使用ObjectMapper 对象提供的两个方法, 可以完成对象和JSON字符串的互转
writeValueAsString: 把对象转为JSON字符串
readValue: 把字符串转为对象
JSON优点
1.简单易用: 语法简单,易于理解和编写,可以快速地进行数据交换
-
跨平台支持: JSON可以被多种编程语言解析和生成, 可以在不同的平台和语言之间进行数据交换和 传输
-
轻量级: 相较于XML格式, JSON数据格式更加轻量级, 传输数据时占用带宽较小, 可以提高数据传输 速度
-
易于扩展: JSON的数据结构灵活,支持嵌套对象和数组等复杂的数据结构,便于扩展和使用
-
安全性: JSON数据格式是一种纯文本格式,不包含可执行代码, 不会执行恶意代码,因此具有较高 的安全性
基于以上特点, JSON在Web应用程序中被广泛使用, 如前后端数据交互、API接口数据传输等
@RequestMapping(value = "/m7")
public Object method7(@RequestBody Person person) {
return person.toString();
}
使用Postman来发送json请求参数


尝试去除掉 @RequestBody 试试
@RequestMapping(value = "/m7")
public Object method7(Person person) {
return person.toString();
}

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

可以看到, 后端正确获取到了URL中的参数

如果方法参数名称和需要绑定的URL中的变量名称一致时, 可以简写, 不用给@PathVariable的属性赋值, 如上述例子中的id变量
如果方法参数名称和需要绑定的URL中的变量名称不一致时, 需要@PathVariable的属性value赋值, 如上述例子中的userName变量
2.4.9 上传文件@RequestPart
后端代码实现
@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;
}
使用Postman发送请求

2.4.10 获取Cookie/Session
回顾 Cookie
HTTP 协议自身是属于 "无状态" 协议
"无状态"的含义指的是:
默认情况下HTTP协议的客⼾端和服务器之间的这次通信,和下次通信之间没有直接的联系.
但是实际开发中,我们很多时候是需要知道请求之间的关联关系的

上述图中的"令牌"通常就存储在Cookie字段中
理解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 数据就会丢失.
Cookie 和 Session 的区别
• Cookie 是客户端保存用户信息的一种机制. Session 是服务器端保存用户信息的一种机制.
• Cookie 和 Session之间主要是通过 SessionId 关联起来的, SessionId 是 Cookie 和 Session 之间的桥梁
• Cookie 和 Session 经常会在一起配合使用. 但是不是必须配合
可以用 Cookie 来保存一些数据在客户端. 这些数据不一定是用户身份信息, 也不一定是 SessionId ◦ Session 中的sessionId 也不需要非得通过 Cookie/Set-Cookie 传递, 比如通过URL传递.
获取Cookie
传统获取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
也有更简洁的方式获取Cookie
@RequestMapping("/getCookie")
public String cookie(@CookieValue("bite") String bite) {
return "bite:" + bite;
}
运行结果:

获取Session
Session 存储和获取
Session是服务器端的机制, 我们需要先存储, 才能再获取
Session 也是基于HttpServletRequest 来存储和获取的
Session存储
@RequestMapping("/setSession")
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();
HttpSession getSession(boolean create) : 参数如果为 true, 则当不存在会话时新建会话; 参数如果 为 false, 则当不存在会话时返回 null
HttpSession getSession(): 和getSession(true) 含义一样, 默认值为true
void setAttribute(String name, Object value): 使用指定的名称绑定一个对象到该 session 会话
Session读取
读取 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;
}
Object getAttribute(String name): 返回在该 session 会话中具有指定名称的对象,如果没有指定名 称的对象,则返回 null.
运行
先设置Session:http://127.0.0.1:8080/param/setSess

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

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

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

2.4.11 获取Header
传统获取 header
获取Header也是从 HttpServletRequest 中获取
@RequestMapping("/getHead")
public Object getHead(HttpServletRequest request){
String header = request.getHeader("User-Agent");
return header;
}
简洁获取 Header
@RequestMapping("/header")
public String header(@RequestHeader("User-Agent") String userAgent) {
return "userAgent:"+userAgent;
}
2.5 响应
在我们前面的代码例子中,都已经设置了响应数据, Http响应结果可以是数据, 也可以是静态页面,也可 以针对响应设置状态码, Header信息等
2.5.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>
后台代码:
html
@RestController
public class IndexController {
@RequestMapping("/index")
public Object index(){
//返回index.html
return "/index.html";
}
}

结果却发现, 页面未正确返回, http响应把 "/index.html" 当做了http响应正文的数据
那Spring MVC如何才能识别出来 index.html 是一个静态页面, 并进行返回呢?
我们需要把 @RestController 改为 @Controller
正确代码如下:
html
@Controller
public class IndexController {
@RequestMapping("/index")
public Object index(){
return "/index.html";
}
}
@Controller:可以跳转页面
@RestController:只会返回文字 / JSON,不能跳页面
@ResponseBody 既是类注解, 又是方法注解
如果作用在类上, 表示该类的所有方法, 返回的都是数据, 如果作用在方法上, 表示该方法返回的是数据
2.5.2 返回数据@ResponseBody
也就是说: 在类上添加 @ResponseBody 就相当于在所有的方法上添加了 @ResponseBody 注解.
同样, 如果类上有 @RestController 注解时:表示所有的方法上添加了 @ResponseBody 注 解, 也就是当前类下所有的方法返回值做为响应数据
如果一个类的方法里, 既有返回数据的, 又有返回页面的, 就把 @ResponseBody 注解添加到对应的方法上即可
html
@Controller
public class IndexController {
@RequestMapping("/index")
public Object index(){
return "/index.html";
}
@RequestMapping("/returnData")
@ResponseBody
public String returnData(){
return "/index.html";
}
}
如果去掉 @ResponseBody 注解, 程序会报404错误
2.5.3 返回HTML代码片段
后端返回数据时, 如果数据中有HTML代码, 也会被浏览器解析
java
@RequestMapping("/returnHtml")
@ResponseBody
public String returnHtml() {
return "<h1>Hello,HTML~</h1>";
}
通过Fiddler观察响应结果, 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";
}
2.5.4 返回JSON
Spring MVC 也可以返回JSON
后端方法返回结果为对象
java
@ResponseBody
@RequestMapping("/returnJson")
public Object returnJson(){
HashMap<Integer,String> hashMap=new HashMap<>();
hashMap.put(1,"zs");
hashMap.put(2,"ls");
return hashMap;
}

2.5.5 设置状态码
Spring MVC会根据我们方法的返回结果自动设置响应状态码, 程序员也可以手动指定状态码
通过Spring MVC的内置对象HttpServletResponse 提供的方法来进行设置
java
@RequestMapping(value = "/setStatus")
@ResponseBody
public String setStatus(HttpServletResponse response) {
response.setStatus(401);
return "设置状态码成功";
}
运行程序, 浏览器响应结果如下


2.5.6 设置Header (了解)
Http响应报头也会向客户端传递一些附加信息, 比如服务程序的名称,请求的资源已移动到新地址等, 如: Content-Type, Local等.
这些信息通过 @RequestMapping 注解的属性来实现
设置Content-Type
我们通过设置 produces属性的值, 设置响应的报头Content-Type
java
@RequestMapping(value = "/returnJson2",produces = "application/json")
@ResponseBody
public String returnJson2() {
return "{\"success\":true}";
}
设置返回类型时, 也可以同步设置响应编码
java
@RequestMapping(value = "/returnJson2",produces =
"application/json;charset=utf-8")
@ResponseBody
public String returnJson2() {
return "{\"success\":true}";
}
设置其他Header
设置其他Header的话, 需要使用Spring MVC的内置对象HttpServletResponse 提供的方法来进行设置
java
@RequestMapping(value = "/setHeader")
@ResponseBody
public String setHeader(HttpServletResponse response) {
response.setHeader("MyHeader","MyHeaderValue");
return "设置Header成功";
}

3. 综合性练习
结合上述内容, 我们可以做一些小案例
主要掌握知识点
-
理解前后端交互过程
-
接口传参, 数据返回, 以及页面展示
3.1 加法计算器
需求: 输入两个整数, 点击"点击相加"按钮, 显示计算结果

3.1.1 准备工作
创建SpringBoot项目: 引入Spring Web依赖, 把前端页面放在项目中
3.1.2 约定前后端交互接口
概念介绍
约定 "前后端交互接口" 是进行 Web 开发中的关键环节
需求分析
加法计算器功能, 对两个整数进行相加, 需要客户端提供参与计算的两个数, 服务端返回这两个整数计算的结果


3.1.3 服务器代码
java
@RestController
@RequestMapping("/calc")
public class Calc {
@RequestMapping("/t1")
public Object t1(Integer n1,Integer n2){
Integer sum=n1+n2;
return "<h1>sum:"+sum+"</h1>";
}
}
3.1.4 调整前端页面代码
添加访问url和请求方式
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="calc/t1" method="post">
<h1>计算器</h1>
数字1:<input type="text" name="n1">
数字2:<input type="text" name="n2">
<input type="submit" value="点击相加">
</form>
</body>
</html>
3.2 用户登录

3.2.1 实现服务器端代码
1. 校验接口
java
@RequestMapping("/login")
public boolean t1(String userName, String password, HttpSession session) {
if (!StringUtils.hasLength(userName) || !StringUtils.hasLength(password)) return false;
if (!"zs".equals(userName) || !"admin".equals(password)) return false;
session.setAttribute("name", userName);
return true;
}
StringUtils.hasLength() 是Spring提供的一个工具方法, 判断字符串是否有值
字符串为null或者""时, 返回false, 其他返回true
java
@RequestMapping("/getLoginUser")
public Object t2(HttpSession session) {
String nameValue = (String) session.getAttribute("name");
if (StringUtils.hasLength(nameValue)) return nameValue;
return "";//空字符串是正常对象,不会报空指针,前端可以安全处理。
}
3.2.2 调整前端页面代码
- 调整登录页面login.html
java
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js">
</script>
<script>
function login() {
$.ajax({
type: "post",
url: "/user/login",
data: {
"userName": $("#userName").val(),
"password": $("#password").val()
},
success: function (result) {
if (result) {
location.href = "/index.html"
} else {
alert("账号或密码有误.");
}
}
});
}
</script>
3.2.3 运行测试


3.3 留言板
需求: 界面如下图所示
-
输入留言信息, 点击提交. 后端把数据存储起来.
-
页面展示输入的表白墙的信息

Lombok是一个Java工具库,通过添加注解的方式,简化Java的开发
1. 引入依赖
java
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
2. 使用
lombok通过一些注解的方式, 可以帮助我们消除一些冗长代码, 使代码看起来简洁一些
java
@Data
public class Person {
private int id;
private String name;
private String password;
}
@Data 注解会帮助我们自动一些方法, 包含getter/setter, equals, toString等
3.更快捷的引入依赖
上述引入lombok依赖, 需要去找lombok的坐标
接下来介绍更简单引入依赖的方式
安装插件EditStarter, 重启Idea
4.服务器代码实现
定义留言对象 MessageInfo 类
java
import lombok.Data;
@Data
public class MessageInfo {
private String from;
private String to;
private String message;
}
创建 MessageController类
java
package com.bit.messagewall.controller;
import com.bit.messagewall.demo.MessageInfo;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.ArrayList;
import java.util.List;
@RestController
@RequestMapping("/message")
public class MessageController {
private List<MessageInfo> messageInfos=new ArrayList<>();
@RequestMapping("/getList")
public List<MessageInfo> getList(){
return messageInfos;
}
@RequestMapping("/publish")
public boolean publish(MessageInfo messageInfo){
System.out.println(messageInfo);
if (StringUtils.hasLength(messageInfo.getFrom())
&& StringUtils.hasLength(messageInfo.getTo())
&& StringUtils.hasLength(messageInfo.getMessage())) {
messageInfos.add(messageInfo);
return true;
}
return false;
}
}
3.4 图书管理系统
需求:
-
登录: 用户输入账号,密码完成登录功能
-
列表展示: 展示图书
1.服务器代码
java
@Data
public class BookInfo {
//图书ID
private Integer id;
//书名
private String bookName;
//作者
private String author;
//数量
private Integer count;
//定价
private BigDecimal price;
//出版社
private String publish;
//状态 0-无效 1-允许借阅 2-不允许借阅
private Integer status;
private String statusCN;
//创建时间
private Date createTime;
//更新时间
private Date updateTime;
}
创建 UserController, 实现登录验证接口
java
package com.bit.library.controller;
import com.bit.library.demo.BookInfo;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
@RestController
@RequestMapping("/book")
public class BookController {
public List<BookInfo> mockData() {
List<BookInfo> books = new ArrayList<>();
for (int i = 0; i < 5; i++) {
BookInfo book = new BookInfo();
book.setId(i);
book.setBookName("书籍" + i);
book.setAuthor("作者" + i);
book.setCount(i * 5 + 3);
book.setPrice(new BigDecimal(new Random().nextInt(100)));
book.setPublish("出版社" + i);
book.setStatus(1);
books.add(book);
}
return books;
}
@RequestMapping("/getList")
public List<BookInfo> getList(){
List<BookInfo> books=mockData();
for (BookInfo book:books){
if (book.getStatus()==1){
book.setStatusCN("可借阅");
}else {
book.setStatusCN("不可借阅");
}
}
return books;
}
}
数据采用mock的方式, 实际数据应该从数据库中获取

4. 应用分层
通过上面的练习, 我们学习了Spring MVC简单功能的开发, 但是我们也发现了一些问题
1. 表现层: 就是展示数据结果和接受用户指令的,是最靠近用户的一层;
2. 业务逻辑层: 负责处理业务逻辑, 里面有复杂业务的具体实现;
3. 数据层: 负责存储和管理与应用程序相关的数据

按照上面的层次划分, Spring MVC 站在后端开发人员的角度上, 也进行了支持, 把上面的代码划分为三个部分:
• 请求处理、响应数据:负责,接收页面的请求,给页面响应数据.
• 逻辑处理:负责业务逻辑处理的代码.
• 数据访问:负责业务数据的维护操作,包括增、删、改、查等操作
这三个部分, 在Spring的实现中, 均有体现

• Controller:控制层。接收前端发送的请求,对请求进行处理,并响应数据。
• Service:业务逻辑层。处理具体的业务逻辑。
• Dao:数据访问层,也称为持久层。负责数据访问操作,包括数据的增、删、改、查.
5. 总结
- 学习Spring MVC, 其实就是学习各种Web开发需要用的到注解
a. @RequestMapping: 路由映射
b. @RequestParam: 后端参数重命名
c. @RequestBody: 接收JSON类型的参数
d. @PathVariable: 接收路径参数
e. @RequestPart: 上传文件
f. @ResponseBody: 返回数据
g. @CookieValue: 从Cookie中获取值
h. @SessionAttribute: 从Session中获取值
i. @RequestHeader: 从Header中获取值
j. @Controller: 定义一个控制器, Spring 框架启动时加载, 把这个对象交给Spring管理. 默认返回 视图.