Any web application requires good design for exception handling because we don’t want to serve container generated page when any unhandled exception is thrown by our application.
Having a well defined exception handling approach is a huge plus point for any web application framework, that being said Spring MVC framework delivers well when it comes to exception and error handling in our web applications.
Spring MVC Framework provides following ways to help us achieving robust exception handling.
These exception handler methods are just like other request handler methods and we can build error response and respond with different error page. We can also send JSON error response, that we will look later on in our example.
If there are multiple exception handler methods defined, then handler method that is closest to the Exception class is used. For example, if we have two handler methods defined for IOException and Exception and our request handler method throws IOException, then handler method for IOException will get executed.
The handler methods in Global Controller Advice is same as Controller based exception handler methods and used when controller class is not able to handle the exception.
SimpleMappingExceptionResolver is the default implementation class, it allows us to configure exceptionMappings where we can specify which resource to use for a particular exception. We can also override it to create our own global handler with our application specific changes, such as logging of exception messages.
Let’s create a Spring MVC project where we will look into the implementation of Controller based, AOP Based and Exception Resolver Based exception and error handling approaches. We will also write a exception handler method that will return JSON response, so if you are new to JSON in Spring, read Spring Restful JSON Tutorial.
Our final project will look like below image, we will look at all the components of our application one by one.
Apart from standard Spring MVC dependencies, we would also need Jackson JSON API dependency for JSON support.
Our final pom.xml file looks like below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
<?xmlversion="1.0"encoding="UTF-8"?>
<projectxmlns=" http://maven.apache.org/POM/4.0.0"xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=" http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.journaldev.spring</groupId>
<artifactId>SpringExceptionHandling</artifactId>
<name>SpringExceptionHandling</name>
<packaging>war</packaging>
<version>1.0.0-BUILD-SNAPSHOT</version>
<properties>
<java-version>1.6</java-version>
<org.springframework-version>4.0.2.RELEASE</org.springframework-version>
<org.aspectj-version>1.7.4</org.aspectj-version>
<org.slf4j-version>1.7.5</org.slf4j-version>
<jackson.databind-version>2.2.3</jackson.databind-version>
</properties>
<dependencies>
<!-- Jackson -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.databind-version}</version>
</dependency>
<!-- Spring -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${org.springframework-version}</version>
<exclusions>
<!-- Exclude Commons Logging in favor of SLF4j -->
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>${org.slf4j-version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
<version>${org.slf4j-version}</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>log4j</groupId>
<artifactId>log4j</artifactId>
<version>1.2.15</version>
<exclusions>
<exclusion>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
</exclusion>
<exclusion>
<groupId>javax.jms</groupId>
<artifactId>jms</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jdmk</groupId>
<artifactId>jmxtools</artifactId>
</exclusion>
<exclusion>
<groupId>com.sun.jmx</groupId>
<artifactId>jmxri</artifactId>
</exclusion>
</exclusions>
<scope>runtime</scope>
</dependency>
<!-- @Inject -->
<dependency>
<groupId>javax.inject</groupId>
<artifactId>javax.inject</artifactId>
<version>1</version>
</dependency>
<!-- Servlet -->
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>servlet-api</artifactId>
<version>2.5</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet.jsp</groupId>
<artifactId>jsp-api</artifactId>
<version>2.1</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>jstl</artifactId>
<version>1.2</version>
</dependency>
<!-- Test -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.7</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<artifactId>maven-eclipse-plugin</artifactId>
<version>2.9</version>
<configuration>
<additionalProjectnatures>
<projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
</additionalProjectnatures>
<additionalBuildcommands>
<buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
</additionalBuildcommands>
<downloadSources>true</downloadSources>
<downloadJavadocs>true</downloadJavadocs>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>2.5.1</version>
<configuration>
<source>1.6</source>
<target>1.6</target>
<compilerArgument>-Xlint:all</compilerArgument>
<showWarnings>true</showWarnings>
<showDeprecation>true</showDeprecation>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.2.1</version>
<configuration>
<mainClass>org.test.int1.Main</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
|
I have updated Spring Framework, AspectJ, Jackson and slf4j versions to use the latest one.
Our web.xml file looks like below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
<?xmlversion="1.0"encoding="UTF-8"?>
<web-appversion="2.5"xmlns=" http://java.sun.com/xml/ns/javaee"
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation=" http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/spring.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<error-page>
<error-code>404</error-code>
<location>/resources/404.jsp</location>
</error-page>
</web-app>
|
Most of the part is for plugging in Spring Framework for our web application, except the error-page defined for 404 error. So when our application will throw 404 error, this page will be used as response. This configuration is used by container when our spring web application throws 404 error code.
I have defined Employee bean as model class, however we will be using it in our application just to return valid response in specific scenario. We will be deliberately throwing different types of exceptions in most of the cases.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
packagecom.journaldev.spring.model;
publicclassEmployee {
privateString name;
privateintid;
publicString getName() {
returnname;
}
publicvoidsetName(String name) {
this.name = name;
}
publicintgetId() {
returnid;
}
publicvoidsetId(intid) {
this.id = id;
}
}
|
Since we will be returning JSON response too, let’s create a java bean with exception details that will be sent as response.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
packagecom.journaldev.spring.model;
publicclassExceptionJSONInfo {
privateString url;
privateString message;
publicString getUrl() {
returnurl;
}
publicvoidsetUrl(String url) {
this.url = url;
}
publicString getMessage() {
returnmessage;
}
publicvoidsetMessage(String message) {
this.message = message;
}
}
|
Let’s create a custom exception class to be used by our application.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
packagecom.journaldev.spring.exceptions;
importorg.springframework.http.HttpStatus;
importorg.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="Employee Not Found")//404
publicclassEmployeeNotFoundExceptionextendsException {
privatestaticfinallongserialVersionUID = -3332292346834265371L;
publicEmployeeNotFoundException(intid){
super("EmployeeNotFoundException with id="+id);
}
}
|
Notice that we can use @ResponseStatus annotation with exception classes to define the HTTP code that will be sent by our application when this type of exception is thrown by our application and handled by our exception handling implementations.
As you can see that I am setting HTTP status as 404 and we have an error-page defined for this, so our application should use the error page for this type of exception if we are not returning any view.
We can also override the status code in our exception handler method, think of it as default http status code when our exception handler method is not returning any view page as response.
Let’s look at our controller class where we will throw different type of exceptions.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
|
packagecom.journaldev.spring.controllers;
importjava.io.IOException;
importjava.sql.SQLException;
importjavax.servlet.http.HttpServletRequest;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.stereotype.Controller;
importorg.springframework.ui.Model;
importorg.springframework.web.bind.annotation.ExceptionHandler;
importorg.springframework.web.bind.annotation.PathVariable;
importorg.springframework.web.bind.annotation.RequestMapping;
importorg.springframework.web.bind.annotation.RequestMethod;
importorg.springframework.web.bind.annotation.ResponseBody;
importorg.springframework.web.servlet.ModelAndView;
importcom.journaldev.spring.exceptions.EmployeeNotFoundException;
importcom.journaldev.spring.model.Employee;
importcom.journaldev.spring.model.ExceptionJSONInfo;
@Controller
publicclassEmployeeController {
privatestaticfinalLogger logger = LoggerFactory.getLogger(EmployeeController.class);
@RequestMapping(value="/emp/{id}", method=RequestMethod.GET)
publicString getEmployee(@PathVariable("id")intid, Model model)throwsException{
//deliberately throwing different types of exception
if(id==1){
thrownewEmployeeNotFoundException(id);
}elseif(id==2){
thrownewSQLException("SQLException, id="+id);
}elseif(id==3){
thrownewIOException("IOException, id="+id);
}elseif(id==10){
Employee emp =newEmployee();
emp.setName("Pankaj");
emp.setId(id);
model.addAttribute("employee", emp);
return"home";
}else{
thrownewException("Generic Exception, id="+id);
}
}
@ExceptionHandler(EmployeeNotFoundException.class)
publicModelAndView handleEmployeeNotFoundException(HttpServletRequest request, Exception ex){
logger.error("Requested URL="+request.getRequestURL());
logger.error("Exception Raised="+ex);
ModelAndView modelAndView =newModelAndView();
modelAndView.addObject("exception", ex);
modelAndView.addObject("url", request.getRequestURL());
modelAndView.setViewName("error");
returnmodelAndView;
}
}
|
Notice that for EmployeeNotFoundException handler, I am returning ModelAndView and hence http status code will be sent as OK (200). If it would have been returning void, then http status code would have been sent as 404. We will look into this type of implementation in our global exception handler implementation.
Since I am handling only EmployeeNotFoundException in controller, all other exceptions thrown by our controller will be handled by global exception handler.
Here is our global exception handler controller class.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
|
packagecom.journaldev.spring.controllers;
importjava.io.IOException;
importjava.sql.SQLException;
importjavax.servlet.http.HttpServletRequest;
importorg.slf4j.Logger;
importorg.slf4j.LoggerFactory;
importorg.springframework.http.HttpStatus;
importorg.springframework.web.bind.annotation.ControllerAdvice;
importorg.springframework.web.bind.annotation.ExceptionHandler;
importorg.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
publicclassGlobalExceptionHandler {
privatestaticfinalLogger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(SQLException.class)
publicString handleSQLException(HttpServletRequest request, Exception ex){
logger.info("SQLException Occured:: URL="+request.getRequestURL());
return"database_error";
}
@ResponseStatus(value=HttpStatus.NOT_FOUND, reason="IOException occured")
@ExceptionHandler(IOException.class)
publicvoidhandleIOException(){
logger.error("IOException handler executed");
//returning 404 error code
}
}
|
Notice that for SQLException, I am returning database_error.jsp as response page with http status code as 200.
For IOException, we are returning void with status code as 404, so our error-page will be used in this case.
As you can see that I am not handling any other types of exception here, that part I have left for HandlerExceptionResolver implementation.
We are just extending SimpleMappingExceptionResolver and overriding one of the method, but we can override it’s most important method resolveException for logging and sending different types of view pages. But that is same as using ControllerAdvice implementation, so I am leaving it. We will be using it to configure view page for all the other exceptions not handled by us by responding with generic error page.
Our spring bean configuration file looks like below.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
|
<?xmlversion="1.0"encoding="UTF-8"?>
<beans:beansxmlns=" http://www.springframework.org/schema/mvc"
xmlns:xsi=" http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans=" http://www.springframework.org/schema/beans"
xmlns:context=" http://www.springframework.org/schema/context"
xsi:schemaLocation=" http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Enables the Spring MVC @Controller programming model -->
<annotation-driven/>
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resourcesmapping="/resources/**"location="/resources/"/>
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:beanclass="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:propertyname="prefix"value="/WEB-INF/views/"/>
<beans:propertyname="suffix"value=".jsp"/>
</beans:bean>
<beans:beanid="simpleMappingExceptionResolver"class="com.journaldev.spring.resolver.MySimpleMappingExceptionResolver">
<beans:propertyname="exceptionMappings">
<beans:map>
<beans:entrykey="Exception"value="generic_error"></beans:entry>
</beans:map>
</beans:property>
<beans:propertyname="defaultErrorView"value="generic_error"/>
</beans:bean>
<!-- Configure to plugin JSON as request and response in method handler -->
<beans:beanclass="org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter">
<beans:propertyname="messageConverters">
<beans:list>
<beans:refbean="jsonMessageConverter"/>
</beans:list>
</beans:property>
</beans:bean>
<!-- Configure bean to convert JSON to POJO and vice versa -->
<beans:beanid="jsonMessageConverter"class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter">
</beans:bean>
<context:component-scanbase-package="com.journaldev.spring"/>
</beans:beans>
|
Notice the beans configured for supporting JSON in our web application. The only part related to exception handling is the simpleMappingExceptionResolver bean definition where we are defining generic_error.jsp as the view page for Exception class. This make sure that any exception not handled by our application will not result in sending server generated error page as the response.
It’s time to look into the last part of our application, our view pages that will be used in our application.
1
2
3
4
5
6
7
8
9
10
11
|
<%@ taglib uri=" http://java.sun.com/jsp/jstl/core" prefix="c" %>
<%@ page session="false" %>
<html>
<head>
<title>Home</title>
</head>
<body>
<h3>Hello ${employee.name}!</h3><br>
<h4>Your ID is ${employee.id}</h4>
</body>
</html>
|
home.jsp is used to respond with valid data, i.e when we get id as 10 in the client request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPEhtml PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<metahttp-equiv="Content-Type"content="text/html; charset=UTF-8">
<title>404 Error Page</title>
</head>
<body>
<h2>Resource Not Found Error Occured, please contact support.</h2>
</body>
</html>
|
404.jsp is used for generating view for 404 http status code, for our implementation this should be the response when we get id as 3 in client request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPEhtml PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " http://www.w3.org/TR/html4/loose.dtd">
<%@ taglib uri=" http://java.sun.com/jsp/jstl/core" prefix="c" %>
<html>
<head>
<metahttp-equiv="Content-Type"content="text/html; charset=UTF-8">
<title>Error Page</title>
</head>
<body>
<h2>Application Error, please contact support.</h2>
<h3>Debug Information:</h3>
Requested URL= ${url}<br><br>
Exception= ${exception.message}<br><br>
<strong>Exception Stack Trace</strong><br>
<c:forEachitems="${exception.stackTrace}"var="ste">
${ste}
</c:forEach>
</body>
</html>
|
error.jsp is used when our controller class request handler method is throwing EmployeeNotFoundException. We should get this page in response when id value is 1 in the client request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPEhtml PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<metahttp-equiv="Content-Type"content="text/html; charset=UTF-8">
<title>Database Error Page</title>
</head>
<body>
<h2>Database Error, please contact support.</h2>
</body>
</html>
|
database_error.jsp is used when our application is throwing SQLException, as configured in GlobalExceptionHandler class. We should get this page as response when id value is 2 in the client request.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPEhtml PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" " http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<metahttp-equiv="Content-Type"content="text/html; charset=UTF-8">
<title>Generic Error Page</title>
</head>
<body>
<h2>Unknown Error Occured, please contact support.</h2>
</body>
</html>
|
This should be the page as response when any exception occurs not handled by our application code and simpleMappingExceptionResolver bean takes care of that. We should get this page as response when id value in client request is anything other than 1,2,3 or 10.
Just deploy the application in the servlet container you are using, I am using Apache Tomcat 7 for this example.
Below images show the different response pages returned by our application based on the id value.
ID=10, valid response.
ID=1, controller based exception handler used
ID=2, global exception handler used with view as response
ID=3, 404 error page used
ID=4, simpleMappingExceptionResolver used for response view
As you can see that we got the expected response in all the cases.
We are almost done with our tutorial, except the last bit where I will explain how to send JSON response from the exception handler methods.
Our application has all the JSON dependencies and jsonMessageConverter is configured, all we need to implement the exception handler method.
For simplicity, I will rewrite the EmployeeController handleEmployeeNotFoundException() method to return JSON response.
Just update EmployeeController exception handler method with below code and deploy the application again.
1
2
3
4
5
6
7
8
9
|
@ExceptionHandler(EmployeeNotFoundException.class)
public@ResponseBodyExceptionJSONInfo handleEmployeeNotFoundException(HttpServletRequest request, Exception ex){
ExceptionJSONInfo response =newExceptionJSONInfo();
response.setUrl(request.getRequestURL().toString());
response.setMessage(ex.getMessage());
returnresponse;
}
|
Now when we use id as 1 in client request, we get following JSON response as shown in the below image.
That’s all for Exception Handling in Spring MVC Framework, please download the application from below URL and play around with it to learn more.