Backend Development 20 min read

Optimizing SpringBoot Startup Time: Analyzing and Reducing Bean Scanning and Initialization Overheads

This article investigates why a SpringBoot service takes 6‑7 minutes to start, identifies the costly bean‑scanning and bean‑initialization phases, and demonstrates how to speed up startup to around 40 seconds using custom SpringApplicationRunListener, BeanPostProcessor monitoring, JavaConfig selective bean registration, and cache auto‑configuration adjustments.

Top Architecture Tech Stack
Top Architecture Tech Stack
Top Architecture Tech Stack
Optimizing SpringBoot Startup Time: Analyzing and Reducing Bean Scanning and Initialization Overheads

0 Background

Our company's SpringBoot project suffered from extremely slow service startup, often taking 6‑7 minutes before the port was exposed, which severely impacted development efficiency. By debugging the SpringApplicationRunListener and BeanPostProcessor mechanisms, we discovered major performance bottlenecks in the bean‑scanning and bean‑injection phases.

Using JavaConfig to register beans and reducing SpringBoot's scan paths, while also optimizing third‑party dependencies, reduced the local startup time from about 7 minutes to roughly 40 seconds. The article covers the following points:

Observing the SpringBoot run method via SpringApplicationRunListener ;

Monitoring bean injection time with BeanPostProcessor ;

Understanding SpringBoot cache auto‑configuration;

Applying JavaConfig‑based selective bean registration;

Improving cache component integration using SpringBoot starter mechanisms.

1 Startup Time Investigation

Two main approaches were considered for diagnosing the long startup:

Inspect the SpringBoot service startup process;

Inspect bean initialization time.

1.1 Observing the SpringBoot run Method

The project uses an internal micro‑service component XxBoot that follows the standard SpringBoot startup flow, 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 ApplicationContext construction performs banner setup, environment preparation, and other standard tasks that rarely cause delays. The real bottleneck lies in the run method's internal stages, which are defined by the SpringApplicationRunListener interface:

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);
}

Only one default implementation exists ( EventPublishingRunListener ), which triggers events at each stage. By providing a custom implementation that logs timestamps at the end of each stage, we can pinpoint which phases consume the most time.

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()); }
}

After registering this listener in META-INF/spring.factories , logs showed the longest delays between contextLoaded and started , which correspond to the refreshContext and afterRefresh calls. The underlying method is AbstractApplicationContext#refresh , a core part of Spring context initialization.

1.2 Monitoring Bean Initialization Time

We also used BeanPostProcessor to record the time before and after each bean's initialization:

public interface BeanPostProcessor {
    default Object postProcessBeforeInitialization(Object bean, String beanName) { return bean; }
    default Object postProcessAfterInitialization(Object bean, String beanName) { return bean; }
}

The actual bean creation occurs in AbstractAutowireCapableBeanFactory#doCreateBean , which eventually calls initializeBean . By storing the start timestamp in postProcessBeforeInitialization and computing the elapsed time in postProcessAfterInitialization , we identified beans that took unusually long to initialize.

@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 revealed a bean that took 43 seconds because it queried a massive amount of configuration data from the database and wrote it to Redis. It also exposed many unrelated third‑party beans (e.g., UPM services) that were unnecessary for the current functionality.

2 Optimization Strategies

2.1 Reducing Excessive Scan Paths

Instead of scanning large third‑party packages, we switched to explicit JavaConfig bean registration. For example, the UPM client was previously injected by adding com.xxx.ad.upm to scanBasePackages . We replaced it with a dedicated configuration class:

@Configuration
public class ThirdPartyBeanConfig {
    @Bean
    public UpmResourceClient upmResourceClient() {
        return new UpmResourceClient();
    }
}

This approach eliminates redundant beans, reduces memory consumption, and avoids unnecessary initialization work.

2.2 Addressing Slow Bean Initialization

For beans with heavy initialization (e.g., loading configuration metadata), we can off‑load the work to a thread pool or apply lazy loading to avoid blocking the main startup thread.

2.3 Understanding SpringBoot Cache Auto‑Configuration

After removing the cache component's scan path, the application still obtained a CacheManager bean. This is because SpringBoot's @EnableCaching triggers CacheAutoConfiguration , which, via CacheConfigurationImportSelector , automatically creates a RedisCacheManager when no user‑provided bean is present ( @ConditionalOnMissingBean(CacheManager.class) ).

Thus, the automatically generated manager was used instead of the intended custom implementation.

2.4 Leveraging Starter Mechanism for Cache Component

To make the cache component truly plug‑and‑play, we added an entry to META-INF/spring.factories under EnableAutoConfiguration :

# EnableAutoConfigurations
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.xxx.ad.rediscache.XxxAdCacheConfiguration

Now the component is auto‑configured without needing to scan its entire package path.

3 New Issues After Optimization

After applying the above changes, startup time dropped to ~40 seconds, but a Redis cache component stopped working in pre‑release testing. The root cause was the same auto‑configuration logic: when the original scan path was removed, SpringBoot generated its own RedisCacheManager , which differed from the custom one expected by the application.

Understanding and controlling SpringBoot's auto‑configuration is essential to avoid such hidden conflicts.

Source: https://juejin.cn/post/7181342523728592955

Performance OptimizationCacheBackend DevelopmentSpringBootAutoConfigurationbean scanning
Top Architecture Tech Stack
Written by

Top Architecture Tech Stack

Sharing Java and Python tech insights, with occasional practical development tool tips.

0 followers
Reader feedback

How this landed with the community

login 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.