This example demonstrates how to modify the default HandlerExceptionResolver functionality in Spring MVC.
The use case we are going to use is: we will map a Spring internal exception to a custom error page. By default, Spring internal exceptions are processed by DefaultHandlerExceptionResolver. This example demonstrates how to selectively modify that default behavior. We will use SimpleMappingExceptionResolver to achieve the desired result.
Prerequisite
Ordering and customization of default HandlerExceptionResolvers.`
Example
Creating SimpleMappingExceptionResolver and registering it as a bean
package com.logicbig.example;
import org.springframework.context.annotation.*;
import org.springframework.core.Ordered;
import org.springframework.http.HttpStatus;
import org.springframework.web.servlet.*;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import java.util.Properties;
@EnableWebMvc
@ComponentScan("com.logicbig.example")
public class AppConfig {
@Bean
HandlerExceptionResolver customExceptionResolver () {
SimpleMappingExceptionResolver s = new SimpleMappingExceptionResolver();
Properties p = new Properties();
//mapping spring internal error NoHandlerFoundException to a view name.
p.setProperty(NoHandlerFoundException.class.getName(), "error-page");
s.setExceptionMappings(p);
//uncomment following line if we want to send code other than default 200
//s.addStatusCode("error-page", HttpStatus.NOT_FOUND.value());
//This resolver will be processed before default ones
s.setOrder(Ordered.HIGHEST_PRECEDENCE);
return s;
}
@Bean
public ViewResolver viewResolver () {
InternalResourceViewResolver viewResolver =
new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
The above SimpleMappingExceptionResolver maps Spring's internal NoHandlerFoundException to a custom page; the rest of the default functionality remains unchanged.
WebApplicationInitializer implementation
package com.logicbig.example;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.FrameworkServlet;
import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;
public class AppInitializer extends
AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return null;
}
@Override
protected Class<?>[] getServletConfigClasses() {
return new Class<?>[]{AppConfig.class};
}
@Override
protected String[] getServletMappings() {
return new String[]{"/"};
}
@Override
protected DispatcherServlet createDispatcherServlet(WebApplicationContext wac) {
DispatcherServlet ds = new DispatcherServlet(wac);
//setting this flag to true will throw NoHandlerFoundException instead of 404 page
// removed in spring 7 ds.setThrowExceptionIfNoHandlerFound(true);
return ds;
}
}
What is throwExceptionIfNoHandlerFound?
NoHandlerFoundException is only thrown if the throwExceptionIfNoHandlerFound property of DispatcherServlet is set to true. In the example below, this is done via DispatcherServlet#setThrowExceptionIfNoHandlerFound(true). In the Spring version this example is pinned to (4.3.5.RELEASE), that property defaults to false.
Note: starting Spring Framework 6.1, throwExceptionIfNoHandlerFound defaults to true, so DispatcherServlet always raises NoHandlerFoundException when no handler is found; the property no longer needs to be set explicitly. As of Spring Framework 7.0, the setThrowExceptionIfNoHandlerFound(boolean)/getThrowExceptionIfNoHandlerFound() methods have been removed from DispatcherServlet entirely.
JSP page
src/main/webapp/WEB-INF/views/error-page.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<body>
<h3>This is custom Spring exception page</h3>
<p>Exception: <b>${exception.message}</b></p>
</body>
</html>
The Controller
package com.logicbig.example;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
@Controller
public class ExampleController {
/**
* This will throw MissingPathVariableException with response code 500
*/
@RequestMapping("/test/{id}")
@ResponseBody
public String handleRequest (@PathVariable("testId") String id) {
return "id: "+id;
}
/**
* The exception should be processed by @ExceptionHandler method
*/
@RequestMapping("/test")
public String handleRequest2 () throws Exception {
throw new IllegalStateException();
}
@ExceptionHandler
@ResponseBody
public String handleException (IllegalStateException b) {
return "from @ExceptionHandler method: " + b ;
}
}
Running The Example
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
output
URI: /test/{id}
This should throw MissingPathVariableException, which is not mapped by our SimpleMappingExceptionResolver.
The above exception is handled by DefaultHandlerExceptionResolver.
URI: /test
This should invoke the handleException(..) method of our controller. This method is annotated with @ExceptionHandler and has a parameter of the matching exception type.
This shows that ExceptionHandlerExceptionResolver is processed as expected.
Other URIs
Other URIs would normally return the container's default 404 error page. In our case, we configured DispatcherServlet to throw NoHandlerFoundException, which is mapped to "error-page" by our SimpleMappingExceptionResolver.
Let's check the response status code using Chrome's DevTools (accessed via F12).
If SimpleMappingExceptionResolver doesn't set any status code explicitly, the code 200 is returned. If we want to return 404 instead, we can use SimpleMappingExceptionResolver#addStatusCode("error-page", 404);
Integration Test
package com.logicbig.example;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.NoHandlerFoundException;
import static org.hamcrest.Matchers.instanceOf;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@ContextConfiguration(classes = AppConfig.class)
public class ExampleControllerTest {
@Autowired
private WebApplicationContext wac;
private MockMvc mockMvc;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(wac).build();
}
@Test
public void testMissingPathVariableMessage() throws Exception {
mockMvc.perform(get("/test/4"))
.andExpect(status().isInternalServerError())
.andExpect(status().reason(
"Required path variable 'testId' is not present."));
}
@Test
public void testExceptionHandlerMethod() throws Exception {
mockMvc.perform(get("/test"))
.andExpect(status().isOk())
.andExpect(content().string(
"from @ExceptionHandler method: "
+ "java.lang.IllegalStateException"));
}
@Test
public void testNoHandlerFound() throws Exception {
mockMvc.perform(get("/otherPage"))
.andExpect(status().isOk())
.andExpect(model().attribute("exception",
instanceOf(NoHandlerFoundException.class)));
}
}
Output$ mvn clean test -Dtest="ExampleControllerTest" [INFO] Scanning for projects... [INFO] [INFO] ---< com.logicbig.example:default-exception-resolver-customization >---- [INFO] Building default-exception-resolver-customization 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ default-exception-resolver-customization --- [INFO] Deleting D:\example-projects\spring-mvc\default-exception-resolver-customization\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ default-exception-resolver-customization --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\default-exception-resolver-customization\src\main\resources [INFO] [INFO] --- compiler:3.16.0:compile (default-compile) @ default-exception-resolver-customization --- [INFO] Recompiling the module because of changed source code. [INFO] Compiling 3 source files with javac [debug target 25] to target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ default-exception-resolver-customization --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\default-exception-resolver-customization\src\test\resources [INFO] [INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ default-exception-resolver-customization --- [INFO] Recompiling the module because of changed dependency. [INFO] Compiling 1 source file with javac [debug target 25] to target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ default-exception-resolver-customization --- [INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.ExampleControllerTest [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.040 s -- in com.logicbig.example.ExampleControllerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 5.541 s [INFO] Finished at: 2026-09-16T02:37:03-05:00 [INFO] ------------------------------------------------------------------------ INFO: Completed initialization in 2 ms INFO: Completed initialization in 2 ms INFO: Completed initialization in 1 ms WARNING: No mapping for GET /otherPage
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 4.2.0.RELEASE - 7.0.6 Version compatibilities of spring-webmvc with this example: Versions in green have been tested.
- spring-test 7.0.6 (Spring TestContext Framework)
- jakarta.servlet-api 6.1.0 (Jakarta Servlet API documentation)
- junit-jupiter-engine 6.0.3 (Module "junit-jupiter-engine" of JUnit)
- hamcrest 3.0 (Core API and libraries of hamcrest matcher framework)
- JDK 25
- Maven 3.9.11
|