Spring Boot 以其“约定优于配置”的理念广受欢迎,极大地简化了开发者的初始配置工作。这种开箱即用的体验,让我们一行 main() 方法就能启动一个项目,看似一切都被安排得明明白白。
然而,随着项目上线、流量增长,许多未曾留意的默认配置,可能会逐渐演变为性能瓶颈乃至线上事故的隐患。默认值是基于通用场景的权衡,未必适配你的特定业务和流量规模。今天,我们就来盘点那些在生产环境中建议调整的 Spring Boot 默认配置,助你提前避坑。
1. Tomcat 连接池配置
Spring Boot 默认使用 Tomcat 作为内嵌 Web 容器,但其默认的连接和线程池配置,在高并发场景下极易成为瓶颈。
默认情况下,max-connections 和最大工作线程数通常较低。当并发请求超过这个阈值,后续请求就会进入等待队列,若队列也满,则直接拒绝连接,影响用户体验。更值得警惕的是,连接超时时间默认可能是无限长,在网络波动或客户端异常时,连接可能被长期挂起而不释放,最终耗尽服务器资源。
建议根据预估的并发量进行合理调整,并设置明确的超时时间。
server:
tomcat:
max-connections: 10000 # 最大连接数
threads:
max: 800 # 最大工作线程数
min-spare: 100 # 最小空闲线程数
accept-count: 100 # 等待队列长度
connection-timeout: 20000 # 连接超时时间(毫秒)
2. 数据库连接池 (HikariCP) 配置
Spring Boot 默认集成 HikariCP 作为数据源,但默认的最大连接池大小(通常为10)对于生产环境下的应用来说往往捉襟见肘。
另一个容易被忽略的配置是连接泄漏检测 (leak-detection-threshold),默认是关闭的。如果你的代码中存在忘记关闭连接的情况,这个“沉默的杀手”将持续消耗连接资源,直到池子耗尽,而你却难以定位问题。
spring:
datasource:
hikari:
maximum-pool-size: 50 # 根据数据库和服务承载能力调整
minimum-idle: 10
connection-timeout: 30000 # 连接获取超时时间
idle-timeout: 600000 # 连接空闲超时
max-lifetime: 1800000 # 连接最大生命周期
leak-detection-threshold: 60000 # 开启泄漏检测(毫秒),超过此时长未归还连接将记录警告
对于数据库相关的深度调优和更多中间件实践,你可以在 数据库/中间件/技术栈 板块找到丰富的讨论。
3. JPA 懒加载与 N+1 查询
Spring Data JPA 中,@OneToMany、@ManyToMany 等关联默认采用 FetchType.LAZY(懒加载)。这个设计旨在避免不必要的查询,但在实际遍历集合时,却可能引发经典的 N+1 查询问题。
例如,查询一个用户列表,然后循环访问每个用户的订单列表。访问主实体(User)时触发1次查询,访问每个子集合(Orders)时又会触发N次查询,性能急剧下降。
@Entity
public class User {
@Id
private Long id;
@OneToMany(fetch = FetchType.LAZY) // 默认就是LAZY
private List<Order> orders;
}
解决方案包括在 Repository 中使用 JOIN FETCH 进行一次性抓取,或使用 @EntityGraph 注解来声明加载策略。
@Query("SELECT u FROM User u LEFT JOIN FETCH u.orders")
List<User> findAllWithOrders();
4. Jackson 的时区序列化
JSON 序列化是前后端交互的基石,而时间格式是其中容易出错的点。Spring Boot 默认使用的 Jackson 库,在序列化 Date、LocalDateTime 等类型时,默认使用 JVM 的默认时区。
这在分布式部署、跨时区服务调用或前端显示时,极易造成时间错乱。例如,上海服务器上显示的时间,在纽约的用户看来可能是错误的。
spring:
jackson:
time-zone: GMT+8 # 明确指定应用使用的时区
date-format: yyyy-MM-dd HH:mm:ss # 指定统一的日期时间格式
serialization:
write-dates-as-timestamps: false # 禁用时间戳格式,使用可读的字符串
5. 日志滚动与清理策略
Spring Boot 默认使用 Logback 记录日志,但其默认配置可能不会对日志文件进行滚动(Rolling)和清理。这会导致单个日志文件无限增大,最终撑满磁盘,触发严重的线上故障。
生产环境必须配置日志滚动策略,按文件大小或时间进行切割,并保留一定历史。
logging:
file:
name: app.log
logback:
rollingpolicy:
max-file-size: 100MB # 单个日志文件最大大小
max-history: 30 # 保留最近30天的日志文件
total-size-cap: 3GB # 所有日志文件总大小上限
此外,根据环境调整日志级别(如生产环境使用 WARN 或 ERROR)也能有效减少 I/O 开销,提升性能。这类运维层面的最佳实践,运维/DevOps/SRE 专区常有深入探讨。
6. 缓存 (@Cacheable) 的默认实现
使用 @Cacheable 注解可以轻松为方法添加缓存。但请注意,其默认的缓存实现是简单的 ConcurrentHashMap。它没有容量限制,也没有过期策略。
这意味着缓存会随着时间无限增长,永不释放,最终引发 OutOfMemoryError(内存溢出)。对于生产环境,务必替换为功能完善的缓存实现,如 Caffeine。
spring:
cache:
type: caffeine
caffeine:
spec: maximumSize=10000,expireAfterWrite=600s # 最大条目数 & 写入后过期时间
7. Actuator 监控端点暴露
Spring Boot Actuator 提供了强大的应用监控和管理端点,如 /health, /env, /configprops 等。在开发阶段,它们非常有用。
但在生产环境,/env 和 /configprops 等端点可能会暴露数据库密码、API密钥等敏感信息,造成严重的安全漏洞。必须严格限制暴露的端点,并结合 Spring Security 进行访问控制。
management:
endpoints:
web:
exposure:
include: health,info,metrics # 只暴露必要的端点
endpoint:
health:
show-details: when-authorized # 健康检查详情仅对授权用户显示
8. 文件上传大小限制
Spring Boot 对文件上传的默认限制较为严格(如单文件1MB,请求总大小10MB)。这在处理用户上传的图片、文档等场景时远远不够。
更糟糕的是,服务器通常会在接收完整个文件后才进行大小校验。用户上传一个100MB的文件,等待几分钟后却得到一个“文件过大”的错误,体验极差。务必根据业务需求提前调整。
spring:
servlet:
multipart:
max-file-size: 100MB # 单个文件最大大小
max-request-size: 100MB # 单次请求总大小
file-size-threshold: 2KB # 超过此大小的文件将写入磁盘临时文件
9. 异步任务 (@Async) 线程池
使用 @Async 注解实现异步方法时,Spring Boot 默认使用 SimpleAsyncTaskExecutor。这个执行器不会复用线程,而是为每个任务创建新线程。
在高并发异步场景下(如批量发送短信、记录日志),系统会疯狂创建线程,导致内存耗尽、CPU 资源大量消耗在线程切换上。必须配置一个自定义的线程池。
spring:
task:
execution:
pool:
core-size: 8 # 核心线程数
max-size: 16 # 最大线程数
queue-capacity: 100 # 等待队列容量
keep-alive: 60s # 空闲线程存活时间
thread-name-prefix: async-task- # 线程名前缀,便于监控
10. 静态资源缓存策略
Spring Boot 默认提供的静态资源(CSS, JS, 图片)服务,没有设置任何 HTTP 缓存头(如 Cache-Control)。这意味着用户的浏览器每次访问页面都会重新请求这些静态文件,即使它们根本没有变化。
这会增加不必要的网络请求,显著降低页面加载速度,尤其对单页应用(SPA)或资源丰富的站点影响巨大。
spring:
web:
resources:
cache:
cachecontrol:
max-age: 365d # 告诉浏览器缓存一年
cache-public: true
chain:
strategy:
content:
enabled: true # 开启基于内容的版本策略(文件MD5戳)
paths: /**
static-locations: classpath:/static/
启用内容版本化后,文件 URL 会附带哈希值(如 app.abc123.js)。文件内容不变,哈希值不变,浏览器就使用缓存;内容一变,哈希值也变,浏览器自然获取新文件。
11. 数据库事务超时
@Transactional 注解默认不设置超时时间。如果一个事务逻辑复杂或处理数据量巨大,它可能会长时间运行并持有数据库锁,阻塞其他事务,导致系统响应缓慢甚至死锁。
特别是批量操作,务必设置合理的事务超时时间,并考虑将大事务拆分为多个小事务分步提交。
@Transactional(timeout = 30, rollbackFor = Exception.class) // 设置30秒超时
public void batchProcess(List<Data> dataList) {
// 分批处理,避免长事务
int batchSize = 100;
for (int i = 0; i < dataList.size(); i += batchSize) {
List<Data> batch = dataList.subList(i,
Math.min(i + batchSize, dataList.size()));
processBatch(batch); // 假设processBatch方法本身也有事务控制
}
}
总结
Spring Boot 的默认配置为我们搭建了一个快速启动的“样板间”,但要将应用部署到生产环境并稳定运行,我们必须根据实际的“居住需求”(业务场景、流量预估、硬件资源)对这个“样板间”进行个性化装修和加固。
上述配置项仅仅是其中一部分常见陷阱。理解这些默认值背后的考量,并根据自身情况进行调整,是每一位 Java 开发者进阶的必经之路。希望本文能帮你提前排查隐患,让系统运行得更加稳健高效。
你在看吗
