本节目标:把 18.2 写好的服务构建成可执行 jar 并运行起来,用 curl 走通完整业务闭环、跑通测试套件,并明确读完本书之后该继续学什么。
适用版本:Spring Boot 4.1.x(Java 21)
18.3 打包与运行
代码写完不等于交付完成。本节做最后三件事:把它构建成能独立运行的产物,用真实请求验证业务闭环,然后给你一份继续前进的路线图。这也是全书最后一节——前面 17 章的知识点,会在这里全部落地。
18.3.1 完整构建流程与真实输出
构建用第 3 章学过的 spring-boot-maven-plugin。一条命令从源码到可执行 jar:
mvn clean package
在 4.1.1 + Java 21 环境下的真实输出(节选):
[INFO] Scanning for projects...
[INFO] Building library-service 0.0.1-SNAPSHOT
[INFO] --- maven-compiler-plugin:3.14.0:compile (default-compile) @ library-service ---
[INFO] Compiling 24 source files with javac [debug release 21] to target/classes
[INFO] --- flyway-maven-plugin:12.4.0:... ---
[INFO] --- spring-boot-maven-plugin:4.1.1:repackage (repackage) @ library-service ---
[INFO] Replacing main artifact with repackaged archive, 48,241,913 bytes of content
[INFO] BUILD SUCCESS
[INFO] Total time: 38.412 s
[INFO] Finished at: 2026-10-09T23:05:11+08:00
首次构建因为要拉依赖会慢一些(约 73 s),之后稳定在 38 s 左右。产物在 target/ 下:
target/
├── library-service-0.0.1-SNAPSHOT.jar ← 可执行 jar,约 48 MB
└── library-service-0.0.1-SNAPSHOT.jar.original ← 原始 jar(不含依赖)
记住第 3 章的结论:发布的是不带 .original 的那个,它含 BOOT-INF/lib 里的全部依赖;.original 只是未重打包的原始产物。4.1 还移除了 layertools,需要分层时改用 jarmode=tools。
18.3.2 本地运行的三种方式
方式一:java -jar——最接近生产的方式,验证的正是发布产物:
java -jar target/library-service-0.0.1-SNAPSHOT.jar
可以带 profile 与端口参数:
java -jar target/library-service-0.0.1-SNAPSHOT.jar \
--spring.profiles.active=dev --server.port=8081
方式二:mvn spring-boot:run——开发期最常用,改代码后重启快,且直接用 src/main/resources 下的配置:
mvn spring-boot:run -Dspring-boot.run.profiles=dev
方式三:IDE 直接运行 LibraryApplication——能打断点、单步调试,调试自动配置与事务时最有用(第 4 章、第 14 章都靠它)。
三者的取舍:
| 方式 | 验证目标 | 适用场景 |
|---|---|---|
java -jar | 发布产物本身 | 发布前冒烟、CI 验证 |
mvn spring-boot:run | 源码 + 开发配置 | 日常开发 |
| IDE 运行 | 可断点调试 | 排查启动与事务问题 |
启动成功的真实日志(4.1.1,注意 4.x 的包名变化):
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v4.1.1)
2026-10-09T23:05:20.435+08:00 INFO 51204 --- [ main] com.example.library.LibraryApplication : Starting LibraryApplication v0.0.1-SNAPSHOT using Java 21.0.12.1
2026-10-09T23:05:20.436+08:00 INFO 51204 --- [ main] com.example.library.LibraryApplication : The following 1 profile is active: "dev"
2026-10-09T23:05:21.027+08:00 INFO 51204 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
2026-10-09T23:05:21.059+08:00 INFO 51204 --- [ main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 601 ms
2026-10-09T23:05:21.285+08:00 INFO 51204 --- [ main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
2026-10-09T23:05:21.421+08:00 INFO 51204 --- [ main] com.example.library.LibraryApplication : Started LibraryApplication in 1.098 seconds (process running for 1.742)
请特别留意包名:4.x 是 o.s.boot.tomcat.TomcatWebServer,而 3.x 是 o.s.b.w.embedded.tomcat.TomcatWebServer。这是 4.0 模块化重构最直观的证据——看到旧包名,说明你的依赖里混进了 3.x 的组件。
18.3.3 用 curl 走一遍完整业务流程
服务起来后,用 curl 把 18.1 的用例跑一遍。先建一本书:
curl -s -X POST http://localhost:8080/api/books \
-H 'Content-Type: application/json' \
-d '{"isbn":"978-7-111-12345-6","title":"深入理解计算机系统","author":"Randal E. Bryant","category":"计算机","totalCopies":3}'
响应(HTTP 201):
{"code":200,"message":"OK","data":{"id":1,"isbn":"978-7-111-12345-6","title":"深入理解计算机系统","author":"Randal E. Bryant","category":"计算机","totalCopies":3,"availableCopies":3}}
查这本新书:
curl -s http://localhost:8080/api/books/1
{"code":200,"message":"OK","data":{"id":1,"isbn":"978-7-111-12345-6","title":"深入理解计算机系统","author":"Randal E. Bryant","category":"计算机","totalCopies":3,"availableCopies":3}}
借书(会员 1 借图书 1):
curl -s -X POST http://localhost:8080/api/loans \
-H 'Content-Type: application/json' \
-d '{"bookId":1,"memberId":1}'
响应(HTTP 201),注意 status 为 BORROWED:
{"code":200,"message":"OK","data":{"id":1,"bookId":1,"memberId":1,"borrowedAt":"2026-10-09","dueAt":"2026-11-08","status":"BORROWED"}}
此时再查图书,availableCopies 应变为 2——这是借书事务改动的直接证据。重复借同一本书会命中 409:
curl -s -X POST http://localhost:8080/api/loans \
-H 'Content-Type: application/json' \
-d '{"bookId":1,"memberId":1}'
{"code":40902,"message":"该会员已借阅此书且未归还","data":null}
还书:
curl -s -X POST http://localhost:8080/api/loans/1/return
{"code":200,"message":"OK","data":{"id":1,"bookId":1,"memberId":1,"borrowedAt":"2026-10-09","dueAt":"2026-11-08","returnedAt":"2026-10-09","status":"RETURNED"}}
最后分页查询,验证第 13 章的分页与排序:
curl -s 'http://localhost:8080/api/books?page=0&size=10&sort=title,asc'
{"code":200,"message":"OK","data":{"items":[{"id":1,"isbn":"978-7-111-12345-6","title":"深入理解计算机系统","totalCopies":3,"availableCopies":3}],"page":0,"size":10,"totalElements":1}}
再故意发一个非法请求,验证第 9 章校验与第 10 章统一响应是否联动:
curl -s -X POST http://localhost:8080/api/books \
-H 'Content-Type: application/json' \
-d '{"isbn":"","title":"","totalCopies":0}'
{"code":40001,"message":"isbn: must not be blank; title: must not be blank; totalCopies: must be greater than or equal to 1","data":null}
HTTP 状态是 400,业务码 40001——契约与 18.1.6 的设计完全一致。走到这里,「建书 → 查书 → 借书 → 还书 → 分页」的闭环就跑通了。
18.3.4 运行测试套件
业务验证完,跑一遍第 17 章写的测试套件:
mvn test
[INFO] Running com.example.library.BookServiceTest
[INFO] Running com.example.library.BookControllerTest
[INFO] Running com.example.library.LoanServiceIntegrationTest
[INFO] Tests run: 23, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
23 个测试覆盖三层:@DataJpaTest 验证 Repository 的查询,@WebMvcTest + @AutoConfigureMockMvc 验证 Controller 的 HTTP 契约,@SpringBootTest 验证借还的事务与库存变化。集成测试里用 Testcontainers 2.0 起一个真实的 PostgreSQL,避免 H2 与生产方言不一致带来的假通过。
4.x 的测试写法务必用新 API:Mock 依赖用 @MockitoBean(3.x 的旧注解已移除),@SpringBootTest 想用 MockMvc 要显式加 @AutoConfigureMockMvc。这些在第 17 章都强调过,此处只是收尾验证。
18.3.5 打包体积与启动时间分析
一个 24 个源文件的项目,可执行 jar 却有 48 MB。体积来自 BOOT-INF/lib 里被嵌套打进的所有依赖:
| 依赖类别 | 大致占比 | 说明 |
|---|---|---|
| Spring 框架 + Boot | 最大头 | webmvc、data-jpa、core、context 等 |
| Hibernate + Jakarta | 次之 | 持久化与 Jakarta EE 11 规范 |
| Tomcat 内嵌 | 中等 | Servlet 容器 |
| Flyway + JDBC 驱动 | 较小 | 迁移与数据库驱动 |
启动时间约 1.1 s(第 3 章实测同量级)。想进一步压缩体积与启动时间,有三条路,都属于后续卷的话题:分层 jar + Docker 缓存(实战卷)、spring.datasource.connection-fetch=lazy 惰性获取连接(4.1 新增,加快启动)、GraalVM 原生镜像(高级卷,启动进入毫秒级)。第 3 章的 layers.idx 四层划分,正是为第一条路准备的。
18.3.6 接下来该学什么
读完本书,你已经能独立写一个分层清晰、有校验、有事务、有迁移、有测试、可打包的服务。但真实系统还有更多维度,本书刻意没有展开——它们分别是实战卷与高级卷的主题。
实战卷面向「把服务做进生产」:多模块工程如何拆分、缓存(Spring Cache + Redis)如何落地、Spring Security 7.0 的认证授权、消息(Kafka / AMQP)的可靠投递、以及 Docker 容器化与可观测性接入。
高级卷面向「看懂框架内部」:Spring Boot 容器的启动流程、AOP 与代理机制、WebFlux 响应式编程、GraalVM 原生镜像,以及最现实的一环——从 3.x 迁移到 4.x 的完整破坏性变更清单与迁移策略。
如果只想挑一条线补充,站内两个专题可以并行阅读:Java 专题 覆盖语言与 JVM 的纵深(集合、并发、GC、模块化),Java 企业级开发专题 覆盖企业应用的工程实践。它们与本书不重复——本书给的是「教学递进的完整路径」,专题给的是「某个点的深挖」。
18.3.7 自查清单
读完这本书,你应该能独立完成下面这些事。能全部打勾,说明入门卷的目标已经达成:
- 用 Spring Initializr 或 IDE 起一个 4.1.x 项目,说明
spring-boot-starter-webmvc与旧名-web的关系 - 解释
@SpringBootApplication背后的自动配置与条件装配,会用--debug看条件报告 - 用
@ConfigurationProperties绑定配置,讲清 profile 与外部化配置的优先级 - 写 REST 控制器,用
@Valid做声明式校验,理解 400 是怎么产生的 - 用
@RestControllerAdvice统一异常与响应结构,把业务码与 HTTP 状态码分开 - 用 Spring Data JPA 做派生查询、JPQL、分页与关联映射,并识别 N+1
- 正确划分
@Transactional边界,说清传播行为与自调用失效的坑 - 用 Flyway 管理表结构的版本演进,知道为什么不用
ddl-auto=update - 配置结构化日志,让日志可被采集
- 写单元测试、切片测试与集成测试,Mock 依赖用
@MockitoBean而非 3.x 的旧注解 - 打包成可执行 jar 并用
java -jar运行,说明 4.1 移除layertools的影响
小结
- 构建用
mvn clean package,产物是不带.original的可执行 jar;首次构建慢、后续约 38 s。 - 三种运行方式各有用途:
java -jar验证发布产物,mvn spring-boot:run用于开发,IDE 用于调试。 - 用 curl 走通「建书 → 查书 → 借书 → 还书 → 分页」闭环,并验证校验失败返回 40001——设计契约全部兑现。
- 测试套件覆盖三层,集成测试用 Testcontainers 起真实数据库;4.x 测试用
@MockitoBean与@AutoConfigureMockMvc。 - 48 MB 体积来自嵌套依赖,约 1.1 s 启动;压缩体积与启动时间的路径属于实战卷与高级卷。
- 继续学习分两条线:实战卷(多模块、缓存、安全、消息、容器化)与高级卷(容器启动、AOP、WebFlux、原生镜像、3.x→4.x 迁移)。
- 自查清单能全部打勾,就说明你已经具备独立交付一个 Spring Boot 服务的能力。
从第 1 章的「它解决了什么问题」,到第 18 章这个能构建、能运行、能测试的完整服务,你已经走完了一条完整的路径。接下来最好的练习,是自己找一个真实的小需求,用这套方法从零做一遍——设计的取舍、实现的分层、打包运行的验证,每一步都会让你对框架的理解再深一层。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。