跳转至
内容字体
东观体
上图东观体
OPPO Sans
江城黑体
霞鹜臻楷
代码字体
DejaVu Sans Mono
DejaVu Sans Mono
Google Sans Code
JetBrains Mono
主题切换
返回顶部

SpringMVC基础

约 4832 个字 426 行代码 5 张图片 预计阅读时间 21 分钟

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
10
@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("/hello")
@RestController
public class HelloController {

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

获取Cookie和Session

关于Cookie和Session的介绍,此处不再提及,具体见HTTP中的Cookie和Session

对于获取Cookie来说,有两种方式:

  1. 使用Servlet的HttpServletRequest对象:可以获取到Cookie中所有的键值对,更常用
  2. 使用注解@CookieValue注解:根据注解指定的key(或者方法形参名,此时不需要单独在注解中指定key名称)获取到具体的某一个Cookie值

例如下面的代码:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@GetMapping("/getCookie")
public String getCookie(HttpServletRequest request) {
    Cookie[] cookies = request.getCookies();
    if(cookies == null)
        return "获取Cookie失败";

    for (Cookie cookie : cookies) {
        System.out.println(cookie.getName() + ":" + cookie.getValue());
    }

    return "获取Cookie成功";
}
Java
1
2
3
4
@GetMapping("/getCookie2")
public String getCookie2(@CookieValue("test_cookie") String name) {
    return "获取指定的Cookie:test_cookie值为:" + name;
}

对于Session来说,在获取之前要先使用HttpServletRequest对象进行创建,该类中有一个getSession方法,该方法有两个版本:

  1. 无参版本getSession():获取已有的HttpSession对象,如果不存在该对象则创建新的HttpSession对象并返回
  2. 有参版本getSession(boolean create):如果createtrue,则获取已有的HttpSession对象,如果不存在该对象则创建新的HttpSession对象并返回(与无参版本效果相同);如果createfalse,则获取已有的HttpSession对象,如果不存在该对象则返回null

例如下面的设置方式:

Java
1
2
3
4
5
6
7
8
@GetMapping("/setSession")
public String setSession(HttpServletRequest request) {
    // 获取Session对象,不存在默认创建
    HttpSession session = request.getSession();
    session.setAttribute("username", "zhangsan");
    session.setAttribute("age", 18);
    return "设置Session成功";
}

Note

需要注意的是,不需要单独调用HttpServletResponse对象将对应的JSESSIONID设置到Cookie中,这一步已经由getSession方法实现

设置完成后即可获取Session,获取方式有三种:

  1. 使用HttpServletRequest对象获取:获取方式详细且可以根据具体的键获取到对应的值
  2. 使用HttpSession对象获取:与第一种方式类似,但是不用显式调用getSession方法,默认情况下调用的是无参getSession
  3. 使用@SessionAttribute注解获取:根据注解指定的key(或者方法形参名,此时不需要单独在注解中指定key名称)获取到具体的值,但是必须确保Session已经创建

例如下面的代码:

Java
1
2
3
4
5
6
7
8
@RequestMapping("/getSession")
public String getSession(HttpServletRequest request) {
    // 获取Session对象,不存在默认创建
    HttpSession session = request.getSession();
    String username = (String) session.getAttribute("username");
    Integer age = (Integer) session.getAttribute("age");
    return "Session中的用户名为:" + username + " Session中的年龄为:" + age;
}
Java
1
2
3
4
5
6
@GetMapping("/getSession3")
public String getSession3(HttpSession session) {
    String username = (String) session.getAttribute("username");
    Integer age = (Integer) session.getAttribute("age");
    return "Session中的用户名为:" + username + " Session中的年龄为:" + age;
}
Java
1
2
3
4
5
// 必须确保已经设置过Session
@GetMapping("/getSession2")
public String getSession2(@SessionAttribute String username, @SessionAttribute("age") String age) {
    return "Session中的用户名为:" + username + " Session中的年龄为:" + age;
}

获取Header的方式有两种:

  1. 使用HttpServletRequest对象:可以获取到所有的Header键值对,更常用
  2. 使用@RequestHeader注解:根据注解指定的key(或者方法形参名,此时不需要单独在注解中指定key名称)获取到具体的某一个Header值

例如下面的代码:

Java
1
2
3
4
5
6
// 使用传统方式获取Header
@GetMapping("/getHeader")
public String getHeader(HttpServletRequest request) {
    String header = request.getHeader("User-Agent");
    return "获取Header成功,值为:" + header;
}
Java
1
2
3
4
5
// 使用注解获取
@GetMapping("/getHeader2")
public String getHeader2(@RequestHeader("User-Agent") String agent) {
    return "获取Header成功,值为:" + agent;
}

响应处理

返回静态页面

返回静态页面的方式比较直接,在项目目录下的resource目录中的static创建一个test.html,内容任意,接着写下下面的代码:

Java
1
2
3
4
5
6
7
@RestController
public class StaticResourceController {
    @GetMapping("/static")
    public String staticResourceResponse() {
        return "test.html";
    }
}

接着,使用浏览器向服务器发送请求:http://localhost:8080/static,会发现页面显示的内容并不是test.html中的内容,而是将test.html作为纯文本渲染到页面上

出现这个问题的原因就在@RestController注解上,查看这个注解的源码:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
    @AliasFor(
        annotation = Controller.class
    )
    String value() default "";
}

对于前三行注解分别表示的含义如下:

  1. @Target:指定注解可以应用的目标元素类型,ElementType.TYPE表示该注解能用于类、接口、枚举等类型声明上,类似的还有ElementType.METHOD表示该注解能用于方法上
  2. @Retention:指定注解的保留策略,即注解在什么时候还有效。RetentionPolicy.RUNTIME表示注解在运行时仍然有效,可以通过反射获取。类似的还有SOURCECLASS
  3. Documented:标记注解是否包含在JavaDoc文档中

重点看后两个注解:

在早期的SpringMVC中,前后端并没有分离,所以有些页面是通过后端返回的,而现在大部分都是前后端分离模式,即后端只需要告诉前端需要的数据,前端根据数据进行页面处理。而@Controller注解出现的时期就是前后端没有分离的早期,所以默认情况下返回的是页面而非数据,而如果要返回数据而非页面就需要使用@ResponseBody注解,此时返回的数据就被当作text/html类型进行处理,在进入了前后端分离时,大部分情况下都是返回数据,所以为了简化开发,将@Controller@ResponseBody进行合并,得到了@RestController注解

基于上面的原因,所以要返回静态页面需要使用@Controller注解而非@RestController注解:

Java
1
2
3
4
5
6
7
@Controller
public class StaticResourceController {
    @GetMapping("/static")
    public String staticResourceResponse() {
        return "test.html";
    }
}

再次发送请求即可看到test.html中的内容

返回数据

上面已经介绍了@ResponseBody注解的基本作用,下面详细介绍该注解:

@ResponseBody既可以用在类上,也可以用在方法上。当注解用在类上,表示这个类所有的方法返回的都是纯text/html数据,而不返回页面;当注解用在方法上,表示当前方法返回的是数据而不是页面,其他方法不受影响

以用在类上为例:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@ResponseBody
@Controller
public class ResponseBodyController {

    // 使用@ResponseBody注解返回text/html数据
    @GetMapping("/responseBody")
    public String response() {
        return "<h1>hello world</h1>";
    }

    @GetMapping("/responseBody")
    public String response2() {
        return "test.html";
    }
}

可以发现两个方法都是将返回的数据直接渲染到页面上,特别是response2方法,并不会返回test.html文件中的内容

返回JSON数据

后端需要向前端返回JSON数据时只需要返回一个对象即可,例如以前面的User类为例,返回一个User对象:

Java
1
2
3
4
5
6
7
8
@RequestMapping("/response")
@RestController
public class ResponseController {
    @GetMapping("/json")
    public User json() {
        return new User("epsda", 18, "男");
    }
}

前端得到的数据如下:

Text Only
1
{"name":"epsda","age":18,"gender":"男"}

设置状态码

设置状态码通过HttpServletResponse对象进行设置即可:

Java
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@RequestMapping("/status")
@RestController
public class StatusCodeController {

    // 设置状态码可以使用HttpServletResponse对象进行设置
    @RequestMapping("/setStatus")
    public String setStatus(HttpServletResponse response) {
        // 错误状态码不影响显示数据
        response.setStatus(404);
        return "设置状态码成功";
    }
}

设置Header

一些常见的头部字段可以通过@RequestMapping注解实现,先看@RequestMapping的源码:

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
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Mapping
@Reflective({ControllerMappingReflectiveProcessor.class})
public @interface RequestMapping {
    String name() default "";

    @AliasFor("path")
    String[] value() default {};

    @AliasFor("value")
    String[] path() default {};

    RequestMethod[] method() default {};

    String[] params() default {};

    String[] headers() default {};

    String[] consumes() default {};

    String[] produces() default {};
}

其中:

  1. value:指定映射的URL
  2. method:指定请求类型,如GET, POST
  3. consumes:指定处理请求的提交内容类型(Content-Type),例如application/json, text/html
  4. produces:指定返回的内容类型,还可以同时设置返回值的字符编码
  5. Params:要求请求中必须包含某些参数值时,才让该方法处理
  6. headers:要求请求中必须包含某些指定的header值,才能让该方法处理请求

自定义Header可以通过HttpServletResponse对象进行设置:

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

    // 使用传统方式设置Header
    @GetMapping("/setHeader")
    public String setHeader(HttpServletResponse response) {
        response.setHeader("Test-Header", "test");
        return "设置Header成功";
    }
}