01|前端人第一次打开 Spring Boot 项目,应该先看哪里?

01|前端人第一次打开 Spring Boot 项目,应该先看哪里?

本篇是"前端工程师转后端"系列的第 1 篇。目标不是马上让你写复杂业务,而是先让你知道:这个后端工程从哪里启动,一个 HTTP 请求从哪里进来,代码为什么要分成 contractservice、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/contractbackend/service 为什么要分开?
  • 为什么一个接口不是直接写在一个文件里,而是要经过 Controller、Facade、DbService、Mapper?
  • RequestResponseEntity 看起来都像 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 缓存等后端学习素材。你的学习路线不应该是孤立地背注解,而应该是围绕这些真实业务链路逐步深入。

本篇结束后,你应该能做到:

  1. 说清楚 backend/contractbackend/service 的区别;
  2. 说清楚 pom.xmlapplication.ymldocker-compose.dev.yml 分别解决什么问题;
  3. 顺着 GET /api/products/{id} 找到 ProductController.detail()IProductFacade.getPublishedProduct()
  4. 大概理解 Controller、Facade、DbService、Mapper、Entity、SQL 的职责边界;
  5. 知道后续学习为什么要先补 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.xmlbackend/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;
  • 后端有两个模块:contractservice
  • 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. 在本项目中对应哪些文件

先看一张工程目录图:

flowchart TD A[fullstack-mall 根目录] --> B[README.md\n项目定位与阅读入口] A --> C[backend/\nJava 后端工程] A --> D[docs/\n学习文档] A --> E[sql/\nMySQL 表结构与种子数据] A --> F[docker-compose.dev.yml\n本地 MySQL 与 Redis] C --> C1[backend/pom.xml\n父工程与版本管理] C --> C2[contract/\n对外契约] C --> C3[service/\n可运行服务] C2 --> C21[Request / Response / enum] C2 --> C22[I...Facade 接口] C3 --> C31[Controller\nHTTP 入口] C3 --> C32[Facade\n业务编排] C3 --> C33[DbService\n数据库业务封装] C3 --> C34[Mapper / Entity\n表映射与 SQL 入口] C3 --> C35[Security / Cache / Config\n安全、缓存、配置]

这张图先解决"文件太多不知道看哪里"的问题。你可以把阅读顺序定成:

  1. 先看根目录 README.md,确认项目做什么、已经实现到哪个阶段;
  2. backend/pom.xml,确认 Java 版本、Spring Boot 版本、模块拆分;
  3. backend/service/pom.xml,确认这个服务用了哪些后端能力;
  4. 找一个最简单的 Controller,例如 ProductController.java
  5. 从 Controller 跳到 IProductFacade.java,再跳到 ProductFacade.java
  6. 需要查库时再看 Entity、Mapper、DbService;
  7. 需要确认字段时再看 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()。完整理解链路可以先看这张图:

sequenceDiagram participant FE as Vue 页面\n示意 participant HTTP as HTTP 请求 participant Security as Spring Security\nFilter Chain participant Controller as ProductController participant Facade as IProductFacade\nProductFacade participant Cache as ProductDetailCacheService\nRedis participant Db as ProductDbService participant Mapper as ProductMapper participant MySQL as MySQL FE->>HTTP: GET /api/products/1001 HTTP->>Security: 进入后端服务 Security->>Controller: 匿名公开接口放行 Controller->>Facade: getPublishedProduct(1001) Facade->>Cache: 尝试读取商品详情缓存 alt 缓存命中 Cache-->>Facade: 返回缓存中的详情 else 缓存未命中或降级 Facade->>Db: 查询公开商品详情 Db->>Mapper: 调用 MyBatis-Plus / SQL Mapper->>MySQL: 查询 mall_product 等表 MySQL-->>Mapper: 返回数据行 Mapper-->>Db: 映射为 Entity Db-->>Facade: 返回 ProductEntity Facade->>Cache: 回填缓存 end Facade-->>Controller: ProductDetailResponse Controller-->>FE: ApiResponse

这张图里,你先不要被 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

你读后端源码时,可以遵循这个顺序:

  1. Controller:这个 HTTP 接口是什么;
  2. Facade interface:这个业务能力叫什么;
  3. Facade implementation:这个能力怎么编排;
  4. DbService / CacheService:数据从哪里来;
  5. Mapper / Entity / SQL:最终怎么查表或更新表。

这种阅读顺序比随机打开文件有效得多。

8. 为什么要分成这么多层

前端同学经常会问:为什么后端不能简单一点?比如一个 Controller 直接查数据库、返回 JSON,不就完了吗?

对于非常小的 Demo,当然可以。但一个电商项目即使是学习项目,也会很快遇到复杂问题:

  • 商品详情公开可见,但管理端商品列表需要管理员权限;
  • 商品可能草稿、上架、下架,不同状态能不能展示不一样;
  • SKU 库存涉及并发,不能两个用户同时买到同一份库存;
  • 订单创建要同时写订单、订单明细、锁库存,任何一步失败都要回滚;
  • 支付回调可能重复到达,需要幂等;
  • Redis 缓存可能命中、未命中、坏 JSON、连接失败;
  • 数据库表字段变更要同步 Entity、Request、Response、测试数据和文档。

如果所有逻辑都写在 Controller 里,几百行之后就会很难维护。分层的目的,是让每个文件只承担它应该承担的职责。

看这张职责边界图:

flowchart LR A[Controller\nHTTP 入口] --> B[Facade\n业务编排] B --> C[CacheService\n缓存策略] B --> D[DbService\n数据库业务操作] D --> E[Mapper\nSQL / CRUD 入口] E --> F[MySQL\n数据真相] B --> G[TransactionService\n跨表事务] G --> D H[contract\nRequest / Response / enum / IFacade] -.定义对外契约.-> A H -.定义业务能力.-> B

你可以用一句话记住每层:

  • 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;
  • 项目拆成 contractservice 两个模块;
  • 统一管理一些依赖版本。

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:保存商品详情缓存等临时性能数据。

前端同学可以这样理解:

flowchart TD A[Spring Boot 后端服务] --> B[MySQL\n持久化业务数据] A --> C[Redis\n缓存热点数据] D[curl / 浏览器 / 前端页面] --> A

如果 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

你应该能看到 backenddocssql 等目录。

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 和统一返回。

相关推荐
用户7713970207061 小时前
深夜食堂:那个让我加班到凌晨的 ! 符号
后端
meilindehuzi_a1 小时前
Vue3进阶基础:指令修饰符、v-model表单绑定、动态样式、计算属性与侦听器实战
前端·javascript·vue.js
csdn2015_1 小时前
vue 前端运行命令
前端·javascript·vue.js
swipe1 小时前
02|从 `pnpm dev` 到 Spring Boot 启动:后端服务到底怎么跑起来?
前端·后端·全栈
网易云信1 小时前
网易智企Data Agent实践入选信通院《智能体创新实践案例汇编》
人工智能·后端·线下活动
jvmind_dev2 小时前
1c1g 容器莫名 OOM Killed?排查 Metaspace 持续增长与脚本引擎的坑
java·后端
geovindu2 小时前
go:Backtracking Algorithm
开发语言·后端·算法·golang·回溯算法
AskHarries2 小时前
图片存储怎么选
后端
阿懂在掘金2 小时前
做命令式弹窗工具应该考虑什么——我为什么把 vue-layerx 设计成这样
前端·前端框架·函数式编程