Backend Development 18 min read

Optimizing SpringBoot Startup Time: Diagnosing Bean Scanning and Initialization Bottlenecks

This article explains how to identify and resolve the severe startup latency of a SpringBoot service by analyzing the SpringApplicationRunListener and BeanPostProcessor stages, narrowing scan packages, manually registering beans with JavaConfig, and handling cache auto‑configuration to reduce launch time from minutes to seconds.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Optimizing SpringBoot Startup Time: Diagnosing Bean Scanning and Initialization Bottlenecks

The author discovered that a SpringBoot microservice took 6‑7 minutes to start, which severely impacted development productivity. By debugging the SpringApplicationRunListener and BeanPostProcessor mechanisms, the main performance bottlenecks were traced to the bean‑scanning and bean‑initialization phases.

1. Investigating the Startup Delay

1.1 Observing the SpringBoot run Method

The application uses an internal component called XxBoot that follows the standard SpringBoot startup flow: constructing an ApplicationContext and invoking its run method. The run method loads all SpringApplicationRunListener implementations via SpringFactoriesLoader and calls their lifecycle callbacks (starting, environmentPrepared, contextPrepared, contextLoaded, started, running, failed).

public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

public static ConfigurableApplicationContext run(String... args) {
    // ...
    SpringApplicationRunListeners listeners = getRunListeners(args);
    listeners.starting();
    try {
        // environment preparation, context creation, refresh, etc.
        listeners.started(context);
        // ...
    } catch (Throwable ex) {
        // failure handling
    }
    listeners.running(context);
    return context;
}

The default implementation EventPublishingRunListener publishes events at each stage, allowing custom listeners to hook into the process.

1.2 Monitoring Bean Initialization Time

To measure the time spent creating each bean, a custom BeanPostProcessor was implemented. The postProcessBeforeInitialization method records the start timestamp, and postProcessAfterInitialization computes the elapsed time.

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

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 unnecessary beans from third‑party dependencies that were being scanned and instantiated.

2. Optimization Strategies

2.1 Reducing Scan Paths

Instead of scanning large package trees, the author switched to explicit JavaConfig bean registration. For example, the UPM client bean was defined manually:

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

This eliminated the injection of unrelated services and controllers, cutting the startup time from ~7 minutes to ~40 seconds.

2.2 Handling Slow Bean Initialization

Beans that performed heavy work during initialization were either refactored to run asynchronously (e.g., via a thread pool) or lazily loaded, depending on the use case.

3. Dealing with Cache Auto‑Configuration

After the scan‑path reduction, the Redis cache component stopped being instantiated, yet the application still obtained a CacheManager bean. The reason is SpringBoot’s auto‑configuration: @EnableAutoConfiguration imports CacheAutoConfiguration , which creates a default RedisCacheManager when no user‑defined bean is present (conditioned on @ConditionalOnMissingBean(CacheManager.class) ).

To keep the custom cache implementation while avoiding unnecessary scanning, the author added a starter entry:

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

This registers the custom configuration via META-INF/spring.factories , allowing SpringBoot to auto‑wire the intended cache manager without scanning the whole package.

Conclusion

By instrumenting the startup lifecycle, narrowing component scans, manually registering required beans, and understanding SpringBoot’s auto‑configuration mechanisms, the service’s launch time was dramatically reduced while preserving functionality.

performanceCacheSpringBootAutoConfigurationBeanPostProcessorbean-scanjava-config
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

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.