跳转至

SpringMVC基础

约 3351 个字 276 行代码 5 张图片 预计阅读时间 15 分钟

MVC架构模式介绍

MVC(Model View Controller)是软件工程中的一种软件架构模式,它把软件系统分为模型(Model)视图(View)控制器(Controller)三个基本部分。用一种业务逻辑、数据、界面显示分离的方法组织代码,将业务逻辑聚集到一个部件里面,在改进和个性化定制界面及用户交互的同时,不需要重新编写业务逻辑

  • MModel模型层:存放和数据库对象的实体类以及一些用于存储非数据库表完整相关的VO(valueObject)对象或者存放一些对数据进行逻辑运算操作的的一些业务处理代码
  • VView视图层:存放一些视图文件相关的代码,例如HTML、CSS、JS
  • CController控制层:接收客户端请求,获得请求数据,最后将准备好的数据响应给客户端

MVC模式工作流程如下图所示:

MVC模式下项目中的常见包:

  • Model模型层:

    1. 实体类包(pojo/entity/bean)专门存放和数据库对应的实体类和一些VO对象
    2. 数据库访问包(dao/mapper)专门存放对数据库不同表格CURD方法封装的一些类
    3. 服务包(service)专门存放对数据进行业务逻辑运算的一些类
  • Controller控制层:控制层包(controller

  • View视图层:Web目录下的视图资源,例如HTML、CSS、JS、图片

SpringMVC介绍

SpringMVC,全称为Spring Web MVC,是基于Servlet API构建的原始Web框架,从一开始就包含在Spring框架中。它的正式名称Spring Web MVC来自其源模块的名称(Spring-webmvc),在大部分情况下都会称为SpringMVC

关于Servlet

Servlet(server applet)是运行在服务端(例如Tomcat)的Java小程序,是SUN公司提供一套定义动态资源规范。从代码层面上来讲Servlet就是一个接口,其主要用来接收、处理客户端请求、响应给浏览器的动态资源。在整个Web应用中,Servlet主要负责接收处理请求、协同调度功能以及响应数据。因此可以把Servlet称为Web应用中的控制器

不是所有的Java类都能用于处理客户端请求,能处理客户端请求并做出响应的一套技术标准就是Servlet,因为其要处理客户端请求,所以Servlet必须在WEB项目中开发且在Tomcat这样的服务容器中运行

在Spring实现MVC时结合了自身项目的特点对MVC模式进行了一定的改变,如下图所示:

可以从图中看到最大的改变就是浏览器直接请求Controller而不是通过视图发起请求

SpringMVC与SpringBoot

SpringMVC是Spring框架的一个Web模块,专注于MVC模式下的Web开发,而SpringBoot是Spring的扩展框架,通过自动配置和starters简化了Spring应用的搭建和部署,而SpringBoot是实现SpringMVC的其中一种方式,通过添加各种依赖和SpringMVC框架来实现Web功能

接下来的介绍会基于SpringBoot项目,在SpringBoot项目中同时介绍SpringMVC的相关知识。而本文档介绍SpringMVC基础内容时从下面三个方面进行:

  1. 建立连接
  2. 请求处理
  3. 响应处理

客户端与服务端建立连接

@RequestMapping介绍

在SpringMVC中,要想做到浏览器可以请求到后端的动态资源可以使用@RequestMapping注解实现URL路由映射。对应的,在具体的类上添加@RestController

例如下面的代码:

Java
1
2
3
4
5
6
7
8
@RestController
public class HelloController {

    @RequestMapping("/sayHello")
    public String hello() {
        return "Hello World!";
    }
}

@RequestMapping注解中,第一个值为路径参数,例如上面例子中的"/sayHello",这个路径可以带/,也可以不带/,但是一般情况下推荐带上/表示路径。接着,在浏览器通过地址http://localhost:8080/sayHello中发起请求即可看到返回的字符串Hello World!

@RequestMapping修饰位置

@RequestMapping既可以修饰类,也可以修饰方法:

  1. @RequestMapping修饰类时,表示请求路径的初始信息
  2. @RequestMapping修饰方法时,表示请求路径的具体信息

当修饰类和方法时,访问的地址就是类路径+方法路径,例如下面的代码:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @RequestMapping("/sayHello")
    public String hello() {
        return "Hello World!";
    }
}

此时的访问路径即为http://localhost:8080/hello/sayHello,一般情况下建议在类上添加@RequestMapping,这样可以避免方法路径的重复导致的服务器错误

@RequestMapping多层路径

@RequestMapping除了设置一层路径以外,还可以设置多层路径,例如:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/user/hello")
@RestController
public class HelloController {

    @RequestMapping("/v1/sayHello")
    public String hello() {
        return "Hello World!";
    }
}

此时的访问路径即为http://localhost:8080/user/hello/v1/sayHello

@RequestMapping@GetMapping@PostMapping

默认情况下,@RequestMapping可以处理所有类型的HTTP请求,包括但不限于GETPOST,如果想要指定只处理GET请求和POST请求,可以设置@RequestMapping的第二个属性值

例如,对于GET请求:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @RequestMapping(value = "/sayHello", method = RequestMethod.GET)
    public String hello() {
        return "Hello World!";
    }
}

此时只有发送了GET请求才可以执行hello()方法

为了简化上面的写法,如果只想让方法处理GET请求可以使用@GetMapping,例如上面的代码可以修改为:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello() {
        return "Hello World!";
    }
}

同样,对于POST请求可以通过@RequestMapping的第二个属性值进行指定,例如下面的代码:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @RequestMapping(value = "/sayHello", method = RequestMethod.POST)
    public String hello() {
        return "Hello World!";
    }
}

同样,如果只想让方法处理POST请求可以使用@PostMapping,例如上面的代码可以修改为:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping("/hello")
@RestController
public class HelloController {

    @PostMapping("/sayHello")
    public String hello() {

        return "Hello World!";
    }
}

对于其他的请求方式也是同样的逻辑,此处不再演示

Note

需要注意的是,@GetMapping@PostMapping不支持类上添加,只支持方法上添加,对于其他针对某一种请求方式的注解也是如此

请求处理

处理一个参数

当请求中只有一个参数,例如?name=zhangsan时,接收方式很简单,只需要在处理方法中定义一个形参名字与请求参数key的名字一样即可,例如下面的代码用于接收name参数:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(String name) {

        return "接收到姓名为:" + name;
    }
}

此时发送请求为http://localhost:8080/hello/sayHello?name=zhangsan即可收到下面的信息:

Text Only
1
接收到姓名为:zhangsan

对于POST请求来说,参数放在请求参数或者请求体都是可以正常接收的,接收代码与GET请求类似:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping(value = "/hello")
@RestController
public class HelloController {

    @PostMapping("/sayHello")
    public String hello(String name) {

        return "接收到姓名为:" + name;
    }
}

但是需要注意的是,如果方法参数是基本数据类型(不包括布尔类型,如果是布尔类型不传递参数时,默认值为false)时,参数不可以不传递,否则就会报错,因为当不传递参数时,如果是引用数据类型,那么值为null可以正常赋值给引用数据类型,但是如果参数是基本数据类型无法将null转换为基本数据类型从而报错。所以在处理基本数据类型的参数时建议使用对应的包装类而非基本数据类型,例如下面的代码:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping(value = "/hello")
@RestController
public class HelloController {

    @PostMapping("/sayHello")
    public String hello(Integer age) {

        return "接收到年龄为:" + age;
    }
}
Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping(value = "/hello")
@RestController
public class HelloController {

    @PostMapping("/sayHello")
    public String hello(int age) {

        return "接收到年龄为:" + age;
    }
}
为什么前端URL参数和后端方法参数一致可以获取到参数

实际上这与Servlet有关,在Servlet开发中,客户端向服务器端发出请求,服务器端的软件Tomcat接收到用户请求后会将请求报文的信息转换为HttpServletRequest对象,该对象中包含着请求中的所有信息,例如请求头、请求行。需要注意,这一过程中的HttpServletRequest对象并不是由程序员手动创建的,而是由Tomcat自动创建,并且此时除了存在HttpServletRequest对象以外,还有一个HttpServletResponse对象,该对象用于存储响应报文信息。在Spring Web MVC中包括了Servlet,所以可以直接在方法参数中使用HttpServletRequest对象,另外因为SpringBoot集成了Tomcat,所以该对象也会被正常设置。以获取参数String name为例,实现下面的代码:

Java
1
2
3
4
5
6
7
@GetMapping("/sayHello")
public String hello2(HttpServletRequest request) {
    // name即为方法参数的名字
    // getParameter中的name即为URL参数的名字
    String name = request.getParameter("name");
    return "接收到姓名为:" + name;
}

如果需要为形参名指定别名,可以在参数部分使用@RequestParam注解,此时前端传递时应该匹配的时@RequestParam中的名称而不是变量名:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(@RequestParam("username") String name) {
        return "接收到姓名为:" + name;
    }
}

在上面的代码中,前端传递参数时key应该为username而不再是name,例如:http://localhost:8080/hello/sayHello?username=zhangsan

但是,一旦指定了别名,如果不传递参数就会出现无法找到指定资源的情况,即不可以不传递参数。以上面的代码演示,使用Postman发送不指定参数的请求结果如下:

出现这个问题可以从@RequestParam源码分析:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public @interface RequestParam {
    @AliasFor("name")
    String value() default "";

    @AliasFor("value")
    String name() default "";

    boolean required() default true;

    String defaultValue() default "\n\t\t\n\t\t\n\ue000\ue001\ue002\n\t\t\t\t\n";
}

@RequestParam注解源码中有一个字段required,这个字段默认值为true,表示一定要传递参数,所以可以通过设置该字段为false使其在不传递参数时也可以正常请求资源,例如下面的代码:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(@RequestParam(value = "username", required = false) String name) {
        return "接收到姓名为:" + name;
    }
}

此时发起请求http://localhost:8080/hello/sayHello即可得到下面的结果:

Text Only
1
接收到姓名为:null

处理多个参数

处理多个参数与处理一个参数比较类似,只需要在方法参数部分添加多个参数即可,例如下面的代码:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(String name, Integer id) {
        return "接收到姓名为:" + name + " id值为:" + id;
    }
}

需要注意的是,URL中的参数顺序可以不与方法参数的顺序一致,但是参数名必须一致,即请求地址可以为http://localhost:8080/hello/sayHello?id=1&name=zhangsan

处理对象

上面介绍了处理多个参数,但是多个参数彼此并没有任何关系,有时前端传递的一个参数可能是属于某一个类的对象的字段值,此时就推荐使用对象而不是使用多个参数的方式,例如下面有一个User类(注意提供gettersetter):

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class User {
    private String name;
    private Integer age;
    private String gender;

    public User(String name, Integer age, String gender) {
        this.name = name;
        this.age = age;
        this.gender = gender;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    @Override
    public String toString() {
        return "User{" + "name='" + name + '\'' + ", age=" + age + ", gender='" + gender + '\'' + '}';
    }
}

处理对象的方式与处理多个参数的方式类似,只需要在方法参数部分添加对象类型的参数即可,例如下面的代码:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(User user) {
        return "接收到用户信息为:" + user;
    }
}

前端传递方式与多个参数一致,例如:http://localhost:8080/hello/sayHello?name=zhangsan&age=18&gender=man。此时可以看到下面的结果:

Text Only
1
接收到用户信息为:User{name='zhangsan', age=18, gender='man'}

需要注意的是,前面在处理一个参数的时候提到过,如果是基本数据类型(除了布尔类型),则参数必须要传递,但是在处理对象时,对象的字段可以是基础数据类型,因为此时基础数据类型存在默认值,如果没有传递则直接使用默认值。此处不再演示,但是还是推荐使用包装类而不是基本数据类型

处理数组

处理数组也是类似,只需要将参数部分修改为数组类型即可:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(String[] params) {
        // 将数组封装成List
        return "接收到参数为:" + List.of(params);
    }
}

此时前端传递参数可以有两种方式:

  1. keyvalue分开指定:http://localhost:8080/hello/sayHello?params=param1&params=param2&params=param3
  2. keyvalue合并指定:http://localhost:8080/hello/sayHello?params=param1,param2,param3

这两种方式都可以正常得到结果:

Text Only
1
接收到参数为:[param1, param2, param3]

但是现在有一个问题,如果是合并指定,那么数组中存储的是三个单独的元素还是一个整体的元素。为了验证这一点,可以在方法中打印数组的大小:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(String[] params) {
        // 将数组封装成List
        return "接收到参数为:" + List.of(params) + " 数组长度为:" + params.length;
    }
}

以合并指定的方式发送请求可以得到如下结果:

Text Only
1
接收到参数为:[param1, param2, param3] 数组长度为:3

通过结果可以看到数组中存储的是三个单独元素而并非一个整体的元素

处理集合

集合与数组比较类似,但是需要注意的是,默认情况下,请求中参数名相同的多个值是封装到数组而并非集合,直接传递给参数的集合会出现错误。为了避免这个问题,可以使用@RequestParam进行参数绑定,确保数组可以转换为集合,以List为例:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(@RequestParam List<String> params) {
        return "接收到参数为:" + params;
    }
}

请求方式与数组一致,此处不再演示

处理JSON数据

对于JSON字符串来说,需要在参数部分使用@RequestBody进行绑定,而JSON字符串中保存的是一个JSON对象,所以考虑使用一个类对象进行接收(此处依旧使用User类),例如下面的代码:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(@RequestBody User user) {
        return "接收到用户信息为:" + user;
    }
}

传递JSON数据时建议在请求体中传递,例如:

JSON
1
2
3
4
5
{
    "name": "zhangsan",
    "age": 18,
    "gender": "man"
}

此时可以得到结果:

Text Only
1
接收到用户信息为:User{name='zhangsan', age=18, gender='man'}

获取到URL中的资源路径

在前面@RequestParam中使用的都是静态的路径,如果路径中存在动态变化的部分,就需要在@RequestParam中使用参数的形式(或者具体请求方式的注解,例如@GetMapping),对应的方法要获取到这个动态变化的参数就需要使用@PathVariable注解,例如下面的代码:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello/{id}")
    public String hello(@PathVariable Integer id) {
        return "接收到用户id为:" + id;
    }
}

需要注意的是,此时必须保证路径参数和方法参数一致,即{id}Integer id一致,此时请求路径为:http://localhost:8080/hello/sayHello/1,得到的结果为:

Text Only
1
接收到用户id为:1

也可以通过@PathVariable指定别名,此时{user_id}需要与@PathVariable("user_id")一致,例如:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello/{user_id}")
    public String hello(@PathVariable("user_id") Integer id) {
        return "接收到用户id为:" + id;
    }
}

此时请求路径为:http://localhost:8080/hello/sayHello/1,得到的结果为:

Text Only
1
接收到用户id为:1

但是需要注意的是,虽然@PathVariable注解也存在required字段,但是不论是修改为true还是false都必须指定路径参数

上传文件

上传文件时使用的参数类型是MultipartFile,例如下面的代码:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping("/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(MultipartFile file) {
        return "接收到文件名为:" + file.getOriginalFilename();
    }
}

如果想为文件指定别名,可以使用@RequestPart注解:

Java
1
2
3
4
5
6
7
8
9
@RequestMapping(value = "/hello")
@RestController
public class HelloController {

    @GetMapping("/sayHello")
    public String hello(@RequestPart("filename") MultipartFile file) {
        return "接收到文件名为:" + file.getOriginalFilename();
    }
}