Spring Boot实战:从踩坑到稳如老狗的两年心路
大家好,我是小陈,目前在某电商中台团队做后端开发。别看我现在写得头头是道,其实刚入职那会儿还是个“Spring Boot Hello World”都要 Google 三遍的菜鸟。更惨的是,我是在试用期第三周就被丢进一个核心订单服务重构项目里——对,就是那种“你先试试,不行再说”的经典开局。
不过嘛,时间过得真快,一晃快两年了。这两年里,我从 Mac 上敲第一行 @SpringBootApplication 开始,经历了无数次凌晨三点的线上报警、被产品经理追着问“为什么接口又超时了”、以及运维大哥一句“这 Pod 又 OOM 了,你看看?”的日常。今天这篇就来聊聊我在 Spring Boot 实战中摸爬滚打出来的一些经验,不讲理论大道理,全是血泪教训和能直接抄的配置。
被逼出来的技术成长
去年双11前一个月,团队接了个硬核任务:把原来单体架构的订单模块拆成微服务,还必须用 Spring Boot + Kubernetes 部署。领导说:“你不是熟悉 K8s 吗?正好练练。”
我当时心里一万个草泥马奔腾而过——我确实玩过 K8s,但那都是本地 Minikube 玩玩,哪见过生产环境上万 QPS 的流量?
更要命的是,公司开发主力是 Windows,但我这人死脑筋,非得用 Mac 写代码(理由很简单:终端好看、命令行顺手)。结果每次联调都要在 Windows 虚拟机里跑一遍,测完再切回 Mac 改代码,效率低到想哭。测试同事都调侃我:“小陈,你这是跨平台开发,还是跨次元作战?”
但吐槽归吐槽,活还得干。于是,这场 Spring Boot 实战之旅,就在 deadline 倒计时 30 天的焦虑中开始了。
第一个坑:配置文件乱成一锅粥
刚开始我以为 Spring Boot 的 application.yml 很简单,dev/test/prod 三个 profile 搞定。结果上线第一天,测试环境连不上数据库——因为我在 application-prod.yml 里写了正确的 DB 地址,但 Jenkins 构建脚本默认加载的是 application-dev.yml!
后来才知道,Spring Boot 的配置加载顺序比我想的复杂得多。官方文档那张图我看了五遍才搞明白:
1. Devtools global settings (开发时)
2. @TestPropertySource / @SpringBootTest(properties)
3. 命令行参数 (--spring.profiles.active=prod)
4. SPRING_APPLICATION_JSON
5. ServletConfig / ServletContext 初始化参数
6. JNDI 属性
7. Java System.getProperties()
8. OS 环境变量
9. RandomValuePropertySource (random.*)
10. 打包 jar 内部的 application-{profile}.yml
11. 打包 jar 外部的 application-{profile}.yml
教训:永远不要依赖默认 profile!我们在 K8s Deployment 里显式指定:
env:
- name: SPRING_PROFILES_ACTIVE
value: "prod"
同时,所有敏感配置(DB 密码、AK/SK)全部交给 Vault 或 K8s Secret 管理,绝不硬编码。现在每次提 PR,CI 流水线都会扫一遍有没有明文密码,扫到直接 block —— 这招是从一次 P0 级事故中学来的,那次我把测试账号密码提交到了 GitLab 公共仓库……(别问,问就是黑历史)
日志与监控:别等线上炸了才想起看日志
早期我们的服务日志就是 System.out.println("来了!"),结果线上出问题时,日志里全是“来了!”、“走了!”、“又来了!”,根本没法排查。
后来痛定思痛,全面接入 ELK(Elasticsearch + Logstash + Kibana),并统一日志格式:
@RestController
@Slf4j // Lombok 注解,自动生成 logger
public class OrderController {
@PostMapping("/create")
public ResponseEntity<?> createOrder(@RequestBody OrderRequest req) {
String traceId = UUID.randomUUID().toString();
log.info("traceId={}, 创建订单请求, userId={}, items={}",
traceId, req.getUserId(), req.getItems());
try {
Order order = orderService.create(req);
log.info("traceId={}, 订单创建成功, orderId={}", traceId, order.getId());
return ResponseEntity.ok(order);
} catch (Exception e) {
log.error("traceId={}, 创建订单失败", traceId, e); // 带异常堆栈
return ResponseEntity.status(500).build();
}
}
}
关键点:
- 每个请求生成唯一
traceId,贯穿整个调用链 - 日志必须包含业务关键字段(userId、orderId)
- 错误日志必须带
e,否则 Kibana 里看不到堆栈
配合 Prometheus + Grafana,我们还暴露了 Spring Boot Actuator 的指标:
management:
endpoints:
web:
exposure:
include: health,info,metrics,prometheus
metrics:
tags:
application: ${spring.application.name}
现在运维大哥再也不用半夜打电话问我“你服务挂没挂”,直接看 Grafana 面板就知道 CPU、内存、GC、HTTP 5xx 的趋势。上周五晚上我还靠这个提前发现了数据库连接池泄漏,避免了一次双11预演事故——那一刻,我觉得自己终于从“背锅侠”进化成了“预警侠”。
性能优化:别让 Spring Boot 成为慢羊羊
有次压测,QPS 刚到 500 就开始大量超时。Profiler 一看,好家伙,90% 的时间花在 ObjectMapper 序列化上!原因是我们每个接口都手动 new 了一个 ObjectMapper,而它内部有复杂的缓存初始化逻辑。
解决方案:全局复用!
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Bean
@Primary
public ObjectMapper objectMapper() {
ObjectMapper mapper = new ObjectMapper();
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
mapper.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);
return mapper;
}
}
另外,数据库查询也是重灾区。早期代码里充斥着:
List<Order> orders = orderRepository.findAll(); // 十万条数据全拉出来!
return orders.stream().filter(o -> o.getStatus() == PAID).collect(...);
这种 N+1 查询和内存过滤,在 K8s 里直接导致 Pod 内存飙升被 OOMKill。后来强制推行:
- 所有查询必须走索引
- 分页查询上限 100 条
- 复杂聚合用 MyBatis Plus 的
QueryWrapper或原生 SQL
附上我们优化前后的对比数据:
| 指标 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 平均响应时间 | 420ms | 85ms | 4.9x |
| P99 延迟 | 1200ms | 210ms | 5.7x |
| 内存占用 (Pod) | 1.2GB | 480MB | 60% ↓ |
| GC 频率 | 15次/分钟 | 2次/分钟 | 86% ↓ |
云原生适配:让 Spring Boot 在 K8s 里舒服地跑
很多人以为 Spring Boot 打个 jar 包扔进容器就完事了,其实不然。我们踩过几个典型坑:
1. 健康检查不兼容
Spring Boot 默认的 /actuator/health 返回 200 就算健康,但 K8s 的 liveness probe 如果太敏感,会导致服务刚启动就被 kill。
做法:
- readinessProbe 检查
/actuator/health/readiness - livenessProbe 检查
/actuator/health/liveness - 初始延迟设长一点(比如 30s),给应用留足启动时间
2. 资源限制没配,被调度器制裁
一开始我们没设 CPU/Memory limit,结果某个服务内存暴涨,把同节点其他 Pod 全挤出去了。后来严格按以下原则配置:
resources:
requests:
memory: "512Mi"
cpu: "500m"
limits:
memory: "1Gi"
cpu: "1000m"
并通过 -XX:+UseContainerSupport 让 JVM 感知容器内存限制(Java 8u191+ 默认开启)。
3. 优雅停机没开,请求被中断
用户支付成功,结果服务滚动更新时进程被 kill,回调没处理——钱付了但订单没生成。恐怖如斯!
解决:开启优雅停机
server:
shutdown: graceful
spring:
lifecycle:
timeout-per-shutdown-phase: 30s
同时 K8s 的 terminationGracePeriodSeconds 设为 45s,确保有足够时间处理完存量请求。
最后一点真心话
写这篇文章的时候,我刚通过转正答辩(没错,试用期真的把我虐哭了)。回头看这两年,Spring Boot 对我来说早已不是“框架”,而是每天打交道的“同事”。它有时候很贴心(自动配置省掉 80% 样板代码),有时候也很傲娇(版本升级一堆 breaking change)。
但正是这些实战中的坑、半夜的报警、和队友一起 debug 到天亮的经历,让我真正理解了什么叫“工程能力”。技术没有银弹,只有不断试错、复盘、沉淀。
如果你也在试用期焦虑,或者刚接手一个烂摊子项目——别慌。记住:每一个稳如泰山的老狗,都曾是瑟瑟发抖的菜鸟。只要肯折腾,Spring Boot 会是你最可靠的战友。
对了,我们组下周还要重构支付回调模块,用 Spring Boot + Kafka + Saga 模式做分布式事务。要是你有相关经验,评论区聊聊?说不定能帮我避开新坑 😅
P.S. 本文所有配置和代码片段都来自我们生产环境(脱敏后),可以直接参考。但别照搬!每家公司网络策略、中间件版本、合规要求都不一样,务必结合自身情况调整。毕竟,别人的最佳实践,可能是你的生产事故。

评论 0