Implementing Bean Asynchronous Initialization in 68 Lines of Code
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
- During Spring context refresh,
BeanPostProcessoridentifies Beans marked with@AsyncInit - Before initialization, the Bean is wrapped with a proxy
- When
init()is invoked, the method execution is delegated to a thread pool - The application waits for all async tasks to complete via
ContextRefreshedEventlistener - 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.