Correctness36 - SpotBugsExtensionForSpringFrameWork/CS5098 GitHub Wiki
Correctness - Using @ExceptionHandler to handle @Async exception
Description
To handle @Async
Exception, we would think about the usual way we did before which is combining @ControllerAdvice
and @ExceptionHandler
to save duplicate code.
In the @Service
class, there is an @Async
method.
@Service
public class FileScanServiceImpl implements FileScanService {
@Override
@Async
public void scanFileScheduler() throws MQException {
try{
messageProducer.putFileNameToMQ(fileName);
} catch (Exception e) {
ExceptionUtility.handleException(e, currentFile);
}
}
public static void handleException(Exception e) throws MQException {
String errMsg = "";
if (e instanceof MQException) {
// some functionality
throw new MQException(subject, errMsg);
}
}
And the author wants it to be handled by @ControllerAdvice
for global exception handling.
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(MQException.class)
@ResponseBody
public void handleMQException(HttpServletRequest request, MQException ex) {
// send email
}
}
(These code could be reduced.)
Theory
The reason why this kind of strategy doesn't work is that the @ExceptionHandler
can only catch "synchronous exceptions".
ExceptionHandlerExceptionResolver
class is the key class to handle all @ExceptionHandler
annotations. Inside this class, the afterPropertiesSet
method will invoke initExceptionHandlerAdviceCache()
method which will find all the class that annotated by @ControllerAdvice
. After finding those class, it will traverse all these classes to find all the methods annotated by @ExceptionHandler
and store in exceptionHandlerAdviceCache
.
private void initExceptionHandlerAdviceCache() {
if (getApplicationContext() == null) {
return;
}
List<ControllerAdviceBean> adviceBeans = ControllerAdviceBean.findAnnotatedBeans(getApplicationContext());
for (ControllerAdviceBean adviceBean : adviceBeans) {
Class<?> beanType = adviceBean.getBeanType();
if (beanType == null) {
throw new IllegalStateException("Unresolvable type for ControllerAdviceBean: " + adviceBean);
}
ExceptionHandlerMethodResolver resolver = new ExceptionHandlerMethodResolver(beanType);
if (resolver.hasExceptionMappings()) {
this.exceptionHandlerAdviceCache.put(adviceBean, resolver);
}
if (ResponseBodyAdvice.class.isAssignableFrom(beanType)) {
this.responseBodyAdvice.add(adviceBean);
}
}
...
}
Before the proceed of exception, a central dispatcher for HTTP request handlers/controllers is needed which is class DispatcherServlet
. Inside the DispatcherServlet
, the "main" method called is doDispatch
which is the method that handle @Controller
methods. In order to handle exceptions, there is a well-designed method to process which is processDispatchResult
inside the doDispatch
method.
However, there is a if-statement before that handler.
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
The above code will determine whether is an async request, if so, the processDispatchResult
will be skipped and therefore can only handle synchronous exceptions.
Solution
Creating a class that implementsAsyncUncaughtExceptionHandler
is the key idea to handle @Async
method.