How to Slash SpringBoot Startup Time by 60%: Proven Optimization Techniques

This article details a comprehensive set of optimizations that reduced a SpringBoot application's startup time from 400‑500 seconds to about 130‑150 seconds, covering Actuator monitoring, Tomcat TLD scan disabling, HBase asynchronous warm‑up, custom BeanPostProcessor timing, JSF consumer proxy refactoring, Tomcat version impacts, and hardware upgrades.

JD Cloud Developers
JD Cloud Developers
JD Cloud Developers
How to Slash SpringBoot Startup Time by 60%: Proven Optimization Techniques

1. Introduction

This article records the optimization measures taken before a major promotion to address the slow startup speed of a SpringBoot application. It explains how to locate blocking points and solve them, using JD's internal RPC framework JSF (similar to Dubbo) on SpringBoot 2.6.2.

2. Problem Background

In production, a single machine deployment of the advertising platform core application takes 400‑500 s, plus about one minute for image download, resulting in a total deployment time close to ten minutes. Long startup times hinder rapid rollback and extend recovery time, affecting business continuity.

The deployment orchestration for hundreds of containers exceeds half an hour, and a single compile‑plus‑deploy cycle in the pre‑release environment takes over ten minutes, severely slowing integration progress.

Therefore, accelerating application startup is urgent; wasted time equals wasted life.

3. Solutions

3.1 Use SpringBoot Actuator

The Actuator provides built‑in endpoints that expose runtime information such as health, environment, logs, metrics, and thread dumps. The startup endpoint can be used to view the startup time of each bean, although the timing may be inaccurate due to cumulative loading of nested beans.

3.2 Analyze Startup Logs

Enabling debug logs allows second‑by‑second analysis of blank periods in the startup log, helping to locate blocking points. This method is less practical for large projects with tens of thousands of log lines.

3.2.1 Tomcat TLD Scan Optimization

Tomcat scans JAR files for TLD files during startup, which is unnecessary for projects that do not use JSP. Disabling this scan by setting tomcat.util.scan.StandardJarScanFilter.jarsToSkip=*.jar in catalina.properties removes the delay.

3.2.2 Asynchronous HBase Warm‑up

Initialization of HBase configuration caused a ~6 s pause. By moving the warm‑up logic to an asynchronous thread (instead of the localhost thread), the delay is eliminated.

3.3 Custom BeanPostProcessor Timing

Most startup time is spent in bean post‑processing. Two custom BeanPostProcessor implementations are introduced to measure the time before and after bean initialization.

@Component
@Slf4j
public class TimeCostPriorityOrderedBeanPostProcessor implements BeanPostProcessor, PriorityOrdered {
    private Map<String, Long> costMap = Maps.newConcurrentMap();
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        costMap.put(beanName, System.currentTimeMillis());
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (costMap.containsKey(beanName)) {
            Long start = costMap.get(beanName);
            long cost = System.currentTimeMillis() - start;
            if (cost > 50) {
                log.info("==>Before method time beanName:{}, cost:{}", beanName, cost);
            }
        }
        return bean;
    }
    @Override
    public int getOrder() { return Integer.MIN_VALUE; }
}

The second processor, ZLowOrderTimeCostBeanPostProcessor, runs last and records the time of the after method.

@Component
@Slf4j
public class ZBeanPostProcessor implements BeanPostProcessor {
    private Map<String, Long> costMap = Maps.newConcurrentMap();
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        costMap.put(beanName, System.currentTimeMillis());
        return bean;
    }
    @Override
    public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
        if (costMap.containsKey(beanName)) {
            Long start = costMap.get(beanName);
            long cost = System.currentTimeMillis() - start;
            if (cost > 50) {
                log.info("==>After method time beanName:{}, cost:{}", beanName, cost);
            }
        }
        return bean;
    }
}

Analysis showed beans annotated with @JsfConsumer were the slowest, because the consumer proxy generation performed a costly refer() call during bean initialization.

3.4 Asynchronous JSF Consumer Export

A dedicated thread pool ( CONSUMER_REFER) is used to execute refer() asynchronously, preventing the main thread from blocking.

public static final ThreadPoolExecutor CONSUMER_REFER = new ThreadPoolExecutor(
        32, 32,
        60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(102400),
        ThreadFactories.create("JSF-REFER"),
        new ThreadPoolExecutor.CallerRunsPolicy());

private void scanConsumer(Object bean, String beanName) {
    Class<?> beanClass = bean.getClass();
    Set<Field> fields = getFieldSetWithSuperClassFields(beanClass);
    for (Field field : fields) {
        boolean accessible = field.isAccessible();
        REFER_COUNT.increment();
        Future<?> referFuture = CONSUMER_REFER.submit(() -> {
            try {
                if (!accessible) field.setAccessible(true);
                JsfConsumer jsfConsumer = field.getAnnotation(JsfConsumer.class);
                if (jsfConsumer != null) {
                    JsfConsumerTemplate tmpl = convert(jsfConsumer, JsfConsumerTemplate.class);
                    if (tmpl != null) {
                        ConsumerConfig consumerConfig = parseAnnotationConsumer(tmpl, field.getType());
                        addFilters(beanName, consumerConfig);
                        Object ref = consumerConfig.refer();
                        if (ref != null) {
                            if (!beanFactory.containsSingleton(field.getName())) {
                                beanFactory.registerSingleton(field.getName(), consumerConfig);
                            }
                            Object fieldBean = beanFactory.getBean(field.getName());
                            field.set(bean, fieldBean);
                        }
                    }
                }
            } finally {
                if (!accessible) field.setAccessible(false);
                REFER_COUNT.decrement();
            }
        });
        REFER_FUTURE.add(referFuture);
    }
}

3.5 Modified JSF ConsumerBean

A custom DelayConsumerBean returns a proxy object immediately and performs the real refer() asynchronously, ensuring the container receives a usable reference without blocking.

public class DelayConsumerBean<T> extends ConsumerConfig<T> implements InitializingBean, FactoryBean<T>, ApplicationContextAware, DisposableBean, BeanNameAware {
    private static final ThreadPoolExecutor CONSUMER_REFER_EXECUTOR = new ThreadPoolExecutor(
            32, 32,
            60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(102400),
            ThreadFactories.create("JSF-REFER-2"),
            new ThreadPoolExecutor.CallerRunsPolicy());
    private static final List<Future<?>> REFER_FUTURE_LIST = Lists.newArrayList();
    @Override
    public T getObject() throws Exception {
        Class<T> iface = (Class<T>) ClassLoaderUtils.forName(this.interfaceId);
        T proxy = (T) Proxy.newProxyInstance(iface.getClassLoader(), new Class[]{iface}, new DelayConsumerInvocationHandler());
        REFER_FUTURE_LIST.add(CONSUMER_REFER_EXECUTOR.submit(() -> super.refer()));
        this.object = CommonUtils.isUnitTestMode() ? null : proxy;
        return this.object;
    }
    // InvocationHandler and other methods omitted for brevity
}

4. Tomcat Version Impact

Two Tomcat versions (8.0.53 and 8.5.42) were compared. The newer version introduced slower class loading, adding a few milliseconds per bean; with over 2,900 beans, this contributed noticeably to startup time.

5. Hardware Considerations

Older machines and low‑performance data centers also increase startup latency. Migrating to newer, higher‑performance servers yielded at least a 20 % speedup.

6. Summary and Results

Optimizations applied:

Disabled Tomcat TLD file scanning.

Asynchronous HBase warm‑up.

Asynchronous generation of JSF consumer client proxy beans via custom BeanPostProcessor.

Identified Tomcat version effects on bean instantiation.

Moved to higher‑performance data centers.

After these changes, startup time dropped from 400‑500 s to roughly 130‑150 s, an improvement of over 60 %.

Optimized version

Before optimization

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaBackend DevelopmentSpringBootBeanPostProcessor
JD Cloud Developers
Written by

JD Cloud Developers

JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.

0 followers
Reader feedback

How this landed with the community

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.