Java Spring Boot(写了 10年 Java 才悟出来:Spring Boot “变快”的真相,不在代码)

Java Spring Boot(写了 10年 Java 才悟出来:Spring Boot “变快”的真相,不在代码)
写了 10年 Java 才悟出来:Spring Boot “变快”的真相,不在代码



我见过太多 Spring Boot 项目:功能没问题、测试全绿、线上也能跑——但一到压测就“像糊在蜂蜜里”。后来我才慢慢意识到:Spring Boot 并不慢,慢的是我们把“默认值”当成了“最佳实践”。

这篇我按“真实踩坑路径”把常见的性能坑拆开讲:从数据库、线程、序列化、启动、JVM、到观测与配置。每一节都给你能落地的动作,你拿去对照自己的项目做一轮体检,效果通常比你想象更明显。


0)先立规矩:别用“感觉”,用“证据”

优化前先把基线立起来,否则你永远在“我觉得快了”。

最少做三件事:

  • 指标:P50 / P95 / P99、RPS、CPU、GC Pause、DB QPS、连接池等待时间
  • 链路:接口耗时拆分(Controller / Service / DB / 外部调用)
  • 对照:同一份压测脚本、同一份数据量、同一份机器规格

如果你现在还没有“应用内部视角”,优先把 Actuator + Micrometer + Prometheus/Grafana 接上,别继续盲飞。


1)数据库:你以为是 “一次查询”,其实是 “101 次查询”

典型症状

列表接口返回 100 条用户,每条用户都要读 orders、roles、tags……然后你写了 getOrders()。

结果 Hibernate 默默给你打出 N+1:
1 次查用户 + N 次查订单 = 延迟直线上升

解决思路(别只会“全改 EAGER”)

我更推荐三个工具组合,按场景选:

A. EntityGraph:声明式把某次查询变“带上关联”

@Entity@NamedEntityGraph(  name = "User.orders",  attributeNodes = @NamedAttributeNode("orders"))class User { ... }@EntityGraph("User.orders")List findAllWithOrders();


B. JOIN FETCH:需要强控制时更直接

@Query("select u from User u join fetch u.orders where u.id in :ids")List findAllWithOrders(@Param("ids") List ids);


C. BatchSize:必须 lazy,但想“批量懒加载”

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")@BatchSize(size = 25)private List orders;


一句话标准:

  • 列表页:宁可 join fetch / entity graph,一次取齐
  • 详情页:可以分层 DTO + 精准查询
  • 超大数据:分页 + 批量策略,别“全拉爆”

2)连接池:默认 10 个连接,够谁用?

很多人把 HikariCP 当“自带就不用管”,然后线上高峰出现:

  • 请求排队、线程卡住
  • DB 明明没满,应用却在等连接

默认池大小通常比较保守;在并发请求 + DB 操作偏多的系统里,连接池就是吞吐量阀门

你可以这样调一个“可用起点”

spring:  datasource:    hikari:      maximum-pool-size: 20      minimum-idle: 5      connection-timeout: 20000      idle-timeout: 300000      max-lifetime: 1200000      leak-detection-threshold: 60000


更关键的“思考方式”

池大小不是越大越好

  • 应用端大了:DB 扛不住、锁竞争更惨
  • 小了:应用端排队、延迟飙升

一个务实的策略是:
先压测看“等待连接”的比例 → 再逐步调大到“等待基本消失但 DB 仍稳定”的点。


3)缓存:不是“全加 @Cacheable 就快”

我也干过这种事:凡是查 DB 的方法都 @Cacheable。最后发现:

  • 热点小对象缓存后更慢(序列化/反序列化、锁、淘汰)
  • 数据频繁变更导致缓存抖动
  • 缓存穿透把 DB 又打穿

缓存的本质是:用空间换时间,但前提是“命中”和“稳定”。

我自己的规则(很朴素但好用)

只缓存同时满足:

  • 获取/计算 > 50ms
  • 访问频繁
  • 变更不那么频繁
  • 能接受短暂不一致

另外,别把“派生字段”单独再缓存一次,应该复用主对象缓存。


4)JSON 序列化:你返回的是“实体”,输出的是“整个世界”

把 JPA Entity 直接当 API Response,Jackson 往往会:

  • 序列化一堆你根本不想给前端的字段
  • 触发懒加载(又回到 N+1)
  • 返回体巨大,网络与序列化双杀

解决方式:DTO。别偷懒。
DTO 不只是性能,它还是 API 契约隔离。

record UserSummaryDTO(Long id, String name) {}@GetMapping("/users/{id}/summary")UserSummaryDTO summary(@PathVariable Long id) {  var u = userService.findById(id);  return new UserSummaryDTO(u.getId(), u.getName());}

5)异步:别让用户等“非关键路径”

注册接口里最常见的慢操作:发邮件、打点、发 MQ、刷缓存……
这些不影响“主流程成功”的动作,就不要阻塞响应

Spring 的 @Async + 明确线程池,是最简单的提速手段之一。

@EnableAsync@Configurationclass AsyncConfig {  @Bean("appExecutor")  Executor appExecutor() {    var ex = new ThreadPoolTaskExecutor();    ex.setCorePoolSize(5);    ex.setMaxPoolSize(10);    ex.setQueueCapacity(200);    ex.setThreadNamePrefix("async-");    ex.initialize();    return ex;  }}@Async("appExecutor")public void sendWelcomeEmail(...) { ... }

额外一句:如果你在 Java 21 + Spring Boot 新版本上,虚拟线程也值得评估,但不要把它当“银弹”。异步边界、IO 依赖、DB 池大小依然决定上限。


6)启动慢:不是“Spring 太重”,是你让它“全加载”

项目一大,启动慢通常来自两件事:

  • 扫描范围太大
  • Bean 过早初始化(eager)

三个立竿见影的动作

A. 缩小扫描包

@SpringBootApplication(scanBasePackages = {  "com.xxx.controller",  "com.xxx.service",  "com.xxx.config"})class App {}


B. 开启 Lazy Init(谨慎:首次调用会变慢)

spring:  main:    lazy-initialization: true


C. spring-context-indexer(大项目很香)
它会在构建期预计算候选组件,减少运行期扫描与反射开销。

进阶:GraalVM Native Image / CRaC(看你的部署形态)

如果你是大量小服务、弹性伸缩、冷启动敏感:Native Image 的收益往往非常直观,但你要接受构建与兼容性成本。


7)JVM:你以为是“内存泄漏”,其实是“GC 在求救”

很多“内存问题”本质是:我见过太多 Spring Boot 项目:功能没问题、测试全绿、线上也能跑——但一到压测就“像糊在蜂蜜里”。后来我才慢慢意识到:Spring Boot 并不慢,慢的是我们把“默认值”当成了“最佳实践”。

这篇我按“真实踩坑路径”把常见的性能坑拆开讲:从数据库、线程、序列化、启动、JVM、到观测与配置。每一节都给你能落地的动作,你拿去对照自己的项目做一轮体检,效果通常比你想象更明显。


0)先立规矩:别用“感觉”,用“证据”

优化前先把基线立起来,否则你永远在“我觉得快了”。

最少做三件事:

  • 指标:P50 / P95 / P99、RPS、CPU、GC Pause、DB QPS、连接池等待时间
  • 链路:接口耗时拆分(Controller / Service / DB / 外部调用)
  • 对照:同一份压测脚本、同一份数据量、同一份机器规格

如果你现在还没有“应用内部视角”,优先把 Actuator + Micrometer + Prometheus/Grafana 接上,别继续盲飞。


1)数据库:你以为是 “一次查询”,其实是 “101 次查询”

典型症状

列表接口返回 100 条用户,每条用户都要读 orders、roles、tags……然后你写了 getOrders()。

结果 Hibernate 默默给你打出 N+1:
1 次查用户 + N 次查订单 = 延迟直线上升

解决思路(别只会“全改 EAGER”)

我更推荐三个工具组合,按场景选:

A. EntityGraph:声明式把某次查询变“带上关联”

@Entity@NamedEntityGraph(  name = "User.orders",  attributeNodes = @NamedAttributeNode("orders"))class User { ... }@EntityGraph("User.orders")List findAllWithOrders();


B. JOIN FETCH:需要强控制时更直接

@Query("select u from User u join fetch u.orders where u.id in :ids")List findAllWithOrders(@Param("ids") List ids);


C. BatchSize:必须 lazy,但想“批量懒加载”

@OneToMany(fetch = FetchType.LAZY, mappedBy = "user")@BatchSize(size = 25)private List orders;


一句话标准:

  • 列表页:宁可 join fetch / entity graph,一次取齐
  • 详情页:可以分层 DTO + 精准查询
  • 超大数据:分页 + 批量策略,别“全拉爆”

2)连接池:默认 10 个连接,够谁用?

很多人把 HikariCP 当“自带就不用管”,然后线上高峰出现:

  • 请求排队、线程卡住
  • DB 明明没满,应用却在等连接

默认池大小通常比较保守;在并发请求 + DB 操作偏多的系统里,连接池就是吞吐量阀门

你可以这样调一个“可用起点”

spring:  datasource:    hikari:      maximum-pool-size: 20      minimum-idle: 5      connection-timeout: 20000      idle-timeout: 300000      max-lifetime: 1200000      leak-detection-threshold: 60000



更关键的“思考方式”

池大小不是越大越好

  • 应用端大了:DB 扛不住、锁竞争更惨
  • 小了:应用端排队、延迟飙升

一个务实的策略是:
先压测看“等待连接”的比例 → 再逐步调大到“等待基本消失但 DB 仍稳定”的点。


3)缓存:不是“全加 @Cacheable 就快”

我也干过这种事:凡是查 DB 的方法都 @Cacheable。最后发现:

  • 热点小对象缓存后更慢(序列化/反序列化、锁、淘汰)
  • 数据频繁变更导致缓存抖动
  • 缓存穿透把 DB 又打穿

缓存的本质是:用空间换时间,但前提是“命中”和“稳定”。

我自己的规则(很朴素但好用)

只缓存同时满足:

  • 获取/计算 > 50ms
  • 访问频繁
  • 变更不那么频繁
  • 能接受短暂不一致

另外,别把“派生字段”单独再缓存一次,应该复用主对象缓存。


4)JSON 序列化:你返回的是“实体”,输出的是“整个世界”

把 JPA Entity 直接当 API Response,Jackson 往往会:

  • 序列化一堆你根本不想给前端的字段
  • 触发懒加载(又回到 N+1)
  • 返回体巨大,网络与序列化双杀

解决方式:DTO。别偷懒。
DTO 不只是性能,它还是 API 契约隔离。

record UserSummaryDTO(Long id, String name) {}@GetMapping("/users/{id}/summary")UserSummaryDTO summary(@PathVariable Long id) {  var u = userService.findById(id);  return new UserSummaryDTO(u.getId(), u.getName());}

5)异步:别让用户等“非关键路径”

注册接口里最常见的慢操作:发邮件、打点、发 MQ、刷缓存……
这些不影响“主流程成功”的动作,就不要阻塞响应

Spring 的 @Async + 明确线程池,是最简单的提速手段之一。

@EnableAsync@Configurationclass AsyncConfig {  @Bean("appExecutor")  Executor appExecutor() {    var ex = new ThreadPoolTaskExecutor();    ex.setCorePoolSize(5);    ex.setMaxPoolSize(10);    ex.setQueueCapacity(200);    ex.setThreadNamePrefix("async-");    ex.initialize();    return ex;  }}@Async("appExecutor")public void sendWelcomeEmail(...) { ... }

额外一句:如果你在 Java 21 + Spring Boot 新版本上,虚拟线程也值得评估,但不要把它当“银弹”。异步边界、IO 依赖、DB 池大小依然决定上限。


6)启动慢:不是“Spring 太重”,是你让它“全加载”

项目一大,启动慢通常来自两件事:

  • 扫描范围太大
  • Bean 过早初始化(eager)

三个立竿见影的动作

A. 缩小扫描包

@SpringBootApplication(scanBasePackages = {  "com.xxx.controller",  "com.xxx.service",  "com.xxx.config"})class App {}


B. 开启 Lazy Init(谨慎:首次调用会变慢)

Java Spring Boot(写了 10年 Java 才悟出来:Spring Boot “变快”的真相,不在代码)

spring:  main:    lazy-initialization: true


C. spring-context-indexer(大项目很香)
它会在构建期预计算候选组件,减少运行期扫描与反射开销。

进阶:GraalVM Native Image / CRaC(看你的部署形态)

如果你是大量小服务、弹性伸缩、冷启动敏感:Native Image 的收益往往非常直观,但你要接受构建与兼容性成本。


7)JVM:你以为是“内存泄漏”,其实是“GC 在求救”

很多“内存问题”本质是:

  • heap 太小导致频繁 GC
  • 参数不匹配 Spring Boot 的分配特征
  • 线上默认值并不等于稳定值

一个可用起点(示例):

java -jar app.jar \  -Xms512m -Xmx2g \  -XX:+UseG1GC \  -XX:MaxGCPauseMillis=200 \  -XX:+UseStringDeduplication



8)生产配置:最“便宜”的 25% 性能提升

很多性能提升不是改代码,而是改几行配置:Tomcat 线程、Hibernate 批处理、日志级别。

server:  tomcat:    threads:      max: 200      min-spare: 10    connection-timeout: 20000    accept-count: 100spring:  jpa:    properties:      hibernate:        jdbc:          batch_size: 20        order_inserts: true        order_updates: true    show-sql: falselogging:  level:    org.springframework: WARN    org.hibernate: WARN    com.yourapp: INFO



最后一段我想强调的

性能优化的核心不是“技巧”,而是“把默认值当成可质疑对象”。
Spring Boot 帮你把项目跑起来,但“跑得快、跑得稳、跑得省”,得靠你理解它默认做了什么、什么时候不适合你、以及你用数据证明改变是有效的。

  • heap 太小导致频繁 GC
  • 参数不匹配 Spring Boot 的分配特征
  • 线上默认值并不等于稳定值

一个可用起点(示例):

java -jar app.jar \  -Xms512m -Xmx2g \  -XX:+UseG1GC \  -XX:MaxGCPauseMillis=200 \  -XX:+UseStringDeduplication



8)生产配置:最“便宜”的 25% 性能提升

很多性能提升不是改代码,而是改几行配置:Tomcat 线程、Hibernate 批处理、日志级别。

server:  tomcat:    threads:      max: 200      min-spare: 10    connection-timeout: 20000    accept-count: 100spring:  jpa:    properties:      hibernate:        jdbc:          batch_size: 20        order_inserts: true        order_updates: true    show-sql: falselogging:  level:    org.springframework: WARN    org.hibernate: WARN    com.yourapp: INFO



最后一段我想强调的

性能优化的核心不是“技巧”,而是“把默认值当成可质疑对象”。
Spring Boot 帮你把项目跑起来,但“跑得快、跑得稳、跑得省”,得靠你理解它默认做了什么、什么时候不适合你、以及你用数据证明改变是有效的。


如果这篇内容对你有帮助,欢迎点赞 、收藏 ⭐、转发给需要的朋友

我会持续分享:

  • Java 核心与高阶实战
  • AI / Agent / 前沿技术落地
  • 真实项目经验 & 架构思考
  • 企业数字化与产品实践

关注我,一起把“技术”真正用在项目和业务里。

你的每一次支持,都是我持续输出高质量内容的最大动力。

文章版权声明:除非注明,否则均为边学边练网络文章,版权归原作者所有