Optimizing SpringBoot Startup Time: Reducing Bean Scanning and Initialization Overhead
This article explains how a SpringBoot service that originally took 6‑7 minutes to start was trimmed to about 40 seconds by analysing the run‑listener phases, pinpointing costly bean‑scanning and post‑processor steps, and applying JavaConfig‑based selective component registration and cache auto‑configuration adjustments.
Background – A SpringBoot project suffered from extremely slow startup (6‑7 minutes) due to heavy bean‑scanning and bean‑injection phases. The author investigated the SpringApplicationRunListener and BeanPostProcessor mechanisms to locate the bottlenecks.
Investigating the run method – By adding a custom MySpringApplicationRunListener that logs timestamps at each lifecycle callback (starting, environmentPrepared, contextPrepared, contextLoaded, started, running, failed), the time spent between contextLoaded and started was identified as the main culprit.
Key findings
The ConfigurationClassPostProcessor (which processes @Configuration , @ComponentScan , @Import , @Bean ) consumes most of the time during the refresh phase.
Scanning large third‑party packages (e.g., com.xxx.ad.upm ) adds linear overhead because many classes are scanned even though they never become beans.
A specific bean performed a 43‑second initialization by loading massive configuration data from the database into Redis.
Optimization 1 – Reduce scan paths – The author removed unnecessary base packages from the main @SpringBootApplication and introduced explicit JavaConfig beans for required third‑party services, e.g.:
@Configuration
public class ThirdPartyBeanConfig {
@Bean
public UpmResourceClient upmResourceClient() {
return new UpmResourceClient();
}
}This eliminated the injection of unrelated services and cut the startup time to ~40 seconds.
Optimization 2 – Monitor bean initialization – A custom TimeCostBeanPostProcessor records the start time in postProcessBeforeInitialization and computes the duration in postProcessAfterInitialization :
@Component
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;
}
}This revealed the slow bean and allowed targeted refactoring (e.g., moving heavy DB‑to‑Redis work to an async task or lazy loading).
Optimization 3 – Cache auto‑configuration – After removing the scan path for the custom Redis cache component, SpringBoot’s own CacheAutoConfiguration created a default RedisCacheManager because of @EnableCaching and the missing‑bean condition. The author solved the conflict by publishing the component as a starter with its own META-INF/spring.factories entry, ensuring the intended cache manager is loaded without relying on package scanning.
Overall, the combined measures reduced the local startup from several minutes to under a minute and clarified how SpringBoot’s automatic configuration can both help and hide issues.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.