《Spring Boot 入门》3.1 写第一个 REST 接口

在 demo 项目里写出第一个 REST 接口:用 @RestController 与 @GetMapping 暴露 /hello,对比返回字符串与返回对象自动 JSON 序列化的差别,掌握 @RequestParam 与 @PathVariable 两种取参方式,并用 curl 完成一次真实的请求与响应验证。

本节目标:在第 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)。它做两件事:

  1. 按返回对象的类型,用反射读出字段,序列化成 JSON 文本;
  2. 把响应头的 Content-Type 设置为 application/json。

这就是「返回字符串」与「返回对象」的本质差别:字符串被当作已完成的响应体,对象则要先经过序列化。判断标准很简单——你返回的是 String(或 byte[])就是原文;返回任何其他类型都会被序列化。

4.x 注意点:Jackson 3 的包名变了

Spring Boot 4.0 把 JSON 库升级到了 Jackson 3,这是影响面最广的变更之一:核心包名从 com.fasterxml.jackson 变成了 tools.jackson。

内容3.x4.x
核心包(ObjectMapper / JsonMapper)com.fasterxml.jackson.databindtools.jackson.databind
注解包(@JsonProperty 等)com.fasterxml.jackson.annotationcom.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。

手把手:改一行代码、重启、再请求

初学者最需要的不是更多概念,而是把「改—跑—验」这个循环走顺。跟着做一遍:

  1. 在 hello 方法里把返回值改成 "Hello, " + name + " — welcome!"。
  2. 回到运行应用的终端,按 Ctrl+C 停掉进程,确认日志里出现 Graceful shutdown complete。
  3. 重新运行 ./mvnw spring-boot:run,等 Tomcat started on port 8080 出现。
  4. 再发一次请求:
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 内嵌服务器与启动流程 。

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. 《Spring Boot 入门》18.3 打包与运行
  2. 《Spring Boot 入门》18.2 实现
  3. 《Spring Boot 入门》18.1 需求与设计