SpringBoot 为何启动慢

06-01 1288阅读

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

Java技术小馆官网https://www.yuque.com/jtostring

SpringBoot 为何启动慢?

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

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 生产环境实测对比

环境

启动时间

已加载 Bean 数量

线程池状态

开发环境

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 函数

免责声明:我们致力于保护作者版权,注重分享,被刊用文章因无法核实真实出处,未能及时与作者取得联系,或有版权异议的,请联系管理员,我们会立即处理! 部分文章是来自自研大数据AI进行生成,内容摘自(百度百科,百度知道,头条百科,中国民法典,刑法,牛津词典,新华词典,汉语词典,国家院校,科普平台)等数据,内容仅供学习参考,不准确地方联系删除处理! 图片声明:本站部分配图来自人工智能系统AI生成,觅知网授权图片,PxHere摄影无版权图库和百度,360,搜狗等多加搜索引擎自动关键词搜索配图,如有侵权的图片,请第一时间联系我们。

目录[+]

取消
微信二维码
微信二维码
支付宝二维码