@Async と @RequestScope を同時につかうとどうなるか
@RequestScope、これは @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) と同じ意味。この Bean はスレッドローカルにひもづけられているHttpServletRequestに保持されている。
@Async は別スレッドで処理を実行するため、間接的にでも @RequestScope な Bean を使っていると以下のような実行時エラーとなる。処理中のスレッドには該当する Bean が保持されていないという旨。
ScopedProxyMode.TARGET_CLASS なのでスタックトレースがややこしい。cglib を使ってクラスのプロキシを作って、プロキシクラス内で Bean をどこかからとってきて呼ぶという動きになっている。おかげでスコープに関わらずコード上は Singleton のように扱えているが、黒魔術はひっかかったとき大変です。
解決方法
https://stackoverflow.com/questions/23732089/how-to-enable-request-scope-in-async-task-executor に良い解決方法が書いてあって、これを使えばよい。やってることは単純で @Async で別スレッドで実行しようとする際に、呼び出し元スレッドのBeanを渡してあげるという感じ。
ただし、どうもコピペでは動かなくて、以下のようにかえてうまくいった。結局 Executor インターフェイスのメソッドだけオーバーライドしたら良い。
@Configuration
@EnableAsync
public class AsyncConfiguration implements AsyncConfigurer {
// Default task executor for @Async annotation
@Override
@Bean
public Executor getAsyncExecutor() {
val executor = new ContextAwarePoolExecutor();
executor.setCorePoolSize(25);
executor.setQueueCapacity(25);
executor.setMaxPoolSize(25);
executor.setThreadNamePrefix("AsyncTask-");
executor.initialize();
return executor;
}
@Override
public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
return null;
}
@SuppressWarnings("serial")
public static class ContextAwarePoolExecutor extends ThreadPoolTaskExecutor {
/**
* Override Executor interface
*/
@Override
public void execute(Runnable task) {
super.execute(new ContextAwareRunnable(task, RequestContextHolder.currentRequestAttributes()));
}
}
public static class ContextAwareRunnable implements Runnable {
private final Runnable task;
private final RequestAttributes context;
public ContextAwareRunnable(Runnable task, RequestAttributes context) {
this.task = task;
this.context = context;
}
@Override
public void run() {
if (context != null) {
RequestContextHolder.setRequestAttributes(context);
}
try {
task.run();
} finally {
RequestContextHolder.resetRequestAttributes();
}
}
}
}