Cutting Spring Boot Startup Time by 50%: From 280 s to 159 s

This article systematically analyzes a Spring Boot project's 280‑second startup, identifies bottlenecks such as bean initialization and sharding data source loading, and applies listener‑based timing, bean‑post‑processor profiling, and asynchronous initialization to reduce launch time to 159 seconds, improving developer efficiency.

vivo Internet Technology
vivo Internet Technology
vivo Internet Technology
Cutting Spring Boot Startup Time by 50%: From 280 s to 159 s

Introduction

The project’s Spring Boot application suffered from a 280‑second startup due to an ever‑growing number of dependencies, which forced the Spring container to load and autowire many components. Long restart times hindered development and testing cycles.

Environment

Spring version: 4.3.22

Spring Boot version: 1.5.19

CPU: i5‑9500

Memory: 24 GB

Startup time before optimization: 280 seconds

Spring Boot Startup Process

The main startup logic resides in

org.springframework.boot.SpringApplication#run(String... args)

. The method creates a StopWatch, prepares the environment, prints the banner, creates the application context, refreshes it, and finally stops the watch.

public ConfigurableApplicationContext run(String... args) {<br/>    StopWatch stopWatch = new StopWatch();<br/>    stopWatch.start();<br/>    // ... initialization steps ...<br/>    refreshContext(context);<br/>    afterRefresh(context, applicationArguments);<br/>    stopWatch.stop();<br/>    return context;<br/>}

Listeners are invoked at multiple lifecycle stages, providing an extension point for custom timing.

Custom SpringApplicationRunListener

By implementing SpringApplicationRunListener and registering it in META-INF/spring.factories, we can log the duration of each startup phase.

@Slf4j<br/>public class MySpringApplicationRunListener implements SpringApplicationRunListener {<br/>    private Long startTime;<br/>    @Override public void starting() { startTime = System.currentTimeMillis(); log.info("Start {}", LocalTime.now()); }<br/>    @Override public void environmentPrepared(ConfigurableEnvironment env) { log.info("Env prepared {} ms", System.currentTimeMillis() - startTime); startTime = System.currentTimeMillis(); }<br/>    // other callbacks omitted for brevity<br/>}

Identifying Slow Beans

Implementing InstantiationAwareBeanPostProcessor allows us to record the time before bean instantiation and after initialization, printing beans whose creation exceeds a threshold.

@Service<br/>public class TimeCostCalBeanPostProcessor implements InstantiationAwareBeanPostProcessor {<br/>    private Map<String, Long> costMap = Maps.newConcurrentMap();<br/>    @Override public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) {<br/>        costMap.putIfAbsent(beanName, System.currentTimeMillis());<br/>        return null;<br/>    }<br/>    @Override public Object postProcessAfterInitialization(Object bean, String beanName) {<br/>        Long start = costMap.get(beanName);<br/>        long cost = System.currentTimeMillis() - start;<br/>        if (cost > 5000) { System.out.println("bean: " + beanName + "\t time: " + cost + "ms"); }<br/>        return bean;<br/>    }<br/>}

Profiling revealed that refreshContext() and, specifically, finishBeanFactoryInitialization() were the main culprits, taking up to 285 seconds.

Optimizing Sharding DataSource Loading

The singletonDataSource bean creates a sharding data source. Its initialization traverses all database tables to load metadata, causing a linear increase in startup time with the number of tables.

@Bean(name = "singletonDataSource")<br/>public DataSource singletonDataSource(DefaultDataSourceWrapper wrapper) throws SQLException {<br/>    wrapper.getMaster().init();<br/>    Map<String, DataSource> map = new HashMap<>();<br/>    map.put("ds0", wrapper.getMaster());<br/>    DataSource shardingDataSource = ShardingDataSourceFactory.createDataSource(map, shardingRuleConfiguration, prop);<br/>    return shardingDataSource;<br/>}

Reducing the number of sharding tables in the test environment or loading metadata lazily cuts a large portion of the delay.

Asynchronous Initialization

Beans such as ActivityServiceImpl perform heavy data queries in afterPropertiesSet(). By moving these calls to a custom thread pool, the overall startup time drops significantly.

public class AsyncInitListableBeanFactory extends DefaultListableBeanFactory {<br/>    @Override protected void invokeInitMethods(String beanName, Object bean, RootBeanDefinition mbd) throws Throwable {<br/>        if (beanName.equals("activityServiceImpl")) {<br/>            AsyncTaskExecutor.submitTask(() -> super.invokeInitMethods(beanName, bean, mbd));<br/>        } else {<br/>            super.invokeInitMethods(beanName, bean, mbd);<br/>        }<br/>    }<br/>}

An ApplicationContextInitializer swaps the default bean factory with the asynchronous version before the context refresh.

public class AsyncBeanFactoryInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {<br/>    @Override public void initialize(ConfigurableApplicationContext ctx) {<br/>        if (ctx instanceof GenericApplicationContext) {<br/>            AsyncInitListableBeanFactory asyncFactory = new AsyncInitListableBeanFactory(ctx.getBeanFactory());<br/>            Field f = GenericApplicationContext.class.getDeclaredField("beanFactory");<br/>            f.setAccessible(true);<br/>            f.set(ctx, asyncFactory);<br/>        }<br/>    }<br/>}

A listener waits for all async tasks to finish before the application is considered fully started, preventing potential null‑pointer issues.

public class AsyncInitCompletionListener implements ApplicationListener<ContextRefreshedEvent>, ApplicationContextAware, PriorityOrdered {<br/>    private ApplicationContext current;<br/>    @Override public void setApplicationContext(ApplicationContext ctx) { this.current = ctx; }<br/>    @Override public void onApplicationEvent(ContextRefreshedEvent e) {<br/>        if (e.getApplicationContext() == current) { AsyncBeanInitExecutor.waitForInitTasks(); }<br/>    }<br/>    @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; }<br/>}

Results

After applying bean‑level profiling, sharding data source tuning, and asynchronous initialization, the startup time dropped from 280 seconds to 159 seconds, a roughly 50 % improvement, greatly enhancing daily development and testing efficiency.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendJavaperformanceshardingstartup-optimizationSpring Bootasynchronous-initialization
vivo Internet Technology
Written by

vivo Internet Technology

Sharing practical vivo Internet technology insights and salon events, plus the latest industry news and hot conferences.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.