3. 操作:响应
让我们来探讨一个 Spring MVC 应用程序的架构:
![]() |
在本章中,我们将探讨将请求 [1] 引导至控制器及负责处理该请求的操作 [2a] 的过程,这一机制被称为路由。 此外,我们还将介绍操作可能向浏览器返回的各种响应(如 [3])。这些响应可能并非 V 视图(如 [4b])。
3.1. 新项目
我们创建一个新的 Spring 项目:
![]() |
- 在 [1-2] 中,我们创建了一个基于 Spring Boot 的新项目;
![]() |
- 在 [3] 中,Maven 项目的名称;
- 在 [4] 中,指定项目编译结果将放置的 Maven 包;
- [5],即编译产物的名称;
- [6],项目描述;
- [7],项目可执行类所在的包;
- 在 [8] 中,指定了项目的类型。这是一个使用 Thymeleaf 视图的 Web 项目。这里可以看到 Spring Boot 项目提供的所有现成的 Maven 依赖项;
- 在 [9] 中,指定由 Maven 构建生成的产物将打包为 jar 归档文件而非 war 文件。该项目将使用其依赖项中包含的嵌入式 Tomcat 服务器;
- 在 [10] 中,继续进行向导;
- 在 [11] 中,指定项目目录;
![]() |
- 在 [12] 中,生成的项目;
- 在 [14-15] 步骤中,重命名 [istia.st.springmvc] 包;
![]() |
- 为 [16],即包的新名称;
- 在 [17] 中,新建项目;
现在我们创建一个新类;
![]() |
- 在 [1-3] 中,我们创建一个新类;
![]() |
- 在 [5] 中为其命名,并在 [4] 中指定其包;
- 在 [6] 中创建新项目;
目前该类定义如下:
package istia.st.springmvc;
public class ActionsController {
}
我们将按以下方式改进该代码:
package istia.st.springmvc;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ActionsController {
}
- 第 6 行:注解 [@RestController] 表明两点:
- 带有此注解的类 [ActionsController] 是一个 Spring 控制器 MVC,因此包含处理客户端 URL 的操作;
- 这些操作的结果会被发送给客户端;
我们遇到的另一个注解 [@Controller] 则有所不同:带有此注解的控制器中的操作会返回应显示的视图名称。随后,该视图与操作为该视图构建的模型相结合,共同生成发送给客户端的响应。
项目结构的变更导致项目配置发生变化:
![]() |
[Application] 类的演变如下:
package istia.st.springmvc.main;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan({"istia.st.springmvc.controllers"})
@EnableAutoConfiguration
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
- 第 9 行:注解 [ComponentScan] 接受一个包名数组作为参数,Spring Boot 将在此数组中查找 Spring 组件。此处我们将包 [istia.st.springmvc.controllers] 放入该数组,以便找到带有注解 [@RestController] 的控制器;
我们将在控制器中构建各种操作,以说明其主要特征。首先,我们将关注在无视图应用程序中,一个操作可能返回的各种响应类型。
3.2. [/a01, /a02] - Hello world
我们的第一个操作如下:
@RestController
public class ActionsController {
// ----------------------- hello world ------------------------
@RequestMapping(value = "/a01", method = RequestMethod.GET)
public String a01() {
return "Greetings from Spring Boot!";
}
}
- 第 4 行:注解 [RequestMapping] 限定了由该注解操作处理的请求:
- [value] 属性是正在处理的 URL,
- 属性 [method] 确定了接受的方法;
因此,方法 [a01] 处理请求 HTTP [GET /a01]。
- 第 5 行:方法 [a01] 返回类型 [String],该类型将原样发送给客户端;
- 第 6 行:返回的字符串;
像之前多次做的那样启动应用程序,然后使用客户端 [Advanced Rest Client], 请求 URL [/a01] 时,使用 GET [1-2]:
![]() |
- 在 [3] 中,即服务器的响应;
- [4],响应的头部信息为 HTTP。 可见所使用的编码是 [ISO-8859-1]。若更倾向于使用 UTF-8 编码,可通过配置进行调整;
- 在 [5] 中,我们使用 Chrome 浏览器请求相同的 URL;
我们在控制器 [ActionsController] 中添加以下操作 [/a02](因此有时会将 URL 与其作为操作名称处理的方法混淆):
// ----------------------- 带重音字符 - UTF8 ------------------------
@RequestMapping(value = "/a02", method = RequestMethod.GET, produces="text/plain;charset=UTF-8")
public String a02() {
return "caractères accentués : éèàôûî";
}
- 第 2 行:[produces="text/plain;charset=UTF-8"] 属性表示该操作发送的文本流采用 [UTF-8] 格式编码。该格式特别支持带重音符号的字符;
要使此新操作生效,我们需要重新启动应用程序:
![]() |
结果如下:
![]() |
- 在 [1] 中,可见服务器发送的文档性质;
- 在 [2-3] 中,重音字符显示正确;
3.3. [/a03]:生成流 XML
我们添加以下操作 [/a03]:
// ----------------------- text/xml ------------------------
@RequestMapping(value = "/a03", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
public String a03() {
String greeting = "<greetings><greeting>Greetings from Spring Boot!</greeting></greetings>";
return greeting;
}
- 第 2 行:[produces="text/xml;charset=UTF-8"] 属性表明该操作发送一个包含按 [UTF-8] 格式编码字符的流 XML;
执行后结果如下:
![]() |
- 在 [1] 中,HTTP 头部明确指出发送的文档是 HTML;
- 在 [2] 中,Chrome 浏览器利用此信息对接收到的 XML 文本进行格式化;
需要提醒的是,在 Chrome 中,可以通过开发者工具窗口(Ctrl+Shift+I)查看客户端与服务器之间的 HTTP 数据交换:

今后,我们将不再系统地对客户端与服务器之间的HTTP通信进行截图。有时,我们将仅列出这些通信的文本内容。
3.4. [/a04, /a05]:生成数据流 jSON
我们添加了以下操作 [/a04]:
// ----------------------- 生成 jSON ------------------------
@RequestMapping(value = "/a04", method = RequestMethod.GET)
public Map<String, Object> a04() {
Map<String, Object> map = new HashMap<String, Object>();
map.put("1", "un");
map.put("2", new int[] { 4, 5 });
return map;
}
- 第 3 行:该操作返回类型为 [Map],即一个字典。 需要注意的是,对于 [@RestController] 类型的控制器,操作的结果即为发送给客户端的响应。由于 HTTP 协议是一种文本行交换协议,因此客户端的响应必须序列化为字符串。 为此,Spring MVC 会使用各种 [Objet <---> chaîne de caractères] 转换器。特定对象与转换器的关联是通过配置实现的。在此,Spring Boot 的自动配置将检查项目的依赖关系:
![]() |
上述 Jackson 依赖项是用于将对象序列化/反序列化为字符串的库。Spring Boot 将使用这些库对操作返回的对象进行序列化/反序列化。 第 9.7 节中提供了一个将 Java 对象序列化/反序列化为 jSON 的 Java 代码示例。
请注意第 2 行中,我们未指定发送响应的类型。接下来我们将查看系统将发送的默认类型。
在 Chrome 中,[1-3] 的结果如下:
![]() |
现在我们添加以下操作 [/a05]:
// ----------------------- 生成 jSON - 2 ------------------------
@RequestMapping(value = "/a05", method = RequestMethod.GET)
public Personne a05() {
return new Personne(1,"carole",45);
}
类 [Personne] 如下所示:
![]() |
package istia.st.sprinmvc.models;
public class Personne {
// 标识符
private Integer id;
// 姓名
private String nom;
// 年龄
private int age;
// 构造函数
public Personne() {
}
public Personne(String nom, int age) {
this.nom = nom;
this.age = age;
}
public Personne(Integer id, String nom, int age) {
this(nom, age);
this.id = id;
}
@Override
public String toString() {
return String.format("[id=%s, nom=%s, age=%d]", id, nom, age);
}
// 获取器和设置器
...
}
执行结果如下:
![]() |
- 在 [1] 中,服务器指出其发送的文档是 jSON;
- 在 [2] 中,接收到的文档为 jSON;
3.5. [/a06]:返回空流
我们添加以下操作 [/a06]:
// ----------------------- 返回空流 ------------------------
@RequestMapping(value = "/a06")
public void a06() {
}
- 第 3 行,操作 [/a06] 未返回任何内容。Spring MVC 随后将向客户端生成一个空响应;
执行结果如下:
![]() |
上文响应中的 HTTP [Content-Length] 属性表明,服务器发送了一个空文档。
3.6. [/a07, /a08, /a09]:数据流类型,与 [Content-Type] 相关
我们添加以下操作 [/a07]:
// ----------------------- text/html ------------------------
@RequestMapping(value = "/a07", method = RequestMethod.GET, produces = "text/html;charset=UTF-8")
public String a07() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- 第 2 行,操作 [/a07] 生成数据流 HTML [text/html];
- 第 4 行:一个字符串 HTML;
执行结果如下:
![]() |
- 在 [1] 中,可以看到 Chrome 已解析了 HTML <h1> 标签,该标签以大号字体显示其内容;
现在,我们对以下 [/a08] 操作进行同样的处理:
// ----------------------- HTML 的 text/plain 格式结果 ------------------------
@RequestMapping(value = "/a08", method = RequestMethod.GET, produces = "text/plain;charset=UTF-8")
public String a08() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- 第 2 行:该操作的响应类型为 [text/plain];
结果如下:
![]() |
- 在 [1] 中,Chrome 未解析 HTML <h1> 标签,因为服务器告知其发送的是 [text/plain] [2] 数据流;
让我们用接下来的 [/a09] 操作再试一次类似的情况:
// ----------------------- HTML 结果(text/xml 格式) ------------------------
@RequestMapping(value = "/a09", method = RequestMethod.GET, produces = "text/xml;charset=UTF-8")
public String a09() {
String greeting = "<h1>Greetings from Spring Boot!</h1>";
return greeting;
}
- 第2行:发送类型为[text/xml]的数据流;
结果如下:
![]() |
- 在 [1] 中,Chrome 未能解析 HTML <h1> 标签,因为服务器告知其发送的是 [text/xml] [2] 数据流。 因此,它将标签 <h1> 视为 XML 标签;
从这些示例中,我们可以了解到服务器响应中的 HTTP [Content-Type] 标头的重要性。浏览器利用该标头来确定如何解析收到的文档;
3.7. [/a10, /a11, /a12]:重定向客户端
我们创建一个新的控制器 [RedirectController]:
![]() |
[RedirectCntroller] 的代码目前如下:
package istia.st.springmvc.controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
@Controller
public class RedirectController {
}
- 第 7 行:使用注解 [@Controller],这意味着从现在起,操作结果的类型 [String] 将默认指代操作或视图的名称;
我们创建以下操作 [/a10]:
// ------------ 跳转至第三方操作 -----------------------
@RequestMapping(value = "/a10", method = RequestMethod.GET)
public String a10() {
return "a01";
}
- 第 4 行:返回结果 'a01',即一个操作的名称。随后将由该操作向客户端发送响应;
以下是一个示例:
![]() |
- 在 [2] 中,我们接收到了来自操作 [/a01] 的数据流;
- 在 [3] 中,浏览器显示的是来自动作 [/a10] 的 URL;
现在我们创建以下操作 [/a11]:
// ------------ 302 临时重定向至第三方操作 -----------------------
@RequestMapping(value = "/a11", method = RequestMethod.GET)
public String a11() {
return "redirect:/a01";
}
我们得到以下结果:
![]() |
- 在 Chrome 日志中,[1-2] 显示了两个请求,一个指向 [/a11],另一个指向 [/a01];
- 在 [3] 中,服务器返回状态码 [302],要求客户端浏览器重定向至URL,该地址由标头 HTTP [Location:] [4] 指定。代码 [302] 是一个临时重定向代码;
随后浏览器向重定向服务器 URL 发出第二次请求:
![]() |
- 在 [5] 中,即客户端的第二个请求;
- 在 [6] 中,客户端浏览器显示重定向请求的 URL;
若需设置永久重定向,则需向客户端发送以下 HTTP 头部:
这表示重定向是永久性的。某些搜索引擎会区分临时重定向(302)和永久重定向(301)。
我们编写操作 [/a12] 来执行此永久重定向:
// ------------ 301 永久重定向至第三方操作 -----------------
@RequestMapping(value = "/a12", method = RequestMethod.GET)
public void a12(HttpServletResponse response) {
response.setStatus(301);
response.addHeader("Location", "/a01");
}
- 第3行:要求Spring MVC注入对象[HttpServletResponse],该对象封装了发送给客户端的响应;
- 第 4 行:设置响应的 [status]、头部 [301] 以及 HTTP:
- 第5行:手动创建以下HTTP头部:
该文件即为重定向文件 URL。
执行后得到以下结果:
![]() | ![]() |
从这个示例中,我们可以了解到如何:
- 生成响应状态码 HTTP;
- 在响应中包含 HTTP 头部;
3.8. [/a13]:生成完整的响应
如类[ResponsesController]的以下操作所示,可以完全控制响应:
![]() |
// ----------------------- 响应生成完成 ------------------------
@RequestMapping(value = "/a13")
public void a13(HttpServletResponse response) throws IOException {
response.setStatus(666);
response.addHeader("header1", "qq chose");
response.addHeader("Content-Type", "text/html;charset=UTF-8");
String greeting = "<h1>Greetings from Spring Boot!</h1>";
response.getWriter().write(greeting);
}
- 第 3 行:操作的结果是 [void]。在此情况下,若要向客户端发送非空响应,需使用 Spring 提供的 [HttpServletResponse response] 对象;
- 第4行:为响应设置一个客户端无法识别的状态码;
- 第 5 行:添加一个客户端无法识别的 HTTP 头部;
- 第 6 行:添加 HTTP [Content-Type] 标头,用于指定将发送的数据流类型,此处为 HTML;
- 第7-8行:响应中紧随HTTP头部之后的文档;
结果如下:
![]() |
- 在 [1] 中,我们可以识别出响应中的元素;
- 在 [2-3] 中,可以看到 Chrome 忽略了以下事实:
- 响应状态 HTTP 并非被识别的状态 HTTP,
- [header1] 头部并非被识别的 HTTP 头部;
如果客户端不是浏览器而是编程客户端,则可以自由使用任意状态码和标头。



























