Optimizing SpringBoot Startup Time: Analyzing Bean Scanning and Initialization Bottlenecks
This article investigates why a SpringBoot service takes 6‑7 minutes to start, identifies the bean‑scanning and bean‑initialization phases as the main performance culprits, and presents concrete JavaConfig and starter‑based solutions to reduce startup time to around 40 seconds while handling cache auto‑configuration pitfalls.
0 Background
The company's SpringBoot project suffered from extremely slow startup, often taking 6‑7 minutes to expose the port, which severely impacted development efficiency. By debugging SpringApplicationRunListener and BeanPostProcessor mechanisms, the author discovered major performance bottlenecks during the bean‑scanning and bean‑injection phases.
Using JavaConfig to register beans and optimizing third‑party dependencies reduced the local startup time from 7 minutes to roughly 40 seconds.
1 Time‑Consuming Issue Investigation
The investigation follows two main ideas: (1) examine the SpringBoot startup run method, and (2) monitor bean‑initialization time.
1.1 Observing SpringBoot run method
The project uses an internal micro‑service component XxBoot whose startup flow mirrors SpringBoot: constructing an ApplicationContext and then invoking its run method.
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
public static ConfigurableApplicationContext run(Class
[] primarySources, String[] args) {
return new SpringApplication(primarySources).run(args);
}The run method creates listeners, prepares the environment, creates the context, refreshes it, and finally calls the runners. The SpringApplicationRunListener interface defines callbacks for each stage (starting, environmentPrepared, contextPrepared, contextLoaded, started, running, failed).
public interface SpringApplicationRunListener {
void starting();
void environmentPrepared(ConfigurableEnvironment environment);
void contextPrepared(ConfigurableApplicationContext context);
void contextLoaded(ConfigurableApplicationContext context);
void started(ConfigurableApplicationContext context);
void running(ConfigurableApplicationContext context);
void failed(ConfigurableApplicationContext context, Throwable exception);
}By implementing a custom listener (e.g., MySpringApplicationRunListener ) and logging timestamps at each callback, the author measured the duration of each phase and identified that the longest delay occurs between contextLoaded and started , which corresponds to the refreshContext and afterRefresh calls.
@Slf4j
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()); }
}1.2 Monitoring Bean initialization time
To pinpoint slow beans, the author leveraged BeanPostProcessor , which provides hooks before and after bean initialization. By recording the start time in postProcessBeforeInitialization and calculating the elapsed time in postProcessAfterInitialization , the initialization cost of each bean can be printed.
public interface BeanPostProcessor {
default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; }
default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; }
} @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) {
if (costMap.containsKey(beanName)) {
long cost = System.currentTimeMillis() - costMap.get(beanName);
if (cost > 0) {
costMap.put(beanName, cost);
System.out.println("bean: " + beanName + "\ttime: " + cost);
}
}
return bean;
}
}Running the service with this processor revealed a bean that took 43 seconds to initialize because it queried a massive amount of configuration data from the database and wrote it to Redis. It also uncovered many third‑party service beans (e.g., UPM) that were unnecessary for the current functionality.
2 Optimization方案
2.1 Reducing excessive scan paths
The straightforward solution is to stop scanning large base packages and explicitly declare required beans via JavaConfig. For example, instead of scanning com.xxx.ad.upm , a configuration class manually creates the UpmResourceClient bean:
@Configuration
public class ThirdPartyBeanConfig {
@Bean
public UpmResourceClient upmResourceClient() {
return new UpmResourceClient();
}
}This eliminates redundant beans, lowers memory consumption, and prevents unrelated services from being injected.
2.2 Solving high bean‑initialization cost
For beans that are inherently slow (e.g., loading massive metadata), the author suggests asynchronous initialization or lazy loading, such as submitting the heavy task to a thread pool.
3 New Issue – Cache Component Failure
After the optimizations, the service started quickly, but during pre‑release testing the Redis cache component stopped working. The component was originally introduced by scanning com.xxx.ad.rediscache , which registers a custom CacheManager . When the scan path was removed, SpringBoot’s own auto‑configuration created a default RedisCacheManager , causing the application to use an unexpected cache manager.
3.1 SpringBoot auto‑configuration pitfalls
SpringBoot’s @EnableAutoConfiguration (included in @SpringBootApplication ) imports a series of auto‑configuration classes via spring.factories . The CacheAutoConfiguration eventually imports RedisCacheConfiguration , which creates a RedisCacheManager when the conditions are met. Because the project also enables caching with @EnableCaching , SpringBoot supplies a default manager when the custom component is not scanned.
3.2 Using starter mechanism for the cache component
Instead of manually scanning the component’s package, the author recommends turning the cache library into a SpringBoot starter. By adding an entry to META-INF/spring.factories : # EnableAutoConfigurations org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.xxx.ad.rediscache.XxxAdCacheConfiguration the custom configuration is automatically imported, preserving the intended CacheManager without requiring a full package scan.
Overall, the article demonstrates a systematic approach to diagnosing SpringBoot startup slowness, applying targeted JavaConfig and starter techniques, and understanding the interplay between manual bean registration and SpringBoot’s auto‑configuration.
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.