Optimizing SpringBoot Startup Time: Diagnosing Bean Scanning and Initialization Bottlenecks
This article analyzes why a SpringBoot service takes minutes to start, identifies the bean‑scanning and bean‑initialization phases as the main performance culprits, and presents practical solutions such as narrowing scan packages, using JavaConfig for explicit bean registration, customizing SpringApplicationRunListener and BeanPostProcessor for timing, and leveraging SpringBoot starter mechanisms to streamline cache configuration.
Background
The company's SpringBoot project suffered from extremely slow startup, often taking 6‑7 minutes before exposing a port. Investigation using SpringApplicationRunListener and BeanPostProcessor revealed major bottlenecks during bean scanning and bean injection.
Investigation Steps
Observe SpringApplication.run execution via SpringApplicationRunListener to locate slow phases.
Monitor bean creation time with a custom BeanPostProcessor .
SpringApplicationRunListener Details
The run method creates a SpringApplicationRunListeners instance and invokes lifecycle callbacks (starting, environmentPrepared, contextPrepared, contextLoaded, started, running, failed). By adding a custom implementation that logs timestamps at each stage, the author pinpointed the longest delay between contextLoaded and started , which involves refreshContext and afterRefresh .
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
public MySpringApplicationRunListener(SpringApplication sa, String[] args) {}
@Override public void starting() { log.info("starting {}", LocalDateTime.now()); }
@Override public void environmentPrepared(ConfigurableEnvironment env) { log.info("environmentPrepared {}", LocalDateTime.now()); }
@Override public void contextPrepared(ConfigurableApplicationContext ctx) { log.info("contextPrepared {}", LocalDateTime.now()); }
@Override public void contextLoaded(ConfigurableApplicationContext ctx) { log.info("contextLoaded {}", LocalDateTime.now()); }
@Override public void started(ConfigurableApplicationContext ctx) { log.info("started {}", LocalDateTime.now()); }
@Override public void running(ConfigurableApplicationContext ctx) { log.info("running {}", LocalDateTime.now()); }
@Override public void failed(ConfigurableApplicationContext ctx, Throwable ex) { log.info("failed {}", LocalDateTime.now()); }
}BeanPostProcessor Timing
A custom BeanPostProcessor records the start time in postProcessBeforeInitialization and computes the elapsed time in postProcessAfterInitialization , printing beans that exceed a threshold.
public class TimeCostBeanPostProcessor implements BeanPostProcessor {
private Map
costMap = Maps.newConcurrentMap();
@Override public Object postProcessBeforeInitialization(Object bean, String beanName) {
costMap.put(beanName, System.currentTimeMillis());
return bean;
}
@Override public Object postProcessAfterInitialization(Object bean, String beanName) {
Long start = costMap.get(beanName);
if (start != null) {
long cost = System.currentTimeMillis() - start;
if (cost > 0) {
costMap.put(beanName, cost);
System.out.println("bean: " + beanName + "\ttime: " + cost);
}
}
return bean;
}
}Results showed a bean taking 43 seconds due to heavy database queries and many third‑party beans being scanned unnecessarily.
Optimization Solutions
1. Reduce Scan Paths
Instead of scanning large third‑party packages (e.g., com.xxx.ad.upm ), declare needed beans explicitly via JavaConfig:
@Configuration
public class ThirdPartyBeanConfig {
@Bean
public UpmResourceClient upmResourceClient() {
return new UpmResourceClient();
}
}This eliminates unrelated beans, cuts memory usage, and shortens startup to ~40 seconds.
2. Address Slow Bean Initialization
For beans that perform heavy work (e.g., loading configuration metadata into Redis), consider asynchronous execution or lazy loading.
3. Resolve Cache Auto‑Configuration Conflict
Removing the scan path for a custom Redis cache component caused SpringBoot’s own CacheAutoConfiguration to create a default RedisCacheManager , which conflicted with the intended implementation. The fix is to expose the component as a starter:
# EnableAutoConfigurations
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.ad.rediscache.XxxAdCacheConfigurationAdding this entry to META-INF/spring.factories lets SpringBoot auto‑configure the custom cache without scanning the whole package.
Conclusion
By narrowing component scan ranges, explicitly registering required beans, measuring bean creation times, and using the starter mechanism for cache configuration, the startup time dropped from ~7 minutes to ~40 seconds, while also uncovering hidden issues such as unintended auto‑configured cache managers.
Architect
Professional architect sharing high‑quality architecture insights. Topics include high‑availability, high‑performance, high‑stability architectures, big data, machine learning, Java, system and distributed architecture, AI, and practical large‑scale architecture case studies. Open to ideas‑driven architects who enjoy sharing and learning.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.