什么是 MVC
Spring MVC 其实是 Spring Web MVC,是基于 Servlet API,所以 Servlet 是 Spring MVC 的 “父亲”。因此,Servlet 那一套编程方法,在 Spring MVC 中,也是可以使用的。但是 Spring MVC 比 servlet 要简单很多。
Spring MVC :一开始就在 Spring 框架当中。只是属于 Spring 中的一个 web 模块。
一个 web 项目,只做三件事:
- 实现用户请求 到 程序 的 链接,就是用户请求可以被 程序接收到。
- 在前后端建立联系的情况下,拿到用户请求的参数。
- 拿到参数之后,进行业务处理,并将其结果返回给前端。
- 使用 Spring MVC 的话,只需要前后端的名称一样就行了。
MVC 定义
MVC 是 Model View Controller (模型视图控制器)的缩写,它是软件⼯程中的⼀种软件架构模式,它把软件系统分为模型、视图和控制器三个基本部分。
- Model(模型) 是应用程序中用于处理应用程序数据逻辑的部分。通常模型对象负责在数据库中存取数据。
- View(视图) 是应用程序中处理数据显示的部分,通常视图是依据模型数据创建的。
- Controller(控制器) 是应⽤程序中处理⽤户交互的部分。通常控制器负责从视图读取数据,控制⽤户输⼊,并向模型发送数据。
四者之间的关系如下:
- 可以把 View,Controller,Model 看作是一个软件(项目)。
- 然后用户通过浏览器发送一个 HTTP 请求到 Controller ,然后 Controller 完成对数据的校验,校验数据的正确性和合法性。
- Controller 校验数据通过之后,就会将请求数据发送到 model,然后 model 对数据库进行处理之后,再把结果发给 Controller
- 然后 COntroller 在把得到的数据交给 View
- View 是服务器的视图层,然后和前端的模板相结合,再加上渲染,然后发给客户,客户就能看懂了。
MVC 和 Spring MVC 的关系
两者的关系,就像 IoC 和 DI 的关系。前者是思想,后者是实现。
Spring MVC 是⼀个实现了 MVC 模式,并继承了 Servlet API 的 Web 框架。因为是 Web 框架,所以⽤户在浏览器中输⼊了 url 之后,我们的 Spring MVC 项 ⽬就可以感知到⽤户的请求。
Spring MVC 的影响
现在绝大部分的 Java 项目都是基于 Spring(或 Spring Boot)的,而 Spring 的核心就是 Spring MVC。现在大部分的 Java 项目都是 Spring MVC 项目。
创建 Spring MVC 项目
创建项目和我们之前创建 Spring Boot 项目一样,只是在添加依赖的时候,选择 Spring web 就好了:
这样就完成了 Spring MVC 项目的创建了。
Spring MVC 作用
- 实现 用户 和 程序 的映射(在浏览器输入 URL 之后,能够在程序中匹配到相应的方法)
- 服务器端要得到用户的请求参数
- 服务器端要将结果返回给用户(前端)
实现用户 和 程序的映射
-
使用 @RequestMapping(“/xxx”) 映射,可以加在 类上面,就是一级目录,然后方法上面再加的话,就是二级目录:
@Controller @ResponseBody @RequestMapping("/user") public class UserController { @RequestMapping("/sayhi") public String sayHi() { return "hello world"; } }
访问方法如下:
user 是一级目录,sayhi 是二级目录。默认是 GET 请求:
-
使用 POST 请求来访问资源,通过 method 参数来设置:
@Controller @ResponseBody @RequestMapping("/user") public class UserController { @RequestMapping(method = RequestMethod.POST, value = "/sayhi") public String sayHi() { return "hello world"; } }
访问结果如下:
就支持 POST 访问了。 -
使用 @PostMapping 来指定 POST 访问:
@Controller @ResponseBody @RequestMapping("/user") public class UserController { @PostMapping("/sayhi") public String sayHi() { return "hello world"; } }
访问结果如下:
-
通过 GetMapping 来进行 GET 访问:
@Controller @ResponseBody @RequestMapping("/user") public class UserController { @GetMapping("/sayhi") public String sayHi() { return "hello world"; } }
访问结果如下:
获取用户请求参数
获取单个参数
通过传入参数,然后返回对象:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@GetMapping("/getuserbyid")
public UserInfo getUserById(Integer id) {
UserInfo userInfo = new UserInfo();
userInfo.setId(id);
userInfo.setUsername("zhangsan");
userInfo.setAge(18);
return userInfo;
}
}
传入 id = 1,只要保证传入的参数和接收的参数一样就好了,运行结果如下:
使用 Integer 是可以接收 null 的。
获取多个参数
通过传多个参数来获取:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login(String username, String password) {
return "用户名:" + username + " 密码:" + password;
}
}
运行结果如下:
也就是获取多个参数和获取一个参数的方法是一样的,只需要传递多个参数就好了。
获取对象
如果有多个参数的时候,通过传对象可以让结果看的更简洁:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/reg")
public String login(UserInfo userInfo) {
return "用户信息:" + userInfo;
}
}
前端传这样的参数:
http://127.0.0.1:8080/user/reg?username=zhangsan&password=123&age=18
运行结果如下:
后端参数重命名
某些特殊情况下,前端传递的参数 key 和我们后端接收的 key 可能不一致比如前端传递了 name 之后,后端是用 username 来接收的,那么就会报错。通过 @RequestParam
来对存在念书重命名:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/reg")
public String login(@RequestParam("name") String username) {
return "用户名:" + username;
}
}
前端参数:
http://127.0.0.1:8080/user/reg?name=zhangsan
运行结果如下:
也就是当使用了这个注解之后,前端必须要传一个 name 参数。否则就报错。如果加了 required 之后,设置 required 的参数为 false,因为默认是 true,就不会报错了,会显示用户名是 null:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/reg")
public String login(@RequestParam(value = "name", required = false) String username) {
return "用户名:" + username;
}
}
运行结果如下:
接收 JSON 对象
使用 RequestBody 注解来完成对象的接收,后端代码如下:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/reg")
public String login(@RequestBody UserInfo userInfo) {
return "用户信息:" + userInfo;
}
}
然后通过 Postman 来模拟:
从 URL 地址当中获取参数
这里并不是从 URL 地址中的参数部分获取参数,而是从 /
来作为分隔符获取参数,通过 @PathVariable 注解来实现
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/info/{id}/{name}")
public String getURLInfo(@PathVariable Integer id, @PathVariable String name) {
return "ID:" + id + " name:" + name;
}
}
前端 URL:
http://127.0.0.1:8080/user/info/2/zhangsan
运行结果如下:
上传文件
通过 @RequestPart 注解来实现对文件的上传,只需要对参数加上注解就好了。然后通过 Spring 的 MultipartFile 来接收文件就好了:
@Slf4j
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/upload")
public boolean upImg(Integer id, @RequestPart("image") MultipartFile file) {
boolean result = false;
//保存图片到本地目录
try {
file.transferTo(new File("D:/img.png"));
} catch (IOException e) {
log.error("上传图片失败: " + e.getMessage());
}
return result;
}
}
Postman 模拟如下:
D 盘的文件如下:
但是如果这样每次的名字都一样的话,就会导致文件覆盖。
配置文件保存路径
直接把路径放在配置文件当中,这样的话,如果项目上线也很容易修改,直接修改为线上环境就好了,application 是必不可少的,不过可以通过 -
在 application 后面 加上名称。通过 application.yml
来配置 配置文件的运行环境:
# 设置配置文件的运行平台
spring:
profiles:
active: dev
开发环境配置:
生产环境如下:
文件路径:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
//从配置文件中读取图片的保存路径
@Value("${img.path}")
private String imgPath;
@RequestMapping("/sayHi")
public String sayHi() {
return "文件路径:" + imgPath;
}
}
访问结果如下:
这样就完成了对图片路径的访问。换成生产环境也一样可以。
图片名不能重复
让图片名不能重复,通过 UUID 来让图片永远不重复。UUID 是自动生成一个全球不同的一串数字。如果用时间戳的话,总会有两个人同时传数据,所以就会导致内容覆盖。
获取图片的格式
就是获取原图片的格式,不能向我们上面这样,改变图片的格式。从最后一个点,向后截取,就能获取到文件的后缀名了:
@Slf4j
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
//从配置文件中读取图片的保存路径
@Value("${img.path}")
private String imgPath;
@RequestMapping("/upload")
public boolean upImg(Integer id, @RequestPart("image") MultipartFile file) {
boolean result = false;
//保存图片到本地目录
String fileName = file.getOriginalFilename();
fileName = fileName.substring(fileName.lastIndexOf("."));
fileName = UUID.randomUUID().toString() + fileName;
try {
file.transferTo(new File(imgPath + fileName));
result = true;
} catch (IOException e) {
log.error("上传图片失败: " + e.getMessage());
}
return result;
}
}
Postman 模拟如下:
保存路径内容如下:
获取 Cookie/Session/header
获取 Request 和 Response 对象
web 程序里面都会有 Request 和 Response 参数,只需要在需要的时候,直接使用就好了:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/sayhi")
public String sayHi(HttpServletRequest request) {
return "hello world " + request.getParameter("name");
}
}
运行结果如下:
获取 Cookie
直接使用 @CookieValue 就好了,先设置好 Cookie:
然后输出:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/cookie")
public String getCookie(@CookieValue("lisi") String cookie) {
return "Cookie Value" + cookie;
}
}
运行结果如下:
如果要读取多个 cookie 的话,就设置多个 Cookie 参数。
获取 Header
直接通过 @RequestHeader 的注解来实现获取 Header,我们获取 Header 里面的 user-agent:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/header")
public String getCookie(@RequestHeader("User-Agent") String userAgent) {
return "user-Agent: " + userAgent;
}
}
运行结果如下:
存储和获取 Session
设置 Session
听过 HttpSession 来设置 session:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/header")
public boolean setSession(HttpServletRequest request) {
boolean result = false;
//得到 Session
HttpSession session = request.getSession(true);//true 是创建会话
//使用 setAtt 设置值
session.setAttribute("userinfo", "userinfo");
return true;
}
}
运行结果如下,设置 Session 就是多了一个 JSESSIONID :
获取 Session
Spring Boot 通过 @SessionAttribute 注解就可以获取到 Session 了:
@Controller
@ResponseBody
@RequestMapping("/user")
public class UserController {
@RequestMapping("/getsession")
public String getSession(@SessionAttribute(value = "userinfo",required = false) String userinfo) {
return "会话:" + userinfo;
}
}
访问结构如下:
加上 required 的时候,如果 session 当中没有 此属性 的时候,就不会报错了。
返回数据
如下代码:
@Controller
@RequestMapping("/user")
public class UserController {
@RequestMapping("/sayhi")
public String sayHi() {
return "hello";
}
}
这样返回的其实是一个 静态页面 的结果:
把代码当中换成 hello.html
,然后写一个页面,就可以了:
@Controller
public class UserController {
@RequestMapping("/sayhi")
public String sayHi() {
return "hello.html";
}
}
运行结果如下:
@ResponseBody,就表示返回非静态页面的数据。
使用 @RestController
通过 @RestController 注解,就可以直接返回非静态页面的数据了:
@RestController
public class UserController {
@RequestMapping("/sayhi")
public String sayHi() {
return "hello.html";
}
}
运行结果如下:
练习
form 表单
通过 MVC 和 form 表单来实现。前端代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>add</title>
</head>
<body>
<form action="/calc">
<h1>计算器</h1>
数字1:<input name="num1" type="text"><br>
数字2:<input name="num2" type="text"><br>
<input type="submit" value=" 点击相加 ">
</form>
</body>
</html>
后端接受的时候,路由也是 /calc
来接收,然后返回页面数据就好了:
@RestController
public class CalcController {
@RequestMapping("/calc")
public String calc(Integer num1, Integer num2) {
if (num1 == null || num2 == null) {
return "<h1>参数错误!</h1><a href='javascript:history.go(-1);'>返回</a>";
}
return "<h1>结果:" + (num1 + num2) + "</h1><a href='javascript:history.go(-1);'>返回</a>";
}
}
后端返回的结果,这里写成了 h1 标签。运行结果如下:
点击之后效果如下:
Ajax
通过 Ajax 来完成用户的登录,通过模拟登录来实现用户交互,前端代码如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="js/jquery-1.9.1.min.js"></script>
<title>Document</title>
<script>
//ajax 提交
function mysub(){
//1.判空 也可以把 jQuery 符号换成 $
var username = jQuery("#username");
var password = jQuery("#password");
//使用 trim 方法来去掉空格
if(jQuery.trim(username.val())==""){
alert("请先输入用户名!");
//把光标重新放到元素上面
username.focus();
return;
}
if(jQuery.trim(password.val())==""){
alert("请先输入密码!");
password.focus(); //光标重制到此元素
return;
}
jQuery.ajax({
url:"/login",
type:"POST",
data:{"username":username.val(),
"password":password.val()},
success:function(result){
alert(JSON.stringify(result));
}
});
}
</script>
</head>
<body>
<div style="text-align: center;">
<h1>登录</h1>
用户:<input id="username">
<br>
密码:<input id="password" type="password">
<br>
<input type="button" value=" 提交 " onclick="mysub()" style="margin-top: 20px;margin-left: 50px;">
</div>
</body>
</html>
后端代码如下:
@RestController
public class UserController {
@RequestMapping("/login")
public HashMap<String, Object> login(String username, String password) {
HashMap<String, Object> result = new HashMap<>();
int state = 200;
int data = -1;//等于 1 的话,就表述登陆成功
String msg = "";
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
if (username.equals("admin") && password.equals("admin")) {
data = 1;
msg = "";
}
} else {
msg = "非法参数";
}
result.put("state", state);
result.put("data", data);
result.put("msg", msg);
return result;
}
}
运行结果如下:
故意填错密码:
填对密码的时候,就和我们约定的结果一样了:
通过 JSON 来传输
通过 JSON 来传输数据的时候,后端就要通过 @RequeestBody 来接收了。然后前端也要指定为 JSON 格式发送。就是转化为 JSON 字符串就好了。前端代码如下:
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<script src="js/jquery-1.9.1.min.js"></script>
<title>Document</title>
<script>
//ajax 提交
function mysub(){
//1.判空 也可以把 jQuery 符号换成 $
var username = jQuery("#username");
var password = jQuery("#password");
//使用 trim 方法来去掉空格
if(jQuery.trim(username.val())==""){
alert("请先输入用户名!");
//把光标重新放到元素上面
username.focus();
return;
}
if(jQuery.trim(password.val())==""){
alert("请先输入密码!");
password.focus(); //光标重制到此元素
return;
}
jQuery.ajax({
url:"login",
type:"POST",
contentType:"application/json",
data:JSON.stringify({"username":username.val(),
"password":password.val()}),
success:function(result){
alert(JSON.stringify(result));
}
});
}
</script>
</head>
<body>
<div style="text-align: center;">
<h1>登录</h1>
用户:<input id="username">
<br>
密码:<input id="password" type="password">
<br>
<input type="button" value=" 提交 " onclick="mysub()" style="margin-top: 20px;margin-left: 50px;">
</div>
</body>
</html>
后端代码如下:
@RestController
public class UserController {
@RequestMapping("/login")
public HashMap<String, Object> login(@RequestBody UserInfo userInfo) {
HashMap<String, Object> result = new HashMap<>();
int state = 200;
int data = -1;//等于 1 的话,就表述登陆成功
String msg = "";
if (StringUtils.hasLength(userInfo.getUsername()) && StringUtils.hasLength(userInfo.getPassword())) {
if (userInfo.getUsername().equals("admin") && userInfo.getPassword().equals("admin")) {
data = 1;
msg = "";
} else {
msg = "用户名或密码不正确";
}
} else {
msg = "用户名或密码不正确";
}
result.put("state", state);
result.put("data", data);
result.put("msg", msg);
return result;
}
}
运行结果如下:
请求转发和请求重定向
请求转发是 forward,请求重定向是 redirect。
请求转发
通过 forward 来实现请求转发:
@Controller
public class UserController {
@RequestMapping("/myforward")
public String myForward() {
return "forward:/hello.html";
}
}
forward 后面加的是请求转发路径。运行结果如下:
直接到了 hello.html 页面了。
请求重定向
请求重定向是 redirect :
@Controller
public class UserController {
@RequestMapping("/myredirect")
public String myRedirect() {
return "redirect:/hello.html";
}
}
运行结果如下:
这里就是直接访问 hello.html 页面了。重定向是发生在客户端的行为,不会经过服务器端。