02|从 pnpm dev 到 Spring Boot 启动:后端服务到底怎么跑起来?
本篇是"前端工程师转后端"系列的第 2 篇。第 1 篇我们先建立了工程地图,知道要从
README.md、backend/pom.xml、Controller、Facade 这些入口开始读。现在进入真正的后端基础:Java 程序如何运行,Maven 如何管理依赖和多模块,Spring Boot 为什么能把一个 Java 工程启动成 HTTP 服务。本篇仍然基于当前fullstack-mall后端工程,不会假设你已经懂 MyBatis-Plus、Redis、JWT,只会先让你把"项目怎么跑起来"这件事弄清楚。
说明:本文中的前端代码仍然是"前端侧示意代码",用于类比。当前仓库没有真实前端源码目录。本文引用的后端文件都来自当前工程。
1. 这篇解决什么问题
前端同学学习后端,经常第一步就卡在"启动方式不熟"。你可能很熟悉:
bash
pnpm install
pnpm dev
也知道前端项目里 package.json 会写依赖、脚本、构建命令。可是打开 Java 后端项目后,你看到的是:
text
backend/pom.xml
backend/contract/pom.xml
backend/service/pom.xml
backend/service/src/main/java/...
backend/service/src/main/resources/application.yml
然后命令变成:
bash
cd backend
mvn -pl service -am spring-boot:run
如果只背命令,你可能能启动一次,但不知道每个词是什么意思。下一次遇到编译失败、依赖找不到、端口冲突、数据库连不上、profile 不对,就会不知道从哪里排查。
本篇解决下面几个问题:
- Java 后端程序和浏览器前端程序的运行模型有什么不同;
- JDK、JVM、Maven、Spring Boot 分别是什么;
pom.xml为什么类似package.json,但又不只是依赖清单;- 当前项目为什么拆成
contract和service两个 Maven 模块; FullstackMallApplication.java为什么是后端启动入口;application.yml、application-local.yml、application-debug.yml分别解决什么问题;- 如何用最小命令启动服务,并从日志、端口、接口验证启动是否成功。
学完本篇,你不一定马上会写复杂 Java 代码,但你应该能回答一句非常关键的话:这个后端服务是如何从源码变成一个正在监听 HTTP 请求的进程的。
2. 用前端知识类比:从 Vite dev server 到 Spring Boot server
先从你熟悉的前端启动流程开始。一个 Vue + Vite 项目通常是:
bash
pnpm install
pnpm dev
背后大概发生这些事:
- pnpm 根据
package.json和 lockfile 下载依赖; - Vite 启动开发服务器;
- 浏览器访问某个端口,例如
http://localhost:5173; - Vite 负责处理模块、热更新、静态资源、代理等;
- 用户操作页面时,前端再通过 Axios 请求后端 API。
Java 后端也有类似流程,只是工具和产物不同:
bash
cd backend
mvn -pl service -am spring-boot:run
背后大概发生这些事:
- Maven 读取
pom.xml,解析项目模块和依赖; - Maven 编译 Java 源码;
- Spring Boot 插件启动
service模块; - JVM 运行
FullstackMallApplication.main(); - Spring Boot 创建应用上下文,扫描 Controller、Service、Config 等 Bean;
- 内嵌 Web 服务器启动,监听
server.port,默认是8080; - 浏览器、curl 或前端页面请求
http://localhost:8080/api/...时,后端 Controller 开始处理请求。
可以用这张图类比:
这个类比能帮助你快速入门,但你要注意两点差异。
第一,前端代码最终大多运行在浏览器里,而 Java 后端代码运行在服务端 JVM 进程里。前端页面刷新可能重建状态,但后端服务是一个长期运行的进程,会同时处理很多用户请求。
第二,前端开发服务器更多服务于开发体验,而后端服务本身就是业务系统。它要连接 MySQL、Redis,要校验 JWT,要执行事务,要记录日志,要处理错误,还要面对并发请求。
所以,学习后端启动不是为了"会敲命令",而是为了理解:一个服务进程启动后,配置、依赖、Bean、端口、数据库连接、缓存连接是如何准备好的。
3. 后端核心概念讲解
3.1 JDK、JVM、Java 源码:先分清三件事
前端世界里,你可能会区分 Node.js、npm、TypeScript、浏览器。Java 世界也有几层:
| 名词 | 先怎么理解 | 类比 |
|---|---|---|
| Java 源码 | .java 文件,人写的代码 |
.ts / .vue 源码 |
| JDK | Java 开发工具包,包含编译器和运行工具 | Node.js + 一些开发工具的组合类比 |
| JVM | Java 虚拟机,真正运行 Java 字节码的地方 | 浏览器 JS 引擎 / Node 运行时的类比 |
| Maven | Java 项目构建和依赖管理工具 | pnpm / npm scripts / 构建工具的组合类比 |
| Spring Boot | 快速搭建可运行后端服务的框架 | Vite + Web server + 依赖装配框架的粗略类比 |
Java 源码不是直接像脚本那样被操作系统运行。它通常先被编译成字节码,再由 JVM 执行。你现在不需要深入 JVM 内存模型,只需要知道:运行 Java 后端时,真正常驻的是一个 JVM 进程。
这和前端很不同。前端业务代码最终主要在用户浏览器里运行,用户 A 和用户 B 的浏览器状态彼此隔离。后端服务则是所有用户共同请求同一个服务进程,服务进程再连接共享的 MySQL 和 Redis。因此后端代码天然要考虑多用户、并发、共享资源和失败恢复。
3.2 Maven:不只是安装依赖
Maven 的核心配置文件叫 pom.xml。当前后端有 3 个重要 POM:
text
backend/pom.xml
backend/contract/pom.xml
backend/service/pom.xml
你可以先把 pom.xml 类比成 package.json,但 Maven 的职责更偏"工程构建"。它不仅声明依赖,还声明:
- 这个项目的
groupId、artifactId、version; - 这个项目是否是父工程;
- 子模块有哪些;
- 使用哪个 Java 版本;
- 依赖版本如何统一管理;
- 构建时使用哪些插件;
- 哪些依赖只在测试或运行时生效。
在当前项目 backend/pom.xml 中,父工程声明了:
- Spring Boot 父依赖版本是
3.5.16; - Java 版本是
21; - 模块包括
contract和service; - MyBatis-Plus、Druid、JJWT 等版本统一在 properties 里定义。
这就像一个前端 monorepo 的根配置:根配置管公共版本和 workspace,子包再声明自己的职责。
3.3 Maven 坐标:groupId、artifactId、version
如果你看过 npm 包,会熟悉包名和版本,例如:
json
{
"name": "my-app",
"version": "1.0.0"
}
Maven 世界里,一个依赖通常用 3 个坐标定位:
xml
<groupId>com.example.fullstackmall</groupId>
<artifactId>fullstack-mall-service</artifactId>
<version>0.0.1-SNAPSHOT</version>
可以这样理解:
groupId:组织或命名空间;artifactId:具体模块 / 产物名;version:版本号。
当前 service 模块依赖 contract 模块时,也是通过 Maven 坐标声明:
xml
<dependency>
<groupId>com.example.fullstackmall</groupId>
<artifactId>fullstack-mall-contract</artifactId>
<version>${project.version}</version>
</dependency>
这说明 service 模块要使用 contract 模块里的 Request、Response、Facade 接口。前端类比就是某个 package 引用了 monorepo 里的另一个 package。
3.4 多模块:为什么有 contract 和 service
当前后端父工程的 <modules> 是:
xml
<modules>
<module>contract</module>
<module>service</module>
</modules>
这表示 backend 下面有两个 Maven 子模块。
contract 的职责是"对外接口、请求和响应模型"。它里面放:
*Request.java;*Response.java;- enum;
I...Facade.java。
service 的职责是"可运行的 Spring Boot 后端服务"。它里面放:
- Controller;
- Facade 实现;
- Entity;
- Mapper;
- DbService;
- Security;
- Cache;
- Config;
- 测试;
application.yml。
为什么要拆?因为对外契约和内部实现的变化频率、关注点不同。前端最关心的是接口能传什么、返回什么;后端内部则关心怎么查库、怎么校验、怎么做缓存、怎么处理事务。把契约单独放出来,有助于保持边界清晰。
你可以用这张图理解:
注意箭头方向:service 依赖 contract。也就是说,服务实现要遵守对外契约。反过来,contract 不应该依赖 service 的内部实现。这个边界类似前端中"类型定义和 API 契约不应该反向依赖页面组件"。
3.5 Spring Boot:把 Java 类装配成服务
Spring Boot 做了很多事,但入门阶段可以先记住三句话:
- 它帮你启动一个 Java 后端应用;
- 它帮你创建和管理对象,也就是 Bean;
- 它帮你把 HTTP 请求分发给 Controller。
当前项目启动类是:
text
backend/service/src/main/java/com/example/fullstackmall/service/FullstackMallApplication.java
代码非常短:
java
@EnableScheduling
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
public class FullstackMallApplication {
public static void main(String[] args) {
SpringApplication.run(FullstackMallApplication.class, args);
}
}
你可以先这样翻译:
main方法是 Java 程序入口;SpringApplication.run(...)启动 Spring Boot 应用;@SpringBootApplication表示这是 Spring Boot 应用主类;@EnableScheduling开启定时任务能力,后续支付超时关单会用到;exclude = UserDetailsServiceAutoConfiguration.class表示排除 Spring Security 默认用户详情自动配置,本项目有自己的认证方式。
不用急着深挖每个注解。你现在只需要知道:后端服务从这个 main 方法开始运行。
3.6 application.yml:后端版运行配置
前端项目有 .env.development、.env.production、vite.config.ts。后端项目也需要配置:端口、数据库、Redis、JWT、业务参数等。当前主要配置在:
text
backend/service/src/main/resources/application.yml
里面可以看到这些内容:
- 应用名:
fullstack-mall-service; - MySQL 连接:
jdbc:mysql://localhost:3308/fullstack_mall; - 数据库账号:默认
mall; - Redis host:默认
localhost; - Redis port:默认
6379; - 服务端口:默认
8080; - MyBatis-Plus 驼峰映射;
- Swagger UI 路径;
- JWT secret 和过期时间;
- 订单支付超时时间;
- 商品详情缓存 TTL。
如果你写前端,看到:
env
VITE_API_BASE_URL=http://localhost:8080
你知道这是运行配置。后端里的 application.yml 同理,只是配置项更多,而且很多配置直接影响服务能不能正常启动。
例如这行:
yaml
server:
port: ${SERVER_PORT:8080}
表示服务默认监听 8080,也可以通过环境变量 SERVER_PORT 覆盖。如果你启动时发现端口冲突,就要想到这里。
再看数据库配置:
yaml
spring:
datasource:
url: ${DB_URL:jdbc:mysql://localhost:3308/fullstack_mall?...}
username: ${DB_USERNAME:mall}
password: ${DB_PASSWORD:mall123}
这表示默认连接本机 3308 端口的 MySQL。如果 Docker 里的 MySQL 没启动,或者端口不是 3308,服务启动或访问数据库时就会出错。
3.7 profile:为什么有 local 和 debug
当前项目还有:
text
backend/service/src/main/resources/application-local.yml
backend/service/src/main/resources/application-debug.yml
application-local.yml 是本地无 Docker 快速启动配置。它使用 H2 内存数据库,并且默认关闭商品详情 Redis 缓存:
yaml
mall:
cache:
product-detail:
enabled: false
这对初学者很友好。因为你刚开始可能还没装 Docker,或者 MySQL / Redis 没启动。local profile 可以让你先把服务跑起来,验证接口链路。
application-debug.yml 则用于调试,默认把端口改成 8081,避免和普通开发实例的 8080 冲突。
前端类比是:
.env.development:开发环境配置;.env.local:本地私有配置;.env.production:生产环境配置。
Spring Boot profile 也是类似思路:同一个项目,在不同场景加载不同配置。
4. 在本项目中对应哪些文件
本篇最重要的文件如下:
| 文件 | 作用 | 前端类比 |
|---|---|---|
backend/pom.xml |
父工程,定义 Spring Boot、Java 版本、模块和公共版本 | monorepo 根 package.json / workspace 配置 |
backend/contract/pom.xml |
契约模块 POM,定义契约模块依赖 | packages/api-types/package.json 类比 |
backend/service/pom.xml |
服务模块 POM,定义 Web、Security、Redis、MyBatis-Plus、MySQL 等依赖 | 应用包 package.json |
backend/service/src/main/java/com/example/fullstackmall/service/FullstackMallApplication.java |
Spring Boot 启动入口 | src/main.ts 的后端类比 |
backend/service/src/main/resources/application.yml |
默认运行配置,连接 MySQL / Redis,设置端口和业务参数 | .env + vite.config.ts 的部分职责 |
backend/service/src/main/resources/application-local.yml |
本地无 Docker 快速启动配置,使用 H2,关闭商品详情缓存 | .env.local 类比 |
backend/service/src/main/resources/application-debug.yml |
Debug 配置,默认端口 8081 | 本地调试专用配置 |
docker-compose.dev.yml |
启动 MySQL 和 Redis | 前端项目较少直接对应,可类比本地依赖服务编排 |
建议你先打开这些文件,不要急着读业务代码。你要先知道后端服务"启动前需要什么、启动时读什么、启动后监听哪里"。
5. Mermaid 图:从源码到可访问接口
下面这张图把本篇主线串起来:
再看配置和外部依赖:
这两张图加在一起,你就能理解一个后端服务的启动需要经过"构建、运行、加载配置、创建对象、监听端口、连接外部服务"几个步骤。
6. 逐段读源码和配置
6.1 读 backend/pom.xml
先看父工程。关键片段是:
xml
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.5.16</version>
</parent>
这说明当前项目继承 Spring Boot 的父工程配置。你可以理解为 Spring Boot 帮你预先管理了很多常见依赖和插件的默认版本,避免你手动拼一堆容易冲突的版本。
然后看项目自己的坐标:
xml
<groupId>com.example.fullstackmall</groupId>
<artifactId>fullstack-mall-backend</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>pom</packaging>
packaging 是 pom,说明它是父工程,不是最终可运行 jar。真正可运行的是 service 模块。
再看模块:
xml
<modules>
<module>contract</module>
<module>service</module>
</modules>
这告诉 Maven:构建父工程时要处理这两个子模块。
最后看 properties:
xml
<java.version>21</java.version>
<mybatis-plus.version>3.5.16</mybatis-plus.version>
<druid.version>1.2.28</druid.version>
<jjwt.version>0.13.0</jjwt.version>
这就像前端项目把重要依赖版本统一放在根配置里。以后升级 MyBatis-Plus 或 JJWT,就可以从这里看版本。
6.2 读 backend/contract/pom.xml
contract 模块比较轻,它的描述是"对外接口、请求和响应模型"。依赖也很少:
- Lombok;
- Jakarta Validation API。
这说明 contract 主要定义模型和接口,不启动 Web 服务,也不连接 MySQL。你在这里看到的 Request、Response、enum,应该尽量保持干净,不要混入 service 的内部实现。
前端类比是:如果你有一个 api-types 包,它只放接口类型,不应该反过来依赖页面组件。
6.3 读 backend/service/pom.xml
service 模块的描述是"可运行的 Spring Boot 后端服务"。这里才是功能依赖最多的地方。
看到:
xml
<artifactId>spring-boot-starter-web</artifactId>
说明项目可以写 HTTP Controller。
看到:
xml
<artifactId>spring-boot-starter-validation</artifactId>
说明项目可以对请求参数做校验,例如必填、长度、数值范围。
看到:
xml
<artifactId>spring-boot-starter-security</artifactId>
说明项目使用 Spring Security 做认证授权。
看到:
xml
<artifactId>spring-boot-starter-data-redis</artifactId>
说明项目可以连接 Redis。
看到:
xml
<artifactId>mybatis-plus-spring-boot3-starter</artifactId>
说明项目使用 MyBatis-Plus 操作数据库。
看到:
xml
<artifactId>mysql-connector-j</artifactId>
说明项目可以连接 MySQL。
对前端同学来说,读 pom.xml 的方法和读 package.json 类似:先不要逐行背,先问"这个依赖让项目具备什么能力"。比如 spring-boot-starter-web 对应 HTTP 接口,spring-boot-starter-security 对应登录权限,mybatis-plus 对应数据库访问,spring-boot-starter-data-redis 对应缓存。
6.4 读启动类 FullstackMallApplication.java
启动类只有十几行,但非常重要:
java
@EnableScheduling
@SpringBootApplication(exclude = UserDetailsServiceAutoConfiguration.class)
public class FullstackMallApplication {
public static void main(String[] args) {
SpringApplication.run(FullstackMallApplication.class, args);
}
}
main 是 Java 程序入口。你可以暂时类比前端的 main.ts,但后端 main 方法启动的是一个长期运行的服务进程。
@SpringBootApplication 是组合注解。入门阶段可以先理解为它告诉 Spring Boot:"从这里开始扫描和启动应用"。默认情况下,Spring 会扫描这个类所在包及其子包。当前包是:
text
com.example.fullstackmall.service
所以 service 包下面的 Controller、Facade、Config 等类会被扫描到。
@EnableScheduling 开启定时任务。当前项目后续支付超时关单会用到调度能力。你现在只需要知道,这个注解使得类似 @Scheduled 的定时任务可以工作。
exclude = UserDetailsServiceAutoConfiguration.class 是和 Spring Security 相关的配置。Spring Security 默认可能会自动创建一个用户体系,但当前项目有自己的 JWT 登录认证,所以排除了默认用户详情自动配置。这个细节第 6 篇再深入。
6.5 读 application.yml
application.yml 是后端运行配置中心之一。先看服务名:
yaml
spring:
application:
name: fullstack-mall-service
服务名在日志、监控、配置中都可能有用。
再看数据库:
yaml
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: ${DB_URL:jdbc:mysql://localhost:3308/fullstack_mall?...}
username: ${DB_USERNAME:mall}
password: ${DB_PASSWORD:mall123}
这里说明默认连接本机 3308 端口的 MySQL,数据库名是 fullstack_mall。${DB_URL:默认值} 这种写法表示:如果环境变量 DB_URL 存在,就用环境变量;否则用冒号后面的默认值。
前端也常见类似写法:开发环境默认 API 地址是 localhost,但生产环境通过环境变量注入真实地址。
再看 Redis:
yaml
spring:
data:
redis:
host: ${REDIS_HOST:localhost}
port: ${REDIS_PORT:6379}
timeout: ${REDIS_TIMEOUT:2s}
这说明默认连接本机 Redis。后续 Redis 篇会讲 key、TTL、缓存失效,现在只要知道连接配置在这里。
再看端口:
yaml
server:
port: ${SERVER_PORT:8080}
所以默认服务地址是:
text
http://localhost:8080
如果端口被占用,你可以设置 SERVER_PORT,或者使用 debug profile。
6.6 读 application-local.yml
application-local.yml 的注释写得很清楚:这是"本地无 Docker 快速启动配置"。它用 H2 内存数据库替代 MySQL,并且初始化本地 schema 和 data。
对初学者来说,这个配置非常重要。因为你学习后端初期可能还没把 Docker、MySQL、Redis 全部准备好。local profile 可以让你先体验 Spring Boot 启动和 HTTP 接口。
但是你也要知道局限:H2 是内存数据库,应用重启后数据会重置;它可以帮助快速学习,但不能完全替代真实 MySQL 行为。后续涉及事务、锁、并发、真实 SQL 方言时,还是要回到 MySQL。
6.7 读 application-debug.yml
debug 配置主要做一件事:把端口默认改成 8081。
yaml
server:
port: ${SERVER_PORT:8081}
为什么需要?因为你可能已经在普通方式启动了一个 8080 服务,又想在 IDEA 里 Debug 再启动一个。如果两个进程都抢 8080,就会端口冲突。debug profile 使用 8081,可以减少冲突。
7. 本地怎么运行 / curl 怎么验证
7.1 方式一:使用 Docker MySQL 和 Redis 的正常开发启动
先在项目根目录启动依赖:
bash
docker compose -f docker-compose.dev.yml up -d mysql redis
然后启动后端:
bash
cd backend
mvn -pl service -am spring-boot:run
命令里的几个部分解释如下:
| 命令片段 | 含义 |
|---|---|
mvn |
调用 Maven |
-pl service |
指定要操作的模块是 service |
-am |
同时构建 service 依赖的模块,例如 contract |
spring-boot:run |
使用 Spring Boot Maven 插件运行应用 |
启动成功后,服务默认监听:
text
http://localhost:8080
你可以用 curl 请求公开接口,例如:
bash
curl -i http://localhost:8080/api/products/1001
如果返回 JSON,说明 HTTP 服务已经正常响应。至于商品是否存在、状态是否上架、缓存是否命中,是后续业务章节要看的内容。
7.2 方式二:使用 local profile 快速启动
如果你暂时不想启动 Docker,可以尝试 local profile:
bash
cd backend
mvn -pl service -am spring-boot:run -Dspring-boot.run.profiles=local
local profile 会使用 H2 内存数据库,并默认关闭商品详情缓存。它适合先学习 Controller、Request、Response、启动流程,不适合验证所有 MySQL 和 Redis 真实行为。
7.3 方式三:Debug profile
如果你在 IDEA 中调试,可以把 Active profiles 设置为:
text
debug
或者命令行:
bash
cd backend
mvn -pl service -am spring-boot:run -Dspring-boot.run.profiles=debug
debug profile 默认端口是 8081。请求时要改成:
bash
curl -i http://localhost:8081/api/products/1001
如果你同时激活多个 profile,例如 local + debug,需要注意配置覆盖关系。入门阶段建议一次只做一个目标:要么快速 local 启动,要么正常 Docker 启动,要么 Debug 调试。
7.4 Maven 常用命令:不要只会一条启动命令
前端同学刚接触 Maven 时,最容易把它理解成"Java 版 npm"。这个类比可以帮助入门,但不能完全等价。npm run dev、npm run build、npm test 往往是项目作者在 package.json 里定义的脚本;而 Maven 有一套相对固定的生命周期,很多命令不是项目作者临时起名,而是 Maven 生态约定出来的阶段。
在当前项目里,下面这些命令最值得你先记住:
bash
cd backend
mvn -pl service -am compile
mvn -pl service -am test
mvn -pl service -am package
mvn -pl service -am spring-boot:run
它们的含义可以这样理解。
compile 负责把主代码编译一遍。你可以把它类比成前端里的 tsc --noEmit 或构建前的类型检查:它不一定真正启动服务,但能告诉你 Java 代码、依赖、注解处理器是否能组成一个可编译的整体。后端开发中,如果你改了 DTO、Facade 接口、Service 实现,先跑一次 compile 是很好的习惯,因为 Java 是静态类型语言,很多错误可以在编译阶段暴露,而不用等到运行时才发现。
test 会执行测试。前端里你可能用 Vitest、Jest、Playwright;后端里通常用 JUnit、Spring Boot Test、Testcontainers。当前项目的 service 模块引入了测试相关依赖,说明它具备写单元测试、集成测试的基础。后续我们讲订单、库存、支付回调时,你会看到测试对后端特别重要,因为这些逻辑不只是页面展示问题,而是会影响数据一致性。
package 会把项目打包。Spring Boot 项目通常会产出一个可以运行的 jar 包。前端打包产物常常是 dist/ 下的一堆静态文件;后端打包产物则更像一个完整服务程序。它包含你的业务代码、依赖、配置入口,部署时可以通过 java -jar ... 启动。理解这一点后,你就会知道:后端上线不是把源码丢到服务器上,而是把一个构建产物以进程方式运行起来。
spring-boot:run 是 Spring Boot Maven Plugin 提供的运行目标。它适合本地开发阶段使用。你可以把它类比成 vite dev:目的不是产出最终上线包,而是快速把服务跑起来,方便你改代码、看日志、验证接口。
这里还要重点解释 -pl service -am。-pl 是 --projects 的缩写,意思是只选择某个模块;service 是当前要启动的模块。-am 是 --also-make 的缩写,意思是同时构建它依赖的模块。因为 service 依赖 contract,所以你不能只关心 service,还要让 Maven 把 contract 一起编译好。前端 monorepo 里也有类似情况:如果 app package 依赖 shared package,启动 app 前也需要保证 shared 已经构建或能被工作区解析。
7.5 从日志判断启动到了哪一步
后端启动失败时,不要一上来就复制整段报错去搜索。更推荐你像调试前端构建一样,先判断它卡在哪个阶段。一个 Spring Boot 服务从命令到可访问,大致经历这些阶段:Maven 阶段、JVM 阶段、Spring 容器阶段、Web 服务器阶段、外部资源连接阶段、业务初始化阶段。
如果错误出现在 Maven 阶段,常见表现是依赖下载失败、找不到模块、编译报错、测试失败。这个阶段服务其实还没有真正启动,端口也不会被监听。你应该看的是 pom.xml、本地 Maven 仓库、Java 版本、命令执行目录,以及最近改动的 Java 类型签名。
如果错误出现在 JVM 阶段,常见表现是 Java 版本不匹配,例如项目要求 Java 21,但你本机实际使用了更低版本。前端里也有类似问题:项目要求 Node 20,但你用 Node 16,就可能出现语法、依赖或构建工具不兼容。后端里可以用下面命令先确认:
bash
java -version
mvn -version
如果错误出现在 Spring 容器阶段,常见表现是某个 Bean 创建失败、依赖注入失败、配置属性绑定失败。你会看到类似 BeanCreationException、UnsatisfiedDependencyException 这样的异常名称。这个阶段最重要的排查思路是:哪个类需要哪个依赖,Spring 为什么没有办法创建它。前端类比的话,就像一个组件 import 了某个模块,但模块没有正确导出;或者一个组件需要某个 Provider,但上层没有提供。
如果错误出现在 Web 服务器阶段,常见表现是端口被占用,或者启动日志里没有出现 Tomcat 监听端口的信息。当前项目默认端口来自 server.port,默认是 8080,debug profile 默认是 8081。如果你本地已经有另一个服务占用了端口,就要换端口或者关闭旧进程。
如果错误出现在外部资源连接阶段,最常见就是 MySQL 或 Redis 连接不上。当前默认配置会连 localhost:3308 的 MySQL 和 localhost:6379 的 Redis;如果你没有启动 docker-compose.dev.yml 里的容器,就可能失败。使用 local profile 时,数据库会切换到 H2 内存数据库,并且商品详情缓存默认关闭,这就能绕开一部分外部依赖,适合先学习启动链路。
所以,后端排错第一问不是"这个异常是什么意思",而是"服务启动走到了哪一层"。只要层次判断正确,后面的搜索范围会小很多。
7.6 profile、环境变量和配置覆盖:后端也有多环境
前端项目里常见 .env.development、.env.production、.env.local。你可能已经习惯通过 VITE_API_BASE_URL 区分本地、测试、线上 API 地址。Spring Boot 的 profile 也是类似思想:同一套代码,在不同环境读取不同配置。
当前工程的主配置是:
text
backend/service/src/main/resources/application.yml
它定义了服务端口、数据库地址、Redis 地址、MyBatis-Plus 配置、Swagger UI 地址、JWT 参数和商品详情缓存参数。这个文件相当于"默认配置"。当你启用 local profile 时,Spring Boot 会额外读取:
text
backend/service/src/main/resources/application-local.yml
当你启用 debug profile 时,Spring Boot 会额外读取:
text
backend/service/src/main/resources/application-debug.yml
覆盖规则可以简单理解为:默认配置先加载,profile 配置再补充或覆盖。比如默认端口是 8080,debug profile 里如果设置端口为 8081,那么最终监听的就是 8081。这和前端里基础 .env 加上 .env.development 的覆盖关系很像。
当前 application.yml 里有很多 ${NAME:default} 形式的配置。它表示:优先读取环境变量 NAME,如果没有,就使用冒号后面的默认值。例如端口配置类似"优先看 SERVER_PORT,没有就用 8080"。前端里的环境变量通常在构建时注入,而后端环境变量更多在运行时读取。这个差异非常关键:前端构建后的静态文件通常不能随便改变构建期变量;后端进程每次启动时都可以根据启动环境读取不同变量。
举个例子,如果你临时想把后端跑到 8090 端口,可以用:
bash
cd backend
SERVER_PORT=8090 mvn -pl service -am spring-boot:run
如果你想启用 local profile,可以用:
bash
cd backend
mvn -pl service -am spring-boot:run -Dspring-boot.run.profiles=local
如果你只是学习后端,不想一开始就被 MySQL、Redis、Docker 全部拦住,建议先用 local profile 跑通;等你理解 Controller 和数据库访问后,再切回默认配置,启动 Docker 里的 MySQL 与 Redis。学习路径不要一上来追求"完全真实生产环境",而要先分层:先让服务启动,再让接口通,再让数据库通,再让缓存通,最后再考虑安全、性能和部署。
7.7 本章建议的调试顺序
前端调试常常是:先看页面是否打开,再看 Network,再看 Console,再看接口响应。后端也应该建立固定顺序。对于当前项目,我建议你每次启动失败都按下面顺序排查:
- 确认目录:命令是否在
backend目录执行; - 确认 Java:
java -version是否满足项目要求; - 确认 Maven:
mvn -version是否使用了正确 JDK; - 确认模块:命令是否带了
-pl service -am; - 确认 profile:你到底在用默认配置、
local,还是debug; - 确认端口:
8080或8081是否被占用; - 确认数据库:默认配置下 MySQL 是否在
3308; - 确认 Redis:默认配置下 Redis 是否在
6379; - 确认接口:用 curl 请求健康检查或商品查询接口;
- 确认日志:从第一条真正的异常开始读,而不是只看最后一行。
这套顺序的价值在于它符合因果链。目录错了,后面所有 Maven 讨论都没意义;Java 版本错了,Spring Boot 还没机会启动;profile 错了,你以为连 H2,实际却在连 MySQL;端口错了,前端请求再正确也访问不到服务。后端学习最怕"同时怀疑所有东西",固定排查顺序能显著降低心智负担。
7.8 给前端同学的一次完整心智演练
最后我们把本章内容串成一次"从零启动"的心智演练。假设你刚拉下这个仓库,第一眼看到后端目录,不要马上猜接口怎么写,也不要马上研究 Redis 和 MyBatis-Plus。第一步先确认项目边界:backend 是后端父工程,contract 是接口契约模块,service 是真正可运行的服务模块。这个判断来自 backend/pom.xml 的 <modules>,不是凭感觉猜出来的。
第二步看运行入口。前端项目通常从 package.json 的 scripts 找入口,再去看 Vite、Nuxt 或 Next.js 的配置;后端项目则先找 Spring Boot 启动类。当前启动类是 FullstackMallApplication.java,它所在包名是 com.example.fullstackmall.service。Spring Boot 默认会从启动类所在包向下扫描组件,因此 Controller、Facade、Mapper、Config 等类如果放在这个包的子包下,就更容易被自动发现。你以后新增后端类时,也要有"包扫描范围"的意识,不要随便放到完全无关的包路径里。
第三步看运行配置。你要知道服务监听哪个端口、连哪个数据库、连哪个 Redis、JWT 多久过期、缓存是否开启。当前这些信息主要在 application.yml。当你使用 local profile 时,配置会切到 H2 内存数据库,并关闭商品详情缓存;当你使用 debug profile 时,端口会变成 8081。所以同一段 Controller 代码,在不同 profile 下可能访问的是不同数据源,这就是很多后端新手"我明明改了数据,接口怎么没变化"的根源。
第四步才是启动命令。如果你希望尽量贴近真实开发环境,就先启动 Docker 里的 MySQL 和 Redis,再运行默认 profile;如果你只是想快速学习 Controller 和参数接收,可以先用 local profile。两种方式没有高低之分,它们服务于不同阶段。默认 profile 更接近真实依赖,适合学习 MySQL、Redis、缓存、事务;local profile 更轻量,适合先跑通 Web 层和 Java 基础。
第五步是验证。后端不能只看"控制台没有报错"就算成功。你至少要做三层验证:进程是否还活着,端口是否在监听,接口是否能返回预期 JSON。前端同学可以把这理解成:页面能打开只是第一层,Network 里接口成功才是第二层,数据渲染正确才是第三层。后端也是一样,启动日志只是过程证据,curl 响应才是接口证据,数据库里的数据变化才是业务证据。
第六步是记录。每次你遇到启动问题,都建议写下这几个字段:使用的命令、启用的 profile、Java 版本、Maven 版本、端口、数据库地址、第一条异常。这样做看似麻烦,但它会训练你用工程化方式排错。后端问题往往不是单点问题,而是代码、配置、环境、依赖共同作用的结果;没有记录,就很容易在同一个坑里反复试错。
如果你能按这六步稳定启动当前项目,那么恭喜你,你已经完成了从"会写页面"到"能掌控一个服务进程"的第一道门槛。后续我们学习 Controller、MyBatis-Plus、Redis、JWT、事务时,都建立在这个启动模型之上。也就是说,后端学习不是从背注解开始,而是从理解工程如何被构建、如何被配置、如何被启动、如何被验证开始。只要这条主线清楚,后面任何框架知识都不会显得孤立。
8. 常见错误
错误 1:在项目根目录直接乱跑 Maven 命令
当前 Maven 后端工程在 backend/ 下。你应该先:
bash
cd backend
再执行 Maven 命令。如果你在仓库根目录直接跑 mvn,很可能找不到 POM。
错误 2:只启动后端,不启动 MySQL
默认 application.yml 连接的是 localhost:3308 的 MySQL。如果 Docker MySQL 没启动,访问数据库相关接口时会失败。解决方式是先启动:
bash
docker compose -f docker-compose.dev.yml up -d mysql redis
或者使用 local profile 快速学习。
错误 3:端口冲突不知道看哪里
如果启动时报端口占用,先看 application.yml 里的 server.port。默认是 8080。你可以停止占用端口的进程,也可以设置 SERVER_PORT,或者使用 debug profile 的 8081。
错误 4:把 H2 local profile 当成真实 MySQL
H2 可以帮你快速跑起来,但它不是 MySQL。某些 SQL 方言、事务并发、锁行为和真实 MySQL 可能不同。学习基础接口可以用 local,验证库存、订单、支付这类核心行为时要回到 MySQL。
错误 5:看到依赖名就开始深挖源码
刚开始不需要读 Spring Boot、MyBatis-Plus、Redis 客户端源码。你先要知道依赖解决什么问题,在本项目哪个文件使用。比如看到 MyBatis-Plus,先知道它帮助 Mapper / Service 操作数据库;看到 Redis,先知道它是缓存;看到 Security,先知道它处理认证授权。
错误 6:不区分 compile、runtime、test 依赖
service/pom.xml 里有些依赖只在测试时用,例如 spring-boot-starter-test、spring-security-test、Testcontainers。有些驱动是 runtime scope,例如 MySQL 驱动。前端也有 dependencies 和 devDependencies 的区别,后端 scope 也会影响依赖在什么时候可用。
错误 7:修改配置后不知道重启
很多后端配置在应用启动时读取。你修改 application.yml 后,通常要重启服务才生效。前端热更新让你习惯保存即生效,但后端配置不一定都是热加载。
9. 本章小练习
练习 1:读父 POM
打开:
text
backend/pom.xml
回答:
- 当前 Java 版本是多少?
- Spring Boot 版本是多少?
- 当前有哪些 Maven 模块?
- MyBatis-Plus 版本在哪里定义?
练习 2:读 service POM
打开:
text
backend/service/pom.xml
找到这些依赖:
spring-boot-starter-web;spring-boot-starter-validation;spring-boot-starter-security;spring-boot-starter-data-redis;mybatis-plus-spring-boot3-starter;mysql-connector-j。
回答:它们分别给项目带来了什么能力?
练习 3:读启动类
打开:
text
backend/service/src/main/java/com/example/fullstackmall/service/FullstackMallApplication.java
回答:
- main 方法在哪里?
SpringApplication.run(...)传入了哪个类?@EnableScheduling可能和后续哪个业务有关?@SpringBootApplication为什么是启动类上最重要的注解?
练习 4:读配置
打开:
text
backend/service/src/main/resources/application.yml
回答:
- 默认 HTTP 端口是多少?
- 默认 MySQL 端口是多少?
- 默认 Redis 端口是多少?
- JWT 过期时间默认是多少秒?
- 商品详情缓存默认是否开启?
练习 5:尝试启动
如果你已经有 Docker,尝试:
bash
docker compose -f docker-compose.dev.yml up -d mysql redis
cd backend
mvn -pl service -am spring-boot:run
如果你暂时没有 Docker,尝试:
bash
cd backend
mvn -pl service -am spring-boot:run -Dspring-boot.run.profiles=local
然后用 curl 请求一个接口,观察响应和日志。
10. 本篇总结
这一章我们没有写业务代码,也没有深入 Redis、MyBatis-Plus、JWT 的具体 API。我们只做了一件非常重要的事:把后端服务启动链路看懂。
你现在应该知道:
- Java 后端运行在 JVM 进程里,不是在浏览器里;
- Maven 读取
pom.xml,管理依赖、模块、编译、测试和启动; - 当前后端是 Maven 多模块项目,父工程下面有
contract和service; contract放对外契约,service是可运行服务;FullstackMallApplication.main()是 Spring Boot 启动入口;application.yml管默认端口、MySQL、Redis、JWT、业务参数;application-local.yml可以让你不依赖 Docker 先快速启动;docker-compose.dev.yml提供真实 MySQL 和 Redis;mvn -pl service -am spring-boot:run是当前项目常用启动命令。
从前端转后端,千万不要跳过这一步。因为以后遇到任何接口问题,你都要先判断:服务有没有启动?端口对不对?profile 对不对?数据库连的是哪一个?Redis 是否开启?依赖是否存在?这些都属于"后端工程运行基础"。
11. 下一章预告
下一篇是第 3 篇:Controller、Request、Response 与统一返回:从 Axios 调接口到 Spring Boot 接参数。
我们会正式进入 HTTP 接口开发基础,重点讲:
@RestController、@RequestMapping、@GetMapping、@PostMapping;@PathVariable、@RequestParam、@RequestBody;- 为什么 Request DTO 不等于 Entity;
- 为什么 Response DTO 不应该直接暴露数据库对象;
@Valid如何做参数校验;ApiResponse<T>如何统一返回给前端;- 前端看到 400、401、403、404、500 时,后端分别可能发生了什么。
学完下一篇,你就能真正从一个接口开始,读懂"前端传参如何变成 Java 对象,Java 对象如何变成 JSON 返回"。