☰
Spring Boot零基础入门day02:手写REST接口实现前后端数据打通
2026/10/20 7:56:12 网站建设 项目流程

从零开始学框架,最怕的就是“能启动”却“不知道怎么写业务”。我在给一个完全零基础的开发者带着做这个“Spring Boot从0到1”系列时,第二天的选题就非常关键了。day01把环境跑通、项目能启动、Hello World出现在浏览器上,这只是第一步。day02如果再停留在“看控制台输出”的层面,大概率会让人失去耐心。所以第二天我安排的是——写真正能对外提供服务的REST接口,把数据从后端返回到前端页面,让整个请求链路先完整跑起来。

这一篇的内容,就是围绕这个目标展开的。你不需要记住太多设计模式,也不需要翻源码,跟着把接口写出来、把参数接进来、把常见坑跳过,就实现了“从能跑”到“能用”的跨越。适合刚学完day01、对Spring Boot只有基础概念的读者,当然,如果你之前用过Spring但想熟悉Spring Boot的自动配置特性,这篇也能帮你把思路捋顺。

1. day02先从“能跑起来”到“能用起来”

第一天跑起来的项目实际上就是一个空壳子,Spring Boot帮我们做了大量基础配置,但你还没让它输出任何有价值的数据。day02的核心目标就是打通一条链路:浏览器发请求 -> Controller接收 -> 返回数据。这是一条最小可用链路,也是后续几乎一切功能的地基。

1.1 为什么第二天就碰REST接口

很多教学博客会建议你先学配置文件、注解原理、Maven模块管理这些基础,然后再写接口。我的习惯正好相反——我先让人把接口写出来,哪怕完全不理解内部原理,只要看到Postman里返回了JSON,这个正反馈就比任何理论都强。兴趣有了,后面补原理才不枯燥。

还有一个现实原因:REST接口是Spring Boot入行最常用的技能。无论是搞后端、转全栈、还是做微服务,接口开发是你唯一能马上用起来、能被同事直观看到成果的工作。和数据库对接、写复杂业务逻辑,那是day04之后的事情,day02不必急着碰。

另外,从学习节奏来说,day01你已经理解了@SpringBootApplication、spring-boot-starter-web这些字面的意思,day02其实是在给你验证一个朴素的道理——Spring Boot默认集成了JSON序列化和HTTP协议的编解码能力,你根本不需要单独配置Servlet或者JSON库。第一次体会到“约定大于配置”,就是写接口的这一刻。

1.2 接口开发的最小闭环理解

我给你画个思路链条,你心里有这个概念,代码写起来才不会乱。

  • 客户端(浏览器/工具)发出HTTP请求,URL里包含路径和参数
  • Spring Boot内置的Tomcat服务器接收到请求,交给核心组件DispatcherServlet
  • DispatcherServlet找到匹配的Controller方法,执行你写的逻辑
  • 方法返回值序列化为JSON,封装成HTTP响应,回到客户端

这个链条里,你此刻只需要关心两个部分:URL怎么写(请求路径),方法里面返回什么(响应内容)。中间怎么匹配、怎么转换,Spring Boot已经干活了。

理解这个闭环还有一个好处——排查问题的时候可以分位置看。请求根本没进后端,还是进来了没找到路径,还是找到了路径但处理报错,三个位置表现出来的现象不一样,排查思路也完全不同。这部分我会在第五节专门展开,现在你只需要记住:别把前端报错一律当成后端代码问题。

2. 写第一个REST接口之前的基础准备

严格来说,如果你第一天是按照官方初始化向导创建的项目,那day02不需要任何额外依赖。spring-boot-starter-web已经包含了Spring MVC、内置Tomcat、JSON消息转换器等全套web能力。所以别一上来就去搜什么“Spring Boot集成JSON依赖”,你真不需要。

2.1 先确认day01的项目结构

打开你day01创建的项目,先确认这些目录在。一个标准的Spring Boot项目骨架是这样的:

demo ├── src │ ├── main │ │ ├── java │ │ │ └── com/example/demo │ │ │ ├── DemoApplication.java (启动类) │ │ │ └── controller/ (day02手动创建) │ │ │ └── UserController.java │ │ └── resources │ │ └── application.properties (配置中心) │ └── test └── pom.xml

重点看两样东西:DemoApplication.java里面的@SpringBootApplication注解,以及controller包的位置。很多人写着写着就报404,九成原因是把Controller放到了启动类所在包的外面。Spring Boot默认只会扫描启动类所在包及其子包,你的controller包必须放在com.example.demo下面,比如com.example.demo.controller,不要自作主张再建一个顶层包。

2.2 理解Spring Boot自动配置做了什么

在第一天你启动项目,看到控制台打了日志,程序就跑起来了。但你真的知道它为什么能跑吗?这里简单补一课。

@SpringBootApplication其实是一个组合注解,它由@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan组成。重点在@EnableAutoConfiguration——Spring Boot会在你引入spring-boot-starter-web这个依赖时,自动帮你完成DispatcherServlet、Tomcat、Jackson等组件的注册。这就是为什么你写接口时不用像传统SSM那样配置一堆xml和JavaConfig类。

实操感受就是:你只需要写Controller类,以及方法上的路由注解,其他一切从简。第一次体会到这种爽感,之后就不会想回老项目用Spring了。

顺带提一句:application.properties现在大概率是空的,但你要知道它是用来放server.port=8081这类配置的地方。不是要你现在写,而是让你知道配置入口在哪。

3. 手写一个能返回JSON的REST接口

本章是整篇文章的实操重头戏。先不用设计复杂的表结构,我们从最经典的UserController开始写。整个过程,你只需要在一个新建的类文件里完成。

3.1 从@RestController开始

在controller包里右键新建一个Java类,命名为UserController,然后敲入下面的内容:

package com.example.demo.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class UserController { @GetMapping("/api/hello") public String hello() { return "Hello, Spring Boot day02!"; } }

写完别急着运行,我带你逐行看。@RestController是核心,它把当前类标记为请求处理器,并且默认类里的每个方法返回值都会以JSON格式写入HTTP响应体。注意区别:如果用的是@Controller,代码里还得额外加@ResponseBody才能返回JSON数据。新手用@RestController最省事,它相当于二者的结合。

方法上的@GetMapping("/api/hello")意思是:当浏览器请求http://localhost:8080/api/hello时,调用这个方法。GetMapping限定了只处理GET请求,这也是REST风格的一种表达方式。

现在启动项目,浏览器访问http://localhost:8080/api/hello。如果能看到页面显示Hello, Spring Boot day02!,说明你的第一个接口已经成功。

3.2 路由注解怎么选

这里算是新手比较容易有选择困难症的地方:GetMapping、PostMapping、RequestMapping到底怎么用?

我的建议是动手记一张表,先背习惯再理解:

注解对应HTTP方法典型使用场景
@GetMappingGET查询数据、访问页面
@PostMappingPOST新增数据、表单提交
@PutMappingPUT更新数据,幂等更新
@DeleteMappingDELETE删除数据
@RequestMapping可指定多个/任意灵活路由,可加method属性

@RequestMapping是老前辈,以前都是靠它加上method = RequestMethod.GET来限定请求方法的。现在直接有专用的组合注解,代码简洁、语义清楚。我的习惯是:看到Mapping开头的注解,就知道该接口是处理什么类型操作的,接口一多起来,阅读成本大大下降。

注意一个细节:路径要以/开头。你可能觉得api/hello也能用,但Spring Mapping规范上要求写完整路径。虽然部分写法被自动纠正,但别给自己埋这种不必要的隐患。统一以/开头,后面再说。

3.3 顺手把热部署配置上

写接口的过程里最烦的一件事就是:每次改代码都要手动重启项目,等启动日志那几行刷完才能验。这个时间看着不长,但一天下来浪费得不少。

在pom.xml的dependencies里加这个依赖:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-devtools</artifactId> <scope>runtime</scope> <optional>true</optional> </dependency>

加上之后,IDEA还需要两步配置才能完全生效。打开Preferences(macOS)或Settings(Windows),找到Build, Execution, Deployment -> Compiler,勾选Build project automatically;接着按快捷键Ctrl+Shift+A(macOS:Shift+Cmd+A),输入Registry,找到compiler.automake.allow.when.app.running,勾选上。两步都做完,改完代码切换到浏览器,页面刷新就是新结果。

这里我发现很多人有个认知误区:以为devtools能让代码“无感热更新”。实际上Spring Boot的热部署是重启机制,不是像前端那种局部模块替换。它重启速度快,但Java类结构重大变化它一样可能失效,比如新增了一个类、改了方法签名。那种情况别纠结,手动重启一次就好。

4. 参数接收套路与POST请求实践

写通了Hello接口,接下来就是接口开发的另一个大头——参数怎么传进来。前端传参数的方式五花八门,后端如果不搞清楚它们的区别,联调时必然抓狂。在day02,我建议你先把GET的两种传参方式、POST的JSON传参方式吃透,这是最常用的三种。

4.1 路径参数和Query参数的选择逻辑

路径参数,在Spring Boot里用@PathVariable接收。常见写法是:

@GetMapping("/users/{id}") public String getUserById(@PathVariable("id") Long id) { return "用户ID: " + id; }

当浏览器请求/users/123时,id就会被自动绑定为123。这种方式适合定位一个唯一资源,比如用户详情、文章详情,URL也能比较简洁地表达资源的层级关系。

Query参数,对应@RequestParam,写法如下:

@GetMapping("/users/list") public String getUsers(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return "page=" + page + ", size=" + size; }

浏览器请求/users/list?page=2&size=20,就能拿到分页参数。适合筛选、列表、分页这类场景。

选择逻辑其实很直白:你要操作的对象是具体的一个,比如“用户123号”,就用路径参数;你要给列表加条件,比如“第2页每页20条”,就用Query参数。混着用也没问题,比如/users/{id}/orders?page=1。重要的是语义一致,别把资源的标识放在Query里,那样URL既不规范,日志排错时也不直白。

一个不起眼的坑:@RequestParam不加defaultValue时是必传参数。如果前端漏传,Spring Boot直接返回400错误,而后端的日志会报MissingServletRequestParameterException。初学时会觉得奇怪:项目没崩,为什么访问报400?看控制台日志就会看到这条关键线索,别慌。

4.2 POST请求与JSON数据接收

GET参数都是拼在URL上的,能承载的数据量有限。到了提交表单、创建资源这种场景,一般用POST加上JSON请求体。后端接收的注解就是@RequestBody。

步骤是两步:第一步,建一个数据类,也就是常说的DTO;第二步,在Controller方法里用@RequestBody接收。

先新建一个类,叫UserDTO:

package com.example.demo.dto; public class UserDTO { private String name; private Integer age; 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; } }

然后写POST接口:

@PostMapping("/users") public String createUser(@RequestBody UserDTO userDTO) { return "收到用户: " + userDTO.getName() + ", 年龄: " + userDTO.getAge(); }

启动项目后,用Postman或者IDEA自带的HTTP Client工具发POST请求,请求体选raw,格式选JSON:

{ "name": "张三", "age": 25 }

你会收到返回文本收到用户: 张三, 年龄: 25。

这里我遇到过很多次的情况是:新手把请求类型选成text/plain,或者没选POST,结果后端一直收不到参数。记住三点:请求方式是POST,请求体是JSON格式,前端传的字段名要和DTO的属性名一致。三点全对,参数稳稳进来。

4.3 一个组合使用的完整Demo

光分开看可能还是不够直观,我把三种参数方式放在一个完整的用户注册场景里,给你看一个组合用法:

@RestController @RequestMapping("/api/users") public class UserController { @GetMapping("/{id}") public String getUser(@PathVariable("id") Long id) { return "查询用户: " + id; } @GetMapping public String listUsers(@RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size) { return "用户列表 第" + page + "页,每页" + size + "条"; } @PostMapping public String createUser(@RequestBody UserDTO userDTO) { return "注册成功: " + userDTO.getName() + " (" + userDTO.getAge() + "岁)"; } }

注意我在类上用了@RequestMapping("/api/users"),这表示类内所有方法的路径都带一个/api/users前缀。之后子路径只需要写/{id}、/、空路径,层级更清晰。

这里有个必须说清楚的前提:DTO类里的getter/setter是一个标准Java Bean必须具备的。Spring Boot在把JSON序列化成对象时,会调用这些方法。如果你用IDE自动生成,那没问题;如果手敲漏了setter,就会遇到“对象创建成功但字段全是null”的现象,排查起来实际上很容易忽略。

5. 常见问题与排查思路实录

这个环节大概是整篇文章经验浓度最高的地方。初学阶段你遇到的90%的报错也就是下面这几个大类。我把它们和对应的排查方向直接列出来,你遇到的时候对应着看就好。

5.1 端口被占用:立刻杀死旧进程

启动项目时如果看到:

Web server failed to start. Port 8080 was already in use.

这几乎可以断定8080端口被其他进程占用了。最常见的场景是上一次运行没关停,或者莫名其妙多开了一个实例。

我的处理方式很简单,直接在终端跑命令看占用情况:

lsof -i :8080

拿到PID之后杀掉进程:

kill -9 PID

如果是Windows环境,用:

netstat -ano | findstr 8080 taskkill /PID 你的PID /F

另外还有一种处理方式:在application.properties里加server.port=8081,换端口启动。适合只想快速跑起来的场景,但不建议养成习惯,因为端口总有冲突的一天,学会清理才是正路。

5.2 中文乱码到底是谁的锅

浏览器返回的中文显示成???或者乱码,这个问题我到现在还会偶尔遇到。它主要出现在两个层面:

第一层是请求/响应的编码。在Spring Boot 2.6+版本中,内置的编码过滤器默认强制UTF-8,大多数情况下不会乱。反而乱码经常出在文件本身。比如你用其他编辑器写了中文,IDEA默认File Encoding是UTF-8,但旧编辑器把它存成了GBK。IDEA左下角会提示编码格式,点开看,如果是GBK,重选UTF-8并Convert即可。

第二层是控制台输出乱码。这不是业务返回乱码,而是日志打印中文乱码。一般需要IDEA的Help -> Edit Custom VM Options里加一行:

-Dfile.encoding=UTF-8

然后重启IDEA。这个坑在旧版本很常见,现在新版本基本默认。

5.3 请求路径404:先分清“谁找不到谁”

浏览器访问接口时返回404,但项目启动正常。这说明请求已经到达Spring Boot,但框架找不到能处理该路径的Handler方法。排查思路按下面这个阶梯来:

先检查URL是否真的匹配Controller里的路径。Spring Boot的匹配是按类级别路径加方法级别路径拼接的,类上写着/api/users,方法上写着/login,完整路径就是/api/users/login。有人会少了类前缀直接访问/login,404很正常。

再检查注解是否写成了@RequestMapping但没有指定method。如果你期望的是POST请求,而前端发的是POST,但注解只能接收GET,框架会返回405而不是404。看到405其实是好事,说明路由匹配上了,但请求方法不对。

最后检查包扫描范围。Controller放的位置不在启动类的子包下,Spring Boot压根没扫描到这个Controller,那无论路径怎么对,都是404。这种错误最隐蔽,我见过有人把controller包建在了src根目录下,怎么调都404。凡是这类问题,先把视觉焦点放在包结构上。

5.4 返回的JSON字段为null:序列化绑定的细节

如果你写了一个返回DTO的接口,启动正常、路径正常、方法正常,但返回的JSON里某些字段是null,比如age是null而name正常。原因通常是前端传给后端的JSON字段名和DTO里的属性名对不上。比如前端传的是Age(大写开头),而后端属性是age,字母大小写不一致,序列化时匹配不上。

另外就是上一节说的漏写setter。Jackson反序列化时依赖setter方法注入字段值,setter缺失就直接null。IDE自动生成的getter/setter可能还会保证一致性,这种只有手敲才容易漏。

如果项目里已经用了Lombok,这种问题会少很多,但Lombok引入了一个新的认知成本:IDE如果没装Lombok插件,编译都过不了。所以day02我不建议急着上Lombok,手写一遍getter/setter反而帮你把Bean结构搞清楚了。

5.5 devtools热部署不生效:别忽略这些细节

加了devtools依赖,项目能启动,但改代码后依然需要手动重启。我排查过几个不同原因:

检查pom.xml依赖的<optional>true</optional>标签是否还在。这个标签容易被复制代码时漏掉。它的作用是让devtools只对当前工程生效,不传递到依赖它的模块。不加也不报错,但热部署行为可能变得不可控,尤其在多模块项目里。

检查IDE的设置是否全部完成。尤其是Registry里的compiler.automake.allow.when.app.running,很多版本默认是false,不开就是热部署失效。有时候改完代码之后DevTools会触发自动重启,但页面刷新还是旧数据,这种通常是浏览器缓存或者静态资源缓存头的问题,加个Ctrl+F5强制刷新基本就能解决。

值得说明的是,devtools默认在application.properties里有触发机制,你可以设置一个spring.devtools.restart.trigger-file,指定某个文件作为触发标志,只有修改这个文件才触发重启。对大型项目处理频繁重启有实际意义。day02阶段没有这个必要,知道有这类配置就行。

5.6 接口访问返回Error Page:看状态码再下手

Spring Boot默认自带一个错误页面,返回内容通常是一大段JSON或者HTML形式的Whitelabel Error Page。很多人一看分布就懵,其实关键在于状态码。400是参数错误,404是路径没匹配上,500是代码运行期异常,405是方法不对。

每种状态码的排查入口都不一样。500这种最让人头疼,因为它意味着Controller方法内部抛了异常,比如空指针、类型转换失败,这些信息都在IDEA控制台里,Stacktrace会告诉你第几行出错了。顺着日志往上翻,八成能定位。我见过有开发者不滚日志,盯着浏览器页面瞎猜半天,真心建议先养成“报错先看日志”的意识。

6. 结尾:关于day02的一些个人体会

我在这几天带人学Spring Boot时,最大的感触是:环境问题、代码问题、配置问题在初学阶段特别容易混淆,而一旦你打通了一个最小接口闭环,后面就没什么能让心态崩掉的坎了。

单看day02的内容,其实每一个点——注解、参数、热部署、调试——拆开来都不算难,但它们组合在一起,就是你后续所有功能的骨架。写CRUD、对接数据库、做权限控制,都是在“能接收请求、能返回数据”这两个基本能力上做文章。

我想额外给你一个小建议:太抽象的教程看十遍不如自己动手改一遍。你今天试过了路径参数和Query参数之后,可以尝试写一个新的接口,要求是:路径为/products/{id},然后通过Query参数传递一个discount数值,返回该商品折扣后的价格。这个小练手不需要引入任何新的依赖,单纯用Map存两三条假数据就行。做完你能彻底搞懂路径参数、Query参数以及Controller返回值类型这三者之间的关系。

这个系列写到day02,我给后续内容的规划是:第三天引入真正的数据层,把接口的数据从内存假数据换成MySQL里的真实记录。到时候你会第一次体会到,Spring Boot的整合能力到底有多省事。建议这几天把基础SQL语句稍微复习一下,后面会衔接得更顺。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询