We can use multiple HandlerExceptionResolver instances at once by combining them with HandlerExceptionResolverComposite.
The following method of HandlerExceptionResolverComposite can be used to specify multiple HandlerExceptionResolvers:
public void setExceptionResolvers(List<HandlerExceptionResolver> exceptionResolvers)
In HandlerExceptionResolverComposite#resolveException(...), the provided list of resolvers are iterated. The first one to return a non-null ModelAndView wins; otherwise null is returned.
Note that if any of the provided HandlerExceptionResolver instances implements the Ordered interface, the value returned by Ordered#getOrder() is not used to sort the resolvers — they are iterated in the list's insertion order instead. So what is Ordered#getOrder() actually used for? It is used by DispatcherServlet to order the default and application-registered exception resolvers before they are assembled into the composite (we'll look at this with an example in the next tutorial).
In the following example, we'll configure two HandlerExceptionResolvers with HandlerExceptionResolverComposite: one is the custom resolver from our last tutorial example, and the other is SimpleMappingExceptionResolver.
Example
Registering HandlerExceptionResolvers in JavaConfig class
package com.logicbig.example;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.web.servlet.HandlerExceptionResolver;
import org.springframework.web.servlet.ViewResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.handler.HandlerExceptionResolverComposite;
import org.springframework.web.servlet.handler.SimpleMappingExceptionResolver;
import org.springframework.web.servlet.view.InternalResourceViewResolver;
import java.util.Arrays;
import java.util.List;
import java.util.Properties;
@EnableWebMvc
@ComponentScan("com.logicbig.example")
public class AppConfig implements WebMvcConfigurer {
@Override
public void extendHandlerExceptionResolvers(
List<HandlerExceptionResolver> resolvers) {
HandlerExceptionToViewResolver r = new HandlerExceptionToViewResolver();
SimpleMappingExceptionResolver s = new SimpleMappingExceptionResolver();
s.setDefaultErrorView("default-error-page");
Properties p = new Properties();
p.setProperty(IllegalAccessException.class.getName(),
"simple-test-error-view");
s.setExceptionMappings(p);
HandlerExceptionResolverComposite c = new HandlerExceptionResolverComposite();
c.setExceptionResolvers(Arrays.asList(r, s));
resolvers.add(c);
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver =
new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/views/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}
The Controller
package com.logicbig.example;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.naming.OperationNotSupportedException;
@Controller
public class ExampleController {
//should be handled by our custom resolver because of @ErrorView
@ErrorView(value = "test-error-view", status = HttpStatus.GONE)
@RequestMapping("/test1")
public String handleRequest () throws Exception {
throw new Exception("test exception 1");
}
//should be handled by handleException() because of @ExceptionHandler.
//Default resolvers are processed before application registered ones
@RequestMapping("/test2")
public String handleRequest2 () throws Exception {
throw new OperationNotSupportedException("test exception 2");
}
//should be handled by SimpleMappingExceptionResolver's default view
@RequestMapping("/test3")
public String handleRequest3 () throws Exception {
throw new Exception("test exception 3");
}
//should be handled by SimpleMappingExceptionResolver
// IllegalAccessException is mapped to view 'test-error-view'
@RequestMapping("/test4")
public String handleRequest4 () throws Exception {
throw new IllegalAccessException("test exception 4");
}
//should be handled by our custom resolver even though IllegalAccessException
//is mapped in SimpleMappingExceptionResolver. Reason: in iteration order
// our custom resolver is first to process
@ErrorView(value = "test-error-view", status = HttpStatus.FORBIDDEN)
@RequestMapping("/test5")
public String handleRequest5 () throws Exception {
throw new IllegalAccessException("test exception 5");
}
@ExceptionHandler
@ResponseBody
public String handleException (OperationNotSupportedException e) {
return "exception :" + e.toString();
}
}
JSP pages
src/main/webapp/WEB-INF/views/test-error-view.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<body>
<h3>Test Error View</h3>
<p>Request Uri: <b>${requestUri}</b></p>
<p>Exception: <b>${exception['class'].name}</b></p>
<p>Message: <b>${exception.message}</b></p>
<p>Response status: <b>${statusValue} (${statusStr})</b></p>
</body>
</html>
src/main/webapp/WEB-INF/views/simple-test-error-view.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<body>
<h3>Simple Test Error View</h3>
<p>Exception: <b>${exception['class'].name}</b></p>
<p>Message: <b>${exception.message}</b></p>
</body>
</html>
src/main/webapp/WEB-INF/views/default-error-page.jsp<%@ page language="java"
contentType="text/html; charset=ISO-8859-1"
pageEncoding="ISO-8859-1"%>
<html>
<body>
<h3>This is the default exception page</h3>
<p>Exception: <b>${exception['class'].name}</b></p>
<p>Message: <b>${exception.message}</b></p>
</body>
</html>
Running the example
To try examples, run embedded Jetty (configured in pom.xml of example project below):
mvn jetty:run
output
/test1
/test2
/test3
/test4
/test5
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 static org.hamcrest.Matchers.containsString;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.view;
@ExtendWith(SpringExtension.class)
@ContextConfiguration(classes = AppConfig.class)
@WebAppConfiguration
public class ExampleControllerTest {
@Autowired
private WebApplicationContext webApplicationContext;
private MockMvc mockMvc;
@BeforeEach
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(webApplicationContext).build();
}
@Test
public void test1() throws Exception {
mockMvc.perform(get("/test1"))
.andExpect(status().isGone())
.andExpect(view().name("test-error-view"));
}
@Test
public void test2() throws Exception {
mockMvc.perform(get("/test2"))
.andExpect(status().isOk())
.andExpect(content().string(containsString("test exception 2")));
}
@Test
public void test3() throws Exception {
mockMvc.perform(get("/test3"))
.andExpect(view().name("default-error-page"));
}
@Test
public void test4() throws Exception {
mockMvc.perform(get("/test4"))
.andExpect(view().name("simple-test-error-view"));
}
@Test
public void test5() throws Exception {
mockMvc.perform(get("/test5"))
.andExpect(status().isForbidden())
.andExpect(view().name("test-error-view"));
}
}
mvn clean test -Dtest="ExampleControllerTest" Output$ mvn clean test -Dtest="ExampleControllerTest" [INFO] Scanning for projects... [INFO] [INFO] -----< com.logicbig.example:handler-exception-resolver-composite >------ [INFO] Building handler-exception-resolver-composite 1.0-SNAPSHOT [INFO] from pom.xml [INFO] --------------------------------[ war ]--------------------------------- [INFO] [INFO] --- clean:3.2.0:clean (default-clean) @ handler-exception-resolver-composite --- [INFO] Deleting D:\example-projects\spring-mvc\handler-exception-resolver-composite\target [INFO] [INFO] --- resources:3.3.1:resources (default-resources) @ handler-exception-resolver-composite --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\handler-exception-resolver-composite\src\main\resources [INFO] [INFO] --- compiler:3.3:compile (default-compile) @ handler-exception-resolver-composite --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 5 source files to D:\example-projects\spring-mvc\handler-exception-resolver-composite\target\classes [INFO] [INFO] --- resources:3.3.1:testResources (default-testResources) @ handler-exception-resolver-composite --- [INFO] skip non existing resourceDirectory D:\example-projects\spring-mvc\handler-exception-resolver-composite\src\test\resources [INFO] [INFO] --- compiler:3.3:testCompile (default-testCompile) @ handler-exception-resolver-composite --- [INFO] Changes detected - recompiling the module! [INFO] Compiling 1 source file to D:\example-projects\spring-mvc\handler-exception-resolver-composite\target\test-classes [INFO] [INFO] --- surefire:3.2.5:test (default-test) @ handler-exception-resolver-composite --- [INFO] Using auto detected provider org.apache.maven.surefire.junit4.JUnit4Provider [INFO] [INFO] ------------------------------------------------------- [INFO] T E S T S [INFO] ------------------------------------------------------- [INFO] Running com.logicbig.example.ExampleControllerTest [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.514 s -- in com.logicbig.example.ExampleControllerTest [INFO] [INFO] Results: [INFO] [INFO] Tests run: 5, Failures: 0, Errors: 0, Skipped: 0 [INFO] [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 2.620 s [INFO] Finished at: 2026-09-14T21:41:40-05:00 [INFO] ------------------------------------------------------------------------
Example ProjectDependencies and Technologies Used: - spring-webmvc 7.0.6 (Spring Web MVC)
Version Compatibility: 3.2.9.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
|