1. Start first
In daily development, almost our projects use exception handlers. We usually customize our own exception handlers to handle all kinds of exceptions, large and small. At present, the most common way to configure exception handlers is to use the combination of @ ControllerAdvice+@ExceptionHandler. Of course, there are other ways, such as implementing the HandlerExceptionResolver interface. This time, let's take a look at how Spring identifies our configured processor and processes it according to our configuration in @ ControllerAdvice and @ ExceptionHandler modes, and how Spring selects when a child exception is thrown in the program, Is it handled as a child exception handler or a parent exception handler? Let's trace the source code to see how it is implemented
2. Case display
In the following code, a global Exception handler is configured to handle three exceptions: RuntimeException, ArithmeticException and Exception. ArithmeticException is a subclass of RuntimeException and RuntimeException is a subclass of Exception
@RestControllerAdvice public class YuqiExceptionHandler { @ExceptionHandler(RuntimeException.class) public String handleRuntimeException(RuntimeException e) { return "handle runtimeException"; } @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException"; } @ExceptionHandler(Exception.class) public String handleException(Exception e) { return "handle Exception"; } }
The following code is our test class. By accessing the request, an arithmetic exception divided by 0 will be triggered.
@RestController @RequestMapping("/test") public class TestController { @GetMapping("/exception") public void testException() { int i = 1 / 0; } }
3. Source code tracking
First, let's look at the execution logic of the dispatcherservlet of the front-end controller, the core doDispatch() method. After the front-end request is intercepted by the dispatcherservlet, the matching handler is found by handlerMapping, and then converted into a suitable handlerAdapter to call the actual method.
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception { HttpServletRequest processedRequest = request; HandlerExecutionChain mappedHandler = null; boolean multipartRequestParsed = false; WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request); try { ModelAndView mv = null; Exception dispatchException = null; try { processedRequest = checkMultipart(request); multipartRequestParsed = (processedRequest != request); // Determine handler for the current request. mappedHandler = getHandler(processedRequest); if (mappedHandler == null) { noHandlerFound(processedRequest, response); return; } //Find the appropriate adapter according to the handler HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler()); String method = request.getMethod(); boolean isGet = HttpMethod.GET.matches(method); if (isGet || HttpMethod.HEAD.matches(method)) { long lastModified = ha.getLastModified(request, mappedHandler.getHandler()); if (new ServletWebRequest(request, response).checkNotModified(lastModified) && isGet) { return; } } if (!mappedHandler.applyPreHandle(processedRequest, response)) { return; } // Make the actual call to the method mv = ha.handle(processedRequest, response, mappedHandler.getHandler()); if (asyncManager.isConcurrentHandlingStarted()) { return; } applyDefaultViewName(processedRequest, mv); mappedHandler.applyPostHandle(processedRequest, response, mv); } catch (Exception ex) { //Method call throws an exception and saves the exception object dispatchException = ex; } catch (Throwable err) { // As of 4.3, we're processing Errors thrown from handler methods as well, // making them available for @ExceptionHandler methods and other scenarios. dispatchException = new NestedServletException("Handler dispatch failed", err); } //At the end of the method call, process the call result (whether it is a normal call or an exception is thrown) processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException); } catch (Exception ex) { triggerAfterCompletion(processedRequest, response, mappedHandler, ex); } catch (Throwable err) { triggerAfterCompletion(processedRequest, response, mappedHandler, new NestedServletException("Handler processing failed", err)); } finally { if (asyncManager.isConcurrentHandlingStarted()) { // Instead of postHandle and afterCompletion if (mappedHandler != null) { mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response); } } else { // Clean up any resources used by a multipart request. if (multipartRequestParsed) { cleanupMultipart(processedRequest); } } } }
Then let's take a look at the processDispatchResult() method to see how it handles the result of the method call
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
if (exception != null) {
//Judge whether the current exception is an instance of ModelAndViewDefiningException
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
//Get the currently executed handler (in this case, the controller)
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
//Call processHandlerException() for further exception handling
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
// Did the handler return a view to render?
if (mv != null && !mv.wasCleared()) {
render(mv, request, response);
if (errorView) {
WebUtils.clearErrorRequestAttributes(request);
}
}
else {
if (logger.isTraceEnabled()) {
logger.trace("No view rendering, null ModelAndView returned.");
}
}
if (WebAsyncUtils.getAsyncManager(request).isConcurrentHandlingStarted()) {
// Concurrent handling started during a forward
return;
}
if (mappedHandler != null) {
// Exception (if any) is already handled..
mappedHandler.triggerAfterCompletion(request, response, null);
}
}
Next, let's look at how processHandlerException() further handles exceptions
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { // Success and error responses may use different content types request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE); // Check registered HandlerExceptionResolvers... ModelAndView exMv = null;
//If the handlerExceptionResolver list in the spring container is not null, it will be traversed. The current container has two handlerexceptionresolvers, as shown in the following figure if (this.handlerExceptionResolvers != null) { for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
//Traverse each handlerExceptionResolver and call its resolveException() method to try to resolve the current exception exMv = resolver.resolveException(request, response, handler, ex);
//If the returned ModelAndView is not null, it means that the handler exception resolver has successfully solved the exception, then it will exit the loop if (exMv != null) { break; } } }
//Determine whether the exception has been resolved if (exMv != null) { if (exMv.isEmpty()) { request.setAttribute(EXCEPTION_ATTRIBUTE, ex); return null; } // We might still need view name translation for a plain error model... if (!exMv.hasView()) { String defaultViewName = getDefaultViewName(request); if (defaultViewName != null) { exMv.setViewName(defaultViewName); } } if (logger.isTraceEnabled()) { logger.trace("Using resolved error view: " + exMv, ex); } else if (logger.isDebugEnabled()) { logger.debug("Using resolved error view: " + exMv); } WebUtils.exposeErrorRequestAttributes(request, ex, getServletName()); return exMv; } throw ex; }
The handlerExceptionResolver held by the container is as follows. The key point is the second, HandlerExceptionResolverComposite. Let's see how it is solved
Next, let's look at how the resolveException() method of HandlerExceptionResolverComposite resolves exceptions. We can see that HandlerExceptionResolverComposite contains three exception parsers. Continue to repeat the operations in the previous disPatcherServlet method by traversing the list, that is, traversing each parser to see if it can solve the exception. The most important is the exception handler exception resolver. Let's see how its resolveException() method handles this exception
ExceptionHandlerExceptionResolver does not implement the resolveException method itself, but directly uses the resolveException method implemented by its superclass AbstractHandlerExceptionResolver. Let's look at the internal specific logic
@Override @Nullable public ModelAndView resolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { //Judge whether the current resolver supports the processing of this handler if (shouldApplyTo(request, handler)) { prepareResponse(ex, response);
//Exception handling, which is provided by its subclass AbstractHandlerMethodExceptionResolver ModelAndView result = doResolveException(request, response, handler, ex); if (result != null) { // Print debug message when warn logger is not enabled. if (logger.isDebugEnabled() && (this.warnLogger == null || !this.warnLogger.isWarnEnabled())) { logger.debug(buildLogMessage(ex, request) + (result.isEmpty() ? "" : " to " + result)); } // Explicitly configured warn logger in logException method. logException(ex, request); } return result; } else { return null; } }
Take a look at the doResolveException() method of AbstractHandlerMethodExceptionResolver
protected final ModelAndView doResolveException( HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) { //Convert handler to handlerMehtod HandlerMethod handlerMethod = (handler instanceof HandlerMethod ? (HandlerMethod) handler : null);
//Call this method to handle the exception, and then start calling the method of ExceptionHandlerExceptionResolver return doResolveHandlerMethodException(request, response, handlerMethod, ex); }
Take a look at the doResolveHandlerMethodException() method of the core parser ExceptionHandlerExceptionResolver
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) { //The core method obtains the Handler mehtod that handles the exception, that is, the method we configured earlier, that is, we really need to decide which method to use to handle the exception, because our previous @ ControllerAdvice will turn the class into a Handler like @ Controller,
The corresponding methods inside are also converted to HandlerMethod ServletInvocableHandlerMethod exceptionHandlerMethod = getExceptionHandlerMethod(handlerMethod, exception); if (exceptionHandlerMethod == null) { return null; } if (this.argumentResolvers != null) { exceptionHandlerMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers); } if (this.returnValueHandlers != null) { exceptionHandlerMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers); } ServletWebRequest webRequest = new ServletWebRequest(request, response); ModelAndViewContainer mavContainer = new ModelAndViewContainer(); ArrayList<Throwable> exceptions = new ArrayList<>(); try { if (logger.isDebugEnabled()) { logger.debug("Using @ExceptionHandler " + exceptionHandlerMethod); } // Expose causes as provided arguments as well Throwable exToExpose = exception; while (exToExpose != null) { exceptions.add(exToExpose); Throwable cause = exToExpose.getCause(); exToExpose = (cause != exToExpose ? cause : null); } Object[] arguments = new Object[exceptions.size() + 1]; exceptions.toArray(arguments); // efficient arraycopy call in ArrayList arguments[arguments.length - 1] = handlerMethod; exceptionHandlerMethod.invokeAndHandle(webRequest, mavContainer, arguments); } catch (Throwable invocationEx) { // Any other than the original exception (or a cause) is unintended here, // probably an accident (e.g. failed assertion or the like). if (!exceptions.contains(invocationEx) && logger.isWarnEnabled()) { logger.warn("Failure in @ExceptionHandler " + exceptionHandlerMethod, invocationEx); } // Continue with default processing of the original exception... return null; } if (mavContainer.isRequestHandled()) { return new ModelAndView(); } else { ModelMap model = mavContainer.getModel(); HttpStatus status = mavContainer.getStatus(); ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model, status); mav.setViewName(mavContainer.getViewName()); if (!mavContainer.isViewReference()) { mav.setView((View) mavContainer.getView()); } if (model instanceof RedirectAttributes) { Map<String, ?> flashAttributes = ((RedirectAttributes) model).getFlashAttributes(); RequestContextUtils.getOutputFlashMap(request).putAll(flashAttributes); } return mav; } }
Let's take a look at how the ExceptionHandlerExceptionResolver obtains the specified method for handling exceptions
protected ServletInvocableHandlerMethod getExceptionHandlerMethod( @Nullable HandlerMethod handlerMethod, Exception exception) { Class<?> handlerType = null; if (handlerMethod != null) { // Local exception handler methods on the controller class itself. // To be invoked through the proxy, even in case of an interface-based proxy.
//Get the actual Bean type of the HandlerMehtod, that is, the type of our Controller -- TestController handlerType = handlerMethod.getBeanType();
//According to this type, get the parser from the cache map of excpetionHandler. At this time, there is no data in the cache map ExceptionHandlerMethodResolver resolver = this.exceptionHandlerCache.get(handlerType); if (resolver == null) {
//If there is no parser of this type in the cache, a new parser is created based on this type resolver = new ExceptionHandlerMethodResolver(handlerType);
//Put into cache Map this.exceptionHandlerCache.put(handlerType, resolver); }
//Through the exception acquisition method, the acquisition here is null Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(handlerMethod.getBean(), method, this.applicationContext); } // For advice applicability check below (involving base packages, assignable types // and annotation presence), use target class instead of interface-based proxy
//Judge whether the Bean type is a proxy class if (Proxy.isProxyClass(handlerType)) {
//If it is a proxy class, we need to get the type of the actual proxy class handlerType = AopUtils.getTargetClass(handlerMethod.getBean()); } } //Traverse the cache collection of controllerAdvice beans in the current container. Currently, there is only one controllerAdvice in the cache, which is the global exception handler we configured earlier for (Map.Entry<ControllerAdviceBean, ExceptionHandlerMethodResolver> entry : this.exceptionHandlerAdviceCache.entrySet()) { ControllerAdviceBean advice = entry.getKey();
//Judge whether the controller type needs to be enhanced. Here, we mainly judge whether the three attributes basePackages on @ ControllerAdvice are overwritten by the base package path,
assignableTypes Or specially designated by this class,annotations And there are specially specified annotations on this class. One of the three conditions is true,If all three properties are null,Then return true if (advice.isApplicableToBeanType(handlerType)) {
//Get the parser corresponding to the controllerAdvice ExceptionHandlerMethodResolver resolver = entry.getValue();
//Gets the method to resolve the exception Method method = resolver.resolveMethod(exception); if (method != null) { return new ServletInvocableHandlerMethod(advice.resolveBean(), method, this.applicationContext); } } } return null; }
ExceptionHandlerMethodResolver. Several methods are nested and called inside the resolvemethod () method. The main one is getMapperMethod
private Method getMappedMethod(Class<? extends Throwable> exceptionType) {
List<Class<? extends Throwable>> matches = new ArrayList<>();
//Traverse the key of the corresponding exception handling method under the parser. Our test class should have three methods, corresponding to exception, runtimeException and arithmeticexception for (Class<? extends Throwable> mappedException : this.mappedMethods.keySet()) {
//Judge whether the currently traversed exception is equal to the incoming exception, or is its parent class or superclass. Obviously, the three we traversed meet the requirements if (mappedException.isAssignableFrom(exceptionType)) {
//If yes, it will be added to the list matches.add(mappedException); } }
//If the list is not empty, it indicates that there are qualified corresponding methods if (!matches.isEmpty()) {
//If there are more than one qualified method, the list will be sorted. Here is to determine how to select multiple methods that can handle the same exception if (matches.size() > 1) {
//Let's focus on the internal sorting rules of this custom sorter matches.sort(new ExceptionDepthComparator(exceptionType)); }
//Return the first element of the sorted list, that is, the one with the lowest depth and the one closest to the incoming exception, which can be understood as the proximity principle. return this.mappedMethods.get(matches.get(0)); } else {
return NO_MATCHING_EXCEPTION_HANDLER_METHOD; } }
According to the sorting rules of ExceptionDepthComparator, we can see that the depth of each exception is calculated. This depth is compared with the incoming exception. How to make the same exception depth is 0, ranging from depth + 1. Then continue to judge with the parent class of the exception, recurse constantly, calculate the depth of all exceptions in the list relative to the incoming exception, and then sort in ascending order.
public int compare(Class<? extends Throwable> o1, Class<? extends Throwable> o2) { int depth1 = getDepth(o1, this.targetException, 0); int depth2 = getDepth(o2, this.targetException, 0); return (depth1 - depth2); } private int getDepth(Class<?> declaredException, Class<?> exceptionToMatch, int depth) { if (exceptionToMatch.equals(declaredException)) { // Found it! return depth; } // If we've gone as far as we can go and haven't found it... if (exceptionToMatch == Throwable.class) { return Integer.MAX_VALUE; } return getDepth(declaredException, exceptionToMatch.getSuperclass(), depth + 1); }
Here we have seen the core code of the @ ControllerAdvice+@ExceptionHandler execution process. There are many source codes of the complete process, which are not shown in this article, but these core parts should let us understand how spring MVC finds a suitable exception processor to handle after throwing an exception.
4. Expansion
In daily development, exceptions of one class may need to be handled in this way, and exceptions of another class need to be handled in that way, which is equivalent to some customized processing. At this time, it is difficult to meet the requirements if there is only one global exception handler in our system. At this time, we can customize multiple global exception handlers, The coverage can be specified on the @ ControllerAdvice attribute, as shown in the following example
We have defined two exception handlers in the program. Yuqi2ExceptionHandler specifies TestController on the assignableTypes attribute on the @ ControllerAdvice annotation. In this way, when arithmetic exceptions are thrown in TestController, Yuqi2ExceptionHandler will handle them. In this way, the customization of special classes is realized
@RestControllerAdvice public class YuqiExceptionHandler { @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException"; } } @RestControllerAdvice(assignableTypes = {TestController.class}) public class Yuqi2ExceptionHandler { @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException v2"; } }
When we have two custom exception handling classes that have special handling for custom classes, how will spring MVC choose? In the following example, both exception handlers specify the effective range as TestController. When TestController triggers an exception, it is tested that yuqi2 is the processor. The debug source code shows that when traversing the ControllerAdviceBean, Yuqi2 is above Yuqi3, so the traversal is directly completed at yuqi2. We can manually add @ Order annotation in the processor to specify their loading properties. For example, yuqi2 adds @ Order (2) and Yuqi3 adds @ Order(1), so that Yuqi3's loading Order takes precedence. When an exception is triggered again, Yuqi3's processing will follow.
@RestControllerAdvice(assignableTypes = {TestController.class}) public class Yuqi2ExceptionHandler { @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException v2"; } } @RestControllerAdvice(assignableTypes = {TestController.class}) public class Yuqi3ExceptionHandler { @ExceptionHandler(ArithmeticException.class) public String handleArithmeticException(ArithmeticException e) { return "handle ArithmeticException v3"; } }
5. Finally
I believe that after reading the source code along with the process, everyone has a clearer understanding of the implementation principles of @ ControllerAdvice and @ ExceptionHandler. I went through the source code again and learned it myself. Due to the author's limited level, there may be some mistakes in the article. I hope you can point out and make common progress.