SpringMVC has a complete system for handling exceptions, and it works very well. This article talks about the exception handling system in SpringMVC. We will sort out the exception system in SpringMVC from beginning to end.

1. Exception Resolver Overview

In SpringMVC’s exception system, the big boss at the top is the HandlerExceptionResolver, an interface with a single method.

1
2
3
4
5
public interface HandlerExceptionResolver {
    @Nullable
    ModelAndView resolveException(
        HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex);
}

The resolveException method is used to resolve the exceptions generated during the request processing and eventually return a ModelAndView.

Let’s look at the HandlerExceptionResolver implementation class.

spring mvc

There are three classes that directly implement the HandlerExceptionResolver interface.

  • HandlerExceptionResolverComposite: This class looks like another combination, so I won’t go over it here.
  • DefaultErrorAttributes: This is used to save the exception attributes.
  • AbstractHandlerExceptionResolver: This has more subclasses.
    • SimpleMappingExceptionResolver: Resolves exceptions through the correspondence between the exception class and View configured in advance.
    • AbstractHandlerMethodExceptionResolver: Handles exception types customized using the @ExceptionHandler annotation.
    • DefaultHandlerExceptionResolver: Handles exceptions by different types.
    • ResponseStatusExceptionResolver: Handles exceptions with the @ResponseStatus annotation.

In SpringMVC, these are the general exception resolvers, so let’s learn them one by one.

2. AbstractHandlerExceptionResolver

AbstractHandlerExceptionResolver is the parent class of the real working exception resolver, so let’s start with its resolveException method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
@Override
@Nullable
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	if (shouldApplyTo(request, handler)) {
		prepareResponse(ex, response);
		ModelAndView result = doResolveException(request, response, handler, ex);
		if (result != null) {
			logException(ex, request);
		}
		return result;
	}
	else {
		return null;
	}
}
  1. first call shouldApplyTo method to determine whether the current parser can handle the exception thrown by the incoming processor, if not, then directly return null, the exception will be given to the next HandlerExceptionResolver to handle.
  2. Call the prepareResponse method to process the response.
  3. call the doResolveException method to actually handle the exception, this is a template method, the specific implementation is in the subclass.
  4. call the logException method to record the exception log information.

There is nothing to say about logging exceptions, doResolveException is an empty template method, so there are two main methods for us here: shouldApplyTo and prepareResponse, let’s look at them separately.

shouldApplyTo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
	if (handler != null) {
		if (this.mappedHandlers != null && this.mappedHandlers.contains(handler)) {
			return true;
		}
		if (this.mappedHandlerClasses != null) {
			for (Class<?> handlerClass : this.mappedHandlerClasses) {
				if (handlerClass.isInstance(handler)) {
					return true;
				}
			}
		}
	}
	return !hasHandlerMappings();
}

There are two objects involved here: mappedHandlers and mappedHandlerClasses.

  • mappedHandlers: stores the handler object (Controller or method in Controller)
  • mappedHandlerClasses: stores the handler’s Class.

We can configure these two objects when configuring the exception resolver to achieve the exception handler only for a certain processor, but in general there is no such need, so you can just do to understand.

If the developer configured mappedHandlers or mappedHandlerClasses at the beginning, then use these two and the processor to compare, otherwise it will return true directly, indicating that the exception handling is supported.

prepareResponse

The prepareResponse method is relatively simple and mainly deals with the cached fields in the response header.

1
2
3
4
5
6
7
8
protected void prepareResponse(Exception ex, HttpServletResponse response) {
	if (this.preventResponseCaching) {
		preventCaching(response);
	}
}
protected void preventCaching(HttpServletResponse response) {
	response.addHeader(HEADER_CACHE_CONTROL, "no-store");
}

This is the general content of AbstractHandlerExceptionResolver, you can see that it is still very easy, next we look at its implementation class.

2.1 AbstractHandlerMethodExceptionResolver

AbstractHandlerMethodExceptionResolver mainly overrides the shouldApplyTo method and the doResolveException method, so let’s look at them one by one.

shouldApplyTo

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
@Override
protected boolean shouldApplyTo(HttpServletRequest request, @Nullable Object handler) {
	if (handler == null) {
		return super.shouldApplyTo(request, null);
	}
	else if (handler instanceof HandlerMethod) {
		HandlerMethod handlerMethod = (HandlerMethod) handler;
		handler = handlerMethod.getBean();
		return super.shouldApplyTo(request, handler);
	}
	else if (hasGlobalExceptionHandlers() && hasHandlerMappings()) {
		return super.shouldApplyTo(request, handler);
	}
	else {
		return false;
	}
}

This piece of logic does not feel like much to say, the judgment logic is basically still calling the parent class shouldApplyTo method to handle.

doResolveException

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
@Override
@Nullable
protected final ModelAndView doResolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	HandlerMethod handlerMethod = (handler instanceof HandlerMethod ? (HandlerMethod) handler : null);
	return doResolveHandlerMethodException(request, response, handlerMethod, ex);
}
@Nullable
protected abstract ModelAndView doResolveHandlerMethodException(
		HttpServletRequest request, HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception ex);

doResolveException is a concrete exception handling method, but it has no substantive operations in it. The concrete things are left to the doResolveHandlerMethodException method, which is an abstract method, and the concrete implementation is in the subclass.

2.1.1 ExceptionHandlerExceptionResolver

AbstractHandlerMethodExceptionResolver has only one subclass which is ExceptionHandlerExceptionResolver, take a look at its doResolveHandlerMethodException method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
@Override
@Nullable
protected ModelAndView doResolveHandlerMethodException(HttpServletRequest request,
		HttpServletResponse response, @Nullable HandlerMethod handlerMethod, Exception exception) {
	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;
	}
}

This method is rather long, but well understood:

  1. First find the method with the @ExceptionHandler annotation and wrap it in a ServletInvocableHandlerMethod object.
  2. If the corresponding method is found, configure a parameter parser, view parser, etc. for the exceptionHandlerMethod.
  3. Next, define an exceptions array, and if an exception chain exists, store the entire exception chain in the exceptions array.
  4. The exceptions array plus handlerMethod, together with the method parameters, call exceptionHandlerMethod.invokeAndHandle to complete the execution of the custom exception method, and the execution result is saved in mavContainer.
  5. If the request ends here, a ModelAndView is constructed and returned directly.
  6. Otherwise, the information is retrieved from the mavContainer and a new ModelAndView is constructed and returned. Also, if there are redirect parameters, they are saved.

This is the general workflow of ExceptionHandlerExceptionResolver, as you can see, it’s still very easy.

2.2 DefaultHandlerExceptionResolver

This is a default exception handler that handles some common exception types, as you can see by its name. Let’s look at its doResolveException method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
@Override
@Nullable
protected ModelAndView doResolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	try {
		if (ex instanceof HttpRequestMethodNotSupportedException) {
			return handleHttpRequestMethodNotSupported(
					(HttpRequestMethodNotSupportedException) ex, request, response, handler);
		}
		else if (ex instanceof HttpMediaTypeNotSupportedException) {
			return handleHttpMediaTypeNotSupported(
					(HttpMediaTypeNotSupportedException) ex, request, response, handler);
		}
		else if (ex instanceof HttpMediaTypeNotAcceptableException) {
			return handleHttpMediaTypeNotAcceptable(
					(HttpMediaTypeNotAcceptableException) ex, request, response, handler);
		}
		else if (ex instanceof MissingPathVariableException) {
			return handleMissingPathVariable(
					(MissingPathVariableException) ex, request, response, handler);
		}
		else if (ex instanceof MissingServletRequestParameterException) {
			return handleMissingServletRequestParameter(
					(MissingServletRequestParameterException) ex, request, response, handler);
		}
		else if (ex instanceof ServletRequestBindingException) {
			return handleServletRequestBindingException(
					(ServletRequestBindingException) ex, request, response, handler);
		}
		else if (ex instanceof ConversionNotSupportedException) {
			return handleConversionNotSupported(
					(ConversionNotSupportedException) ex, request, response, handler);
		}
		else if (ex instanceof TypeMismatchException) {
			return handleTypeMismatch(
					(TypeMismatchException) ex, request, response, handler);
		}
		else if (ex instanceof HttpMessageNotReadableException) {
			return handleHttpMessageNotReadable(
					(HttpMessageNotReadableException) ex, request, response, handler);
		}
		else if (ex instanceof HttpMessageNotWritableException) {
			return handleHttpMessageNotWritable(
					(HttpMessageNotWritableException) ex, request, response, handler);
		}
		else if (ex instanceof MethodArgumentNotValidException) {
			return handleMethodArgumentNotValidException(
					(MethodArgumentNotValidException) ex, request, response, handler);
		}
		else if (ex instanceof MissingServletRequestPartException) {
			return handleMissingServletRequestPartException(
					(MissingServletRequestPartException) ex, request, response, handler);
		}
		else if (ex instanceof BindException) {
			return handleBindException((BindException) ex, request, response, handler);
		}
		else if (ex instanceof NoHandlerFoundException) {
			return handleNoHandlerFoundException(
					(NoHandlerFoundException) ex, request, response, handler);
		}
		else if (ex instanceof AsyncRequestTimeoutException) {
			return handleAsyncRequestTimeoutException(
					(AsyncRequestTimeoutException) ex, request, response, handler);
		}
	}
	catch (Exception handlerEx) {
	}
	return null;
}

As you can see, this is actually based on different exception types, and then call different classes to handle the exception. Here the relevant processing is relatively easy, to HttpRequestMethodNotSupportedException for example, exception handling is to do some configuration of the response object, as follows.

Configure the response header, then sendError, and finally return an empty ModelAndView object.

In fact, here brother exception handling methods are more or less the same, Song will not repeat it.

2.3 ResponseStatusExceptionResolver

This is used to handle exceptions of ResponseStatusException type, or ordinary exception classes marked with the @ResponseStatus annotation. Let’s look at its doResolveException method.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
@Override
@Nullable
protected ModelAndView doResolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	try {
		if (ex instanceof ResponseStatusException) {
			return resolveResponseStatusException((ResponseStatusException) ex, request, response, handler);
		}
		ResponseStatus status = AnnotatedElementUtils.findMergedAnnotation(ex.getClass(), ResponseStatus.class);
		if (status != null) {
			return resolveResponseStatus(status, request, response, handler, ex);
		}
		if (ex.getCause() instanceof Exception) {
			return doResolveException(request, response, handler, (Exception) ex.getCause());
		}
	}
	catch (Exception resolveEx) {
	}
	return null;
}

If it is, it will call the resolveResponseStatusException method directly to handle the exception information, if not, it will find the @ResponseStatus annotation on the exception class and find the relevant exception information from it, then call the resolveResponseStatus method to handle it.

As you can see, there are two types of exceptions handled by ResponseStatusExceptionResolver.

  • Exception classes that inherit directly from ResponseStatusException, which can extract the desired information directly from it.
  • A normal exception class annotated with @ResponseStatus, in which case the exception information is extracted from the @ResponseStatus annotation.

This is relatively simple, nothing more.

2.4 SimpleMappingExceptionResolver

SimpleMappingExceptionResolver displays different error pages for different exceptions. Some of you may not have used SimpleMappingExceptionResolver, so here’s a brief explanation of its use.

SimpleMappingExceptionResolver configuration is very simple, directly provide an instance of SimpleMappingExceptionResolver can be, as follows.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
@Bean
SimpleMappingExceptionResolver simpleMappingExceptionResolver() {
    SimpleMappingExceptionResolver resolver = new SimpleMappingExceptionResolver();
    Properties mappings = new Properties();
    mappings.put("java.lang.ArithmeticException", "11");
    mappings.put("java.lang.NullPointerException", "22");
    resolver.setExceptionMappings(mappings);
    Properties statusCodes = new Properties();
    statusCodes.put("11", "500");
    statusCodes.put("22", "500");
    resolver.setStatusCodes(statusCodes);
    return resolver;
}

In mappings, we configure the correspondence between exceptions and view, and write the full path of the exception class, followed by 11 and 22 for the view name; statusCodes configures the mapping between view and response status code. After the configuration, if our project throws an ArithmeticException exception at runtime, the 11 view will be shown, and if our project throws a NullPointerException exception at runtime, the 22 view will be shown.

This is the usage, after understanding the usage it is easy to understand when we look at the source code, let’s look at the doResolveException method directly.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
@Override
@Nullable
protected ModelAndView doResolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	String viewName = determineViewName(ex, request);
	if (viewName != null) {
		Integer statusCode = determineStatusCode(request, viewName);
		if (statusCode != null) {
			applyStatusCodeIfPossible(request, response, statusCode);
		}
		return getModelAndView(viewName, ex, request);
	}
	else {
		return null;
	}
}
  1. First call the determineViewName method to determine the name of the view.
  2. Next, call determineStatusCode to see if the view has a corresponding statusCode.
  3. Call the applyStatusCodeIfPossible method to set the statusCode to the response, this method is very simple, not much to say.
  4. call getModelAndView method to construct a ModelAndView object to return, in the construction, while setting the exception parameter, the key of the exception information is exception by default.

In the above process, there are two relatively long methods, here we need to talk to you two extra words.

determineViewName

This is based on the exception type to find the view name, we look at the specific way to find.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
@Nullable
protected String determineViewName(Exception ex, HttpServletRequest request) {
	String viewName = null;
	if (this.excludedExceptions != null) {
		for (Class<?> excludedEx : this.excludedExceptions) {
			if (excludedEx.equals(ex.getClass())) {
				return null;
			}
		}
	}
	if (this.exceptionMappings != null) {
		viewName = findMatchingViewName(this.exceptionMappings, ex);
	}
	if (viewName == null && this.defaultErrorView != null) {
		viewName = this.defaultErrorView;
	}
	return viewName;
}
  1. if the current exception is contained in excludedExceptions, then return null directly (meaning that the current exception is ignored and handled directly by default).
  2. if exceptionMappings is not null, then directly call findMatchingViewName method to find the view name corresponding to the exception (exceptionMappings variable is the mapping relationship we configured earlier), the specific way to find is to iterate through the mapping table we configured earlier.
  3. If the corresponding viewName is not found and the user has configured defaultErrorView, the defaultErrorView will be assigned to viewName and viewName will be returned.

determineStatusCode

1
2
3
4
5
6
7
@Nullable
protected Integer determineStatusCode(HttpServletRequest request, String viewName) {
	if (this.statusCodes.containsKey(viewName)) {
		return this.statusCodes.get(viewName);
	}
	return this.defaultStatusCode;
}

This one is easier, go directly to statusCodes to see if there is a status code corresponding to the view, and if so, return it directly, if not, return a default one.

3. HandlerExceptionResolverComposite

Finally, there is a HandlerExceptionResolverComposite that needs to be introduced to you, which is a combination of exception handlers to proxy for the exception handlers that actually do the work.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
@Nullable
public ModelAndView resolveException(
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) {
	if (this.resolvers != null) {
		for (HandlerExceptionResolver handlerExceptionResolver : this.resolvers) {
			ModelAndView mav = handlerExceptionResolver.resolveException(request, response, handler, ex);
			if (mav != null) {
				return mav;
			}
		}
	}
	return null;
}

Its resolveException method is relatively simple, and we’ve seen this written many times, so we won’t go over it again.

Reference https://juejin.cn/post/6951317320845230116