Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Bean Asynchronous Initialization in 68 Lines of Code

Tech May 7 6

Background

When examining SOFABoot source code, I discovered an interesting feature for accelerating Spring application startup: asynchronous Bean initialization.

The core mechanism allows Bean initialization methods to execute in background threads, significantly reducing startup time when multiple Beans have time-consuming initialization logic.

Demo Scenario

Consider a typical Spring Boot application with two Beans, each requiring 5 seconds for initialization:

@Component
public class ServiceA {
    public void init() throws InterruptedException {
        Thread.sleep(5000);
        System.out.println("ServiceA initialized by: " + Thread.currentThread().getName());
    }
}

@Component
public class ServiceB {
    public void init() throws InterruptedException {
        Thread.sleep(5000);
        System.out.println("ServiceB initialized by: " + Thread.currentThread().getName());
    }
}

With synchronous initialization, startup takes approximately 10+ seconds due to sequential execution.

Implementation Principle

The mechanism operates through several key components:

1. Custom Annotation

Define a annotation to mark Beans eligible for async initialization:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface AsyncInit {
    boolean value() default true;
}

2. BeanPostProcessor

A BeanPostProcessor intercepts Bean initialization and wraps eligible Beans with a dynamic proxy:

public class AsyncInitBeanPostProcessor implements BeanPostProcessor {
    
    @Override
    public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
        Class<?> beanClass = bean.getClass();
        
        if (beanClass.isAnnotationPresent(AsyncInit.class)) {
            return Proxy.newProxyInstance(
                beanClass.getClassLoader(),
                beanClass.getInterfaces(),
                (proxy, method, args) -> {
                    if ("init".equals(method.getName())) {
                        return AsyncTaskExecutor.submitTask(() -> {
                            try {
                                return method.invoke(bean, args);
                            } catch (InvocationTargetException e) {
                                throw new RuntimeException(e);
                            }
                        });
                    }
                    return method.invoke(bean, args);
                }
            );
        }
        return bean;
    }
}

3. Thread Pool Management

A dedicated executor manages async tasks:

public class AsyncTaskExecutor {
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final AtomicReference<ThreadPoolExecutor> EXECUTOR = new AtomicReference<>();
    private static final List<Future<?>> FUTURES = new ArrayList<>();

    public static Future<?> submitTask(Runnable task) {
        if (EXECUTOR.get() == null) {
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CPU_COUNT + 1, CPU_COUNT + 1, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadPoolExecutor.CallerRunsPolicy()
            );
            EXECUTOR.compareAndSet(null, executor);
        }
        Future<?> future = EXECUTOR.get().submit(task);
        FUTURES.add(future);
        return future;
    }

    public static void awaitCompletion() {
        for (Future<?> future : FUTURES) {
            try {
                future.get();
            } catch (InterruptedException | ExecutionException e) {
                Thread.currentThread().interrupt();
            }
        }
        FUTURES.clear();
        if (EXECUTOR.get() != null) {
            EXECUTOR.get().shutdown();
            EXECUTOR.set(null);
        }
    }
}

4. Startup Synchronization

Listen for ContextRefreshedEvent to ensure all async initialization completes before the application starts serving requests:

@Component
public class AsyncInitReadyListener implements ApplicationListener<ContextRefreshedEvent> {
    
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        AsyncTaskExecutor.awaitCompletion();
    }
}

Minimal Implementation

Combining the essential components yields a 68-line solution:

// AsyncTaskExecutor.java (45 lines)
public class AsyncTaskExecutor {
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final AtomicReference<ThreadPoolExecutor> EXECUTOR = new AtomicReference<>();
    private static final List<Future<?>> FUTURES = new ArrayList<>();

    public static Future<?> submitTask(Runnable task) {
        if (EXECUTOR.get() == null) {
            ThreadPoolExecutor executor = new ThreadPoolExecutor(
                CPU_COUNT + 1, CPU_COUNT + 1, 30, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy()
            );
            EXECUTOR.compareAndSet(null, executor);
        }
        Future<?> future = EXECUTOR.get().submit(task);
        FUTURES.add(future);
        return future;
    }

    public static void awaitCompletion() {
        for (Future<?> future : FUTURES) {
            try { future.get(); } 
            catch (Exception e) { throw new RuntimeException(e); }
        }
        FUTURES.clear();
        if (EXECUTOR.get() != null) {
            EXECUTOR.get().shutdown();
            EXECUTOR.set(null);
        }
    }
}

// AsyncInitReadyListener.java (23 lines)
@Component
public class AsyncInitReadyListener implements ApplicationListener<ContextRefreshedEvent> {
    @Override
    public void onApplicationEvent(ContextRefreshedEvent event) {
        AsyncTaskExecutor.awaitCompletion();
    }
}

Register the BeanPostProcessor via spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  com.example.config.AsyncInitBeanPostProcessor

Usage

Annotate Beans requiring async initialization:

@AsyncInit
@Component
public class HeavyWeightService {
    @PostConstruct
    public void init() throws InterruptedException {
        Thread.sleep(5000);
        System.out.println("Initialized: " + Thread.currentThread().getName());
    }
}

How It Works

  1. During Spring context refresh, BeanPostProcessor identifies Beans marked with @AsyncInit
  2. Before initialization, the Bean is wrapped with a proxy
  3. When init() is invoked, the method execution is delegated to a thread pool
  4. The application waits for all async tasks to complete via ContextRefreshedEvent listener
  5. Only after all initialization tasks finish does the application accept traffic

Key Advantages

  • Faster startup: Multiple initialization tasks execute in parallel
  • Non-blocking: Main thread continues processing while initialization runs in background
  • Transparent: Calling code requires no modification
  • Controlled concurrency: Thread pool size adapts to CPU cores

This approach demonstrates Spring's extensibility through BeanPostProcessor and ApplicationListener, enabling powerful cross-cutting behavior without modifying existing code.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.