01|前端人第一次打开 Spring Boot 项目,应该先看哪里?
本篇是"前端工程师转后端"系列的第 1 篇。目标不是马上让你写复杂业务,而是先让你知道:这个后端工程从哪里启动,一个 HTTP 请求从哪里进来,代码为什么要分成
contract、service、Controller、Facade、DbService、Mapper、Entity、SQL 这些层。后续学习 Java、MySQL、MyBatis-Plus、JWT、Redis、订单事务时,你都会回到这张工程地图上定位。
说明:本文出现的 Vue / Axios / TypeScript 代码都是"前端侧示意代码",用于帮助你从前端经验迁移到后端理解。当前仓库没有真实前端源码目录,本文所有后端路径都基于当前仓库真实文件。
开篇背景:为什么前端现在也要懂后端
先聊一点真实工作里的变化。以前很多团队会把岗位分得很清楚:前端只写页面、交互、组件和接口调用;后端只写接口、数据库、缓存和权限。前端同学遇到接口报错,就把 Network 截图丢给后端;后端同学改完接口,再告诉前端字段变了、路径变了、状态码变了。这个模式当然还能工作,但在现在很多公司、很多业务线里,边界已经没有那么硬了。
尤其是中小团队、创业团队、内部系统、后台管理系统、AI 应用、低代码平台、增长活动和 B 端业务,老板或业务方往往不关心"你是前端还是后端",他们只关心这个需求什么时候能上线:页面要有,接口要通,数据要能存,权限不能乱,线上出问题要有人能排查。于是前端同学经常会遇到这些场景:
- 后端排期不够,你需要自己补一个简单查询接口;
- 后台页面已经写好了,但发现接口字段不够,需要自己加字段;
- 联调时接口返回 401、403、404、500,你不能只说"后端问题",还要能定位大概是哪一层;
- 商品、订单、支付、库存这些业务越来越靠近数据正确性,前端如果完全不懂后端,很难判断需求风险;
- AI 编程工具越来越强,能帮你生成代码,但你必须看得懂 Controller、Service、SQL、事务和缓存,否则生成错了也不知道。
所以这套文章不是想把你从前端"改造成纯后端",而是帮你补上后端视角。你已经会前端,这反而是优势:你知道用户怎么操作页面,知道接口怎么被调用,知道浏览器 Network 里哪些信息有用,知道 TypeScript 类型和接口契约的重要性。接下来要做的,是把这些经验迁移到服务端:把 axios.get() 的另一端看明白,把 JSON 字段背后的 DTO、Service、数据库表、缓存和权限看明白。
你可以把这次学习理解成一次"从接口调用者走到接口提供者"的升级。以前你更多站在浏览器里问:"这个接口怎么用?"现在你要开始站在服务端问:"这个接口应该怎么设计?数据从哪里来?失败时怎么返回?并发时会不会错?以后字段变了怎么不坑前端?"等你能回答这些问题,你就不只是会写页面的前端,而是能完整交付业务闭环的全栈工程师。哈哈,听起来有点卷,但换个角度看,这也是前端同学最容易突破天花板的一条路。
1. 这篇解决什么问题
如果你长期写前端,第一次打开一个 Java 后端项目,很容易产生几个疑问:
pom.xml是什么?它和package.json有什么关系?backend/contract和backend/service为什么要分开?- 为什么一个接口不是直接写在一个文件里,而是要经过 Controller、Facade、DbService、Mapper?
Request、Response、Entity看起来都像 TypeScript interface,它们到底有什么区别?- MySQL、Redis、JWT、Spring Security、MyBatis-Plus 都在项目里,但我应该先看哪个?
- 我作为前端转后端,第一天读这个工程到底应该从哪里开始?
本篇文章只解决一件事:帮你建立当前后端工程的整体地图。
你暂时不需要完全掌握 Redis,也不需要理解 MyBatis-Plus 的所有 API,更不需要一下子搞懂订单事务和支付回调。你只需要先知道这些后端能力在项目中分别处于什么位置,解决什么问题,将来要沿着哪条路径学习。
你可以把本篇当成第一次进入新公司的后端项目时,老同事给你做的工程导览:先告诉你项目目录,告诉你从哪个接口开始读,告诉你不要把业务逻辑乱写到 Controller,也告诉你为什么数据库才是后端系统的数据真相。
当前项目的根目录是:
text
/Users/zz/code/learning/fullstack-mall
项目 README 把它定义为"二手电商全栈学习项目",当前以后端学习为重点,已经覆盖阶段 0~9。也就是说,这不是一个空壳 Demo,它已经包含商品、分类、SKU、购物车、订单、支付、Redis 缓存等后端学习素材。你的学习路线不应该是孤立地背注解,而应该是围绕这些真实业务链路逐步深入。
本篇结束后,你应该能做到:
- 说清楚
backend/contract和backend/service的区别; - 说清楚
pom.xml、application.yml、docker-compose.dev.yml分别解决什么问题; - 顺着
GET /api/products/{id}找到ProductController.detail()和IProductFacade.getPublishedProduct(); - 大概理解 Controller、Facade、DbService、Mapper、Entity、SQL 的职责边界;
- 知道后续学习为什么要先补 Java、Spring Boot、MySQL、MyBatis-Plus、JWT、Redis 基础。
2. 用前端知识类比后端工程
前端项目里,我们习惯看到这样的结构:
text
frontend-app/
├── package.json
├── src/
│ ├── main.ts
│ ├── router/
│ ├── pages/
│ ├── components/
│ ├── api/
│ ├── stores/
│ └── types/
└── vite.config.ts
你大概知道:
package.json管依赖和脚本;main.ts是浏览器应用入口;router/管页面路由;api/封装 Axios 请求;types/定义 TypeScript 类型;stores/管状态;vite.config.ts管构建和开发服务器。
打开当前后端工程,你会看到另一套名字:
text
fullstack-mall/
├── backend/
│ ├── pom.xml
│ ├── contract/
│ └── service/
├── docs/
├── sql/
└── docker-compose.dev.yml
先不要被 Java 名词吓到。你可以暂时这样类比:
| 前端熟悉的东西 | 后端对应概念 | 在本项目中的位置 | 重要区别 |
|---|---|---|---|
package.json |
pom.xml |
backend/pom.xml、backend/service/pom.xml |
Maven 不只安装依赖,还负责 Java 编译、测试、打包、多模块关系 |
main.ts |
Spring Boot 启动类 | FullstackMallApplication.java |
后端启动后常驻运行,监听 HTTP 请求 |
| Vue Router | Controller URL 映射 | *Controller.java |
Controller 不是页面路由,它是服务器 HTTP 入口 |
| Axios request type | Request DTO | contract/*Request.java |
DTO 是运行时 Java 对象,会参与参数绑定和校验 |
| Axios response type | Response DTO | contract/*Response.java |
后端实际返回 JSON 前,会先组装 Java Response 对象 |
api/product.ts |
Facade 契约 | IProductFacade.java |
Facade 表示后端对外用例,不只是发请求函数 |
| composable / service | Facade 实现 | ProductFacade.java |
Facade 会做权限、业务编排、数据转换 |
| 状态管理 store | 数据库 / 缓存 | MySQL / Redis | 前端状态通常在浏览器内,后端数据要服务所有用户且可持久化 |
| mock data | seed SQL / 测试数据 | sql/02_seed.sql、测试资源 |
后端测试数据要符合数据库约束 |
| 浏览器 DevTools | 日志、curl、IDEA Debug、数据库查询 | 测试类、日志、SQL、Redis CLI | 后端问题经常需要跨 HTTP、Java、DB、缓存定位 |
这个类比只是入门桥梁,不能完全等同。比如 Controller 看起来像前端路由,但 Controller 面对的是来自网络的不可信输入,必须考虑参数校验、权限、并发、异常返回。又比如 DTO 看起来像 TypeScript interface,但 TypeScript interface 编译后不存在,而 Java DTO 是运行时真的会被创建出来的对象,Spring 会把 JSON 请求体绑定到这些对象上,还会根据注解做校验。
从前端转后端,最重要的思维变化是:前端通常关注"用户此刻看到什么",后端必须关注"所有用户、所有请求、所有数据在并发和失败情况下是否仍然正确"。
举一个商品详情页的例子。前端看这个需求,可能会想:
ts
// 前端侧示意代码:当前仓库没有这个文件
async function fetchProductDetail(id: number) {
const res = await axios.get(`/api/products/${id}`)
return res.data.data
}
前端的关注点通常是:接口路径是什么、参数是什么、返回字段是什么、loading 怎么处理、错误 toast 怎么展示。
后端看到同一个需求,会继续追问:
- 这个接口是否允许匿名访问?
- 商品不存在返回什么业务码?
- 商品未上架能不能被公开访问?
- 分类被停用后商品详情还能不能展示?
- 查询 MySQL 太慢时是否需要 Redis 缓存?
- Redis 挂了时接口是失败还是降级查 MySQL?
- 返回给前端的字段能不能直接暴露数据库所有字段?
- 日志里如何定位一次请求?
这些问题就是后端工程分层的原因。后端代码不是为了显得复杂才拆层,而是因为不同问题应该放在不同地方解决。
3. 后端核心概念先认识名字
在第 1 篇里,我们先认识名字,不展开全部细节。
3.1 Maven:后端版依赖和构建系统
当前项目的后端父工程是 backend/pom.xml。如果你熟悉前端,可以先把它理解成后端世界的 package.json,但它比 package.json 更强调模块、编译、测试和打包。
backend/pom.xml 里定义了:
- 项目使用 Spring Boot;
- Java 版本是 21;
- 后端有两个模块:
contract和service; - MyBatis-Plus、Druid、JJWT 等版本由父工程统一管理。
backend/service/pom.xml 则是可运行后端服务的依赖清单。你可以在那里看到 Spring Web、Validation、Redis、Security、OpenAPI、MyBatis-Plus、MySQL、H2、Testcontainers 等依赖。以后你看到"项目为什么能写 Controller""为什么能连 MySQL""为什么能用 Redis",都要回到这个文件找依赖来源。
3.2 Spring Boot:后端应用运行容器
Spring Boot 可以先理解为"后端应用运行框架"。前端的 Vite dev server 启动后,负责把页面资源和热更新服务起来;Spring Boot 启动后,负责监听 HTTP 端口,把请求交给 Controller,并管理项目里的各种 Bean。
当前项目的启动类是:
text
backend/service/src/main/java/com/example/fullstackmall/service/FullstackMallApplication.java
你后续会看到很多类上有 @RestController、@Service、@Configuration 等注解。它们的共同点是:这些类会被 Spring 扫描和管理,成为 Spring 容器里的对象。项目中经常用 @Resource 把一个 Bean 注入到另一个 Bean 里,比如 ProductController 里注入 IProductFacade。
前端里你可能会这样导入函数:
ts
// 前端侧示意代码
import { getProductDetail } from '@/api/product'
后端里常见的是把对象交给 Spring 管理,然后通过依赖注入拿到它:
java
@Resource
private IProductFacade productFacade;
这不是简单的语法差异。后端对象可能涉及事务代理、安全代理、生命周期、配置注入、线程安全等问题,所以由 Spring 容器统一管理。
3.3 Controller:HTTP 请求入口
Controller 是后端最像"路由"的一层。当前公开商品接口在:
text
backend/service/src/main/java/com/example/fullstackmall/service/product/ProductController.java
这个类上有 @RestController 和 @RequestMapping("/api/products")。其中 @RequestMapping 表示这一组接口的公共路径前缀是 /api/products。
里面的 detail() 方法上有 @GetMapping("/{id}"),所以它对应的完整路径就是:
text
GET /api/products/{id}
如果前端请求:
text
GET /api/products/1001
Spring 就会把路径里的 1001 绑定到方法参数 id 上,然后执行 ProductController.detail()。
但注意:Controller 不应该承载复杂业务。它主要做几件事:接收 HTTP 参数,调用 Facade,包装统一响应,返回给前端。真正的业务规则不要堆在 Controller 里。
3.4 Contract:后端对外契约
当前项目有一个单独模块:
text
backend/contract
你可以把它理解为"前后端都应该关心的接口契约层"。这里面放的是 Request、Response、enum、I...Facade 这类对外模型。
比如商品模块的 Facade 接口在:
text
backend/contract/src/main/java/com/example/fullstackmall/contract/product/IProductFacade.java
这个接口定义了商品模块对外提供哪些能力:创建商品、变更状态、管理端查询、公开查询、公开详情、更新副标题等。
为什么要有契约?因为后端不能只写"内部实现"。前端关心的是"我能调用什么接口、提交什么字段、拿到什么字段"。后端内部关心的是"怎么查库、怎么校验、怎么组装数据"。contract 就像一条边界线,让对外结构更稳定。
前端写 TypeScript 时,你可能会定义:
ts
// 前端侧示意代码
type ProductDetail = {
id: number
title: string
subtitle: string
description: string
}
后端的 ProductDetailResponse 就承担类似作用,但它不是编译期类型提示而已,它会真实参与 JSON 序列化,最后返回给浏览器。
3.5 Facade:业务编排层
Facade 这个词对前端同学可能有点陌生。你可以先把它类比成"前端里的业务 service / composable",但后端 Facade 的职责更重。
在本项目里,Controller 通常不直接操作数据库,而是调用 I...Facade。Facade 实现类负责:
- 获取当前登录用户;
- 判断业务状态;
- 调用缓存服务或数据库服务;
- 把 Entity 转成 Response;
- 协调多个模块;
- 在必要时调用事务服务。
比如商品模块的实现类是:
text
backend/service/src/main/java/com/example/fullstackmall/service/product/ProductFacade.java
后续讲商品详情时,我们会沿着 ProductController.detail() 进入 ProductFacade.getPublishedProduct(),再看它如何决定先查 Redis 还是回源 MySQL。
3.6 Entity、Mapper、DbService:数据访问相关层
后端最终要处理持久化数据。前端的状态在刷新页面后可能消失,但后端系统需要把商品、订单、支付、用户这些数据长期保存下来。当前项目主要使用 MySQL 保存业务数据。
这几类文件经常一起出现:
text
ProductEntity.java
ProductMapper.java
ProductDbService.java
你可以先这样理解:
- Entity:Java 对象和数据库表之间的映射;
- Mapper:MyBatis-Plus 用来执行 SQL / CRUD 的入口;
- DbService:把底层数据操作包装成更符合业务语义的方法。
比如 ProductEntity 对应 mall_product 表,ProductMapper 继承 MyBatis-Plus 的 BaseMapper<ProductEntity>,ProductDbService 再对查询商品、更新商品等动作做封装。
你现在不需要记住 MyBatis-Plus 的所有语法。只要先知道:后端不是直接拿 JSON 当数据库,后端会把数据库行映射成 Java Entity,再经过业务层组装成 Response 返回给前端。
3.7 SQL:数据库结构的最终落点
当前项目的数据库表结构主要在:
text
sql/01_schema.sql
这里定义了商品表、分类表、SKU 表、购物车表、订单表、支付表等。对后端来说,数据库不是可有可无的"存一下数据",而是很多业务规则的最终保障:主键、唯一约束、索引、字段类型、非空约束、状态字段都会影响系统行为。
比如前端页面可以在表单上限制标题必填,但真正可靠的约束必须在后端校验和数据库结构里落地。因为任何人都可以绕过前端页面直接请求接口,后端不能相信"前端一定会传对"。
3.8 Redis:服务端共享缓存
Redis 在本项目阶段 9 中用于商品详情缓存。你可以先把它类比成前端里的 memory cache 或 localStorage,但有几个关键区别:
- Redis 运行在服务端,不在用户浏览器里;
- 多个后端实例可以共享同一个 Redis;
- Redis 里的数据通常是性能优化层,不是业务真相来源;
- Redis 可能故障,所以后端要决定故障时是失败还是降级。
本项目商品详情链路里,Redis 是 MySQL 前面的性能层。公开商品详情先尝试查缓存,缓存命中就少查一次 MySQL;缓存没有命中,或者 Redis 降级时,再回源 MySQL。后续第 7 篇和第 14 篇会专门讲 Redis,现在你只需要知道它在工程中的位置。
3.9 Spring Security 和 JWT:认证授权入口
前端同学通常熟悉 Token:登录成功后拿到 Token,存到本地,后续请求放到 Authorization header。后端要做的是反向工作:收到请求后解析 Token,判断这个用户是谁,有没有权限访问当前接口。
本项目使用 Spring Security 和 JWT。安全相关代码主要在:
text
backend/service/src/main/java/com/example/fullstackmall/service/security
backend/service/src/main/java/com/example/fullstackmall/service/config/SecurityConfig.java
backend/service/src/main/java/com/example/fullstackmall/service/auth
后续讲登录鉴权时,我们会专门解释 401 和 403 的区别。现在先记住一句话:前端负责携带 Token,后端负责验证 Token 是否可信,并决定能不能访问接口。
4. 在本项目中对应哪些文件
先看一张工程目录图:
这张图先解决"文件太多不知道看哪里"的问题。你可以把阅读顺序定成:
- 先看根目录
README.md,确认项目做什么、已经实现到哪个阶段; - 看
backend/pom.xml,确认 Java 版本、Spring Boot 版本、模块拆分; - 看
backend/service/pom.xml,确认这个服务用了哪些后端能力; - 找一个最简单的 Controller,例如
ProductController.java; - 从 Controller 跳到
IProductFacade.java,再跳到ProductFacade.java; - 需要查库时再看 Entity、Mapper、DbService;
- 需要确认字段时再看
sql/01_schema.sql。
不要第一天就从 sql/01_schema.sql 头到尾背表结构,也不要第一天就打开 Redis 缓存服务深挖异常分支。读新项目最怕"从最复杂的地方开始",这样很容易挫败。正确方式是从一个最熟悉的 HTTP 请求入口开始,沿着调用链一点点展开。
下面列出第 1 篇最重要的文件:
| 文件 | 你现在需要知道什么 |
|---|---|
README.md |
项目是二手电商全栈学习项目,当前以后端为重点,已完成阶段 0~9 |
backend/pom.xml |
Maven 父工程,定义 Java 21、Spring Boot 3.5.16、多模块结构 |
backend/contract/pom.xml |
对外契约模块,放 Request、Response、Facade 接口 |
backend/service/pom.xml |
可运行服务模块,包含 Web、Validation、Security、Redis、MyBatis-Plus、MySQL 等依赖 |
docker-compose.dev.yml |
本地 MySQL 8.4 和 Redis 7.4 的启动配置 |
backend/service/src/main/java/com/example/fullstackmall/service/FullstackMallApplication.java |
Spring Boot 启动类 |
backend/service/src/main/java/com/example/fullstackmall/service/product/ProductController.java |
公开商品接口入口 |
backend/contract/src/main/java/com/example/fullstackmall/contract/product/IProductFacade.java |
商品模块对外用例契约 |
backend/service/src/main/java/com/example/fullstackmall/service/product/ProductFacade.java |
商品业务编排实现 |
backend/service/src/main/java/com/example/fullstackmall/service/product/entity/ProductEntity.java |
商品表对应的 Java Entity |
backend/service/src/main/java/com/example/fullstackmall/service/product/mapper/ProductMapper.java |
商品表 MyBatis-Plus Mapper |
sql/01_schema.sql |
数据库表结构 |
5. 一张请求链路图:从前端页面到后端分层
现在用一个你熟悉的场景来串起来:商品详情页请求商品详情。
前端侧示意代码可能是:
ts
// 前端侧示意代码:当前仓库没有这个文件
async function loadProductDetail(productId: number) {
const response = await axios.get(`/api/products/${productId}`)
return response.data.data
}
后端侧实际入口在 ProductController.detail()。完整理解链路可以先看这张图:
这张图里,你先不要被 Redis 和 MyBatis-Plus 细节卡住。你要先建立 3 个核心认识。
第一,HTTP 请求不会直接访问数据库。前端发出的请求先到 Spring Boot 后端服务,再由 Controller 接住。Controller 只是入口,不是数据库访问层。
第二,业务逻辑有专门的位置。本项目用 Facade 做业务编排。比如公开商品详情要判断商品是否上架、分类是否启用、是否走缓存、返回哪些字段,这些都不应该散落在 Controller 里。
第三,数据库和缓存是后端内部细节。前端只看到 JSON,但后端要决定数据从 Redis 来还是从 MySQL 来。Redis 是性能层,MySQL 是数据真相来源。后续你学 Redis 时,就会知道为什么修改商品后要删除缓存,为什么 Redis 故障不能让公开详情接口完全不可用。
6. 逐段读源码:从 ProductController 开始
第 1 次读后端源码,不要贪多。我们只读公开商品详情入口。
文件路径:
text
backend/service/src/main/java/com/example/fullstackmall/service/product/ProductController.java
这个类的核心信息是:
java
@RestController
@RequestMapping("/api/products")
public class ProductController {
@Resource
private IProductFacade productFacade;
@GetMapping("/{id}")
public ApiResponse<ProductDetailResponse> detail(
@PathVariable Long id,
HttpServletRequest servletRequest
) {
ProductDetailResponse response = productFacade.getPublishedProduct(id);
return ApiResponse.success(response, TraceIdContext.get(servletRequest));
}
}
我们一行一行用前端视角翻译。
6.1 @RestController:这是一个返回 JSON 的 HTTP 入口类
你可以把 @RestController 理解为告诉 Spring:这个类里的方法要作为 HTTP 接口暴露,并且返回值通常会被转换成 JSON。
前端路由里你可能写:
ts
// 前端侧示意代码
{
path: '/products/:id',
component: ProductDetailPage
}
但 Controller 不是页面路由。前端路由决定浏览器显示哪个页面,后端 Controller 决定服务器收到某个 HTTP 请求后由哪个 Java 方法处理。
6.2 @RequestMapping("/api/products"):公共路径前缀
这表示当前类下的接口都以 /api/products 开头。你可以把它理解成前端 api/product.ts 文件里统一封装商品接口的 base path。
6.3 @Resource private IProductFacade productFacade:依赖注入
这行表示 Controller 需要一个商品业务对象,但它不自己 new,而是让 Spring 注入。
前端中你经常 import 一个函数或对象。Java 后端里也可以 new 对象,但在 Spring 项目中,业务组件通常由 Spring 容器管理。这样做的原因包括:统一生命周期、方便配置、方便测试、支持事务代理、支持安全和切面能力。
你现在只需要记住:看到 @Resource,说明这个类依赖另一个 Spring 管理的 Bean。
6.4 @GetMapping("/{id}"):这是 GET 商品详情接口
@GetMapping("/{id}") 和类上的 /api/products 拼起来,就是:
text
GET /api/products/{id}
@PathVariable Long id 表示路径里的 {id} 会被绑定到 Java 参数 id。
如果请求是:
text
GET /api/products/1001
那么方法里的 id 就是 1001。
6.5 productFacade.getPublishedProduct(id):把业务交给 Facade
Controller 没有自己查数据库,也没有自己判断商品状态,而是把事情交给 productFacade。
这是本项目非常重要的风格:Controller 负责 HTTP,Facade 负责业务编排。
如果你以后新增接口,千万不要一股脑把所有逻辑写进 Controller。你应该先问:这是参数接收问题,还是业务规则问题,还是数据库访问问题?不同问题要放在不同层。
6.6 ApiResponse.success(...):统一响应格式
前端最怕每个接口返回结构都不一样。有的接口返回 { data },有的返回 { result },有的失败时直接返回字符串。后端项目通常会定义统一响应结构。
当前项目用的是 ApiResponse<T>。Controller 最后返回:
java
ApiResponse.success(response, TraceIdContext.get(servletRequest))
你可以先理解为统一包一层:业务数据是 response,同时带上 traceId,方便排查问题。
以后前端拿到响应时,看到的通常不是裸的 ProductDetailResponse,而是类似:
json
{
"code": "SUCCESS",
"message": "success",
"data": {
"id": 1001,
"title": "..."
},
"traceId": "..."
}
具体字段以项目里的 ApiResponse.java 为准。第 3 篇会专门讲 Request、Response 和统一返回。
7. 从 Controller 跳到 IProductFacade
现在我们看 Controller 调用的接口:
text
backend/contract/src/main/java/com/example/fullstackmall/contract/product/IProductFacade.java
它是一个 Java interface。对前端同学来说,可以先把 interface 理解为"对外能力清单"。商品模块暴露了哪些能力,这里能看到。
你会看到类似这些方法:
createProduct(...):管理员创建商品草稿;changeStatus(...):管理员变更商品状态;queryAdminProducts(...):管理端分页查询商品;queryPublishedProducts(...):公开分页查询已上架商品;getPublishedProduct(...):查询公开商品详情;updateSubtitle(...):管理员修改商品副标题。
这对前端转后端非常有价值。因为你可以先不看实现,只看接口清单,快速知道商品模块支持哪些业务用例。
这就像前端项目里先打开 api/product.ts:
ts
// 前端侧示意代码
export function getPublishedProduct(id: number) {}
export function queryPublishedProducts(params: ProductQuery) {}
export function updateProductSubtitle(id: number, body: SubtitleUpdate) {}
区别是:前端 api/product.ts 是"调用后端接口",后端 IProductFacade 是"定义后端业务能力"。它的实现通常在 ProductFacade.java。
你读后端源码时,可以遵循这个顺序:
- Controller:这个 HTTP 接口是什么;
- Facade interface:这个业务能力叫什么;
- Facade implementation:这个能力怎么编排;
- DbService / CacheService:数据从哪里来;
- Mapper / Entity / SQL:最终怎么查表或更新表。
这种阅读顺序比随机打开文件有效得多。
8. 为什么要分成这么多层
前端同学经常会问:为什么后端不能简单一点?比如一个 Controller 直接查数据库、返回 JSON,不就完了吗?
对于非常小的 Demo,当然可以。但一个电商项目即使是学习项目,也会很快遇到复杂问题:
- 商品详情公开可见,但管理端商品列表需要管理员权限;
- 商品可能草稿、上架、下架,不同状态能不能展示不一样;
- SKU 库存涉及并发,不能两个用户同时买到同一份库存;
- 订单创建要同时写订单、订单明细、锁库存,任何一步失败都要回滚;
- 支付回调可能重复到达,需要幂等;
- Redis 缓存可能命中、未命中、坏 JSON、连接失败;
- 数据库表字段变更要同步 Entity、Request、Response、测试数据和文档。
如果所有逻辑都写在 Controller 里,几百行之后就会很难维护。分层的目的,是让每个文件只承担它应该承担的职责。
看这张职责边界图:
你可以用一句话记住每层:
- Controller:我接到了什么 HTTP 请求?
- Request / Response:前端能传什么、后端返回什么?
- Facade:这个业务用例要怎么完成?
- CacheService:能不能先从缓存拿,什么时候失效?
- DbService:我要按业务语义读写哪些数据?
- Mapper:底层 SQL / CRUD 怎么执行?
- Entity:数据库行在 Java 里长什么样?
- SQL:表结构和约束最终是什么?
- TransactionService:多个表要不要一起成功或一起失败?
这些层不是为了"架构好看",而是为了把复杂性放在合适的位置。
9. 先看 Maven 和依赖:这个后端工程具备哪些能力
前端看一个项目,通常先看 package.json,因为它告诉你用了 Vue 还是 React,用了 Vite 还是 Webpack,用了 Pinia 还是 Redux。后端同理,先看 pom.xml。
当前项目有 3 个重要的 Maven 文件:
text
backend/pom.xml
backend/contract/pom.xml
backend/service/pom.xml
backend/pom.xml 是父工程。你可以从这里知道:
- 使用 Spring Boot;
- Java 版本是 21;
- 项目拆成
contract和service两个模块; - 统一管理一些依赖版本。
backend/contract/pom.xml 是契约模块。它依赖 Lombok 和 Validation API,说明这里主要是请求、响应、枚举、接口声明,不是可运行服务。
backend/service/pom.xml 是真正的服务模块。这里的依赖能告诉你项目具备哪些后端能力:
| 依赖方向 | 说明 | 后续在哪篇学 |
|---|---|---|
| Spring Web | 写 HTTP Controller,返回 JSON | 第 2、3、8 篇 |
| Validation | 对 Request 参数做校验 | 第 3、15 篇 |
| Spring Data Redis | 使用 Redis 缓存 | 第 7、14 篇 |
| Spring Security | 认证授权 | 第 6 篇 |
| JJWT | 创建和校验 JWT | 第 6 篇 |
| Springdoc OpenAPI | 生成接口文档 | 第 3 篇会简单提到 |
| MyBatis-Plus | 操作 MySQL 表 | 第 5 篇 |
| Druid | 数据库连接池 | 第 4、5 篇会认识 |
| MySQL Connector | 连接 MySQL | 第 4 篇 |
| H2 / Testcontainers | 自动化测试使用 | 后续调试和测试篇提到 |
这一步非常像前端看依赖。只不过前端看到 vue-router 就知道有路由,后端看到 spring-boot-starter-web 就知道可以写 Controller;前端看到 pinia 知道有状态管理,后端看到 mybatis-plus 知道项目用 ORM / Mapper 操作数据库。
10. 本地环境:为什么需要 docker-compose
前端项目很多时候 pnpm dev 就能跑起来,因为页面本身不一定依赖本地数据库。但后端服务通常依赖外部基础设施,比如 MySQL 和 Redis。
当前项目的本地依赖在:
text
docker-compose.dev.yml
这个文件定义了两个服务:
- MySQL 8.4:保存用户、商品、SKU、购物车、订单、支付等业务数据;
- Redis 7.4:保存商品详情缓存等临时性能数据。
前端同学可以这样理解:
如果 MySQL 没启动,后端很多查询和写入接口就无法正常工作。因为商品、订单、用户这些数据不能只存在 Java 内存里。
如果 Redis 没启动,取决于代码设计,有些缓存功能会降级,有些测试可能会跳过。当前项目 README 提到阶段 9 已经做了 Redis fail-open 降级,也就是说公开商品详情在 Redis 故障时应该尽量回源 MySQL,而不是让用户完全不可用。这个点现在知道即可,后续 Redis 篇会深入。
你常见的本地启动思路是:
bash
# 启动 MySQL 和 Redis
docker compose -f docker-compose.dev.yml up -d mysql redis
# 启动后端服务
cd backend
mvn -pl service -am spring-boot:run
如果只想快速看服务是否活着,可以请求健康检查接口。具体接口后续可以从 HealthController.java 找。
11. 从前端调用角度理解统一响应和 traceId
前端调用后端接口时,最希望响应结构稳定。例如:
ts
// 前端侧示意代码
const res = await axios.get('/api/products/1001')
if (res.data.code === 'SUCCESS') {
render(res.data.data)
} else {
showToast(res.data.message)
}
后端项目通常会统一返回结构,让前端不用为每个接口写一套特殊解析逻辑。当前项目的 Controller 返回 ApiResponse<T>,这就是统一响应思想。
TraceIdContext.get(servletRequest) 则和排查问题有关。当前端用户反馈"商品详情打不开"时,你不能只说"我这里是好的"。后端需要通过 traceId、日志、接口路径、用户 ID、错误码定位这一请求发生了什么。
前端排查问题时会看浏览器 Network 面板;后端排查问题时会看日志、traceId、数据库、Redis、测试和 Debug。traceId 就像一条线,把一次请求在后端的流转串起来。
12. 当前阶段不要急着深入的东西
你已经看到了很多后端名词:Redis、MyBatis-Plus、JWT、事务、Mapper、Entity、SQL、缓存穿透、幂等、状态机。不要急着全部学完。
第 1 篇只要求你建立地图。下面这些内容我们会在后续文章逐步展开:
12.1 Java 和 Maven
你后续需要知道 Java class、interface、注解、泛型、集合、异常、时间、金额、Maven 依赖、模块构建。但现在只要知道:Maven 负责后端依赖和构建,Spring Boot 负责把 Java 服务跑起来。
12.2 Spring Boot Web
你后续需要掌握 @RestController、@RequestMapping、@GetMapping、@PostMapping、@RequestBody、@PathVariable、@Valid。但现在只要知道:Controller 是 HTTP 入口。
12.3 MySQL 和 MyBatis-Plus
你后续需要理解表、字段、主键、索引、SQL、事务、Entity、Mapper、Wrapper、分页。现在只要知道:MySQL 是数据真相,MyBatis-Plus 帮 Java 代码操作 MySQL。
12.4 Spring Security 和 JWT
你后续需要理解登录、Token、Filter、SecurityContext、401、403、管理员权限。现在只要知道:前端携带 Token,后端验证 Token 和权限。
12.5 Redis
你后续需要理解 key、value、TTL、序列化、Cache Aside、缓存穿透、击穿、雪崩、写后失效、fail-open。现在只要知道:Redis 是服务端共享缓存,MySQL 才是业务真相来源。
12.6 事务和状态机
你后续需要理解订单为什么需要事务、库存为什么要原子扣减、支付回调为什么要幂等、超时关单为什么要状态条件更新。现在只要知道:后端不仅处理"请求成功",还要处理"并发、失败、重复请求、外部回调"。
13. 本地怎么运行和验证
第 1 篇不要求你完全跑通所有业务,但你需要知道基础命令在哪里。
13.1 查看项目状态
在项目根目录:
bash
pwd
find . -maxdepth 2 -type d | sort
你应该能看到 backend、docs、sql 等目录。
13.2 启动本地依赖
bash
docker compose -f docker-compose.dev.yml up -d mysql redis
这个命令会根据 docker-compose.dev.yml 启动 MySQL 和 Redis。MySQL 映射到本机 3308 端口,Redis 映射到本机 6379 端口。
如果你还没有 Docker,先不要卡在这里。你可以先读代码和文档。真正开始接口联调时,再处理 Docker 环境。
13.3 启动后端服务
bash
cd backend
mvn -pl service -am spring-boot:run
这里的 -pl service -am 可以先粗略理解为:启动 service 模块,并自动构建它依赖的模块。因为 service 依赖 contract,所以 Maven 会一起处理。
13.4 用 curl 请求接口
后续你可以用 curl 模拟前端请求。例如商品详情:
bash
curl -i http://localhost:8080/api/products/1001
如果你平时只用浏览器和 Axios,建议开始习惯 curl。curl 能让你脱离页面,直接验证后端接口。后端开发经常需要先证明接口本身对不对,再讨论前端页面怎么展示。
14. 常见错误
错误 1:一上来就读最复杂的业务
很多人打开项目后直接看订单、支付、Redis 缓存,结果越看越乱。正确做法是从简单公开 GET 接口开始,例如 GET /api/products/{id}。先建立 Controller → Facade → 数据层的基本链路,再看复杂分支。
错误 2:把 Controller 当成万能文件
前端写页面时,一个组件里可能既有请求、状态、事件、展示。后端不能这样随意混。Controller 不应该直接写复杂业务,也不应该直接散落 SQL。否则权限、事务、缓存、异常都会变得难维护。
错误 3:把 DTO、Entity、数据库表混为一谈
Request / Response 面向接口,Entity 面向数据库表,SQL 定义真实表结构。它们字段可能相似,但职责不同。比如数据库里可能有 created_by,但公开商品详情 Response 不一定要暴露这个字段给前端。
错误 4:以为前端校验足够安全
前端表单可以限制必填、长度、格式,但后端不能相信前端。因为用户可以绕过页面直接调用接口。真正可靠的校验要在后端 Request 校验、业务校验、数据库约束里共同完成。
错误 5:以为 Redis 是数据库
Redis 很快,但本项目里 Redis 主要是缓存层。商品详情缓存可以加速读取,但业务真相仍然在 MySQL。修改商品后要考虑缓存失效,Redis 故障时要考虑降级策略。
错误 6:只看成功流程,不看失败流程
前端学习接口时容易只看 200 成功。但后端必须定义失败:参数错返回什么,未登录返回什么,没权限返回什么,商品不存在返回什么,库存不足返回什么,支付重复回调怎么办。后续每篇都要练习失败场景。
错误 7:不看 README 和 docs,直接全局搜索
当前项目文档已经很丰富。README 说明了项目阶段、代码职责和阅读入口,docs/backend-basics 里有大量基础文档。先读入口文档,再按链路看源码,会比盲目搜索效率高很多。
15. 本章小练习
请你打开项目,自己完成下面几个练习。不要急着写代码,只做定位和理解。
练习 1:找到启动类
找到:
text
backend/service/src/main/java/com/example/fullstackmall/service/FullstackMallApplication.java
回答:这个类为什么像前端的 main.ts?它和 ProductController.java 是什么关系?
练习 2:找到商品详情接口
找到:
text
backend/service/src/main/java/com/example/fullstackmall/service/product/ProductController.java
回答:GET /api/products/{id} 对应哪个 Java 方法?路径里的 {id} 绑定到哪个参数?
练习 3:找到商品业务契约
找到:
text
backend/contract/src/main/java/com/example/fullstackmall/contract/product/IProductFacade.java
回答:商品模块对外提供了哪些能力?哪些像公开接口?哪些像管理端接口?
练习 4:找到 Maven 依赖
打开:
text
backend/service/pom.xml
回答:你能找到 Spring Web、Spring Security、Redis、MyBatis-Plus、MySQL 相关依赖吗?它们分别对应后续哪些学习主题?
练习 5:画出自己的调用链
用你自己的话画一遍:
text
前端请求 GET /api/products/1001
-> ProductController.detail
-> IProductFacade.getPublishedProduct
-> ProductFacade
-> CacheService / DbService
-> Mapper
-> MySQL
-> ProductDetailResponse
-> ApiResponse
-> 前端页面
如果现在你还不知道 ProductFacade 里面的具体代码,不要紧。第 1 篇只要求你知道下一站在哪里。
16. 第 1 篇总结:先建立地图,再深入细节
前端转后端最容易犯的错,是把后端当成"会连接数据库的接口文件"。真实后端项目要解决的问题比这多得多:请求入口、参数校验、权限、业务状态、事务、数据库约束、缓存、日志、测试、失败降级、并发安全。也正因为问题复杂,后端工程才需要分层。
本项目的基本地图可以总结为:
text
README.md:先知道项目是什么
backend/pom.xml:知道 Java / Spring Boot / Maven 模块
backend/contract:知道对外 Request / Response / Facade 接口
backend/service:知道 Controller / Facade / DbService / Mapper / Cache / Security 实现
sql:知道 MySQL 表结构和数据约束
docker-compose.dev.yml:知道本地 MySQL / Redis 怎么启动
docs:知道学习路线和阶段说明
如果你只记一句话,请记住:
从前端请求出发,先找 Controller;从 Controller 找 Facade;从 Facade 再看缓存、数据库和事务。不要一开始陷入所有后端名词。
这就是你读当前后端工程的第一条主线。
16.1 给前端同学的一条读源码规则
再补充一条非常实用的读源码规则:永远从"用户动作"开始,不要从"框架名词"开始。
比如你想学习商品详情,不要先问"RedisOperatorClient 的所有方法是什么",也不要先问"MyBatis-Plus 的 Wrapper 有多少种写法"。你应该先还原用户动作:用户打开商品详情页,页面需要商品标题、副标题、描述、价格、库存等信息,于是发起 GET /api/products/{id}。从这个动作出发,你自然会找到 Controller;Controller 只做入口,于是你继续找 Facade;Facade 发现需要数据,于是你继续看缓存和数据库;数据库字段不清楚时,再回到 SQL 表结构。这样读代码,后端知识会围绕业务逐步长出来,而不是变成一堆孤立名词。
这也对应前端学习新项目的经验。你不会第一天就把所有组件库源码读完,而是会先打开一个页面,看它的路由、接口、状态和组件拆分。后端也是一样,先选择一个小接口当入口,再沿着调用链走。每走到一个不懂的概念,只需要先问三个问题:它解决什么问题?它在当前项目哪个文件里?如果它出错,前端会看到什么现象?这三个问题比死记注解更重要。
17. 下一章预告
下一篇我们会进入基础篇第 2 章:Java、Maven 与 Spring Boot 启动基础。
你会学到:
- Java 后端项目和 Node / pnpm 项目的相似点与不同点;
pom.xml的基本结构;- Maven 多模块为什么有父工程、
contract模块和service模块; - Spring Boot 启动类为什么能把整个服务跑起来;
application.yml这类配置文件在后端中扮演什么角色;- 如何用最小命令启动后端服务并观察日志。
学完下一章,你会对"这个项目为什么能运行起来"有更清晰的认识,然后第 3 章再专门讲 Controller、Request、Response 和统一返回。