I also ran into that problem. It is important to configure the ThreadPoolTaskExecutor correctly using the DelegatingSecurityContextAsyncTaskExecutor
. Also it is important to call the initialize() method, otherwise an error is thrown.
@Bean("threadPoolTaskExecutor")
public TaskExecutor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(1000);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("Async-");
executor.initialize(); // this is important, otherwise an error is thrown
return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}
The method in your business logic which is called asynchronously:
@Override
@Async("threadPoolTaskExecutor")
public void methodName() {
[..]
}
Update - other approach - more information: The solution described above works perfectly fine, if you only want to delegate the security context (i.e. the authentication information).
However, in certain situations you might also want to delegate other thread information, like the MDC context or request context, or you just want to have more control on how things are passed to the executor thread. If this is the case, you can bind the local thread information to the executor thread manually. The idea how this can be done is already described in the answer of @axtavt, but we can now use a TaskDecorator to do it in a more elegant way. The TaskDecorator stores the context of the request thread in variables and binds them in a closure so that the context can be accessed in the executor thread. When the thread execution is finished, the context is cleared from the executor thread, so that the information are gone when the thread is reused.
private static class ContextCopyingDecorator implements TaskDecorator {
@NonNull
@Override
public Runnable decorate(@NonNull Runnable runnable) {
// store context in variables which will be bound to the executor thread
RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
SecurityContext securityContext = SecurityContextHolder.getContext();
Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();
return () -> { // code runs inside executor thread and binds context
try {
if (requestAttributes != null) {
RequestContextHolder.setRequestAttributes(requestAttributes);
}
if (securityContext != null) {
SecurityContextHolder.setContext(securityContext);
}
if (mdcContextMap != null) {
MDC.setContextMap(mdcContextMap);
}
runnable.run();
} finally {
MDC.clear();
RequestContextHolder.resetRequestAttributes();
SecurityContextHolder.clearContext();
}
};
}
}
During the TaskExecutor creation, the TaskDecorator is assigned to the TaskExecutor (with the setTaskDecorator method).
@Bean("threadPoolTaskExecutor")
public TaskExecutor getAsyncExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(20);
executor.setMaxPoolSize(1000);
executor.setWaitForTasksToCompleteOnShutdown(true);
executor.setThreadNamePrefix("Async-");
executor.setTaskDecorator(new ContextCopyingDecorator());
return executor;
}
You could also combine both approaches (e.g. use TaskDecorator to copy MDC context and still use DelegatingSecurityContextAsyncTaskExecutor for security context), but I would not recommend to do it because it increases complexity. If you use TaskDecorator anyway, you can also set the security context with it and if you only need the security context to be set, just use the DelegatingSecurityContextAsyncTaskExecutor approach.