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:
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.
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 ResourceHandler
s 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 ResourceHandler
s 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 ResourceHandler
s 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 ResourceHandler
s will not be added.
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
Thus, we have two ResourceHandler
s 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 ResourceHandler
s 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 theDispatcherServlet
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!