一. Spring Boot 里的 Spring MVC
- Spring Boot 是什么 ?
Spring Boot 在之前的文章中有介绍过, 它是一个快速构建生产级别的 Spring 引用程序的框架, 它基于 Spring Frameword, 并通过自动配置、约点大于配置、快速开发等方式, 开发人员更快地创建出高效、可靠的 Spring 应用程序
- Spring MVC 是什么?
Spring MVC 是基于 Servlet API 构建的原始 Web 框架, 它全称叫做 Spring Web MVC 来自于其源模块的名称( spring-webmvc ), 也就是我们现在常叫做的 Spring MVC
Spring MVC 中的 MVC 全程为 Model View Controller, M-模型, V-视图, C-控制器, 也就是视图模型控制器, 它是一种软件构架模式( 它是后面出现的一种设计模式 )
所以 MVC 是一种思想, 而 Spring MVC 是对 MVC 的一种具体实现
- Spring Boot 和 Spring MVC 之前有什么关系 ?
对于这二者之间, Spring Boot 是构建在 Spring Framework 之上的, 而 Spring MVC 是 Spring Framework 中的一个模块, 因此 Spring Boot 中也包含了 Spring MVC.
具体来说便是, Spring Boot 中默认使用 Spring MVC 作为 Web 开发框架, 并通过自动配置的方式简化了 Spring MVC 的配置过程, 开发人员通过添加正确的依赖和一些简单的配置, 快速的搭建一个完整的 Web 应用程序
因此可以说 Spring Boot 是围绕 Spring MVC 和其他 Spring 模块构建的一个全新引用程序开发框架, 在提供 Spring MVC 功能的同时又提供了额外的便利功能, 使得开发可以更加高效便捷的构建 Web 应用程序
二. Spring MVC 的具体使用
1. Spring MVC 的创建
我们前面说道, Spring MVC 是 Spring 中的一个模块而已, 因此我们在用 Spring Boot 框架开发时, 只需要添加 Spring MVC 的起步依赖就好. 并且通过添加 Spring Web 依赖说明时, 就可以看到 Spring Web 其实就是 Spring MVC
2. 和浏览器建立连接
2.1 @RequestMapping 注解建立连接
通过标准分层, 在controller 包下创建一个 UserController, 并添加一个方法, 方法上添加 @RequestMapping 注解
// 等同于 @ResponseBody 和 @Controller 注解
@RestController // 让 Spring 框架启动时加载, 并且返回的非页面数据
public class UserController {
@RequestMapping("/user1")
public String fun1() {
return "这是 user1 路由下的 fun1 方法";
}
}
启动项目后, 此时通过本地 localhost 就可以访问当前页面了.这样就和浏览器建立了连接
可以看到的是, 这里@RequestMapping 注解是 Spring Web 程序中最常用的一个注解, 它用来注册接口的路由映射的( 当用户访问一个 url 时, 将用户的请求对应到程序中某个类下的某个方法的过程就为路由映射 ).
- 而这里的 @RequestMapping 注解除了上面加在了方法上, 它也可以加载类上, 此时访问的 url 就为类上的路径 + 方法的路径
// 等同于 @ResponseBody 和 @Controller 注解
@RestController // 让 Spring 框架启动时加载, 并且返回的非页面数据
@RequestMapping("user")
public class UserController {
@RequestMapping("/user1")
public String fun1() {
return "这是 user1 路由下的 fun1 方法";
}
}
只访问方法上的路径是不对的, 必须是类路径加方法路径
- 方法上和类上也可以实现多级路由
// 等同于 @ResponseBody 和 @Controller 注解
@RestController // 让 Spring 框架启动时加载, 并且返回的非页面数据
@RequestMapping("user")
public class UserController {
@RequestMapping("/user1/fun")
public String fun1() {
return "这是 user1 路由下的 fun1 方法";
}
}
具体要如何实现, 可以根据自己的需求来实现.
- @RequestMapping 是什么类型的请求 ?
首先, 它肯定是支持 Get 请求的, 因为我们之前访问 localhost 时, 使用的就是 Get 方法. 那它支不支持其他方法呢 ?
// 等同于 @ResponseBody 和 @Controller 注解
@RestController // 让 Spring 框架启动时加载, 并且返回的非页面数据
public class UserController {
@RequestMapping("/user1")
public String fun1() {
return "这是 user1 路由下的 fun1 方法";
}
}
- 使用 Postman 构造一个 Post 请求和 Delete 请求看看, 还能访问这个方法嘛 ?
可以看到的是, @RequestMapping 注解不指定特定路由方法下, 是支持多种路由请求的, 具体有哪些我们可以在源码中看到, 下面这些都是 @RequestMapping 注解支持的路由方法
b. 只允许 Post 请求或者 Get 请求能实现嘛 ?
// 等同于 @ResponseBody 和 @Controller 注解
@RestController // 让 Spring 框架启动时加载, 并且返回的非页面数据
public class UserController {
@RequestMapping(value = "/user1", method = RequestMethod.POST)
public String fun1() {
return "这是 user1 路由下的 fun1 方法";
}
}
在@RequestMapping 里设置 method 属性, 选择路由方法为 POST. 在去通过 Postman 构建一个 Get 请求, 看看还能发送成功嘛 ?
可以看到, 此时它报了 405, 提示方法不被允许, 因此可以看到在 @RequestMapping 注解里, 不仅可以设置多级路由, 还可以限定路由的访问方法
只能由指定的路由方法访问 ! ! !
2.2 其他注解建立连接
上面的指定路由方法建立连接, 在我们的 Spring Web 里提供了更便捷的注解进行连接, 例如只允许 Post 的路由方法
@PostMapping("/user2")
public String fun() {
return "这是 user2 路由下的 Post治党路由方法";
}
还是一样, 当我们去通过 Postman 进行构造一个 GET 方法访问时, 是不被允许的
只有 POST 方法才可以, 其他路由方法都不行
3. 获取参数
建立连接后, 对于我们的项目中, 最重要的就是获取 web 项目中的一些参数.
3.1 获取 url 参数
@RequestMapping("/user3")
public String getParameter(String name) {
return "这是获取到的参数 : " + name;
}
建立路由方法, 启动项目后访问, 此时我们并没有传入参数, 因此此处为默认值 ( 需要注意, 包装类的默认值也是 null )
当我们 url 中传入参数时, 可以正确获取到参数属性
需要注意的时, 必须和方法的参数一致, 才能在 url 中获取到正确的属性值, 当参数为 user 时, 与方法中的参数对不上, 因此无法正确获取
**既然可以获取一个参数, 那么是不是也可以获取两个参数呢 ? **
@RequestMapping("/user5")
public String getParameter1(String name, String password) {
return "获取到的 name : " + name + " password : " + password;
}
注意, 此时我在 url 中是先输入的 password 后 输入的 name 但是, 并不会影响正确获取参数, 因为此处是通过 key 和 value 去匹配的, 只需要 key 正确即可匹配对应的 value
之前我们 Servlet 是通过 request 去获取 url 的参数的, 而 Spring MVC 值基于 Servlet API 的, 因此 Servlet 的方法也是可以在这里使用的
@RequestMapping("/user4")
public String getParameter1(HttpServletRequest request, HttpServletResponse response) {
return "这是获取到的参数 : " + request.getParameter("name");
}
这里的 request 和 response 等同于是框架内置的, 可以直接使用, 因此这里也是可以正确获取的
3.2 获取 url 对象
Web 项目中除了获取 URL 参数外, 获取对象也是非常常见和重要的, 下面就来看如何获取一个对象
- 先构建一个实体类 userInfo
@Data
public class UserInfo {
private int age;
private String name;
private String password;
}
- 构造路由方法
@RequestMapping("/reg")
public Object reg(UserInfo userInfo) {
return userInfo;
}
- 访问路由方法观察
是不是特别简单 ? 并且, 我们前端传来的是参数, 怎么会变成了对象呢 ?** 原因就在于我们的框架帮我们做了这事, 当程序执行后, Spring MVC 会自动将我们的参数对象映射, 只需要我们传入的属性和对象拥有的属性一致, 框架会根据返回的类型, 自动帮我们匹配是返回标签 还是 JSON 对象返回**
例如 :
@RequestMapping("/html")
public Object reg1() {
return " 这是一个 HTML 标签
";
}
在抓包确认一下, 框架自动将我们返回的 Object 对象转为需要的 HTML 形式了
3.3 参数重命名( 前端传入参数和后端参数不一致 )
3.3.1 @Value 注解
在生产中, 前后端的约定不一致时, 比如在注册中, 前端传入的是 username 而后端规定要使用的为 name, 这时候怎么办 ? 如果还用之前的方法, 那么我们将无法获取到对应的属性值, 为了解决这个问题, Spring Boot 框架提供了 @Value 注解, 将指定属性获取后赋值给指定参数, 例如下面 :
@RequestMapping("/reg1")
public Object reg1(@Value("username") String name, String password) {
return "name : " + name + " password : " + password;
}
此时, 我们方法中的参数为 name, 但 url 中传入的为 username 但是同样正确获取了 username 的参数. 由于我们此处的重命名, 将 username 属性对应的值获取后赋值给了 name 属性
3.3.2 @RequestParam 注解
使用 @RequestParam 注解同样可以重命名参数, 但是 @RequestParam 注解还有一个别的功能, 设置参数是否为非必传参数
@RequestMapping("/reg2")
public Object reg2(@RequestParam("username") String name, String password) {
return "name : " + name + " password : " + password;
}
同样, 使用@RequestParam 是否能够重命名呢 ? 运行后发现是可以的
那么, 刚刚说的是否为非必传参数又是怎么回事呢 ? 当我们不传入 password, 预期应该是 password 为 null, 并且可以正确访问
当我们尝试不传 username 属性看看, 预期是否和之前一样, 不传入那么默认为 null 呢 并且可以正确访问呢 ?
可以看到, 是无法正确访问的, 并且还给我们提示了报错信息 : username 为一个该方法的必传参数
因此, 在重命名时, 如果使用 @RequestParam 注解会带来一个必传参数的问题, 那么, 如何解决这个问题呢 ?
我们可以在 @RequestParam 里面添加 required 属性, 可以看到, 默认是 true 的代表这是一个必传参数, 当设置为 false 时, 此时这个重命名的参数就不是必传的了
@RequestMapping("/reg2")
public Object reg2(@RequestParam(value = "username", required = false) String name, String password) {
return "name : " + name + " password : " + password;
}
此时不传入重命名后的 username 也可以正确访问了.
3.4 获取 JSON 对象
前面获取的参数, 都是 URL 的形式获取的参数, 哪前端传来的是 JSON 对象, 那么该如何接受获取呢 ?
对于 UserInfo 这个对象, 如果传入的是一个 JSON 格式的 UserInfo, 在用之前的获取方式还能行得通吗 ?
@RequestMapping("/reg")
public Object reg(UserInfo userInfo) {
System.out.println(userInfo);
return userInfo;
}
利用 Postman 构造请求提交一个 UserInfo 到后端 :
可以看到, 虽然传入了一个 UserInfo 对象, 但是后端根本没有收到正确的对象, 该怎么解决这问题呢 ?
3.4.1 @RequestBody 注解
在 Spring MVC 里提供了 @RequestBody 注解来接收传来的 JSON 对象
@RequestMapping("/reg3")
public Object reg3(@RequestBody UserInfo userInfo) {
return userInfo;
}
利用 Postman 构造请求 :
可以看到, 后端是成功的拿取到了前端传来的这个 JSON 格式的对象
对于前端传来的 JSON 对象, Spring Boot 框架和之前一样, 会将拿到的信息赋值给参数, 并自动适配数据格式
3.5 路径传参
3.5.1 @PathVariable 注解
这样传入参数, 和之前 URL 传参中 user?username=张三&password=123 来说, 搜索引擎抓取关键字的权重更高, 能让人更多的搜索到. 同时当传入的参数更多时, Path 传参的 URL 更简洁.
@RequestMapping("/reg4/{username}/{password}")
public String reg3(@PathVariable String username, @PathVariable String password) {
return "username : " + username + " password : " + password;
}
通过 path : reg/4/zhangsan/123 访问, 此处username 就对应到了 zhangsan, password 就对应到了 123, 但这里并不是键值的关系, 因此它对位置是非常敏感的, 无法调换位置, 比如下面这段代码, 此时在去通过刚刚的 Path 获取是无法正确获取的
可以看到, 此时username=123, 而password=zhangsan, 所以 Path 路径的参数它不是键值的关系, 受到 URL 位置对应影响, 这点需要注意
3.5.2 @PathVariable 注解的重命名
对于 @PathVariable 也是有重命名规则的, 并且它还和我们之前的 @RequestParam 注解一样, 有则必传参数的原则, 当设置了 @PathVariable 注解后, 该参数就为必传参数, 如果不需要可以将 required 属性设置为 false.
@RequestMapping("/reg4/{username}/{password}")
public String reg3(@PathVariable String username, @PathVariable String password) {
return "username : " + username + " password : " + password;
}
当我们不传入password 时, 再去访问就会报错, 想要 password 不传, 在 @PathVariable 参数后面设置 required 属性为 false 即可
在来看看 @PathVariable 重命名, 还是和前面的重命名一样的
同样还是重命名后, 将获取到的 name 的属性值赋值给 username 使用
3.6 获取上传文件
前端除了传来参数、对象意外, 还会传来一些文件, 因此对于如何获取上传的文件也是非常重要的
3.6.1 MultipartFile 对象
@RequestParam 注解中, 可以设置参数名称, 例如我设置的 myimg, 而 MultipartFile 接口代表了上传文件, 用于处理 HTTP 请求中上传文件的相关操作, 在 Spring MVC 中通常会将上传文件包装成 MultipartFile 对象进行处理
@RequestMapping("/upload")
public String upLoad(@RequestParam("myimg") MultipartFile file) throws IOException {
File saveFile = new File("E:\Javacode\spring\spring-mvc\myimg.png"); // 文件保存的路径
try {
file.transferTo(saveFile); // 上传文件 - 本身是没有返回值的, 通过 try catch 来检验是否上传成功
return "文件上传成功! ";
}catch (IOException e) {
e.printStackTrace();
}
return "文件上传失败 !";
}
利用 Postman 构造上传文件
当上传成功后, 可以在之间设置的保存文件路径中查看是否成功上传保存
同样, 该图片是可以正常打开的
PS : 需要注意的是, 图片上传的单次文件默认最大大小为 1MB, 单次请求默认最大大小为 10MB, 如果需要上传更大的, 可以在配置文件中进行设置
# 设置单次图片最大大小
spring.servlet.multipart.max-file-size=100MB
# 设置单次请求最大大小
spring.servlet.multipart.max-request-size=100MB
虽然上面的代码是成功的获取了前端传来的文件, 但是它并不适用于生产之中, 原因在于每次上次的文件都会进行覆盖上一个文件, 因为它的名称是一样的, 会在相同路径下保存同一个名称的文件而覆盖上一个文件, 因此不具备生产能力. 那么, 要如何解决这个问题, 让每次生产的文件都不一样呢 ?
想要不重覆, 可以用时间戳、 时间戳加随机数、系统时间等方法, 但是尽管时间戳在不断变化, 但是当并发量够大时, 总有可能出现二者相同时间上传文件, 导致其重复的. 为了更好地解决上面问题, 我们引入了 UUID( 通用唯一标识符 ) .
文件名解决后, 还需要解决一件事, 那就是文件的后缀名, 之前我们用的是固定的文件后缀名 .png, 但是用户上传的文件是很多的, 后缀名不是固定的, 因此我们要想办法获取到用户上传的后缀名并且将获取到的后缀名添加到保存的文件后缀中.
@RequestMapping("upload2")
public String upload2(@RequestParam("myimg") MultipartFile file) {
// 创建文件名名称
String fileName = UUID.randomUUID() + //文件名
file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(".")); // 后缀名
// lastIndexOf 左闭右开需要注意, 包含分割后的 . 的
File saveFile = new File("E:\Javacode\spring\spring-mvc\" + fileName); // 保存文件
try {
file.transferTo(saveFile); // 上传文件
return "上传文件成功 !";
}catch (IOException e) {
e.printStackTrace();
}
return "上传文件失败";
}
启动后用 Postman 构建表达请求发送文件
查看是否发送成功
同时检查我们的文件是否保存在对应路径之中, 可以看到此时文件名已经正确保存并且生成了一串文件名非常复杂的名称, 这个就是 UUID 生成的
即使多次上传同一文件, 它也不会覆盖
3.7 获取 Cookie (@CookieValue 注解)
之前说过, Spring MVC 是基于 Servlet API 的, 因此之前的获取 Cookie 的方法任然可以用, 不同的是用 Servlet 里的 request 去获取 Cookie 获取的是整个页面的所有 Cookie, 也就是得到的是一个 Cookie 数组
@RequestMapping("getCK")
public void getCookie(HttpServletRequest request) {
Cookie[] cookies = request.getCookies();
}
如果想要去获取指定的某个 Cookie, 还需要去遍历这个 Cookie 数组去拿取指定的 Cookie, 会比较麻烦, 因此在 Spring MVC 中提供了 @CookieValue 注解, 该注解添加后, 一样是一个必传参数, 并且前端必须有这个 Cookie 否则会错误, 也可以通过设置 required 属性来解决这个问题
@RequestMapping("getCK2")
public String getCookie(@CookieValue("spring") String spring) {
return spring;
}
由于并没有事先在浏览器中构造一个名为 spring 的 cookie, 因此此时直接去访问时获取不到的
由于我们并没有指定的 Cookie, 因此利用浏览器开发者工具伪造一个 Cookie 来进行验证
构造好后, 再去获取就可以成功拿取到了
3.8 获取 Header (@RequestHeader 注解)
请求头里面包含了许多信息, 通过@RequestHeader 注解也是很容易获取到的, 先来回顾一下 Header 请求头中都有哪些字段 ( 下面是我的浏览器中的请求字段 )
这些字段都是可以通过 @RequestHeader 注解来获取的, 尝试获取一下当前的 Host, 还和之前一样, 获取注解里面指定的 Host 属性后赋值给 host 变量
@RequestMapping("getHD")
public String getHeader(@RequestHeader("Host") String host) {
return host;
}
可以看到, 我当前的 Host 如下 :
其他的字段也是同样的获取方法, 只需修改获取的指定属性就行, 大家可以试试 !
3.9 获取 Session (@RequestAttribute 注解)
在那之前, 先回顾之前 Servlet 如何创建会话, 由于此处我没有具体的业务代码, 因此我手动建立一个会话 SESSION_KEY
private static final String SESSION_KEY = "DEFAULT_VALUE";
@RequestMapping("/createSession")
public void CreateSession(HttpServletRequest request) {
HttpSession session = request.getSession(true); // 没有 session 就创建 session
session.setAttribute(SESSION_KEY, "username"); // 将username存入到会话中
}
通过 Servlet 来获取 Session, 在获取之前先要运行建立会话路由方法才能有会话可获取
@RequestMapping("/getSession")
public String getSession2(HttpServletRequest request) {
HttpSession session = request.getSession(false); // 有则获取, 没有就不建立
return " Servlet 获取 : " + (String) session.getAttribute(SESSION_KEY);
}
访问 Servlet 获取 Session 的路由方法
访问注解方式获取 Session 的路由方法
4. 返回数据
获取前端的参数非常重要, 但是给前端返还数据一样是很重要的, 下面就来看几个常见的返回数据
4.1 返回静态页面
static 包底下创建一个 spring.html 文件, 并在文件中写一个 Spring MVC 标签
DOCTYPE html>
html lang="en">
head>
meta charset="UTF-8">
meta http-equiv="X-UA-Compatible" content="IE=edge">
meta name="viewport" content="width=device-width, initial-scale=1.0">
title>Documenttitle>
head>
body>
h1>Spring MVCh1>
body>
html>
之前我们返回的都是数据, 而并非页面, 因此我们使用了 @RestBody 注解 或者 @RestController 注解, 但此处我们要返回的就是一个静态页面, 而并非数据, 因此此处不需要在加上面的注解了
@Controller
@RequestMapping("/test")
public class ControllerDemo {
@RequestMapping("/getHtml/test2")
public String getHtml() {
return "spring.html"; // 返回的液面必须与创建的静态页面名称一致
}
}
根据路由方法访问, 结果出现了 404, 根本获取不到对应的页面
这是为什么 ? 当我们在返回的 spring.html 中不加上斜杠, 也就是 “/spring.html” 时, 去访问路由方法并不会在根目录底下去获取, 因此获取不到指定页面, 当我们加上 ” / ” 以后, 就会让它在根目录底下去获取, 从而可以正确拿到返回的页面
4.2 返回 JSON 对象
除了静态页面, 后端也会经常给前端返回 JSON 格式的对象
@RestController
@RequestMapping("/resJson")
public HashMapString, String> resJson() {
HashMapString, String> map = new HashMap>();
map.put("zhangsan", "1");
map.put("lisi", "2");
return map;
}
还是一样的, 加了注解以后, Spring Boot 框架会自动将你返回的数据进行包装.
4.3 请求转发和请求重定向
Spring MVC 天生就是返回静态页面的, 因此对于重定向、转发等返回是很容易的.
@Controller
public class ControllerDemo {
// 请求转发
@RequestMapping("/fw")
public String index2() {
return "forward: /spring.html";
}
}
有没有发现, 这和我们之前返回静态页面好像是一样的 ? 其实不然, 当我们访问路由返回的是一个静态页面时, 由于我们访问的是接口, 而你要返回一个静态页面, 实际上就是一个请求转发的过程.
看看请求重定向 :
@Controller
public class ControllerDemo {
// 请求重定向
@RequestMapping("/re")
public String index() {
return "redirect: /spring.html";
}
}
惊奇的发现, 我们访问重定向的路由地址不是 localhost:8080/re 嘛 ? 怎么页面展示出来结果后, URL 就变成了 localhost:8080/spring.html, 我们可以抓包看看
可以看到, 当访问路由地址时, 它直接 302 给我们跳转到了 spring.html 页面.
那么, 请求转发和请求重定向有什么区别呢 ?
- 请求转发地址没变, 而请求重定向后地址会发生改变
- 请求转发由服务端转发, 而请求重定向重新定位到资源
- 请求转发由服务端转发, 有可能造成原外部资源不能访问; 而请求重定向与直接访问新地址效果一样, 不存在原来外部资源不能访问
请求转发由服务器内部进行转发, 用户的感知是不明显的, 因为它的地址并没有发生变化, 但是给你展示的是你想要的页面; 但是重定向用户感知是比较明显的, 它类似于通过他人之手, 直接让你去找谁谁谁( 你重定向的位置 ).
外部资源不能访问 : 经过服务器进行转发的, 服务器并不像你重定向一样明确的知道你要的是什么, 有可能会出错, 并不觉得清楚你需要什么. 当层级目录过多, 服务器去转发一个静态页面的时候有可能就会出错.