16 Spring Exception Common Errors

16 Spring Exception Common Errors #

Hello, I am Fu Jian.

Today, we will learn about the exception handling mechanism in Spring. Spring provides a comprehensive exception handling framework for us to handle exceptions during application development. However, we may encounter some troubles when using it. In the following, I will take you through two typical error cases and guide you to delve into the source code to gain a deeper understanding.

Case 1: Be careful with Filter Exceptions #

To facilitate the explanation, we will continue to use the case of student registration used in the previous lesson on transaction processing to discuss the issue of exception handling:

@Controller
@Slf4j
public class StudentController {
    public StudentController(){
        System.out.println("construct");
    }

    @PostMapping("/regStudent/{name}")
    @ResponseBody
    public String saveUser(String name) throws Exception {
        System.out.println("......User registration succeeded");
        return "success";
    }
}

For security purposes, we need to add protection to the request by verifying the token to ensure the validity of the request. This token needs to be included in the header of each request, with the key “Token”.

To validate this token, we have introduced a filter to handle this validation work. Here, I am using a simple token: 111111.

When the token validation fails, a custom NotAllowException is thrown and handled by Spring:

@WebFilter
@Component
public class PermissionFilter implements Filter {
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("token");

        if (!"111111".equals(token)) {
            System.out.println("throw NotAllowException");
            throw new NotAllowException();
        }
        chain.doFilter(request, response);
    }

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    @Override
    public void destroy() {
    }
}

NotAllowException is just a subclass of RuntimeException:

public class NotAllowException extends RuntimeException {
    public NotAllowException() {
       super();
    }
}

At the same time, a RestControllerAdvice has been added to handle this exception. The handling method is also simple, it returns a resultCode of 403:

@RestControllerAdvice
public class NotAllowExceptionHandler {
    @ExceptionHandler(NotAllowException.class)
    @ResponseBody
    public String handle() {
        System.out.println("403");
        return "{\"resultCode\": 403}";
    }
}

To verify the failure scenario, we simulated a request and added a Token value of 111 to the HTTP request header. This would cause an error, and we can see if it would be handled by the NotAllowExceptionHandler.

However, on the console, we only see the following output, which actually indicates that the NotAllowExceptionHandler did not take effect:

throw NotAllowException

Let’s think about where the problem lies. Let’s first understand the exception handling process in Spring.

Case analysis #

Let’s first review the filter execution process diagram mentioned in Lesson 13. Here I have refined it:

Image

From this diagram, we can see that Spring enters the Servlet-related processing only after all the filters have been executed. The DispatcherServlet is the core of the entire Servlet processing, implementing the Front Controller design pattern, and providing a centralized access point for Spring Web MVC and responsible for the dispatching of responsibilities. It is here that Spring handles the correspondence between requests and handlers, as well as the problem we are concerned about in this case - global exception handling.

Actually, now that we know the rough reason why exceptions in filters cannot be uniformly handled, it is because the exception handling occurs in the red zone in the above diagram, i.e., doDispatch() in DispatcherServlet. At this time, all the filters have already been executed.

Next, we will analyze the logic of Spring Web for global exception handling and gain a deeper understanding of its internal principles.

First, let’s understand how ControllerAdvice is loaded by Spring and exposed externally. In the core configuration class WebMvcConfigurationSupport of Spring Web, the handlerExceptionResolver() method decorated with @Bean calls addDefaultHandlerExceptionResolvers() to add default exception resolvers.

@Bean
public HandlerExceptionResolver handlerExceptionResolver(
      @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager) {
   List<HandlerExceptionResolver> exceptionResolvers = new ArrayList<>();
   configureHandlerExceptionResolvers(exceptionResolvers);
   if (exceptionResolvers.isEmpty()) {
      addDefaultHandlerExceptionResolvers(exceptionResolvers, contentNegotiationManager);
   }
   extendHandlerExceptionResolvers(exceptionResolvers);
   HandlerExceptionResolverComposite composite = new HandlerExceptionResolverComposite();
   composite.setOrder(0);
   composite.setExceptionResolvers(exceptionResolvers);
   return composite;
}

Finally, as shown in the call stack graph below, Spring instantiates the ExceptionHandlerExceptionResolver class.

Image

From the source code, we can see that the ExceptionHandlerExceptionResolver class implements the InitializingBean interface and overrides the afterPropertiesSet() method.

public void afterPropertiesSet() {
   // Do this first, it may add ResponseBodyAdvice beans
   initExceptionHandlerAdviceCache();
    // Omitted non-critical code
}

And in initExceptionHandlerAdviceCache(), it completes the initialization of all ExceptionHandlers in ControllerAdvice. The specific operation is to find all beans annotated with @ControllerAdvice and put them in the member variable exceptionHandlerAdviceCache.

In our case, it refers to the NotAllowExceptionHandler exception handler.

private void initExceptionHandlerAdviceCache() {
   // Omitted non-critical code
   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);
}
// Omit non-critical code

From here, we can summarize that handlerExceptionResolver() in WebMvcConfigurationSupport instantiates and registers an instance of ExceptionHandlerExceptionResolver. All exception handlers annotated with @ControllerAdvice will be automatically scanned and loaded into the class member variable exceptionHandlerAdviceCache when ExceptionHandlerExceptionResolver is instantiated.

When the first request occurs, initHandlerExceptionResolvers() in DispatcherServlet will obtain all instances of HandlerExceptionResolver registered with Spring. Since ExceptionHandlerExceptionResolver implements the HandlerExceptionResolver interface, these instances of HandlerExceptionResolver will be written to the class member variable handlerExceptionResolvers.

Here is a partial code snippet from the core method doDispatch() in the DispatcherServlet class:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // Omit non-critical code

    try {
        ModelAndView mv = null;
        Exception dispatchException = null;
        try {
            // Omit non-critical code
            // Find the handler corresponding to the current request and execute it
            // Omit non-critical code
        }
        catch (Exception ex) {
            dispatchException = ex;
        }
        catch (Throwable err) {
            dispatchException = new NestedServletException("Handler dispatch failed", err);
        }
        processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
    }
    // Omit non-critical code

When Spring executes a user request and an exception occurs during the “find” and “execute” process for the handler corresponding to the request, the exception is assigned to dispatchException and then passed to processDispatchResult() for further processing.

private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
    @Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
    @Nullable Exception exception) throws Exception {
    boolean errorView = false;
    if (exception != null) {
        if (exception instanceof ModelAndViewDefiningException) {
            mv = ((ModelAndViewDefiningException) exception).getModelAndView();
        }
        else {
            Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
            mv = processHandlerException(request, response, handler, exception);
            errorView = (mv != null);
        }
    }
    // Omit non-critical code

After further processing, if exception is not null, it proceeds to processHandlerException() for handling.

protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
    @Nullable Object handler, Exception ex) throws Exception {
    // Omit non-critical code

    ModelAndView exMv = null;
    if (this.handlerExceptionResolvers != null) {
        for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
            exMv = resolver.resolveException(request, response, handler, ex);
            if (exMv != null) {
                break;
            }
        }
    }
    // Omit non-critical code

Then, processHandlerException() retrieves valid exception resolvers from the class member variable handlerExceptionResolvers and resolves the exception.

Clearly, the handlerExceptionResolvers must include the ExceptionHandlerExceptionResolver wrapped by our declared NotAllowExceptionHandler#NotAllowException exception handler.

Problem Fix #

To leverage Spring MVC’s exception handling mechanism, we need to make some modifications to the Filter. We manually catch exceptions and hand them over to the HandlerExceptionResolver for processing.

We can modify PermissionFilter like this, injecting the HandlerExceptionResolver:

@Autowired
@Qualifier("handlerExceptionResolver")
private HandlerExceptionResolver resolver;

Then, in the doFilter method, we catch exceptions and delegate them to the HandlerExceptionResolver for handling:

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    HttpServletRequest httpServletRequest = (HttpServletRequest) request;
    HttpServletResponse httpServletResponse = (HttpServletResponse) response;
    String token = httpServletRequest.getHeader("token");
    if (!"111111".equals(token)) {
        System.out.println("throw NotAllowException");
        resolver.resolveException(httpServletRequest, httpServletResponse, null, new NotAllowException());
        return;
    }
    chain.doFilter(request, response);
}

When we make a request with an incorrect token, we get the following output on the console:

throw NotAllowException
403

The returned JSON is:

{"resultCode": 403}

When we use the correct token for the request, these error messages are no longer present. With this, the problem is resolved.

Case 2: Special 404 Exception #

Continuing with the student registration case, in order to prevent some abnormal access, we need to record all 404 status access records and return a custom result.

When using RESTful interfaces, we usually return JSON data in a unified format, as follows:

{"resultCode": 404}

However, Spring has a default resource mapping for 404 exceptions, and it does not return the result we want, nor does it record this type of error.

So we added an ExceptionHandlerController, which is declared as @RestControllerAdvice to globally capture exceptions thrown in Spring MVC.

The purpose of ExceptionHandler is to capture the specified exceptions:

@RestControllerAdvice
public class MyExceptionHandler {
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(Exception.class)
    @ResponseBody
    public String handle404() {
        System.out.println("404");
        return "{\"resultCode\": 404}";
    }
}

We tried sending an incorrect URL request to the /regStudent interface we implemented earlier, changing the request address to /regStudent1, and got the following result:

{"timestamp":"2021-05-19T22:24:01.559+0000","status":404,"error":"Not Found","message":"No message available","path":"/regStudent1"}

Obviously, this result is not what we want. It seems to be the default result of Spring. What caused Spring not to use our defined exception handler?

Case Analysis #

We can start analyzing from the core processing code of exception handling, the doDispatch() method in DispatcherServlet:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
    // omit irrelevant code
    mappedHandler = getHandler(processedRequest);
    if (mappedHandler == null) {
        noHandlerFound(processedRequest, response);
        return;
    }
    // omit irrelevant code
}

First, the getHandler() method is called to get the current request handler. If it cannot be obtained, then noHandlerFound() is called:

protected void noHandlerFound(HttpServletRequest request, HttpServletResponse response) throws Exception {
    if (this.throwExceptionIfNoHandlerFound) {
        throw new NoHandlerFoundException(request.getMethod(), getRequestUri(request),
            new ServletServerHttpRequest(request).getHeaders());
    } else {
        response.sendError(HttpServletResponse.SC_NOT_FOUND);
    }
}

The logic of noHandlerFound() is very simple. If the throwExceptionIfNoHandlerFound property is true, it directly throws a NoHandlerFoundException. Otherwise, it further obtains the corresponding request handler for execution and returns the execution result to the client.

So, the truth is very close to us. We just need to set throwExceptionIfNoHandlerFound to true by default, so that the NoHandlerFoundException will be thrown and caught by doDispatch(). Then, as introduced in case 1, the custom exception handler MyExceptionHandler will be executed.

Therefore, let’s give it a try. Since the corresponding Spring configuration item for throwExceptionIfNoHandlerFound is throw-exception-if-no-handler-found, we add it to the application.properties configuration file and set its value to true.

After setting it up, restart the service and try it again. You will find that there is no change in the result, and this problem has not been solved.

In fact, there is another pit here. In the WebMvcAutoConfiguration class of Spring Web, two default ResourceHandler instances are added by default, one is used to handle the request path /webjars/*_*_, and the other is /**.

Even if the current request does not define any corresponding request handler, getHandler() will always get a handler to handle the current request, because the second matching /** path ResourceHandler determines that any request path will be handled by it. The condition mappedHandler == null will never be met, so noHandlerFound() will not be executed. Then, the NoHandlerFoundException will not be thrown, and it cannot be further handled by the subsequent exception handler.

Now, let’s further understand the detailed logic of this default ResourceHandler that was added by default through the source code.

First, let’s understand how ControllerAdvice is loaded by Spring and exposed to the outside.

Also in the WebMvcConfigurationSupport class, the resourceHandlerMapping() method annotated with @Bean creates an instance of the ResourceHandlerRegistry, and registers the ResourceHandler to the ResourceHandlerRegistry instance through addResourceHandlers():

@Bean
@Nullable
public HandlerMapping resourceHandlerMapping(
    @Qualifier("mvcUrlPathHelper") UrlPathHelper urlPathHelper,
    @Qualifier("mvcPathMatcher") PathMatcher pathMatcher,
    @Qualifier("mvcContentNegotiationManager") ContentNegotiationManager contentNegotiationManager,
    @Qualifier("mvcConversionService") FormattingConversionService conversionService,
    @Qualifier("mvcResourceUrlProvider") ResourceUrlProvider resourceUrlProvider) {

    Assert.state(this.applicationContext != null, "No ApplicationContext set");
    Assert.state(this.servletContext != null, "No ServletContext set");

    ResourceHandlerRegistry registry = new ResourceHandlerRegistry(this.applicationContext,
        this.servletContext, contentNegotiationManager, urlPathHelper);
    addResourceHandlers(registry);

    AbstractHandlerMapping handlerMapping = registry.getHandlerMapping();
    if (handlerMapping == null) {
        return null;
    }
    handlerMapping.setPathMatcher(pathMatcher);
    handlerMapping.setUrlPathHelper(urlPathHelper);
    handlerMapping.setInterceptors(getInterceptors(conversionService, resourceUrlProvider));
    handlerMapping.setCorsConfigurations(getCorsConfigurations());
    return handlerMapping;
}

Finally, through the getHandlerMapping() method in the ResourceHandlerRegistry instance, a SimpleUrlHandlerMapping instance is returned, which loads the collection of all ResourceHandlers and registers it in the Spring container:

protected AbstractHandlerMapping getHandlerMapping() {
    // omit irrelevant code
    Map<String, HttpRequestHandler> urlMap = new LinkedHashMap<>();
    for (ResourceHandlerRegistration registration : this.registrations) {
        for (String pathPattern : registration.getPathPatterns()) {
            ResourceHttpRequestHandler handler = registration.getRequestHandler();
            // omit irrelevant code
            urlMap.put(pathPattern, handler);
        }
    }
    return new SimpleUrlHandlerMapping(urlMap, this.order);
}

Let’s take a look at the call stack screenshot:

[Image Here]

From the code snippet, we can see that the method addResourceHandlers() in the current approach eventually executes the addResourceHandlers() method in the WebMvcAutoConfiguration class. Through this method, we can determine which collection of ResourceHandlers are registered in the Spring container:

public void addResourceHandlers(ResourceHandlerRegistry registry) {
   if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
   Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
   CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
   if (!registry.hasMappingForPattern("/webjars/**")) {
      customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
   }
   String staticPathPattern = this.mvcProperties.getStaticPathPattern();
   if (!registry.hasMappingForPattern(staticPathPattern)) {
      customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
            .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
            .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
   }
}

Thus, we can verify the conclusion we came to earlier, which is that two ResourceHandlers are added here: one to handle requests with the path /webjars/*_*_, and another to handle requests with the path /**.

Please pay attention to the conditional statement at the beginning of the method. If this.resourceProperties.isAddMappings() is false, the method will return immediately and the two ResourceHandlers will not be added.

if (!this.resourceProperties.isAddMappings()) {
  logger.debug("Default resource handling disabled");
  return;
}

Thus, we have two ResourceHandlers that have been instantiated and registered in the Spring container: one to handle requests with the path /webjars/*_*_, and another to handle requests with the path /**.

Similarly, when the first request occurs, the initHandlerMappings() method in the DispatcherServlet will retrieve all instances of HandlerMapping registered in Spring. The SimpleUrlHandlerMapping class happens to implement the HandlerMapping interface, and these instances of SimpleUrlHandlerMapping will be stored in the member variable handlerMappings.

private void initHandlerMappings(ApplicationContext context) {
   this.handlerMappings = null;
// omitted non-essential code
   if (this.detectAllHandlerMappings) {
      // Find all HandlerMappings in the ApplicationContext, including ancestor contexts.
      Map<String, HandlerMapping> matchingBeans =
            BeanFactoryUtils.beansOfTypeIncludingAncestors(context, HandlerMapping.class, true, false);
      if (!matchingBeans.isEmpty()) {
         this.handlerMappings = new ArrayList<>(matchingBeans.values());
         // We keep HandlerMappings in sorted order.
         AnnotationAwareOrderComparator.sort(this.handlerMappings);
      }
   }
   // omitted non-essential code
}

Let’s now understand how the ResourceHandler wrapped as handlerMappings is consumed and processed by Spring.

Let’s review the core code in the doDispatch() method of the DispatcherServlet:

protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
        // omitted non-essential code
         mappedHandler = getHandler(processedRequest);
         if (mappedHandler == null) {
            noHandlerFound(processedRequest, response);
            return;
         }
         // omitted non-essential code
}

The getHandler() method here will iterate through the handlerMappings member variable:

protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
   if (this.handlerMappings != null) {
      for (HandlerMapping mapping : this.handlerMappings) {
         HandlerExecutionChain handler = mapping.getHandler(request);
         if (handler != null) {
            return handler;
         }
      }
   }
   return null;
}

Since there is an instance of SimpleUrlHandlerMapping here, which intercepts requests for all paths:

Therefore, the mappedHandler == null condition will not be satisfied in the doDispatch() method, and thus the code will not reach the noHandlerFound() method. As a result, the NoHandlerFoundException exception will not be thrown and will not be further processed by subsequent exception handlers.

Fixing the Issue #

How can we solve this problem? Do you still remember the first two lines of code in the addResourceHandlers() method of the WebMvcAutoConfiguration class? If this.resourceProperties.isAddMappings() is false, this method will return immediately and the two ResourceHandlers will not be added.

public void addResourceHandlers(ResourceHandlerRegistry registry) {
   if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
   // omitted non-essential code
}

The isAddMappings() method calls ResourceProperties’s isAddMappings() method, which is as follows:

public boolean isAddMappings() {
   return this.addMappings;
}

Now, the answer is right here. We need to add the following two configuration properties:

spring.resources.add-mappings=false
spring.mvc.throwExceptionIfNoHandlerFound=true

And modify the @ExceptionHandler in MyExceptionHandler to NoHandlerFoundException:

@ExceptionHandler(NoHandlerFoundException.class)

This case is relatively common in real production environments. Knowing how to solve it is the first step, and understanding its internal mechanism is even more important. Furthermore, when you further study the code, you will find that there is not just one solution to this problem, leaving the rest for you to explore.

Key takeaways #

Through the introduction of the above two cases, I believe you have gained a further understanding of the exception handling mechanism in Spring MVC. Let’s review the key points once again:

  • The doDispatch() method in the DispatcherServlet class is the core of the entire servlet processing. It not only handles request dispatching, but also provides a series of functionalities such as unified exception handling.
  • WebMvcConfigurationSupport is a crucial configuration class in Spring Web. It relies on this class to register the wrapper for exception handlers (HandlerExceptionResolver) and resource handlers (SimpleUrlHandlerMapping).

Thought Exercise #

In this lesson, the two cases will traverse the corresponding resource processors and exception handlers when the first request is sent, and register them in the class member variables of the corresponding DispatcherServlet. Do you know how it is triggered?

Looking forward to your thoughts, let’s discuss in the comments section!