SpringBoot 为何启动慢

想获取更多高质量的Java技术文章?欢迎访问 Java技术小馆官网,持续更新优质内容,助力技术成长!

SpringBoot 为何启动慢?

某天你刚写完一个 SpringBoot 接口,点击运行后盯着控制台发呆:"怎么还卡在 Tomcat started on port 8080?这启动速度也太慢了吧!" 此时你脑海中浮现出面试官的灵魂拷问:"SpringBoot 为什么启动慢?"

但真相是:当你在开发环境看到 "Started Application in 5.2 seconds" 时,实际上 SpringBoot 已经处于 "满载(Full Load)" 状态。 这个状态下的启动时间,和你在生产环境中看到的启动时间可能天差地别!

一、SpringBoot 启动慢?先看这三个关键数据

案例:一个普通项目的启动时间线

2023-08-01 10:00:00.000 INFO  [main] o.s.b.StartupInfoLogger : Starting Application
2023-08-01 10:00:00.500 INFO  [main] o.s.c.s.ClassPathXmlApplicationContext : Refreshing...
2023-08-01 10:00:03.200 INFO  [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-08-01 10:00:04.100 INFO  [main] o.s.b.w.e.tomcat.TomcatWebServer : Tomcat started on port 8080
2023-08-01 10:00:05.200 INFO  [main] o.s.s.concurrent.ThreadPoolTaskExecutor : Initializing ExecutorService
2023-08-01 10:00:05.250 INFO  [main] o.s.b.StartupInfoLogger : Started Application in 5.25 seconds 

看起来耗时 5.25 秒?但其中有三个关键阶段:

  1. Classpath 扫描(0-0.5s)
  2. Bean 初始化(0.5-4.1s)
  3. 满载状态(4.1-5.25s)

开发环境 vs 生产环境实测对比

开发环境

5.2s

150+

完整初始化

生产环境

2.1s

80

延迟加载

结论:开发环境中的 "慢启动" 其实是满载状态的表现!

二、深挖 "满载" 的本质

1. SpringBoot 启动的三个阶段

public class SpringApplication {
    // 核心启动流程
    public ConfigurableApplicationContext run(String... args) {
        // 阶段1:环境准备(约20%时间)
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();
        
        // 阶段2:上下文加载(约50%时间)
        ConfigurableApplicationContext context = createApplicationContext();
        refreshContext(context);
        
        // 阶段3:满载阶段(约30%时间)
        afterRefresh(context, applicationArguments);
        stopWatch.stop();
        // 输出 Started Application in xxx seconds
    }
}

2. 开发环境为何更慢?

开发环境的特殊配置:

# application-dev.properties
spring.devtools.restart.enabled=true # 热部署
spring.jpa.show-sql=true            # 显示SQL
management.endpoints.web.exposure.include=* # Actuator全开

这些配置会导致:

  • 多加载 20% 的监控 Bean
  • 增加 15% 的类路径扫描
  • 初始化调试用线程池

3. 满载的三大特征

// 1. 所有@Bean方法已执行
@Bean 
public DataSource dataSource() { // 此时已初始化完成
    return new HikariDataSource();
}

// 2. 所有CommandLineRunner已运行
@Component
public class InitRunner implements CommandLineRunner {
    @Override
    public void run(String... args) { // 该方法已执行
        // 初始化业务数据
    }
}

// 3. Tomcat线程池就绪
tomcat.getConnector().getExecutor() // 返回非空线程池

三、你的项目真的慢吗?

方法1:使用 Actuator 的启动时间端点

# application.yml
management:
  endpoints:
    web:
      exposure:
        include: startup

请求 /actuator/startup 返回:

{
  "springBootVersion": "3.1.2",
  "timelines": {
    "spring.beans.instantiate": {
      "startTime": "2023-08-01T10:00:00.500Z",
      "endTime": "2023-08-01T10:00:03.200Z",
      "duration": "PT2.7S"
    },
    "tomcat.start": {
      "startTime": "2023-08-01T10:00:03.200Z",
      "endTime": "2023-08-01T10:00:04.100Z",
      "duration": "PT0.9S" 
    }
  }
}

方法2:Bean 加载时间排序

@Autowired
private ApplicationContext context;

public void printBeanInitTimes() {
    ((AbstractApplicationContext) context)
        .getBeanFactory()
        .getBeanDefinitionNames()
        .stream()
        .map(name -> new AbstractMap.SimpleEntry<>(
            name, 
            ((RootBeanDefinition) context.getBeanDefinition(name))
                .getResourceDescription()))
        .sorted((e1, e2) -> Long.compare(
            getInitTime(e1.getKey()), 
            getInitTime(e2.getKey())))
        .forEach(e -> System.out.println(e.getKey() + " : " + getInitTime(e.getKey())));
}

输出示例:

myDataSource : 1200ms
entityManagerFactory : 800ms
transactionManager : 400ms

四、让启动速度提升 300%

方案1:延迟加载(实测减少40%时间)

# application.properties
spring.main.lazy-initialization=true # 全局延迟加载

// 或针对特定Bean
@Lazy
@Bean
public MyHeavyBean heavyBean() { ... }

方案2:砍掉不必要的自动配置

// 手动排除自动配置类
@SpringBootApplication(exclude = {
    DataSourceAutoConfiguration.class,
    HibernateJpaAutoConfiguration.class
})

// 或使用条件注解
@Configuration
@ConditionalOnProperty(name = "app.feature.cache.enabled")
public class CacheAutoConfiguration { ... }

方案3:线程池延迟初始化

@Bean
public ThreadPoolTaskExecutor taskExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(0); // 初始0线程
    executor.setMaxPoolSize(20);
    executor.setWaitForTasksToCompleteOnShutdown(true);
    executor.initialize(); // 首次任务提交时初始化
    return executor;
}

方案4:生产环境预热(Docker 实测效果)

# Dockerfile
FROM openjdk:17-jdk-slim
COPY target/app.jar /app.jar
# 预热命令(不暴露端口)
RUN java -Dserver.port=-1 -jar /app.jar --spring.main.lazy-initialization=true &
  sleep 30 && \
  pkill -f 'java.*app.jar'
# 正式启动
CMD ["java", "-jar", "/app.jar"]

五、三个黄金法则

法则1:区分环境配置

# application-prod.properties
spring.main.lazy-initialization=true
spring.jpa.properties.hibernate.temp.use_jdbc_metadata_defaults=false
spring.devtools.restart.enabled=false

法则2:监控先行,优化在后

推荐工具:

  • SpringBoot Actuator(内置监控)
  • Java Flight Recorder(JVM 级分析)
  • Arthas(动态诊断)

法则3:接受合理的启动时间

不同场景的合理启动时间:

Serverless 函数

<1s

微服务实例

5-10s

传统单体应用

20-30s

数据分析批处理任务

1-5min

六、GraalVM 原生镜像的降维打击

一个简单的对比测试:

# 传统JAR启动
java -jar app.jar → 4.1s

# 原生镜像启动
./app → 0.05s

实现步骤:

  1. 添加 GraalVM 依赖
<dependency>
    <groupId>org.graalvm.nativeimage</groupId>
    <artifactId>native-image-maven-plugin</artifactId>
    <version>22.3.1</version>
</dependency>

  1. 构建原生镜像
mvn -Pnative native:compile

想获取更多高质量的Java技术文章?欢迎访问 Java技术小馆官网,持续更新优质内容,助力技术成长!

#springboot#
全部评论

相关推荐

评论
点赞
1
分享

创作者周榜

更多
牛客网
牛客企业服务