19-[生产实践] 那些年,我们踩过的 Spring Boot 默认配置“大坑”

19-[生产实践] 那些年,我们踩过的 Spring Boot 默认配置“大坑”
Prorise7. [生产实践] 那些年,我们踩过的 Spring Boot 默认配置“大坑”
摘要: Spring Boot 以“开箱即用”和“约定优于配置”闻名,极大地简化了开发。然而,这种便捷性背后隐藏着诸多为生产环境埋下的“地雷”。本章将逐一揭露那些在生产环境中可能引发性能瓶颈、内存溢出甚至安全漏洞的默认配置,并提供经过实战检验的最佳实践,帮助您构建一个真正健壮、稳定的线上应用。
7.1. Tomcat Web 容器:脆弱的默认并发能力
陷阱: Spring Boot 默认的内嵌 Tomcat 服务器,其最大连接数(max-connections
)和最大工作线程数(max-threads
)都默认为 200
。
后果:
在高并发场景下,这意味着服务器最多只能同时处理 200 个请求。一旦并发数超过 200,后续的请求就会进入等待队列,导致响应时间急剧增加,甚至出现请求超时。对于任何有一定流量的线上应用,这个默认值都远远不够,是首要的性能瓶颈。
解决方案:
根据服务器规格和预估流量,合理调高这些参数。
文件路径: demo-admin/src/main/resources/application.yml
1 | server: |
7.2. HikariCP 数据库连接池:吝啬的默认连接数
陷阱: Spring Boot 默认的 HikariCP 数据库连接池,其最大连接数(maximum-pool-size
)仅为 10
。
后果:
对于一个典型的 Web 应用,10 个数据库连接很快就会被耗尽,尤其是在有慢查询或长事务的情况下。当连接池被占满,新的数据库请求只能排队等待,导致整个应用响应缓慢,甚至完全卡死。
解决方案:
根据业务的数据库访问频次和并发量,调整连接池大小。同时,强烈建议开启连接泄漏检测。
文件路径: demo-admin/src/main/resources/application.yml
1 | spring: |
7.3. Jackson JSON 序列化:危险的时区处理
陷阱: Spring Boot 默认的 Jackson 序列化 LocalDateTime
等时间对象时,会使用服务器的系统默认时区,并且默认可能将日期序列化为时间戳 (Timestamps)。
后果:
在分布式、跨国部署的系统中,服务器可能位于不同时区。同一个存储在数据库中的 UTC 时间,在东京的服务器上序列化返回,和在伦敦的服务器上序列化返回,可能会是两个不同的字符串值,引发客户端的逻辑混乱。
解决方案:
强制指定一个统一的时区(如 GMT+8
),并统一日期格式化字符串。
文件路径: demo-admin/src/main/resources/application.yml
1 | spring: |
7.4. 日志配置:被“撑爆”的磁盘
陷阱: Spring Boot 默认的 Logback 日志配置,只会将日志输出到控制台,如果配置了 logging.file.name
,日志会写入文件,但没有滚动和归档策略。
后果:
一个长时间运行的线上应用,其日志文件会无限增长,直到有一天它会默默地占满服务器的整个磁盘空间,导致应用崩溃甚至服务器宕机。
解决方案:
通过 logback-spring.xml
配置文件,定义精细的日志滚动和归档策略。
文件路径: demo-admin/src/main/resources/logback-spring.xml
(新增文件)
1 |
|
7.5. Spring Cache:会无限增长的内存缓存
陷阱: 如果不引入任何第三方缓存依赖,Spring Cache 默认使用 ConcurrentHashMap
作为缓存实现。这是一个没有大小限制、没有过期策略的纯内存 Map。
后果:
在一个多实例部署的生产环境中,这种默认缓存是完全不可用的,因为它无法在多个实例间共享。即便是在单体应用中,如果被缓存的数据不断增多,最终也会因为缓存无限增长而导致内存溢出(OOM)。
解决方案:
明确指定一个专业的缓存提供商。在我们的项目中,我们已经配置了 Redis 作为分布式缓存,这已经是一个最佳实践。这里再次强调其重要性。
文件回顾: demo-framework/src/main/java/com/example/demoframework/config/CacheConfig.java
1 | // 我们已经通过这个配置类,将 Spring Cache 的后端实现指向了 Redis |
7.6. Spring Actuator:默认的“信息裸奔”
陷阱: Spring Boot Actuator 为了方便,默认可能会通过 JMX 或 Web 暴露大量监控端点,其中部分端点(如 /env
, /configprops
)可能包含数据库密码、API密钥等敏感信息。
后果:
如果 Actuator 的端口被意外暴露到公网上,将造成严重的信息泄漏事故。
解决方案:
遵循“最小权限”原则,只暴露绝对必要的、且不含敏感信息的端点。
文件路径: demo-admin/src/main/resources/application.yml
1 | management: |
7.7. 文件上传:过小的默认尺寸限制
陷阱: Spring Boot 默认的文件上传大小限制非常小:单个文件 1MB
,单次请求总大小 10MB
。
后果:
这个配置在开发环境用小文件测试时通常不会暴露问题。一旦上线,用户上传一份稍大(例如 2MB)的合同 PDF 或高清图片时,就会在等待了漫长的上传过程后,收到一个 MaxUploadSizeExceededException
异常,用户体验极差。
解决方案:
根据业务需求,合理配置上传文件的大小限制。
文件路径: demo-admin/src/main/resources/application.yml
1 | spring: |
7.8. @Async 异步任务:失控的默认线程池
陷阱: 使用 @Async
注解时,若不自定义线程池,Spring Boot 默认使用 SimpleAsyncTaskExecutor
。这个执行器不会复用线程,而是为每一个异步任务都创建一个全新的线程。
后果:
在有大量异步任务(如发送邮件、记录日志)的生产环境中,系统会不断创建新线程,每个线程都会消耗内存(默认1MB栈空间)。最终将耗尽系统线程资源,导致应用性能急剧下降甚至崩溃。
解决方案:
必须自定义一个生产级的、基于线程池的 TaskExecutor
Bean。我们在第 5.1.3 节已经这样做了,这同样是必须遵守的最佳实践。
文件回顾: demo-framework/src/main/java/com/example/demoframework/config/AsyncConfig.java
1 | // 我们已经通过这个配置,提供了一个核心线程数与CPU核心数挂钩的、 |
7.9. 静态资源:被遗忘的浏览器缓存
陷阱: Spring Boot 默认不为 /static/
目录下的静态资源(CSS, JS, 图片等)设置任何 HTTP 缓存头(Cache-Control
)。
后果:
浏览器每次访问页面,都会向服务器重新请求所有静态资源,即使这些资源从未改变。这极大地增加了网络流量,并严重拖慢了页面加载速度,尤其是在网络不佳的环境下。
解决方案:
为静态资源配置一个长周期的缓存策略,并启用内容版本化(Content Versioning)。
文件路径: demo-admin/src/main/resources/application.yml
1 | spring: |
开启内容版本化后,Spring Boot 会根据文件内容生成一个哈希值,并将其嵌入到 URL 中(例如 style-a1b2c3d4.css
)。只有当文件内容改变时,URL 才会变化,浏览器才会下载新文件,否则将一直使用本地缓存。
7.10. @Transactional
事务:无尽等待的默认超时
陷阱: @Transactional
注解默认的事务超时时间(timeout
)依赖于底层事务系统的默认值,通常是没有超时限制。
后果:
一个长时间运行的事务(例如,一个需要处理几万条数据的大批量导入任务)会一直持有数据库连接和相关表的锁,这会阻塞其他所有需要访问这些表的操作,导致整个系统响应缓慢甚至假死。
解决方案:
- 为所有可能耗时较长的事务方法设置一个合理的超时时间。
- 对于大批量数据处理,应在代码层面进行分批处理,将一个大事务拆分为多个小事务。
- 总是明确指定
rollbackFor = Exception.class
,确保任何异常都能触发回滚。
代码示例:
1 | // 设置事务超时时间为 30 秒,并对所有异常回滚 |
写在最后:
Spring Boot 的“约定优于配置”确实为开发省去了很多麻烦,但这份“省心”绝不应该延伸到生产环境。每一项默认配置背后,都藏着框架设计者的权衡与假设,而这些假设,在我们的真实业务场景中未必成立。作为专业的开发者,我们的职责就是洞悉这些默认值,并根据我们的业务需求和性能目标,做出明确的、显式的配置。提前优化配置,是对系统负责,也是对自己负责。