한국어 | English
The Multi-Datasource-Query-Counter library counts database queries per API request. By default, it uses @RequestScope
to ensure each request has its own counter.
However, in asynchronous environments (CompletableFuture
, @Async
, WebFlux, etc.), execution switches to new threads, which cannot access the original request context, resulting in missed query counts.
This guide explains how to properly share the QueryCountPerRequest
object across threads in asynchronous environments.
In a typical Spring application, a request is processed within a single thread. However, when using asynchronous code:
- Execution switches to a new thread.
- The new thread has a new context.
So, the new thread cannot access the original request context.
Spring provides TaskDecorator
which allows copying the request context to new threads before executing asynchronous tasks.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import java.util.concurrent.Executor;
@EnableAsync
@Configuration
public class AsyncConfig {
@Bean(name = "asyncExecutor")
public Executor asyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// Configure thread pool settings according to your application needs... (skip)
executor.setTaskDecorator(task -> {
// Get the QueryCountPerRequest from the current thread
QueryCountPerRequest currentQueryCountPerRequest = QueryCountPerRequestHolder.INSTANCE.get();
return () -> {
try {
// Propagate the QueryCountPerRequest to the new thread
if (currentQueryCountPerRequest != null) {
QueryCountPerRequestHolder.INSTANCE.set(currentQueryCountPerRequest);
}
task.run();
} finally {
// Clear the QueryCountPerRequest after the task is complete
QueryCountPerRequestHolder.INSTANCE.remove();
}
};
});
executor.initialize();
return executor;
}
}
When using CompletableFuture directly, use the custom Executor:
@Autowired
@Qualifier("asyncExecutor")
private Executor asyncExecutor;
public CompletableFuture<List<User>> findAllUsersAsync() {
return CompletableFuture.supplyAsync(() -> {
return userRepository.findAll(); // Query count point.
}, asyncExecutor); // Specify the custom executor.
}
When using the @Async annotation, explicitly specify the configured Executor:
@Async("asyncExecutor") // Specify the bean name explicitly.
public CompletableFuture<User> findUserByIdAsync(Long id) {
return CompletableFuture.completedFuture(
userRepository.findById(id).orElse(null) // Query count point.
);
}
(WIP)