In depth analysis of spring MVC exception handling system

@[TOC] there is a complete exception handling system in spring MVC. This system is very easy to use. Today, SongGe will spend some time talking about the exception handling system in spring MVC. Let's sort out the exception system in spring MVC from beginning to end.

1. Overview of exception resolver

In the exception system of spring MVC, the top-level Boss is HandlerExceptionResolver, which is an interface with only one method:

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

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

Let's look at the implementation class of HandlerExceptionResolver:

There are three classes that directly implement the HandlerExceptionResolver interface:

  • HandlerExceptionResolverComposite: This is another combination. We have seen xxxComposite many times in the recent source code analysis, so I won't repeat it here.

  • DefaultErrorAttributes: This is used to save exception attributes.

  • AbstractHandlerExceptionResolver: this has many subclasses:

    • SimpleMappingExceptionResolver: resolve exceptions through the corresponding relationship between the pre configured exception class and View.
    • AbstractHandlerMethodExceptionResolver: handles exception types customized with the @ ExceptionHandler annotation.
    • DefaultHandlerExceptionResolver: handles exceptions according to different types.
    • ResponseStatusExceptionResolver: handle exceptions with @ ResponseStatus annotation.

In spring MVC, these are the general exception parsers. Next, let's learn these exception parsers one by one.

2.AbstractHandlerExceptionResolver

AbstractHandlerExceptionResolver is the parent class of the real working exception parser. Let's start with its resolveException method.

@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 the shouldaplyto method to determine whether the current parser can handle the exception thrown by the incoming processor. If not, it will directly return null, and this exception will be handed over to the next HandlerExceptionResolver for processing.
  2. Call the prepareResponse method to process the response.
  3. Call the doResolveException method to actually handle exceptions. This is a template method, which is implemented in subclasses.
  4. Call the logException method to record the exception log information.

There's nothing to say about recording exception logs. doResolveException is an empty template method, so there are mainly two methods for us: shouldaplyto and prepareResponse. Let's look at them separately.

shouldApplyTo

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();
}

Two objects are involved here: mappedHandlers and mappedHandlerClasses:

  • mappedHandlers: stores processor objects (controllers or methods in controllers)
  • mappedHandlerClasses: stores the Class of the processor.

When configuring the exception parser, we can configure these two objects to realize that the exception handler only serves one processor, but generally speaking, there is no such requirement, so we just need to know.

If the developer configures mappedHandlers or mappedHandlerClasses at the beginning, compare them with the processor. Otherwise, it will directly return true, indicating that the exception handling is supported.

prepareResponse

The prepareResponse method is relatively simple. It mainly deals with the cache fields of the response header.

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, which is still very easy. Next, let's look at its implementation class.

2.1 AbstractHandlerMethodExceptionResolver

AbstractHandlerMethodExceptionResolver mainly rewrites shouldaplyto method and doResolveException method, one by one.

shouldApplyTo

@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() &amp;&amp; hasHandlerMappings()) {
		return super.shouldApplyTo(request, handler);
	}
	else {
		return false;
	}
}

There's nothing to say about this feeling. The judgment logic basically calls the shouldaplyto method of the parent class.

doResolveException

@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 specific exception handling method, but there is no substantive operation in it. The specific things are left to the doResolveHandlerMethodException method, which is an abstract method and implemented in subclasses.

2.1.1 ExceptionHandlerExceptionResolver

AbstractHandlerMethodExceptionResolver has only one subclass, ExceptionHandlerExceptionResolver. Take a look at its doResolveHandlerMethodException method:

@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&lt;&gt;();
	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) &amp;&amp; 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;
	}
}

Although this method is relatively long, it is easy to understand:

  1. First, find the method with @ ExceptionHandler annotation and encapsulate it into a ServletInvocableHandlerMethod object (SongGe has introduced the ServletInvocableHandlerMethod object in the previous article. For details, see: Can Spring Boot define the method of the interface as private?).
  2. If the corresponding method is found, configure the parameter parser and view parser for exceptionHandlerMethod. For these parsers, refer to SongGe's previous articles: How to customize the parameter parser in SpringBoot?,In depth analysis of spring MVC parameter parser,How to unify the API interface response format in Spring Boot?.
  3. Next, define an exceptions array. If there is an exception chain in the exception, the whole exception chain will be stored in the exceptions array.
  4. The exceptions array and handlerMethod together form the method parameters and call exceptionhandlermethod Invokeandhandle completes the execution of the custom exception method, and the execution result is saved in the mavContainer.
  5. If the request ends here, a ModelAndView will be constructed directly to return.
  6. Otherwise, take out the information from the mavContainer, build a new ModelAndView, and return. At the same time, if there are redirection parameters, save them (for redirection parameters, see: Can parameters in spring MVC be passed like this? Up!).

This is the general workflow of ExceptionHandlerExceptionResolver. You can see that it is still very easy.

2.2 DefaultHandlerExceptionResolver

This is a default exception handler according to its name. It is used to handle some common exception types. Let's take a look at its doResolveException method:

@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 types of exceptions, and then invoke different classes to handle the exception. Relevant processing here is relatively easy. Take HttpRequestMethodNotSupportedException as an example. Exception handling is to configure the response object, as follows:

protected ModelAndView handleHttpRequestMethodNotSupported(HttpRequestMethodNotSupportedException ex,
		HttpServletRequest request, HttpServletResponse response, @Nullable Object handler) throws IOException {
	String[] supportedMethods = ex.getSupportedMethods();
	if (supportedMethods != null) {
		response.setHeader("Allow", StringUtils.arrayToDelimitedString(supportedMethods, ", "));
	}
	response.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED, ex.getMessage());
	return new ModelAndView();
}

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

In fact, my brother's exception handling methods are similar here. Brother song won't repeat it.

2.3 ResponseStatusExceptionResolver

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

@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;
}

It can be seen that the first exception is whether the exception type is ResponseStatusException or if it is, then the resolveResponseStatusException method is called directly to handle the exception information. If not, the @ResponseStatus annotation on the exception class is searched, and the abnormal information is found out, then the resolveResponseStatus method is called for processing.

You can see that there are two types of exceptions handled by ResponseStatusExceptionResolver:

  • The exception class directly inherits from ResponseStatusException. This exception class can directly extract the desired information from it.
  • Ordinary exception class annotated by @ ResponseStatus. In this case, exception information is extracted from @ ResponseStatus annotation.

This is relatively simple. There's nothing to say.

2.4 SimpleMappingExceptionResolver

SimpleMappingExceptionResolver displays different error pages according to different exceptions. Some friends may not have used SimpleMappingExceptionResolver, so brother song will briefly talk about the usage here.

The configuration of SimpleMappingExceptionResolver is very simple. You can directly provide an instance of SimpleMappingExceptionResolver, as follows:

@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;
}

Configure the corresponding relationship between the exception and the view in mappings. Write the full path of the exception class. The following 11 and 22 represent the view name; The mapping relationship between view and response status codes is configured in statusCodes. After the configuration is completed, if our project throws an ArithmeticException exception at runtime, view 11 will be displayed, and if our project throws a NullPointerException exception at runtime, view 22 will be displayed.

This is the usage. After understanding the usage, let's look at the source code, which is easy to understand. Let's directly look at the doResolveException method:

@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 determinestaticode to check whether the view has a corresponding statusCode.
  3. Call the applyStatusCodeIfPossible method to set the statusCode to the response. This method is very simple and not much to say.
  4. Call getModelAndView method to construct a ModelAndView object and return. During construction, set exception parameters at the same time. The key of exception information is exception by default.

There are two additional methods to compare with the above process.

determineViewName

This is to find the view name according to the exception type. Let's see the specific search method:

@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 &amp;&amp; this.defaultErrorView != null) {
		viewName = this.defaultErrorView;
	}
	return viewName;
}
  1. If the current exception is included in excludedexception, null will be returned directly (meaning that the current exception is ignored and handled directly in the default way).
  2. If exceptionMappings is not null, directly call the findMatchingViewName method to find the view name corresponding to the exception (the exceptionMappings variable is the mapping relationship we configured earlier). The specific search method is to traverse the mapping table we configured earlier.
  3. If the corresponding viewName is not found and the user has configured the defaultErrorView, assign the defaultErrorView to the viewName and return the viewName.

determineStatusCode

@Nullable
protected Integer determineStatusCode(HttpServletRequest request, String viewName) {
	if (this.statusCodes.containsKey(viewName)) {
		return this.statusCodes.get(viewName);
	}
	return this.defaultStatusCode;
}

This is relatively easy. Go directly to the statusCodes to check whether there is a status code corresponding to the view. If there is, it will be returned directly. If not, it will be returned as a default.

3.HandlerExceptionResolverComposite

Finally, there is also a HandlerExceptionResolverComposite, which needs to be introduced to you. This is a combined exception handler, which is used to represent the real exception handlers.

@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. We have seen this writing method many times and will not repeat it.

4. Summary

Well, today I'll talk to you about the exception handling system in spring MVC. On the whole, it's not difficult. My friends can taste it in detail</ string,></throwable>

Keywords: Java

Added by lisa3711 on Mon, 07 Mar 2022 18:03:03 +0200