今天是系统学习后端的第 4 天。前三天我把 Java 基础和 HTTP 那部分重新过了一遍,本来计划里今天应该继续啃集合框架,但昨天看完 HTTP 的 Request/Response 之后,我实在忍不住了,决定提前把 Spring Boot 跑起来。结果这一跑,就把我从前端转到后端之后最迷糊的一个问题给解开了:一个接口到底是怎么从浏览器请求变成 JSON 返回的。
如果你也是前端转后端,或者刚接触后端开发不久,今天这份笔记应该能帮到你。我不会只贴“照着做就能跑”的步骤,还会把今天从搭项目到连数据库的完整过程、遇到的报错和排查思路都写下来。后端学习和前端学习在思路上真的差很多,有些东西只有自己踩一遍,才知道为什么大家都说“日志是后端开发者的好朋友”。
1. 前三天在打什么底子:为什么第四天才开始碰 Spring Boot
1.1 我的后端学习路线(第四天修订版)
我在网上翻了不少后端开发学习路线,基本都长这样:Java 基础语法 → 面向对象 → 集合框架 → MySQL → JDBC → Spring Boot → 项目实战。这个路线本身没毛病,但说实话,对一个已经写了几年前端的人来说,前面这些内容里有一部分是重叠的,比如编程思维、数据结构、API 设计这些东西,前端开发里也一直在用。
我原来的计划是第四天继续学 Java 集合框架,但昨天看 HTTP 协议时,突然产生了一个想法:与其把 Java 基础全啃完再碰框架,不如先让 Spring Boot 跑起来,写一个最简单的接口,亲眼看到请求是怎么进来、数据是怎么返回的。有了整体认知之后,再回头补语言细节,会踏实很多。所以我临时改了计划,今天一整天都花在“搭项目 + 写接口 + 连数据库”上。
这个决定现在回头看是值得的。因为后端的知识链条太长,如果一直停留在理论阶段,很容易学着学着就放弃了。反而是先跑通一个有反馈的小闭环,比如“前端发请求 → 后端接口收到 → 查数据库 → 返回 JSON”,后面再往这个闭环里填细节,会更有方向感。
1.2 前端经验和后端认知差在哪里
作为一个前端开发者,我最开始对后端的理解是极其片面的。我以为后端就是写一堆接口,前端发起请求,后端返回数据,完事了。但实际上,前端关注的核心是交互和展示,而后端关注的是数据怎么流转、服务怎么稳定、权限怎么控制、性能怎么优化。这个认知差异在第一天体会还不深,到了今天写接口时就开始显现了。
比如前端写页面时,页面挂了最多是白屏,刷新一下可能就好了。但后端写接口时,如果接口能不能扛住并发、数据库连接会不会泄漏、接口会不会把敏感信息暴露出去,这些一旦出问题,影响的是所有调用方。今天我用 JdbcTemplate 查数据库时,只查了一条列表数据,但我已经能感觉到,真正的后端开发绝不是把 SQL 写出来那么简单。
所以我对自己的学习路线做了一个补充:基础语法和框架要并行学,但必须在学某个框架特性时,明确知道它在整条链路里处于哪个位置。比如今天学的@RestController,它只是 Spring MVC 里的控制器层,真正的核心还在后面的服务层和数据层。有了这个坐标感,学起来才不会乱。
2. 十分钟搭出 Spring Boot 骨架:从初始化到看见 Tomcat 日志
2.1 为什么选 Spring Boot 而不是 Servlet/JSP 或者其他框架
很多学习路线会建议先从 Servlet 和 JSP 学起,理由是“先理解底层原理”。但我的实际感受是,Servlet 那套年代感太强了,写起来配置繁琐,而且和现在前后端分离的主流开发方式已经差别很大。直接学 Spring Boot,虽然一开始会有一个“黑盒”阶段,但它能让人以最小的成本把整个链路跑通,后面的原理可以再逐个击破。
我当时也纠结过要不要学 Python 的 FastAPI,因为它在某些场景下确实更轻量。但考虑到国内 Java 后端岗位和 Spring Boot 生态的成熟度,还是决定先把 Java 这条路走稳。Spring Boot 最大的优势是“约定优于配置”:默认端口、默认序列化方式、默认依赖管理都配好了,初学者只需要关注业务代码。
2.2 创建项目的实操步骤
我用的方式是直接打开 start.spring.io ,这个官方初始化工具比在 IDEA 里新建项目更直观。具体配置是这样的:
- 构建工具:Maven,它是目前最常见的 Java 项目构建工具,管理依赖和打包都靠它。
- 语言:Java,版本选 17,因为 Spring Boot 3.x 默认要求 Java 17 及以上。
- Spring Boot 版本:我选了 3.2.x 的稳定版本。这里提醒一下,如果你刚学,别一上来就选最新版,过新的版本可能导致第三方依赖还没跟上,反而增加排查成本。
依赖那里我一开始只勾了一个Spring Web,这足够把一个接口跑起来了。生成压缩包后解压,用 IDEA 打开,等 Maven 把依赖下载完,就能直接运行主类里面带main方法的那个文件。控制台出现Tomcat started on port 8080的时候,说明你的第一个后端服务已经起来了。
2.3 项目文件里需要先认识的三样东西
刚打开一个 Spring Boot 项目时,目录结构会让人有点懵,但其实最开始只需要认识三个地方。
第一个是pom.xml,Maven 的核心配置文件。你以后要加什么依赖,比如连 MySQL、接 Redis,基本都是往这里面添加。第二个是src/main/java下的主启动类,类上面有个@SpringBootApplication注解,这个注解同时包含了组件扫描、自动配置等能力,也是程序入口。第三个是src/main/resources/application.properties,这个文件是改配置的地方,改端口、改数据库连接都靠它。
我当时犯了一个小错误,一直在src/main/resources下面找什么web.xml,找了一会儿才反应过来,Spring Boot 里这些东西早就不需要手写了。这也是我说它“约定优于配置”的原因。
3. 第一个接口的全过程:请求是怎么变成 JSON 返回给前端浏览器的
3.1 按下刷新按钮后,后端服务内部经历了什么
在写代码之前,我觉得有必要把整条链路先搞清楚,因为这是我今天最大的收获。前端在浏览器里访问http://localhost:8080/api/hello时,后端实际上做了这样一串事情:
浏览器先做 DNS 解析,把localhost解析到 127.0.0.1,然后建立 TCP 连接,发出一个 HTTP GET 请求。这个请求到达 Tomcat(内嵌在 Spring Boot 里)之后,Spring MVC 的DispatcherServlet会根据 URL 去找对应的@RequestMapping映射方法。找到之后,方法里的代码开始执行,返回一个对象,框架里的 Jackson 组件会自动把这个对象序列化成 JSON 字符串,再通过 HTTP 响应返回给前端。
这一步看似简单,但它解释了一个非常关键的问题:为什么后端方法的返回值可以直接是一个 Java 对象,而前端却能收到 JSON?答案就是后面这个自动序列化的过程。如果你也在前端做过JSON.parse,那你现在应该明白,后端在返回时已经在悄悄替你完成一次反向操作了。
3.2 用 Java 写一个可以“动起来”的接口
我在新建的 Controller 类里写了第一个接口,代码并不复杂:
@RestController @RequestMapping("/api/hello") public class HelloController { @GetMapping public Map<String, Object> hello() { Map<String, Object> data = new HashMap<>(); data.put("message", "Hello, 今天开始学后端了!"); return data; } }这里用的是@RestController,意思是这个类里的每个方法返回值都会直接写入 HTTP 响应体,而不会再走视图解析器去渲染一个页面。在前后端分离的项目里,绝大多数接口就是用这种方式返回 JSON 的。
启动项目后,浏览器访问http://localhost:8080/api/hello,就能看到:
{"message":"Hello, 今天开始学后端了!"}那一刻确实挺开心的。因为我看到的不只是几个文字,而是整个请求链路真实地跑通了。对于习惯在浏览器里调接口的前端同学来说,这个反馈可能不算什么,但对于第一次以“后端编写者”身份看到这个结果的人来说,意义完全不同。
3.3 参数传递的三种常见姿势
后端接口不可能是死数据,我们需要让前端把参数传过来。今天我把最常用的三种方式都写了一遍,分别是路径参数、查询参数和请求体 JSON。
路径参数适合标识资源,比如GET /api/students/1表示获取 id 为 1 的学生,用@PathVariable接收:
@GetMapping("/{id}") public R<Student> getById(@PathVariable Integer id) { // 假装查到了数据 }查询参数适合筛选条件,比如GET /api/students/query?name=张三,用@RequestParam接收。注意@RequestParam默认是必须传的,如果不传会直接报 400,所以可以用required = false或者defaultValue让它变可选。
请求体 JSON 适合创建或更新数据,比如POST /api/students,请求体里放一段 JSON,用@RequestBody接收,Spring 会自动把 JSON 映射成一个 Java 对象。这里有个容易踩的坑:JSON 里的字段名要和 Java 类属性名一致,或者有对应的序列化配置,否则映射出来就是 null。
这三种方式覆盖了 CRUD 接口里绝大多数参数场景,后面写项目时会经常用到。
4. 前后端分离的第一课:接口返回结构和跨域问题的处理
4.1 应该把接口返回结构先约定好,而不是想到哪写到哪
以前我做前端时最怕的一种后端接口,就是每个接口返回的数据格式都不一样。有的直接返回数组,有的返回对象,有的出错时返回一段文本。虽然都能解析,但维护起来非常痛苦。
所以今天我在项目里加了一个简单的统一返回类,把接口返回值包一层。核心结构就三个字段:code表示业务状态码,message表示提示信息,data表示真正的数据。这个结构和很多公司内部约定的R对象类似。
public class R<T> { private Integer code; private String message; private T data; public static <T> R<T> ok(T data) { R<T> r = new R<>(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } }这样做的好处是前端在封装 axios 拦截器时,只需要统一处理code字段。等于后端给前端一个稳定的“契约”,而不是每个接口各写各的。今天这个体会特别深,因为写后端如果只顾自己方便,最后坑的是整个联调流程。
4.2 跨域问题:为什么接口在 Postman 里能通,浏览器里却不行
今天我在前后端联调时遇到了一个非常典型的问题:后端接口在 Postman 里访问完全正常,但从前端项目里发请求就被浏览器拦截了。报错信息里清清楚楚写着CORS,这就是所有前后端分离项目都绕不开的跨域问题。
要理解 CORS,得先明白一个事实:跨域限制是浏览器做的安全策略,不是后端接口本身拒绝访问。也就是说,你的请求确实发到了后端,后端也确实返回了数据,但浏览器在读取响应时发现Access-Control-Allow-Origin这个响应头不存在或者不匹配,就把数据拦截了。Postman 这类工具没有浏览器同源策略限制,所以不会出现这个问题。
解决方式有两种。第一种是在接口或者 Controller 上加@CrossOrigin注解,适合临时联调。第二种是全局配置一个WebMvcConfigurer,统一设置允许跨域的路径和来源,这种更适合正式项目。但要注意,生产环境不要把跨域配置写得过于宽松,否则任何网站都能调用你的接口,安全风险很大。
4.3 接口文档先行的习惯培养
今天写接口时,我突然意识到一件事:以前我在前端等接口,觉得后端把接口写完就行。但当我今天自己站到“接口提供方”这个位置上时,才明白接口设计里最关键的是“约定”。路径怎么命名、参数怎么传、返回结构是什么,这些如果等代码写完再沟通,效率会非常低。
所以今天我养成了一个习惯:每写一个接口,先把请求方法和返回结构在 Apifox 里定义好,再开始写代码。这样后面做前端页面时,我只需要看文档,不用反复去问后端。对于团队协作来说,这个习惯比写十个接口都重要。后面学若依框架时,我也打算重点看它工程里对接口和权限是怎么做约定的。
5. 数据库接入:让接口的数据不再写死
5.1 为什么第四天就急着连 MySQL
按照不少人的学习节奏,数据库应该放到后面慢慢学。但我的想法是,后端大多数核心业务其实都是对数据的操作,哪怕你接口写得再漂亮,如果没有真实的数据来源,始终感觉浮在表面。所以我今天决定把 MySQL 也拉进来,走一遍“接口 → 查数据库 → 返回 JSON”的完整路径。
很多人说后端开发就是“增删改查”,这句话对,也不对。对的地方是,日常业务确实大量围绕数据操作进行;不对的地方是,真正生产级的数据访问要考虑索引、事务、连接池、缓存、读写分离等一堆问题。但作为一个第四天学习者,先会增删改查并理解整体流程,是合理的第一步。
5.2 安装 MySQL 并完成建库建表
我用的是 MySQL 8.x,安装时有一个细节要注意:字符集要选utf8mb4,不然以后存中文表情或者其他字符时容易出问题。装好之后,命令行里执行下面这几条 SQL,先建库再建表。
CREATE DATABASE student_db DEFAULT CHARACTER SET utf8mb4; USE student_db; CREATE TABLE student ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, age INT ); INSERT INTO student (name, age) VALUES ('张三', 20), ('李四', 21);这里把数据库名取为student_db,后面在 Spring Boot 配置连接时会用到。表结构很简单,但足够演示了。插入两条数据后,可以用SELECT * FROM student;确认一下。我建议初学者要习惯手写 SQL,不要一上来就依赖可视化工具,连最基本的表结构和增删改查都应该能脱稿写出来。
5.3 Spring Boot 里连 MySQL 的最小配置和查询代码
第一步是在pom.xml里加上两个依赖:spring-boot-starter-jdbc和mysql-connector-j。第一个是 Spring 对 JDBC 的封装,第二个是 MySQL 官方驱动。加完依赖之后,在application.properties里配置数据源:
spring.datasource.url=jdbc:mysql://localhost:3306/student_db?useSSL=false&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true spring.datasource.username=root spring.datasource.password=你自己的密码 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver这个连接串里的参数每个都值得注意。useSSL=false是因为本地开发没必要启用 SSL;characterEncoding=utf8防止中文乱码;serverTimezone=Asia/Shanghai解决 MySQL 8 时区问题;allowPublicKeyRetrieval=true是为了兼容 MySQL 8 默认的认证插件,下面排坑部分我会专门讲。
配置好之后,我直接用了JdbcTemplate来查数据。这是 Spring 提供的一个轻量级数据库操作类,第四天用非常合适,因为不用像 MyBatis 那样再引入一堆配置。查询学生的接口可以写成这样:
@GetMapping("/list") public R<List<Student>> list() { String sql = "SELECT id, name, age FROM student ORDER BY id DESC"; List<Student> students = jdbcTemplate.query(sql, (rs, rowNum) -> { Student s = new Student(); s.setId(rs.getInt("id")); s.setName(rs.getString("name")); s.setAge(rs.getInt("age")); return s; }); return R.ok(students); }访问http://localhost:8080/api/students/list时,接口返回的就是数据库里真实的记录了。看到 JSON 里出现表里的数据时,我才算真正把“后端接口”和“数据库”连到了一起。那种从页面到数据库、再从数据库到页面的完整感,是只看教程体会不到的。
6. 今天踩过的三个坑:日志信息才是排查问题的钥匙
6.1 端口占用:8080 is already in use...
今天第一次启动项目时就报了端口占用,因为我之前跑别的服务时没关干净。报错日志里其实写得很清楚:Port 8080 was already in use。但第一次看到时我还是慌了一下,因为日志里一大片英文,很难定位。
排查步骤其实很简单。Windows 上可以直接执行netstat -ano | findstr 8080找到占用 8080 端口的进程 PID,然后去任务管理器结束掉;Mac 或 Linux 上用lsof -i :8080看到 PID 之后kill -9 PID。如果这个服务不想用 8080,也可以在application.properties里写server.port=8081换一个端口。
我在这里想强调一句,也是我今天感触最深的一点:后端开发遇到报错,第一反应应该是去看日志,从头看到尾。日志里通常已经告诉了你原因,很多人懒得看,直接复制报错去搜,反而浪费更多时间。报错日志不可怕,可怕的是不读日志直接乱试。
6.2 数据库连接报错:Public Key Retrieval is not allowed
连接 MySQL 时我又踩了一个经典的坑。配置好数据源后,启动项目访问接口,结果报错Public Key Retrieval is not allowed。这几乎是所有用 MySQL 8 的初学者都会遇到的一个问题。
原因大概是这样的:MySQL 8 默认使用caching_sha2_password认证插件,客户端第一次连接时需要通过 RSA 公钥去传输密码。但 JDBC 驱动出于安全考虑,默认不允许自动获取公钥,所以会抛出这个异常。对策是在连接串上加上allowPublicKeyRetrieval=true。也可以把数据库用户的认证插件改成mysql_native_password,但整体来说改连接串更常见。
这里我想额外提醒一句:网上很多解决方案会让你直接在连接串里写很多“万能参数”,但不理解时不要盲目复制的。至少应该知道每个参数是在解决什么问题,不然以后换一个环境报错,你还是不知道从哪下手。
6.3 中文乱码和时区问题
第三个坑是中文乱码。我建库的时候明明用了utf8mb4,但接口返回的数据里中文还是显示成问号。后来检查发现,连接串里的characterEncoding=utf8没有加,导致客户端和服务器之间通信时用了错误的字符集。
还有一个容易忽略的时区问题。MySQL 8 初始化时如果没有指定时区,很可能默认使用服务器的系统时区,而 Java 应用这边默认的是 UTC,两边时间对不上就会出现时间字段差了 8 小时的情况。所以在连接串上写死serverTimezone=Asia/Shanghai是一个相对稳妥的做法。
关于这类问题,我建议所有后端初学者在做数据库配置时,把连接串当一个整体来记,而不是每次报错再加一个参数。因为连接串本身就是在和数据库“对话”,每一段参数都代表着一种约定,理解它,比死记硬背强。
6.4 日志里 ERROR 和 WARN 的区别
虽然今天没有专门研究日志框架,但我在排坑时还是留意了一下日志级别。简单来说,ERROR一般表示程序需要介入处理的问题,比如连接不上数据库、空指针异常;WARN表示当前不影响运行,但存在隐患的情况,比如某个配置即将被弃用。学会从日志里找关键字,排查速度会快很多。
后端开发过程中,日志不是可有可无的东西。前端的console.log可以随手打,但后端的日志往往要考虑到输出位置、格式、级别和敏感信息脱敏。这些我后面会专门花时间学,但今天已经体会到它的价值了。
7. 第四天结束后的复盘:后端学习需要的不是堆量,而是打通链路
7.1 今天给我最大冲击的三个认知变化
第一个认知变化是:写后端绝不只是写接口。接口只是最外层,接口后面还有参数校验、业务逻辑、数据库操作、异常处理、权限控制、日志记录。今天我只做了非常初级的接口和查询,但这已经足以看出后端工程师需要考虑的问题维度比我想象中要多。
第二个认知变化是:日志和报错信息是排查问题的钥匙,不是敌人。我以前看到英文报错会很焦虑,今天的经验告诉我,只要静下心从第一行读到最后一行,大部分问题都能定位到一个很具体的原因。端口被占、驱动缺失、参数不对,这些报错信息里几乎都写了答案。
第三个认知变化是:前后端能不能顺利配合,取决于“约定”而不是“实现”。这个约定包括接口路径、参数格式、返回结构、错误码含义。今天写统一返回结构时我感受特别深,这比写十行八行核心代码更能体现后端设计能力。
7.2 接下来几天的安排
今天的笔记写到这里,但我很清楚这只是开始。明天我打算用 MyBatis 替换掉今天用的 JdbcTemplate,把数据访问层正式搭起来,同时补一下事务的知识。因为事务这个东西在数据库操作里太重要了,不学的话以后万一在项目里遇到“钱扣了但订单没生成”这类问题,会完全不知道原因。
再后面,我会把这张学生表扩成一个真正的前后端分离小项目,加上新增、修改、删除接口,再用 Vue 写个简单管理页面来联调。接口文档、统一返回、跨域配置这些今天已经打好基础,后面就是在同一个环路上不断加深。
第四天最大的感受就是:学习后端开发,最重要的不是今天看了几个视频、背了几个注解,而是把链路跑通,亲眼看到数据从浏览器请求一路流到数据库,再原路返回。有了这个全局画面,后面的每一步都会走得更踏实。