Spring MVC

引言

什么是 Spring MVC

Spring MVC 全称为 Spring Web MVC.
Spring MVC 是一个 Web 框架.
Spring MVC 是基于 Servlet API 构建的.

Spring Web MVC 是基于 Servlet API 构建的原始 Web 框架,它也是存在于 Spring 框架之中的,通俗的说,它其实就是 Spring 框架中的 Web 模块。 所以说,之前 Servlet 拥有的 " request 和 response ",我们依然能够在此框架中使用。

Spring、Spring MVC、Spring Boot

Spring,Spring Boot,Spring MVC 三者的关系,Spring 是最核心,其他两者都是基于 Spring 拓展出来的东西。

Spring MVC 是随着 Spring 的诞生而存在的一个框架。Spring 和 Spring MVC 诞生的历史是比较久远,在它们之后才有了 Spring Boot. 之前我们说 Spring Boot 是 Spring 的脚手架,也就是说 Spring Boot 就是 Spring 框架的框架,它的存在,就是为了程序员更快速地使用 Spring 框架。

现在绝大部分的 Java 项目都是基于 Spring (或 Spring Boot) 的,而 Spring 的核心就是 Spring MVC. 实际上,我们基于 Spring Boot 框架添加一个 【 Spring Web 依赖】 ,此时项目就变成了一个 Spring MVC 项目。所以,在当前主流的网络开发环境下,一个 Spring Boot 项目,我们也可以说是一个 Spring MVC 项目。

MVC

MVC:Model View Controller. ( 模型、视图、控制器 )

0

一、请求映射

通俗来说,请求映射就是前后端交互的约定。

在 Spring MVC 中,注解 " @RequestMapping " 就是请求映射,它表示一个路由。

@Controller
@ResponseBody // 返回的是一个单纯的数据,并不是一个 HTML 页面
@RequestMapping("/user") // 类上的注解,表示一级访问目录 (可省略)
public class UserController {

    @RequestMapping("/hello") 
    public String hello() {
        return "hello world" ;
    }
    
}

1-1

" @RequestMapping " 注解,既可以放在类上,也可以放在方法上。如果两者同时都加上了此注解,那么类上的表示一级目录,方法上的就表示二级目录。但是,类上可以省略路由,方法上的却不可以省略。 因为归根结底,方法就是用来将后端的数据写入 HTTP 响应的正文之中的,所以如果我们不给方法加上路由,就等于我们写的业务逻辑,前端拿不到了。

限制前端访问的方法

我经过 Postman 测试后,可以发现 " @RequestMapping " 注解支持很多方法,例如 GET 、POST 、PUT 等常用方法…那么,我们是否可以尝试着限制前端只能按照一种方法发送 HTTP 请求呢?

答案是可以的,请继续往下看。

我们创建一个 hello2 方法,在里面进行限制:前端只能使用 POST 方法来访问当前方法,如果是其他方法就会报错。

@Controller
@ResponseBody // 返回的是一个单纯的数据,并不是一个 HTML 页面
@RequestMapping("/user") // 类上的注解,表示一级访问目录 (可省略)
public class UserController {

    @RequestMapping(method = RequestMethod.POST, value = "/hello2")
    public String hello2() {
        return "Welcome to the world!";
    }
    
}

如下图所示,当我们使用 GET 方法的时候,后端就会报错,其中 " 405 " 这个状态码就表示方法不允许的意思。

1-2

实际上,spring 框架为我们提供了更简单的写法,如下所示:

更推荐的写法:

// 前端只能使用 POST 方法访问当前方法
@PostMapping("/hello3")
public String hello3(){
    return "好好学习,天天向上";
}

// 前端只能使用 GET 方法访问当前方法
@GetMapping("/hello4")
public String hello4() {
    return "坚持锻炼!";
}

注意事项

在 " @RequestMapping " 注解后的路由地址,我们应该将字母都写成小写,因为我们当前的 Spring MVC 就是一个 Web 项目,以后肯定会部署到服务器上的,那么 Linux 系统 与 Windows 系统,在环境上,它们是不一样的。所以,为了避免大小写带来的地址出错问题,我们应该统一写成小写。实在不行,就用下划线进行数据之间的分割。

事实上,我们平时用电脑浏览器打开一个网页,会发现整串 URL 基本上都是小写,不管是 http,还是 https,都是如此。

二、服务器获取前端参数

1. 获取查询字符串的参数

@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

    @RequestMapping("/findid")
    public String findId(Integer id){
        return "id: " + id;
    }
     
}

1-3

注意事项

① 服务器端按照前端的查询字符串来得到其数据,这是 GET 方法常用的传参形式。

② URL 中的参数要和后端接收的参数匹配,才能够正确接收。

③ 后端接收参数的时候,形参应该规定 Integer 包装类,或是 String 类型。这样一来,即使前后端的参数不匹配,也不会给前端报错。

针对上面的第三点,很多人可能不理解,比方说,我将刚刚的代码写成 int 类型的接收形式,并故意将前端传入的参数写错,如下所示:

@RequestMapping("/findid")
public String findId(int id){
    return "id: " + id;
}

我们可以看到下面结果是一个 500 的状态码,对于我们程序员来说,肯定是明白这是服务器端出错了,但是,对于一个普通用户来说,他是很懵的。所以为了防止这种情况发生,我们就应该考虑到这样的结果。

1-4

2. 获取查询字符串的多个参数

@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

    @RequestMapping("/info")
    public String getInfo(String userid, String username) {
        return "账号: " + userid + ", 昵称: " + username;
    }
    
}

1-5

注意事项

① 查询字符串的多个参数和单个参数的写法差不多,都是以 " ? " 为起始标志。

② 一个参数就会对应一个值,合在一起看就是一个键值对。所以,多个键值对之间以 " & " 作为分割,键和对之间使用 " = " 分割。

③ 查询字符串的参数不需要按照后端形参的顺序来写。比方说,后端的形参是先接收 " userid ",再接收 " username ",而前端先传 " username ",后传 " userid " ,结果并不妨碍后端接收到参数。这里应该明确,后端接收参数的时候,是通过 " 键 ",来找 " 值 " 的。这就带来了一个好处,一个项目需要临时加访问参数的时候,对于前后端的代码,没有什么影响。

3. 服务器端重命名前端发来的参数

现在一个公司的项目,基本都是前后端分离的,大部分情况,前端的人管不着后端,后端的人也管不着前端,很多事情没法达成一致。就比如,前端传入了一个 " id " 的参数名,但是后端人员认为 " id " 可能会与后面的代码出现冲突,所以就想办法就此参数的名字进行重命名为 " userid ".

针对这种情况,下面就进行演示:

@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

    @RequestMapping("/info2")
    public String getInfo2(@RequestParam( value = "id") String userid, String username) {
        return "账号: " + userid + ", 昵称: " + username;
    }

}

1-6

注意事项

① 我们不仅可以重命名一个参数,也可以重命名多个参数。需要被重命名的参数,将其写在 " @RequestParam " 注解写在内部,重命名后的名字写在注解后面。

② 实际上,此注解中还有一个隐藏的 " required " 属性," required " 默认值为 true,表示规定前端传入的参数必须是 " id " ,否则就会报错,如下图所示:

1-7

所以,相比于初始的写法,我更推荐下面的写法,它可以有效地避免前端出错。

@RequestMapping("/info2")
public String getInfo2(@RequestParam( value = "id", required = false) String userid, String username) {
    return "账号: " + userid + ", 昵称: " + username;
}

1-8

4. 获取前端发送 form 表单格式的数据

详解下面代码的过程:前端将的数据写在 HTTP 请求的正文中,后端在这之前准备好一个实体类来接收数据,接着在后端这里,正文中的数据被转换成了一个 Java 对象,最后,后端考虑以什么样的形式再将数据返回给前端看。

实体类:

@Data
public class UserInfo {
    private int id;
    private String name;
    private int age;
}
@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

    @RequestMapping("/register")
    public String register( UserInfo userInfo) {
        return "用户信息: " + userInfo;
    }
    
}

利用 Postman 发送数据给后端:

1-9

抓包结果:

2-1

注意事项

① " application/x-www-form-urlencoded " 是浏览器默认的格式,它将 HTTP 请求的正文中的数据进行了特殊转码。可以发现,它的格式就是 " key=value&key=value " 这样的键值对,与我们之前说的查询字符串是一样的。

② 如果前端需要将数据放在 HTTP 请求的正文中,通过浏览器输入 URL,这是做不到的。常用构造正文的方法有两个,一个是使用 " JS " 的 " ajax " 代码,一个是使用 Postman 软件。前者多用于项目,后者使用起来简单方便。

③ 这里的 Spring MVC 项目十分智能,在后端的方法中,我们拿了一个实体类的对象作为参数,那么前端所传入的参数值,就会自动填充到当前的实体类中。思考一下,我们之前使用的 Servlet 项目,它就需要程序员手动地为实体类赋值。所以说,整个框架为我们做了很多事情,但这一切的前提是,我们需要遵守框架的规则。

比方说:我们前端传的参数是 " id " ,后端的类对象中的字段也应该是 " id ",两者必须对应起来,才能起到效果。

④ 其实后端在拿参数、返回给对象的时候,用到了 " Getter, Setter, toString " 方法,我们却没有感知到,这是因为我们最初在上面的 " UserInfo " 实体类中,添加了注解 " @Data ",它提供了这些方法,简化了我们的操作。

5. 获取前端发送 json 格式的数据

详解下面代码的过程:前端将 json 格式的数据写在 HTTP 请求的正文中,后端在这之前准备好一个实体类来接收数据,接着在后端这里,json 数据被转换成了一个 Java 对象,最后,后端考虑以什么样的形式再将数据返回给前端看。

实体类:

@Data
public class UserInfo {
    private int id;
    private String name;
    private int age;
}
@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

    @RequestMapping("/register2")
    public String register2( @RequestBody UserInfo userInfo) {
        return "用户信息: " + userInfo;
    }

}

利用 Postman 发送一个 json 格式的数据给后端:

1-9

抓包结果:

1-0

注意事项

如果后端知道即将接收的是一个 json 数据的话,那么就应该加上 " @RequestBody " 注解,否则就会接收不到。因为既然 HTTP 请求中正文类型明确了是 json,如果后端不标明注解的话,就相当于破坏了约定。

如果我们将上面的 " @RequestBody " 取消掉,其他代码不变,那么结果如下:

2-1

6. 获取 URL 中的路径变量

@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

    @RequestMapping("/getvariable/{id}/{name}")
    public String getVariable( @PathVariable Integer id, @PathVariable String name) {
        return "ID: " + id + ", name: " + name;
    }

}

2-2

注意事项

① 获取 URL 中的路径变量这种方式,它的参数是通过 "/ " 连接的,所以,我们就能知道,这些参数好像伪装成了路径的一部分。

② 相比于 " 查询字符串 " 的参数来说,这里的路径变量参数是写死的。Spring MVC 为我们提供了 " /路径/{}{}… " 这样的方式,所以,我们接收的时候,方法的形参需要注意写法,与路径变量的顺序保持一致。

比方说:前端路径不变,但是我们将后端的形参部分调换位置,就会出现如下情况:

@RequestMapping("/getvariable/{id}/{name}")
public String getVariable( @PathVariable Integer name, @PathVariable String id) {
    return "ID: " + id + ", name: " + name;
}

2-3

7. 获取前端发送的文件

@Slf4j
@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

    @RequestMapping("/loadimg")
    public boolean loadImg(Integer uid, @RequestPart("img")MultipartFile file) {
        try {
            // 文件保存的地方
            file.transferTo(new File("D:/Data/image1.png"));
            return true;
        } catch (IOException e) {
            log.error("上传图片失败:" + e.getMessage());
        }
        return false;
    }

}

利用 Postman 发送一张图片给后端:

2-4

抓包结果:

2-5

注意事项

① 如果 HTTP 请求中正文的数据是一个文件,那么 " multipart/form-data " 即表示当前格式,图片是一个文件,那么图片就是此格式。

② 我们利用抓包的时候,可以看到图片是乱码,其实也就是二进制的数据,这些二进制的数据被放在正文中,并以 boundary 作为边界,放入边界之内。此外,我们刚刚上传图片时附带的 " id " 和 " img " 参数,也都与图片一样,放在了边界之内。

③ 后端接收参数一般为一个 " id " ,或者其他的唯一类型,这样可以为我们锁定用户。我们可以联想上传 QQ 头像,QQ 号是唯一的,但 QQ 昵称并不是。

④ 上传文件的时候,Spring MVC 为我们提供了两个功能。其一是 " @RequestPart " 注解,它让我们明确了上传文件时的对应参数;其二是 " file.transferTo() " 方法,它可以让我们保存到服务器端的某个位置。本质上,框架的底层就是先以流对象的形式读取用户发来的文件数据,再次以流对象的形式写入文件中。

⑤ 我们在上传图片的同时,也要考虑到上传失败的情况, 因为 Java 流对象读写文件本身就是有可能出问题的,比方说:文件重名、文件所存目录不存在等等…所以,我们就可以利用 " @Slf4j " 这个注解来实现日志错误信息。

⑥ 虽然我们这一次存储文件,执行起来没有错误,但我们应该思考一个问题:这一次存储图片的时候,放在了本地的 Windows 系统下的一个固定的文件夹,也就是说,我们这一次是将图片的路径写死了,而且是写在了开发环境下吗,实际上,我们以后将项目部署在 Linux 服务器上的时候,用户上传的文件应该放在云服务器上才对。

针对上面的第 ⑥ 点,前后端交互不用变,但我们应该重新考虑一下业务逻辑。

8. 将前端发来的文件保存在不同环境下

带着上面的疑问,我们要考虑三点:

① 不同环境的目录位置
② 获取原来上传图片的后缀格式
③ 生成新的图片名称,名称不能重复,所以使用【UUID】

不同环境的目录位置:

2-6

如果要区分保存到开发环境还是生产环境下,我们就可以通过配置文件来约定。但是,这里需要注意一个问题,不管是 properties 还是 yml 文件,它们的后缀名称可以自定义,比方说:开发环境可以起名为 -develop;生产环境可以起名为 -produce. 但所有配置文件的名称前缀必须是 " application ",这是一种标准,即 Spring 框架的规则。

后端代码:

@Slf4j
@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

	// 引入路径
    @Value("${img.path}")
    private String imgPath;

	@RequestMapping("/loadimg2")
    public boolean loadImg2(Integer uid, @RequestPart("img")MultipartFile file) {
        String fileName = file.getOriginalFilename(); // 获取原文件名
        int pos = fileName.lastIndexOf(".");
        String postfix = fileName.substring(pos); // 拿到文件后缀
        fileName = UUID.randomUUID().toString() + postfix; // 生成新的文件名

        try {
            // 将新的文件放在环境目录中,保存起来
            file.transferTo(new File(imgPath + fileName));
            return true;
        } catch (IOException e) {
            log.error("上传图片失败,原因:" + e.getMessage());
        }
        return false;
    }

}

利用 Postman 发送一张图片给后端:

2-7

我上传了三次图片,可以发现最终生成的文件名不一样,如下图所示:

2-8

这里抓包结果就不看了,和之前的差不多。

注意事项

① 我们通过配置文件这种方式,可以选择不同系统、不同环境下的存储路径。所以,我们就不能忘记使用配置文件,我们应该通过 " @Value " 注解方式,将文件路径引入到代码中。在这里,我选择了将文件保存至本地中,也就是 " -dev " 开发环境。

② 这里的 " UUID " 表示的是全世界唯一的 ID,或者说是全局 ID,高级语言一般都有此功能,Java 同样为我们实现了这个功能,它能生成一段随机字符串,供我们使用。我们不但可以利用 UUID 作为目录的名字,也可以让其作为文件的名字,如果一个用户需要一个文件夹,就可以选择前者,如果将所有用户的信息放在一个文件夹,就可以选择后者。

③ 业务逻辑代码中有个很关键的点,为什么一定要拿到用户上传的文件后缀格式呢?

答:用户上传的文件不仅仅是 " xxx.png “,” xxx.jpg ",还有可能是 " xxx.mp4 “,” xxx.gif " …如果我们需要 " UUID " 生成一个独一无二的名字,就需要获取文件格式。因为根据用户上传的特定格式,后端必须定义相同的文件格式,只是名字发生变化而已。所以,我们就可以通过 " 截取字符串 " 的经典方式来实现,如下所示:

String fileName = file.getOriginalFilename(); // 获取原文件名
int pos = fileName.lastIndexOf(".");
String postfix = fileName.substring(pos); // 拿到文件后缀
fileName = UUID.randomUUID().toString() + postfix; // 生成新的文件名

9. 获取 header 请求头中的字段信息

一个 HTTP 请求的报文中,一般分为三个部分:首行、请求头、正文。

header 请求头中,我们常见的有 Content-Length, Content-Type, User-Agent…

我们就尝试获取一下 " User-Agent " ,看看效果。

@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

	// 方式一
    @RequestMapping("/getheader")
    public String getHeader(HttpServletRequest request) {
        return "header: " + request.getHeader("User-Agent");
    }

	// 方式二
    @RequestMapping("/getheader2")
    public String getHeader2(@RequestHeader("User-Agent") String userAgent) {
        return "header: " + userAgent;
    }
    
}

2-9

注意事项

① 方式一是通过 Servlet 实现的,方式二是通过 Spring 框架提供的注解实现的,我们查看后端返回的数据,发现两种方式都可行。

② 这里就需要明确了,Spring MVC 其实创建的就是一个 Web 项目,它是基于 Servlet 实现的,所以,我们在此项目中,依然可以使用 HttpServletRequest 和 HttpServletResponse 这两个类,及它们提供的方法。

10. 获取请求头中的 Cookie

模拟 cookie:

3-1

获取 cookie:

@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

	// 方式一
    @RequestMapping("/cookie")
    public void getCookie(HttpServletRequest request) {
        Cookie[] cookies = request.getCookies();
        for (Cookie item : cookies) {
            log.info("Cookie name: " + item.getName() + ", Cookie value: " + item.getValue());
        }
    }

	// 方式二
    @RequestMapping("/cookie2")
    public String getCookie2(@CookieValue("Jack") String cookie) {
        return "Cookie value: " + cookie;
    }
    
}

方式一日志打印:

3-2

方式二数据返回结果:

3-3

注意事项

我们需要知道,cookie 实际上是由服务器端产生的,之后再放到前端的。它里面实际上放着 sessionID 键值对,我们需要先模拟一下,也就是先将键值对存进去,才能获取。

11. 获取请求中的 session

先存储 session:

@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

    @RequestMapping("/setsession")
    public void setSession(HttpServletRequest request) {
        // 不存在,就创建
        HttpSession httpSession = request.getSession(true);
        httpSession.setAttribute("username", "Jack");
    }
    
}

获取 session:

@Controller
@ResponseBody 
@RequestMapping("/user") 
public class UserController {

	// 方式一
    @RequestMapping("/getsession")
    public String getSession(HttpServletRequest request) {
    	// 不存在,不创建
        HttpSession httpSession = request.getSession(false);
        if (httpSession != null && httpSession.getAttribute("username") != null) {
            return  (String)httpSession.getAttribute("username");
        }
        return "未找到 sessionID 对应的值";
    }

	// 方式二
    @RequestMapping("/getsession2")
    public String getSession2(@SessionAttribute(value = "username", required = false ) String username) {
        return "会话: " + username;
    }
    
}

3-4

注意事项

① session 和 cookie 一样,一开始也是由服务器产生的,所以只能先存后取。

② 存 session 的时候,只能通过 Servlet 那样的方式,通过 " request.getSession " 来实现。

③ 取 session 的时候,依旧有两种方式,通过注解的方式明显简单很多,我们也要同时设置 " @SessionAttribute " 的 " required " 属性,以防前端出现异常。

④ session 会话机制常用于验证用户登录场景,它需要与 cookie 进行配合使用,这一点我们感知不到,如果需要理解的小伙伴,可以点击下面的链接,这是我以前写的博客,详解了两者的联系。

【Cookie 和 Session】

三、服务器端返回数据给前端

我们先来看一个程序:

@Controller
public class UserController2 {

    @ResponseBody
    @RequestMapping("/hello_world")
    public String hello() {
        return "hello.html";
    }

    @RequestMapping("/hello_world2")
    public String hello2() {
        return "hello.html";
    }

}

3-5

注意事项

① " @ResponseBody " 注解既可以加在方法上,也可以加在类上。如果此注解仅修饰一个方法了,表示此方法会返回一个非静态页面的数据;如果此注解修饰类了,表示类下的所有方法都会返回一个非静态页面的数据。

② 上面的程序,为我们演示了加或不加注解 " @ResponseBody " 的区别,如果加上了此注解,就表示后端返回一个纯数据给前端;如果未加上此注解,就表示后端返回一个页面给前端,也就是说,默认情况下,框架认为你会返回一个页面。

③ 关于后端给前端返回的数据是一个普通的类型,还是 json 格式的数据… 框架已经自动为我们做好了判断。我们只需要按照平时写代码的方式去写就好,给方法一个 return 语句,让框架为我们自动判断。例如:我们平时用的最多的就是 json 格式,我们只需要返回一个实体类的对象,框架会自动为我们转换成 json 格式的数据。

④ HTML 页面应该部署到 【resources】的 【static】目录下,表示静态页面的意思。另外,形如一些 " CSS、JS、jQuery " 等前端数据也应该放在此目录下,这是我们的 Spring MVC 项目在创建之初时,框架就已经提供的目录。放在其他目录下,框架是找不到的,即 " 约定大于配置 ",我们要遵守框架的规则。

3-6

@RestController 注解

@RestController
public class UserController2 {
    
    @RequestMapping("/hello_world")
    public String hello() {
        return "hello.html";
    }

}

3-7

我们查看 " @RestController 注解 " 的源码,可以发现,它组合了 " @Controller " 和 " @ResponseBody " 注解,所以,它可以直接代替后两个注解。

3-7

四、Spring 官方提供的更多的注解

https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#mvc-ann-requestmapping
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

十七ing

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值