文章目录
- 一、什么是 Spring MVC
- (一)MVC的定义
- (二)MVC 和 Spring MVC 的关系
- (三)为什么要学习 Spring MVC
- 二、Spring MVC使用方法和技巧
- (一)Spring MVC 创建和连接
- (二)实现用户和程序的映射
- 1. @RequestMapping
- 2. @RequestMapping可以处理哪种请求
- 3. @GetMapping 和 @PostMapping
- (三)服务器端获取到用户的请求参数
- 1. 获取单个参数
- 2. 获取多个参数
- 3. 获取对象
- 4. 后端参数重命名
- 5. @RequestBody 接收 JSON 对象
- 6. 获取 URL 中参数 @PathVariable
- 7. 获取文件
- 8. 保存文件优化
- 9. 获取Cookie/Session/header
- (四)服务器端将结果返回给用户
- 1. 返回静态页面
- 2. 返回一个非静态页面的数据
- 3. 返回数据的格式
- 4. 请求转发和请求重定向
一、什么是 Spring MVC
首先Spring MVC 的全程是 Spring Web MVC,其次根据官方解释,Spring MVC 有两个关键要素:
- Spring MVC 是一个 Web 框架
- Spring MVC 是基于 Servlet API 构建的
(一)MVC的定义
MVC 是 Model View Controller 的缩写,它是软件工程中的一种软件架构模式,他把软件系统分为模型、视图和控制器三部分
- Model(模型):是应用程序中用于处理应用程序数据逻辑的部分,通常模型对象负责在数据库中存取数据
- View(视图):是应用程序中处理数据显示的部分。通常视图是是依据模型数据创建的
- Controller(控制器):是应⽤程序中处理⽤户交互的部分。通常控制器负责从视图读取数据,控制⽤户输⼊,并向模型发送数据
(二)MVC 和 Spring MVC 的关系
MVC 是⼀种思想,⽽ Spring MVC 是对 MVC 思想的具体实现
总的来说,Spring MVC 是⼀个实现了 MVC 模式,并继承了 Servlet API 的 Web 框架。既然是 Web 框架,那么当⽤户在浏览器中输⼊了 url 之后,我们的 Spring MVC 项⽬就可以感知到⽤户的请求
(三)为什么要学习 Spring MVC
现在绝大部分 Java 项目都是基于 Spring(或 Spring Boot)的,而 Spring 的核心就是 Spring MVC。在创建 Spring Boot 项⽬时,我们勾选的 Spring Web 框架其实就是 Spring MVC 框架。咱们之所以要学习 Spring MVC 是因为它是⼀切项⽬的基础,我们以后创建的所有 Spring、Spring Boot 项⽬基本都是基于 Spring MVC 的
二、Spring MVC使用方法和技巧
(一)Spring MVC 创建和连接
我们上面说过,Spring MVC框架实际就是 Spring Web 框架,因此这里创建 Spring MVC 项目和创建 Spring Boot 项目步骤相同,我们只需要注意添加 Spring Web 框架即可
更重要的是,由于 Spring MVC 框架是 Web 框架,因此实现前后端交互是必然的,学习如何实现前后端交互才是我们的重中之重
(二)实现用户和程序的映射
1. @RequestMapping
@RequestMapping 是 Spring Web 应用程序中最常用的注解之一,它是用来注册接口的路由映射的
路由映射:当一个用户访问一个 url 时,将用户的请求对应到程序中某个类的某个方法的过程就叫路由映射
使用方法:
如下方代码和运行结果所示,@RequestMapping 注解是用来设置访问路由的,它既可以修饰类,也可以修饰方法,当它只修饰方法时,那么只需要方法路由就可以访问,当它同时修饰类和方法时,访问的地址必须是类的路由 + 方法路由
类和方法都被修饰
@Controller
// 在高版本的 Spring Boot 这里不加"/"也可以访问到,但是低版本不行,因此为了兼容,作者建议加上
@RequestMapping("/user")
// 作用:表示返回的是一个非静态页面的数据
// 如果不加,默认情况下返回的是一个静态页面
@ResponseBody
public class UserController {
@RequestMapping("/hello")
public String hello() {
return "hello world!";
}
}
运行结果:
只有方法被修饰
@Controller
// 作用:表示返回的是一个非静态页面的数据
// 如果不加,默认情况下返回的是一个静态页面
@ResponseBody
public class UserController {
@RequestMapping("/hello")
public String hello() {
return "hello world!";
}
}
运行结果:
2. @RequestMapping可以处理哪种请求
我们上述的访问都是通过浏览器的地址栏直接访问,而这种请求格式都是 GET 请求, 关于 POST 请求,我们使用 PostMan 进行测试,如图:
由上图可知,@RequestMapping 同样可以处理 POST 请求,因此我们说,它既可以处理 GET 请求,也可以处理 POST 请求
在某种情况下,可能要求我们只能处理 GET/POST 请求,那么我们就需要对 @RequestMapping 进行属性设置,接下来给大家演示让 @RequestMapping 只能接收处理 POST 请求的设置:
@Controller
// 作用:表示返回的是一个非静态页面的数据
// 如果不加,默认情况下返回的是一个静态页面
@ResponseBody
public class UserController {
// 此时路由也必须明确是 value
@RequestMapping(value = "/hello", method = RequestMethod.POST)
public String hello() {
return "hello world!";
}
}
测试结果:
GET请求返回报错:
POST请求正常返回hello world!:
3. @GetMapping 和 @PostMapping
顾名思义,@GetMapping 和 @PostMapping 同样也可以注册接口的路由映射,但是与 @RequestMapping 不同的是,它俩要么只能支持 GET 请求,要么只能支持 POST 请求,写法与 @RequestMapping 一致
(三)服务器端获取到用户的请求参数
1. 获取单个参数
在 Spring MVC 中我们可以直接用方法中的参数来实现传参,如下代码:
@RequestMapping("/getuserbyid")
// 参数获取时必须使用包装类,避免异常的发生
// 当前端传过来一个 null,避免直接在传参阶段就异常
// 而是应该由我们对它进行非空校验,给前端返回一个更友好的界面
public User getUserById(Integer id) {
// 设置非空校验
if(id == null) {
return null;
}
User user = new User();
user.setId(id);
user.setUsername("zhangsan");
user.setSex("男");
return user;
}
测试结果;
抓包结果:
由上述测试结果和抓包结果我们可以验证三点:
- 直接设置方法参数获取单个参数的方式是可行的
- 要想成功的传递参数,参数的key值必须与方法参数中的名称一致
- 在我们返回的结果是一个对象时,Spring MVC 会自动将返回结果转为一个 json 格式的数据
2. 获取多个参数
获取多个参数的方法其实和获取单个参数的方法一致,只需要多设置几个方法参数即可,代码示例:
@RequestMapping("/login")
public String login(String username, String password) {
return "username: " + username + " | password: " + password;
}
测试结果:
由上述结果又可以证明两点:
- 设置多个方法参数就能实现多个参数的获取
- 获取参数与参数的顺序无关,而是与传参的 key 值有关
3. 获取对象
当前端传过来很多的数据的时候,此时再通过多个参数获取方式进行获取虽然可行,但是极其麻烦,因此我们就可以创建一个对象,将那些参数都作为该对象的属性,通过获取对象,让 Spring MVC 自动将对象中的属性进行赋值,代码示例:
@Data
public class User {
private Integer id;
private String username;
private String password;
private String sex;
}
@RequestMapping("/logon")
public String logon(User user) {
return "用户信息:" + user;
}
测试结果:
由上述运行结果可知,上述的说法是正确的,我们只需要保证对象中的属性名与传过来的参数 key 值一致即可
4. 后端参数重命名
其实我们是可以通过设置使得前后端参数名不一致也能正确接收的,这个设置的出现一方面解决了可能因为前后端沟通不足或有分歧导致接口参数名不一致的问题,另一方面也使得程序猿可以花费极少的时间去处理参数名可能不一致的问题。核心注解就是:@RequestParam,代码示例:
@RequestMapping("/login")
public String login(@RequestParam("name") String username, String password) {
return "username: " + username + " | password: " + password;
}
测试结果:
但是这种方法不适用于对象中的属性
除了属性重命名的功能之外,@RequestParam 还有很重要的注意事项,那就是如果加了 @RequestParam ,那么默认情况下,该参数就被设为必传参数,如果不传,就会报错,如图所示:
这种情况,我们可以通过设置属性取消这种必须性,代码示例:
@RequestMapping("/login")
public String login(@RequestParam(value = "name", required = false) String username, String password) {
return "username: " + username + " | password: " + password;
}
5. @RequestBody 接收 JSON 对象
经过测试,我们发现使用之前通过设置方法参数来获取参数的方法接收 JSON 数据时,接收不了,如下图:
因此我们就引出 @RequestBody 注解,通过这个注解来获取 JSON 中的数据,但是这个注释也有一些注意事项
1. 该注解在一个方法中只能被使用一次
代码示例:
@RequestMapping("/login")
public String login(@RequestBody String username,@RequestBody String password) {
System.out.println(password);
return "username: " + username + " | password: " + password;
}
测试结果:
2. 该注解可以被 String 类型接收,但是没有意义,一般我们都是使用对象进行接收
代码示例:
@RequestMapping("/login")
public String login(String username,@RequestBody String password) {
System.out.println(password);
return "username: " + username + " | password: " + password;
}
测试结果:
可以看到,这个结果并不是我们想要的
3. 正确使用方法,使用对象接收,对属性进行赋值
代码示例:
@RequestMapping("/logon")
public String logon(@RequestBody User user) {
return "用户信息:" + user;
}
测试结果:
6. 获取 URL 中参数 @PathVariable
获取 URL 中参数并不是指 ? 后面附带的参数,而是作为路由,是 URL 中路由的名字。听起来有点奇怪,明明我们可以通过那种普通的方式就能发送和接收参数,为什么还要搞这么复杂,在路由中设置特殊的参数,其实,这么做也是有好处的。我们在使用搜索引擎时,URL 的变动性一定是比参数的变动性小得多,因此 URL 中的信息权重就是更高,那么用户的搜索结果中该地址的信息就会被放到前面
核心用法:
- 给 URL 中的路由参数设置对应的方法参数,并在参数前加上 @PathVariable
- 在设置路由时,将我们需要用到的路由参数加上,并用"{}"包住
代码实现:
@RequestMapping("/hello/{name}/{password}")
public String hello(@PathVariable String name, @PathVariable String password) {
return "name: " + name + " | password: " + password;
}
测试结果:
7. 获取文件
在传过来的信息中,还有很重要的一个信息就是文件,包括图片、视频、音频等数据流,此时我们想要接收,就要借助 @RequestPart 注解和专门用来接收文件的文件类 MultipartFile
核心用法:
- 设置一个 MultipartFile 类的参数,参数名无所谓
- 在 MultipartFile 参数前加上注解 @RequestPart,可以指定过来参数的名称
- 将接收到的流对象保存到本地
代码实现:
/**
* 修改图片
* @param id 用户id
* @param file 上传的文件
* @return 修改结果
*/
@RequestMapping("/updateimg")
// 在添加 @RequestPart 注解的同时,说明前端传过来的文件参数 key 值是 img
public boolean updateImg(Integer id, @RequestPart("img") MultipartFile file){
boolean result = false;
// 此时已经获取到图片文件,将文件保存到本地
// 同时规定保存的文件名和文件类型
try {
// transferTo 需要处理异常
file.transferTo(new File("D:\\Data\\img.png"));
result = true;
} catch (IOException e) {
// 可以对其进行日志打印练习
log.error("上传图片失败: " + e.getMessage());
}
return result;
}
Postman发送文件的方式:
测试结果:
8. 保存文件优化
上述保存文件的路径和名称是我们将其写死的,这在实际开发中,几乎没什么用,因此我们需要进行三点优化:
- 设置保存路径(不同环境下路径不同,不能写死)
- 设置图片名称(图片名不能重复)
- 获取原上传图片的格式(不至于说只有.png这么一种)
- 除上述三点优化之外,实际开发过程中还要做一些校验,例如明确文件格式只能是哪几种,文件的最大大小是多少等等,这些校验优化这里不做说明
解决方案:
- 设置保存路径,我们可以通过不同的配置文件来配置不同的保存路径
- 设置图片名称
解决方法:UUID类中的randomUUID()
方法
百度百科:UUID是指在一台机器上生成的数字,它保证对在同一时空中的所有机器都是唯一的。通常平台会提供生成的API。按照开放软件基金会(OSF)制定的标准计算,用到了以太网卡地址、纳秒级时间、芯片ID码和随机数
代码示例: - 获取原上传图片的格式
MultipartFile的类中提供了一个方法getOriginalFilename()
,该方法可以获取到原文件的文件名,那么我们只需要截取最后一个"."到末尾的文件名即可
代码示例:String fileName = file.getOriginalFilename(); fileName = fileName.substring(fileName.lastIndexOf('.'));
最终代码示例:
// 在公共配置文件中设置当前环境的配置文件
// 并获取当前配置文件中的图片保存地址
@Value("${img.path}")
private String imgPath;
@RequestMapping("/updateimg")
// 在添加 @RequestPart 注解的同时,说明前端传过来的文件参数 key 值是 img
public boolean updateImg(Integer id, @RequestPart("img") MultipartFile file){
boolean result = false;
String fileName = file.getOriginalFilename();
fileName = fileName.substring(fileName.lastIndexOf('.'));
// 图片文件名称
fileName = UUID.randomUUID().toString() + fileName;
// 此时已经获取到图片文件,将文件保存到本地
// 同时规定保存的文件名和文件类型
try {
// transferTo 需要处理异常
file.transferTo(new File(imgPath + fileName));
result = true;
} catch (IOException e) {
// 可以对其进行日志打印练习
log.error("上传图片失败: " + e.getMessage());
}
return result;
}
测试结果:
最终,图片就被保存在相应的目录下且文件名绝对不会重复
9. 获取Cookie/Session/header
Spring MVC由于自身就是在 Servlet API 的基础上做成的框架,因此天然支持原来 Servlet 的 HttpServletRequest 和 HttpServletResponse,只需要在方法的参数位置写明即可,这里介绍的是通过注解的方式进行获取
1. Cookie的获取
传统的Servlet是将整个 cookie 都拿到了之后手动查找,而在 Spring MVC 里,我们可以通过 @CookieValue 的方式获取我们要的 cookie
获取Cookie代码示例:
@RequestMapping("/cookie")
public String getCookie(@CookieValue("zhangsan") String cookie) {
return "cookie value: " + cookie;
}
运行结果:
传统的获取方式和 Spring MVC 的获取方式各有好处,如果我们想要获取的 Cookie 只有一两个,那么使用注解会更好,但是如果我们想要获取的数量比较多,那么传统的方式对我们来说更加友好
2. Header 信息的获取
与获取 Cookie 类似,代码示例:
@RequestMapping("getua")
public String getHeader(@RequestHeader("User-Agent") String userAgent) {
return "User-Agent:" + userAgent;
}
测试结果:
3. 存储和获取Session
存储 Session,Servlet 和 Spring MVC的做法是一致的,示例代码如下:
@RequestMapping("setsion")
public boolean setSession(HttpServletRequest req) {
// 创建 HttpSession 对象
// boolean 值 true 表示如果没有 session,就创建一个
// false 正好相反,没有就不创建
HttpSession session = req.getSession(true);
// 设置 session 的值
session.setAttribute("user", "user");
return true;
}
设置 Session 之前:
设置 Session 之后:
通过fiddler抓包我们可以看到,返回的响应里面比平常新增加了一个header项,就是Set-Cookie
,如下图所示:
存储 Session 只有一种方法,但是获取 Session 有两种,分别是 Servlet 和 Spring MVC
Servlet写法:
@RequestMapping("/getsion")
public String getSession(HttpServletRequest req) {
String result = null;
HttpSession session = req.getSession(false);
if(session != null && session.getAttribute("user") != null) {
result = (String) session.getAttribute("user");
}
return result;
}
Spring MVC 写法:
@RequestMapping("/getsion2")
public String getSession2(@SessionAttribute(value = "user", required = false) String key) {
return "会话:" + key;
}
这里要注意一点:@SessionAttribute 同样也是默认要求参数必须不为 null ,因此我们需要对它的 required 属性进行设置
(四)服务器端将结果返回给用户
1. 返回静态页面
Spring MVC 和 Spring Boot 默认情况下返回的就是视图(xxx.html),静态页面默认情况下是放在resources.static
包下,我们不需要设置更多的东西,代码示例:
@Controller
@RequestMapping("/test")
public class TestController {
@RequestMapping("/hi")
public String hello() {
return "/hi.html";
}
}
注意,这里返回的静态页面 前必须加上 “/”,如果不加"/",当我们只给方法添加 @RequestMapping 注解时没问题,可以正常访问静态页面,但是一旦给类也加了 @RequestMapping,那么此时就会报出404!!!作者因为这个问题在这里困扰了很久,希望大家能吸取教训
运行结果:
2. 返回一个非静态页面的数据
1. 使用 @ResponseBody 注解
该注解修饰类,表示当前类中的所有方法都会返回一个非静态页面的数据;修饰方法,表示当前方法会返回一个非静态页面的数据
代码示例:
@Controller
@RequestMapping("/test")
@ResponseBody
public class TestController {
@RequestMapping("/hi")
public String hello() {
return "/hi.html";
}
}
运行结果:
2. 使用 @RestController
该注解相当于一个组合注解,它自身被 @Controller 和 @ResponseBody 修饰,因此集成了二者的功能,一个注解就能搞定控制器注入和设置返回非静态页面的功能
代码示例:
@RequestMapping("/test")
@RestController
public class TestController {
@RequestMapping("/hi")
public String hello() {
return "/hello.html";
}
}
测试结果:
3. 返回数据的格式
Spring MVC是非常智能的,并不需要我们添加什么特殊的注解,直接根据返回值来决定返回的类型
1. 返回text/html数据
代码示例:
@RequestMapping("/html")
public String getHtml() {
return "<h1>这是一条text/html格式的数据</h1>";
}
运行结果:
抓包结果:
2. 返回 json 数据
代码示例
@RequestMapping("/json")
public Map<String, String> getJson() {
Map<String, String> map = new HashMap<>();
map.put("zhangsan", "123");
map.put("lisi", "456");
map.put("wangwu", "789");
return map;
}
运行结果:
抓包结果:
4. 请求转发和请求重定向
return 不仅可以返回一个视图,还可以实现跳转,跳转的方式有两种:
- forward 请求转发
- redirect 请求重定向
1. 请求转发 forward
代码示例:
// 实现方式1
@RequestMapping("/hi2")
public void hi2(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
// req的获取请求调度器
// forward需要传递 req 和 resp 参数
req.getRequestDispatcher("/hello.html").forward(req, resp);
}
// 实现方式2
@RequestMapping("/hi")
public String hi() {
return "forward:/hello.html";
}
// 更简单的方式,直接返回就是请求转发
@RequestMapping("/hi")
public String hi() {
return "/hello.html";
}
运行结果:
抓包结果:
由上图可知,请求转发是服务器自己去获取静态页面,然后将静态页面的内容返回回来,因此前端只需要发送一次请求
2. 请求重定向 redirect
代码示例:
// 实现方式1
@RequestMapping("/hello2")
public void hello2(HttpServletResponse resp) throws IOException {
resp.sendRedirect("/hi.html");
}
// 实现方式2
@RequestMapping("/hello")
public String hello() {
return "redirect:/hi.html";
}
运行结果:
抓包结果:
由上图可知,请求重定向的执行过程是服务器向客户端发送了一个请求重定向的响应,并告诉客户端请求重定向的路径,由抓到的包为302
包可证明,然后客户端再向服务器新的 url 发送一个请求获得响应,最后浏览器 url 上面展示的也就是新的 url 地址,整个过程需要客户端发送两次请求
forward VS redirect
- 定义不同
请求转发发生在服务器程序内部,当服务器收到一个客户端的请求之后,将请求转发到目标地址,获得响应之后,将响应内容返回到客户端
请求重定向发生在服务器和客户端之间,当服务器收到客户端的请求之后,向客户端发送一个302
暂时重定向响应,客户端根据重定向的 url 重新发送一个请求,并获取响应 - 请求方不同
请求转发时新地址的请求方是服务器,请求重定向时新地址的请求方是客户端 - 请求转发必须是在同一台服务器下完成,重定向可以在不同的服务器下完成
- 最终 url 地址不同
请求转发的地址仍是第一次请求时的 url 地址,请求重定向的地址是最终跳转的 url 地址 - 代码实现不同
如上面代码所示