本节目标:在第 2 章搭好的 demo 项目里写出第一个 REST 接口,理解
@RestController与@GetMapping,分清「返回字符串」和「返回对象(自动 JSON)」的差别,掌握@RequestParam与@PathVariable两种取参方式,并用 curl 验证真实响应。
适用版本:Spring Boot 4.1.x(Java 21)
从第 2 章的项目出发
第 2 章我们用 Spring Initializr 生成了名为 demo 的项目,并确认了目录结构。现在它应该长这样(只列出与本节相关的部分):
demo/
├── pom.xml
├── mvnw
├── mvnw.cmd
└── src
├── main
│ ├── java
│ │ └── com/example/demo
│ │ └── DemoApplication.java
│ └── resources
│ └── application.properties
└── test
└── java
└── com/example/demo
└── DemoApplicationTests.java
此时 com.example.demo 包下只有一个 DemoApplication。它负责启动整个应用,但还没有任何对外暴露的接口——也就是说,即使你把项目跑起来,访问任何地址都只会得到 404。
本节要做的,就是往这个空壳里加第一个真正能用的东西:一个 REST 接口。
小提醒:本节所有示例都写在
src/main/java/com/example/demo/目录下,与DemoApplication同一个包。Spring Boot 默认只扫描启动类所在包及其子包,把控制器放这里最省心。
第一个接口:HelloController
在 com.example.demo 包下新建文件 HelloController.java:
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "Hello, Spring Boot 4!";
}
}
就这么十几行,一个可用的 HTTP 接口就诞生了。
逐行解释
@RestController:这是两个注解的组合——@Controller加@ResponseBody。它告诉 Spring「这个类里的方法返回值直接写进 HTTP 响应体,不要去解析成页面模板名」。写 JSON 接口时永远用它。@GetMapping("/hello"):把 HTTP 的GET /hello请求映射到下面这个方法。它是@RequestMapping(method = RequestMethod.GET)的简写,可读性更好。- 方法返回
String:返回值被当作响应体原文写入,Content-Type默认为text/plain;charset=UTF-8。 - 类必须被 Spring 扫描到才会生效。因为它在
com.example.demo包下、与DemoApplication同级,所以会被自动注册为 Bean,无需任何额外配置。
启动项目并发出第一次请求
先启动应用(第 2 章已经讲过 ./mvnw spring-boot:run,这里用你顺手的方式即可)。等看到日志里的 Tomcat started on port 8080 之后,另开一个终端发请求:
curl -i http://localhost:8080/hello
你会看到类似输出:
HTTP/1.1 200
Content-Type: text/plain;charset=UTF-8
Content-Length: 21
Date: Sat, 12 Sep 2026 02:01:33 GMT
Hello, Spring Boot 4!
三个关键信息:状态码 200、Content-Type 是 text/plain、响应体就是那串字符串。这证明请求确实被你的方法处理了。
返回对象:让 Spring 自动序列化成 JSON
真实项目几乎不会返回一句纯文本。把方法改成返回一个对象试试。先定义一个记录类型:
package com.example.demo;
import java.time.Instant;
public record Greeting(String message, Instant time) {
}
再在 HelloController 里加一个方法:
@GetMapping("/greeting")
public Greeting greeting() {
return new Greeting("你好,Spring Boot 4", Instant.now());
}
重启后请求它:
curl -i http://localhost:8080/greeting
HTTP/1.1 200
Content-Type: application/json
{"message":"你好,Spring Boot 4","time":"2026-09-12T02:03:10.482Z"}
注意两处变化:Content-Type 变成了 application/json,返回的对象被自动写成了 JSON。
为什么对象会变成 JSON
@RestController 的返回值不再走「视图解析」,而是交给一组 HttpMessageConverter 处理。其中负责对象的是 MappingJackson2HttpMessageConverter(4.x 中底层为 Jackson 3 的 JsonMapper)。它做两件事:
- 按返回对象的类型,用反射读出字段,序列化成 JSON 文本;
- 把响应头的
Content-Type设置为application/json。
这就是「返回字符串」与「返回对象」的本质差别:字符串被当作已完成的响应体,对象则要先经过序列化。判断标准很简单——你返回的是 String(或 byte[])就是原文;返回任何其他类型都会被序列化。
4.x 注意点:Jackson 3 的包名变了
Spring Boot 4.0 把 JSON 库升级到了 Jackson 3,这是影响面最广的变更之一:核心包名从 com.fasterxml.jackson 变成了 tools.jackson。
| 内容 | 3.x | 4.x |
|---|---|---|
核心包(ObjectMapper / JsonMapper) | com.fasterxml.jackson.databind | tools.jackson.databind |
注解包(@JsonProperty 等) | com.fasterxml.jackson.annotation | com.fasterxml.jackson.annotation(不变) |
也就是说,只有注解还在老的 com.fasterxml.jackson.annotation 下,其余绝大多数类都搬到了 tools.jackson。如果你在代码里自定义序列化器,导包时务必写 tools.jackson;但给字段加 @JsonProperty("user_name") 这类注解,导入路径仍是 com.fasterxml.jackson.annotation.JsonProperty。
还有一条要记住:自定义 ObjectMapper(4.x 为 JsonMapper)Bean 不再能替换自动配置。想微调序列化行为,应使用 JsonMapperBuilderCustomizer(3.x 的 Jackson2ObjectMapperBuilderCustomizer 已改名)。
本节不深入自定义序列化,先把接口写通。Jackson 3 的完整迁移细节在高级卷展开。
取参方式一:@RequestParam
最常见的取参是查询字符串(URL 里 ? 后面的部分)。把 /hello 改成接收一个 name:
@GetMapping("/hello")
public String hello(@RequestParam(name = "name", defaultValue = "World") String name) {
return "Hello, " + name + "!";
}
name = "name":绑定 URL 里的name参数。若参数名与方法形参名一致,也可省略写成@RequestParam String name。defaultValue = "World":不传时使用默认值。不写defaultValue且不传,会直接返回 400,这是新手最常踩的坑。
curl "http://localhost:8080/hello?name=Ada"
# Hello, Ada!
curl "http://localhost:8080/hello"
# Hello, World!
取参方式二:@PathVariable
另一种取参把变量嵌进路径里,常用于「某个资源的 id」:
@GetMapping("/users/{id}")
public String user(@PathVariable("id") Long id) {
return "user id = " + id;
}
{id} 是路径占位符,@PathVariable("id") 把它取出来并转成 Long。访问:
curl "http://localhost:8080/users/42"
# user id = 42
注意类型转换:如果请求 /users/abc,Spring 无法把 abc 转成 Long,会返回 400 Bad Request,而不是 500。这说明路径变量已经参与了类型绑定。
两种取参方式怎么选
| 维度 | @RequestParam | @PathVariable |
|---|---|---|
| 位置 | 查询字符串 ?k=v | 路径片段 /users/{id} |
| 语义 | 筛选、可选、有默认值 | 定位唯一资源、必填 |
| 缺省行为 | 不传即 400(除非 defaultValue) | 路径不匹配则 404 |
| 典型场景 | 分页 ?page=2&size=20、搜索 | /users/42、/orders/A1001 |
一条经验法则:「这是哪一个」用 @PathVariable,「怎么筛」用 @RequestParam。
手把手:改一行代码、重启、再请求
初学者最需要的不是更多概念,而是把「改—跑—验」这个循环走顺。跟着做一遍:
- 在
hello方法里把返回值改成"Hello, " + name + " — welcome!"。 - 回到运行应用的终端,按
Ctrl+C停掉进程,确认日志里出现Graceful shutdown complete。 - 重新运行
./mvnw spring-boot:run,等Tomcat started on port 8080出现。 - 再发一次请求:
curl "http://localhost:8080/hello?name=Ada"
# Hello, Ada — welcome!
改动的字符串立刻出现在响应里,说明整个链路是通的。这个循环会贯穿全书:改代码 → 重启 → curl 验证。等你觉得每次手动重启太慢,第 16 章的日志与开发体验章节会给出更省事的手段。
完整代码回顾
把本节所有改动合并,最终的 HelloController.java 是:
package com.example.demo;
import java.time.Instant;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(@RequestParam(name = "name", defaultValue = "World") String name) {
return "Hello, " + name + "!";
}
@GetMapping("/greeting")
public Greeting greeting() {
return new Greeting("你好,Spring Boot 4", Instant.now());
}
@GetMapping("/users/{id}")
public String user(@PathVariable("id") Long id) {
return "user id = " + id;
}
}
配套的 Greeting.java:
package com.example.demo;
import java.time.Instant;
public record Greeting(String message, Instant time) {
}
对照这份代码,你应该能回答四个问题:哪个方法返回纯文本、哪个返回 JSON、哪个参数来自查询字符串、哪个参数来自路径。能答全,这一节的目标就达成了。
常见坑
- 访问 404:路径拼错,或控制器不在启动类的包及子包下,没被扫描到。先确认 URL,再确认包位置。
- 访问 500 且日志报
HttpMessageNotWritableException:返回对象里有 Jackson 无法序列化的字段(例如自引用、无 getter 的复杂类型)。 @RequestParam不传参数就 400:忘了defaultValue,或把required默认的true当成可省略。- 返回对象时字段顺序不固定:JSON 字段顺序不是契约,客户端不应依赖顺序;真要固定可用
@JsonPropertyOrder。 - 把
@Controller当@RestController用:@Controller的方法默认返回视图名,结果会被当成模板去解析,往往报模板找不到。
小结
这一节我们完成了从 0 到 1 的跨越:在 demo 里新增 HelloController,用 @RestController + @GetMapping 暴露了 /hello;理解了返回 String 是原文、返回对象会自动经 Jackson 序列化成 JSON;用 @RequestParam 取查询参数、用 @PathVariable 取路径变量;最后用 curl 走通了一次真实的「改—跑—验」循环。特别记住 4.x 的 Jackson 3 包名迁移:核心类是 tools.jackson,注解仍在 com.fasterxml.jackson.annotation。
下一步的问题很自然:这个应用到底是怎么跑起来的?内嵌的 Tomcat 从哪来?SpringApplication.run() 在幕后做了什么?
阅读导航:上一节:2.3 认识 Spring Initializr 生成的项目结构 · 下一节:3.2 内嵌服务器与启动流程 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。