本文源码位置: Spring-MVC
1. Spring MVC 概要
摘自Spring官方: 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), but it is more commonly known as “Spring MVC”.
上面一大段话主要讲的是:Spring Web MVC是基于Servlet API构建的原始web框架,从它一诞生就包含在Spring Framework中,现在更多地被概括为Spring MVC
【经典问题】Spring/Spring Boot/Spring MVC 有什么区别?
发布的先后顺序:Spring -> Spring MVC -> Spring Boot
- Spring指的是Spring Framework,是一个完整的框架,核心是IoC和AOP
- Spring Boot是Spring Framework的一个子项目,它是作为Spring Framework的脚手架而存在的,旨在简化 Spring 应用和服务的创建、开发与部署,并且简化了配置,使得 Java 开发者可以快速启动、运行和测试一个简单的 Spring 应用;
- Spring MVC 属于 Spring 的一个模块,在 Spring MVC 诞生前,开发Java web项目主流还是使用原生的Servlet框架,而 Spring MVC 则是基于Servlet API的 web 框架,目的是简化web开发的操作。
1.1 MVC的定义
MVC是软件工程中的一种架构模式,MVC三个字母分别代表Model(模型)、View(视图)、Controller(控制器),也就是说MVC是由这三个部分构成的。
以下单的业务为例:用户在页面点击下单,下单成功后在页面上通知用户下单成功以及显示相应的订单的信息
- 用户通过页面下单触发点击事件,发送HTTP请求到其路由映射的
Controller(控制器)
,也就是后端的第一层,它相当于是一个安检口,用来检查前端传来的参数是否合法等; - 通过Controller的检查后,将请求信息分发给
Model(业务模型)
对请求进行相应业务的处理,如将新的订单入库,以及查询订单的操作; - Model层处理完相应业务后,将该订单的信息数据,从数据库中取出给
Model(数据模型)
,以Model为原型分装成响应信息返回给Controller; - Controller得到对应数据后,将返回数据给
View(视图)
; View(视图)
通过返回的数据,通过JSP等方式将数据可视化在页面上,作为HTTP响应展现给用户,此时用户就能在页面中看到下单的信息了。
该架构模式出现的很早,缺点是通过JSP将数据可视化的方式将前后端的代码混在一起了,不利于前后端工程师的分工(前端工程师不怎么熟悉java语法,java工程师不熟悉前端语法),已经不适用于如今的项目,为了弥补这方面带来的不便,才出现了老生常谈的前后端分离。
1.2 MVC 和 Spring MVC的关系
MVC是一种思想,而 Spring MVC 是对其的具体实现。
但是由于前后端的分离,把View(视图)的部分给淡化了,现在更多将Spring MVC 叫做 Spring Web。
从讲Spring Boot的第一篇文章开始,其实就在使用Spring Web,当时是在starter中引入的依赖:
项目启动后,就能在pom.xml中找到Spring Boot项目中对Spring Web的依赖:
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>
2. Spring MVC 主要功能
无论学习 Spring MVC 还是 Servlet 无非就是主要围绕下面三个功能学习:
- 连接的功能: 将用户(浏览器)和 Spring 程序通过路由关系连接起来,也就是访问一个地址能够调用 Spring 程序中的路由方法
- 获取参数: 用户访问的时候会携带一些参数,在程序中要想办法获取参数
- 返回数据: 执行业务逻辑之后,程序要将用户需要的数据返回给浏览器
2.1 Spring MVC 创建和连接
2.1.1 项目创建
Spring MVC 的项目创建这里就不赘述了,在starter中选择Spring Web 就相当于创建了Spring MVC的项目。
2.1.2 建立连接
接下来,创建一个UserController类,通过@RequestMapping
注解配置路由,实现用户到Spring程序的连接:
@Controller// 让 Spring 框架启动时,加载
@ResponseBody//返回非页面数据
@RequestMapping("user")//路由注册
public class UserController {@RequestMapping("/hi")//路由注册public String doController() {return "<h1> Hello Spring MVC !<h1>";}
}
注意事项:
- 只能在
@Controller
的类中配置路由,否则路由不生效; @ResponseBody
可以修饰类也可以修饰方法,作用是通过适当的转换器转换为指定的格式之后,写入到response对象的body区,使用此注解之后不会再走视图处理器,如果没有这个注解只能返回一个页面;- 前面的篇章使用的
@RestController
是组合注解,注解中包含@Controller
和@ResponseBody
- 与Servlet的路由配置不同,
@RequestMapping
注解是可以精确到方法上的。
访问配置文件中配置的默认端口号8080,并通过相应路由访问到程序中的路由方法:
2.1.3 指定请求类型
可以通过Postman测试一下,当前的Spring版本下,@RequestMapping是否支持接收所有类型的请求,这里测试下POST和PATCH:
我当前的Spring版本为2.7.3,通过测试得知@RequestMapping是可以接收所有请求的。
但是如果有一个需求,要求是该路由只能接收Post请求。
1) 使用@RequestMapping
如果还是使用@RequestMapping
注解就需要设置method
参数了,参数的值需要结合枚举类RequestMethod
进行设置:
@RestController
@RequestMapping("user")//路由注册
public class UserController {@RequestMapping(value = "/hi", method = RequestMethod.POST)//路由注册public String doController() {return "<h1> Hello Spring MVC !<h1>";}
}
观察@RequestMapping(value = "/hi", method = RequestMethod.POST)
:当添加了method参数后,我们发现"/hi"
也需要添加value参数,当只有一个参数的时候,可以只设置"/hi"
,此时"/hi"
为value的值。
这时再使用其他类型的请求,就会发现报了 “405” 的错误,表示请求方法不被允许:
2) 使用@PostMapping
除了设置method
参数的方式,还可以使用请求类型相应的注解来制定请求类型(@PostMapping只支持post请求、@GetMapping只支持get请求):
@RestController
@RequestMapping("user")//路由注册
public class UserController {@PostMapping("/hi")public String doController() {return "<h1> Hello Spring MVC !<h1>";}
}
2.1.4 设置多个请求类型
此时@PostMapping
等注解就不管用了,只能通过@RequestMapping
的method
属性设置多个值:
@RestController
@RequestMapping("user")//路由注册
public class UserController {@RequestMapping(value = "/hi", method = {RequestMethod.POST, RequestMethod.GET})//路由注册public String doController() {return "<h1> Hello Spring MVC !<h1>";}
}
2.2 获取参数
Spring MVC是基于Servlet的框架,内置了两个隐藏的参数,HttpServletRequest和HttpServletResponse,如果想要使用这两个对象,只要在方法的参数中加入这两个类型,因此围绕HttpServletRequest
对象获取参数的写法在这里也是适用的。
2.2.1 获取单个参数
方式一:使用request对象接收
@RequestMapping("/getOneParam1")
public String getOneParam1(HttpServletRequest request) {return "Hi," + request.getParameter("name");
}
这种方式既麻烦又不安全:通过这种方式获取的参数只能是String类型的,如果此时程序中需要的是一个Interger类型的年龄,那么只能通过Integer.valueOf()
实现
@RequestMapping("/getOneParam1")
public String getOneParam1(HttpServletRequest request) {//return "Hi," + request.getParameter("name");Integer age = Integer.valueOf(request.getParameter("age"));return "value: " + age;
}
但如果前端没有传这个参数,在不额外做判断空指针的处理的话,就会抛java.lang.NumberFormatException
的异常:
方式二:使用单个参数接收
注意:参数名必须和前端匹配
@RequestMapping("/getOneParam2")
public String getOneParam2(String name) {return "Hi," + name;
}
通过方式二,获取Integer类型的年龄
@RequestMapping("/getOneParam2")
public String getOneParam2(Integer age) {//return "Hi," + name;return "value: " + age;
}
即使前端不传参数,也不会出现异常:
2.2.2 获取多个参数
方式一:使用多个参数接收
Spring MVC 传递多个参数的时候,只要在方法中添加多个参数即可,获取参数时只需要保证参数名对应,获取参数的结果只和参数的名称有关,和顺序无关
@RequestMapping("/getMoreParam")
public String getMoreParam(String name, Integer age) {return "name: " + name + " | " + "age: " + age;
}
前端传参数的时候顺序与程序中获取参数的顺序不同,但还是顺利拿到结果:
如果参数少的时候还可以这样做,但如果有极端情况需要传成百上千个参数的话,用这种做法就显得太抽象了。
方式二:使用对象接收
编写一个实体类:
@Data
public class User {private Integer id;private String name;private Integer age;private String gender;
}
使用对象接收:
@RequestMapping("/getMoreParam2")
public String getMoreParam(User user) {return user.toString();
}
前端传参数给后端后,Spring MVC 检测到你是用一个对象来接收的,就会通过setter
方法来将对应的参数注入到对象中,如果前端传的参数实体类中没定义该属性就不处理,如果对象中某些属性前端没有传参数,属性就为默认值null
:
2.2.3 获取JSON对象 —— @RequestBody
大部分情况下前面提到的获取参数的方式都能解决,但如果参数是一个JSON对象就得用特殊的方式获取了。
编写一个login的路由方法用于测试:
@RequestMapping("/login")
public String login(String name, String password) {return "name: " + name + " | " + "password: " + password;
}
前端传参方式介绍(简单了解)
前端将数据传递给后端大概有三种方式:
- Url中携带QueryString直接访问路由
- Form表单提交
- Ajax提交
URL传参:前面所使用的都是通过URL直接携带QueryString访问的,这里就不过多赘述了。
From表单传参(下面的method参数可以填get/post):
<body><form method="xxx" action="/user/login"><h1>登录</h1><div>用户:<input id="name" name="name"/></div><div>密码:<input id="password" name="password"/></div><input type="submit" value="提交"></form>
</body>
页面的显示如下:
1. method=“get”
点击提交后抓包我们可以观察到From表单的get方法穿参其实就跟URL传参是一样的,因此我们的后端可以顺利的获取到数据
2. method=“post”
点击提交后抓包我们发现这次的QueryString不在url中了,而是在body(请求体)中,并且body中的参数的content-type
为application/x-www-form-urlencoded
,抓包数据如下:
此时虽然QueryString到了body中,但是格式还是和URL中的一样,因此我们的后端还是能顺利的拿到数据:
Ajax传参
1. 通过Ajax传一个js对象
<body><h1>登录</h1><div>用户:<input id="name" name="name"/></div><div>密码:<input id="password" name="password"/></div><input type="button" onclick="submit()" value="提交">
</body>
<script>function submit() {jQuery.ajax({url:"/user/login",type:"POST",data:{"name":jQuery("#name").val(),"password":jQuery("#password").val(),},success:function(body) {alert(body);console.dir(body);}})}
</script>
点击提交后抓包我们发现虽然此时是通过Ajax传参,但本质还是和Form表单的Post方法传参是一样的,body中的参数的content-type
为application/x-www-form-urlencoded
,抓包数据如下:
因此后端还是可以获取到参数并返回给前端:
2. 通过Ajax传一个JSON对象
当我尝试用Postman传一个JSON对象给前端时,奇怪的事发生了,这时候我们的后端无法再顺利拿到数据了
使用@RequestBody+对象接收
@RequestBody
主要用来接收前端传递给后端的JSON对象的数据的,需要搭配实体类来使用,由于我们需要传password
的参数,因此我们在实体类中加上password
字段:
@Data
public class User {private Integer id;private String name;private Integer age;private String gender;private String password;
}
接下来我们就可以使用@RequestBody
来获取JSON对象了,具体代码如下:
@RequestMapping("/login2")
public String loginAjax(@RequestBody User user) {return "name: " + user.getName() + " | " + "password" + user.getPassword();
}
通过Postman重新发送请求,这次我们终于成功获取到了JSON对象。
2.2.4 获取文件 —— @RequestPart
引入需求:如用户修改头像的业务,用户想要通过前端页面的上传头像功能将桌面上的一个可爱小猫图片上传给后端。
使用@RequestPart+MultipartFile对象接收
跟JSON一样,文件也是一个比较特殊的对象,Spring MVC 提供了一个简化上传操作的工具类 —— MultipartFile
,当前端上传一个文件给后端时,后端需要使用@RequestPart注解来修饰MultipartFile
对象来接收前端发来的文件。
注意事项:
-
@RequestPart
注解需要设置一个参数,与前端的参数的key值相对应; -
服务器在用
MultipartFile
对象接收之后,应该保存到服务器本地上,不然程序执行完该对象就会被GC回收掉。MultipartFile
对象有一个transferTo()
方法,可以将该文件复制到本地的路径中
具体代码如下:
@RequestMapping("/upload")
public String upload(@RequestPart("myfile")MultipartFile file, ServletRequest request) throws IOException {String path = "/Users/chenshu/Code/classcode_java/blog-demo/Spring-MVC/src/main/resources/static/img";file.transferTo(new File(path+"/cat.jpg"));return "success";
}
使用Postman来模拟用户上传头像的HTTP请求:
成功保存到相应路径下:
2.2.5 获取Cookie/Session/Header
获取Header —— @RequestHeader
传统方法:
@RequestMapping("/header1")
public String getHeader1(HttpServletRequest request) {String header = request.getHeader("User-Agent");return "User-Agent: " + header;
}
通过@RequestHeader获取:
@RequestMapping("/header2")
public String getHeader2(@RequestHeader("User-Agent") String header) {return "User-Agent: " + header;
}
获取Cookie —— @CookieValue
通过该路由方法给浏览器设置一个Cookie值用于测试:
@RequestMapping("/setCookie")
public void setCookie(HttpServletResponse response) {Cookie cookie = new Cookie("status", "OK");response.addCookie(cookie);
}
传统方法:
@RequestMapping("/cookie1")
public String getCookie1(HttpServletRequest request) {String ret = "status: ";Cookie[] cookies = request.getCookies();for (Cookie cookie : cookies) {if (cookie.getName().equals("status")) {ret += cookie.getValue();}}return ret;
}
通过@CookieValue获取:
@CookieValue
中的参数设置的是Cookie的键,该注解的作用是在所有Cookie中找对应键的Cookie,并将该Cookie的值并赋给一个String
@RequestMapping("/cookie2")
public String cookie(@CookieValue("status") String status) {return "status: " + status;
}
获取Session —— @SessionAttribute
通过该路由方法给浏览器设置一个Session用于测试:
@RequestMapping("/setSession")
public String setSession(HttpServletRequest request) {HttpSession session = request.getSession(true);if (session!=null) {session.setAttribute("username", "zhangsan");}return "session 存储成功.";
}
传统方法:
@RequestMapping("/session1")
public String getSession1(HttpServletRequest request) {HttpSession session = request.getSession(false);String username = null;if (session != null && session.getAttribute("username") != null) {username = (String) session.getAttribute("username");}return "username: " + username;
}
通过@SessionAttribute获取:
value
参数代表的是Session里的键,作用是在Session的所有键值对中找到对应的键,并将该键所对应的值赋给一个String;required
参数默认为true,表示没有找到value参数指定的键就直接报错,设置为false的话则不报错。
@RequestMapping("/session2")
public String getSession2(@SessionAttribute(value = "username", required = false) String username) {return "username: " + username;
}
2.2.6 后端参数重命名 —— @RequestParam
某些特殊的情况下,前端传递的参数的key不满足后端的参数命名规范,比如前端传递了一个money
表示用户的金额,你认为这个参数的命名模棱两可,想使用balance
来接收,出现这种情况,我们就可以使用@RequestParam
来重命名前端的参数。
@RequestMapping("/showBalance")
public String renameParam(@RequestParam("money") String balance){return "账户余额为: " + balance;
}
注意事项:当使用了这个注解后,前端一定得传键为"money"
的参数,否则就会报400错误
2.2.7 设置参数必传 —— required参数
在Session中就经常使用这个参数,前面也提到过,这里还是单独拿出来一谈,required参数默认是true的,如果不想报错的话,需要设置required
参数为false
@RequestMapping("/showBalance")
public String renameParam(@RequestParam(value = "money", required = false) String balance){return "账户余额为: " + balance;
}
这样就不会报错了:
2.2.8 获取URL中参数 —— @PathVariable
有些特殊的场景,需要将参数直接放在URL里,与放在"?"
后面的QueryString
中不同,它是直接将参数放在路径上的:
对于上面那种情况,我们需要在@RequestMapping
中告知程序路由上的哪些玩意是参数,如"/login2/{username}/{password}"
,表示login2后面紧跟的两个子路径是作为参数传递的
然后使用@PathVariable
注解修饰变量来接收参数
@RequestMapping("/login2/{username}/{password}")
public String login2(@PathVariable String username, @PathVariable String password) {return "username: " + username + " | " + "password: " + password;
}
成功取到URL中的参数:
2.3 返回数据
Spring MVC 中可以通过 Controller 返回数据给前端
如果没加@RequestBody
注解的话,Spring MVC就会根据return的值去找view(视图)的路径,在HTTP响应报文中返回text/html
类型的HTML源码从而展示在页面上。
如果加了@RequestBody
注解,SpringSpring MVC会根据你在Java中返回的类型来自动设置HTTP响应包中的的Content-Type
(包含数据类型和编码方式),并且根据Content-Type
中的数据类型和编码方案,自动转化为相应的数据并返回。
2.3.1 添加@RequestBody返回String
前面我们所用的方式都是返回一个String给前端,通过抓包我们可以看到,响应的Content-Type
为text/html
类型,浏览器会将返回的数据当作HTML页面解析,本质上就跟没有添加@RequestBody
返回视图是一样的。
但是有一点不一样,并且由于Idea中的默认编码方式是UTF-8
,因此 Spring MVC 设置HTTP响应报文中的Content-Type
会携带charset=UTF-8
,浏览器也会以UTF-8
的格式来解码,看2.3.2 到返回静态页面
的时候会更直观地看出差别。
2.3.2 返回JSON对象
一个json
对象其实就是大括号中包含一个个键值对,原生的Servlet想要返回JSON对象,通常是使用一个外部的JSON框架如jackson
,将一个对象或者对象列表转化成Json格式的字符串,并通过设置HTTP响应报文中的Content-Type
字段,从而告知浏览器要把数据以Json的格式解析。
String jsonString = objectMapper.writeValueAsString(obj);
resp.setContentType("application/json;charset=utf8");
resp.getWriter().write(jsonString);
在Spring MVC中内置了jackson
框架,如果你在类上或者方法上添加了@RequestBody
注解,当你返回一个对象、Map、列表
时,会自动将它们转化成json格式发送给前端
@RequestMapping("/userinfo1")
public User getUserInfo1() {User user = new User();user.setId(1);user.setName("zhangsan");user.setPassword("123");user.setGender("male");user.setAge(20);return user;
}
返回对象:
一个普通对象可以映射一个Json对象,对象中有哪些属性,Json对象就对应拥有哪些键。
@RequestMapping("/userinfo1")
public User getUserInfo1() {User user = new User();user.setId(1);user.setName("zhangsan");user.setPassword("123");user.setGender("male");user.setAge(20);return user;
}
返回Map: 由于Map是键值对的形式,一个Map对象也可以映射一个Json对象,Map中有哪些键,Json对象就对应拥有哪些键。
@RequestMapping("/userinfo2")
public Map<String, Object> getUserInfo2() {HashMap<String, Object> user = new HashMap<>();user.put("name", "zhangsan");user.put("password", "123");return user;
}
返回List: 一个List对应多个Json对象,多个Json对象用[ ]
来包裹起来,List里面可以有普通对象,也可以有Map对象。
@RequestMapping("/userinfo3")
public List<User> getUserInfo3() {List<User> userList = new ArrayList<>();User user1 = new User();user1.setName("zhangsan");User user2 = new User();user2.setName("lisi");userList.add(user1);userList.add(user2);return userList;
}
2.3.2 返回静态页面
去掉@ResponseBody
注解,Spring MVC就知道你要返回的是一个静态页面:
@Controller
@RequestMapping("/test")
public class TestController {@RequestMapping("/index")public String index() {return "/index.html";}
}
index.html
如下:
<html>
<header><meta charset="UTF-8">
</header>
<body>
<h1>hello word!!!</h1>
<p>this is a html page</p>
<p>返回静态页面的测试</p>
</body>
</html>
访问对应路由后确实展示出了我们所编写的页面:
注意事项: 由于浏览器是按HTML中的<header>
标签中设置的charset进行解码,因此抓包数据中的Content-Type
没有设置charset类型:
我们必须在<header>
设置默认的字符编码,否则浏览器会以默认的解码方案来解码,如果我把<header>
中配置的UTF-8
的字符编码删除,显示的页面出现的中文就是乱码:
2.3.4 请求转发和请求重定向
除了返回一个静态页面,还可以实现跳转,跳转的方式有两种
- redirect:请求重定向
- forward:请求转发
请求重定向: 你发送一个请求给后端,请求成功到达路由后,HTTP请求报文中的状态码为302请求重定向,后端在HTTP响应报文中添加Location字段中告诉你重定向的位置,然后浏览器会自动完成页面的跳转,也就相当于重新发起了一次HTTP请求
@RequestMapping("/redirect")
public String index2() {return "redirect:/index.html";
}
下面我通过抓包演示请求重定向
在浏览器中输入重定向的路由:
在用户的视角下:输入上面的URL后,直接跳转到下面的页面:
抓包看到HTTP请求的状态码为302 Found
HTTP响应中多了一个Location字段,浏览器就是通过该字段进行重新跳转到新页面:
请求转发: 当你发送一个请求给后端,后端会携带者你的HTTP请求,转发到新的路由,你会发现浏览器上的地址不会变,然后将新的路由返回的信息展现在你的页面上,HTTP请求报文的状态码为200,对用户来说是透明的。
@RequestMapping("/forward")
public String index3() {return "forward:/index.html";
}
下面我通过抓包演示请求转发
用户视角:输入URL按下回车键后即使路由没发生变化,还是将index.html
页面展现给你
抓包看到HTTP请求的状态码为200 OK:
通过在HTTP响应报文中返回页面的text/html
,将页面显示在用户视角中:
在index.html中引入了外部css样式如下:
再次访问test/forward路由,发现由于请求转发给后端是将页面通过HTTP响应报文以test/html
的形式返回给前端从而显示到页面上,因此访问不到外部的css样式:
forward和redirect具体区别总结:
- 请求重定向(redirect)将请求重新定位到资源;请求转发(forward)服务器端转发
- 请求重定向地址发生变化,请求转发地址不发生变化。
- 请求重定向相当于前端发送了两次HTTP请求,而请求重定向只发送一次HTTP请求
- 由于请求转发给后端是将页面通过HTTP响应报文以
test/html
的形式返回给前端从而显示到页面上,因此如果引用了外部css/js等,可能造成外部资源访问不到
3. 总结
所谓 Spring MVC 就是基于 Servlet API 的框架,所谓 Spring MVC 程序开发讲的就是三件事情:建立连接、获取参数以及返回数据,本文对这三件事进行了详细的讲解,足以在开发中应对绝大多数需求。