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.

Link

https://stackoverflow.com/questions/61885358/async-will-not-call-by-controlleradvice-for-global-exception