本文总结 Spring Boot 启动耗时过长的常见原因,并给出一整套可落地的排查方案 ,包括启用
startup端点分析、编写 Bean 初始化耗时记录器、输出 Top10 耗时 Bean 等,帮助快速定位并解决启动瓶颈问题。
📌 1. 启用 Actuator startup 端点
Spring Boot 2.4+ 引入了 ApplicationStartup 机制,可以记录启动各阶段耗时。
我们可以借助 spring-boot-starter-actuator 和 /startup 端点来分析:
添加依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
配置 application.yml
这里我们将 Actuator 单独跑在 12306 端口上,路径为 /java:
yaml
management:
server:
port: 12306
endpoints:
web:
base-path: /java
exposure:
include: '*'
endpoint:
startup:
enabled: true
在启动类中设置 BufferingApplicationStartup
必须在 run() 之前设置:
java
@SpringBootApplication
public class ServiceApplication {
public static void main(String[] args) {
SpringApplication app = new SpringApplication(ServiceApplication.class);
app.setApplicationStartup(new BufferingApplicationStartup(2048));
app.run(args);
}
}
访问启动耗时信息
应用启动后,访问:
http://localhost:12306/java/startup
即可查看所有 Bean 初始化和自动配置加载的耗时信息。
📌 2. 打印 Bean 初始化耗时日志(实时查看)
访问 /startup 虽然可以分析启动,但需要打开浏览器。
我们可以编写一个 BeanPostProcessor,在启动时实时输出每个 Bean 的初始化耗时:
java
@Component
public class BeanStartupTimeLogger implements SmartInstantiationAwareBeanPostProcessor {
private final ConcurrentHashMap<String, Long> startTimes = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, Long> durations = new ConcurrentHashMap<>();
@Override
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
startTimes.put(beanName, System.nanoTime());
return bean;
}
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
Long start = startTimes.remove(beanName);
if (start != null) {
long durationMs = (System.nanoTime() - start) / 1_000_000;
durations.put(beanName, durationMs);
System.out.printf("[Bean Init] %-60s %d ms%n", beanName, durationMs);
}
return bean;
}
/** 启动完成后打印 Top 10 */
@EventListener(ContextRefreshedEvent.class)
public void onContextRefreshed() {
System.out.println("\n================= Bean 初始化耗时 Top 10 =================");
durations.entrySet().stream()
.sorted(Map.Entry.comparingByValue(Comparator.reverseOrder()))
.limit(10)
.forEach(e -> System.out.printf("%-60s %d ms%n", e.getKey(), e.getValue()));
System.out.println("==========================================================\n");
}
}
效果示例:
[Bean Init] requestMappingHandlerMapping 956 ms
[Bean Init] jacksonObjectMapper 758 ms
[Bean Init] jdbcTemplate 586 ms
...
================= Bean 初始化耗时 Top 10 =================
requestMappingHandlerMapping 956 ms
jacksonObjectMapper 758 ms
jdbcTemplate 586 ms
...
==========================================================
📌 3. 常见启动慢的根因定位
根据统计出的 Top 耗时 Bean,通常可以快速推断原因:
| 耗时 Bean | 典型原因 | 优化建议 |
|---|---|---|
requestMappingHandlerMapping |
Controller 数量多,包扫描范围大 | 限定 @ComponentScan 范围 |
jacksonObjectMapper |
类/模块过多,加载慢 | 减少 @JsonComponent/@JsonMixin 模块 |
jdbcTemplate / transactionManager |
初始化访问数据库,连接慢 | 配置连接池超时,延迟初始化 |
redisKeyValueTemplate / reactiveRedisTemplate |
引入 Redis Repository 自动配置 | 如果未使用 Repository,可 exclude 掉 |
openApiResource |
Springdoc-OpenAPI 初始化大量类 | 只在开发环境启用 swagger |
📌 4. 通用优化策略
- 使用
spring.main.lazy-initialization=true推迟非必要 Bean 初始化 - 精简
@ComponentScan扫描包范围 - 排除不必要的 Starter / AutoConfiguration
- 检查数据库、Redis 等外部依赖的连接耗时(DNS/网络/超时)
- 使用 JFR / VisualVM 等分析启动 CPU 时间
📌 5. 总结
通过 /startup + BeanStartupTimeLogger,可以精准捕捉每个 Bean 初始化耗时,定位 Spring Boot 启动慢的根因。
相比盲目猜测,这种方式:
- 无侵入,随时打开关闭
- 统计结果可排序分析
- 适合持续优化启动性能