A good REST API should handle the exception properly and send the proper response to the user. The user should not be rendered with any unhandled exception. In this Spring Boot Exception Handling article, we will learn how to handle in exception in Spring Boot RESTful Web Services using @RestControllerAdvice and @ExceptionHandler
What is @RestControllerAdvice ?
@RestControllerAdvice is the combination of both @ControllerAdvice and @ResponseBody. We can use the @ControllerAdvice annotation for handling exceptions in the RESTful Services but we need to add @ResponseBody separately.
Folder Structure:
- Create a Maven project (maven-archetype-quickstart) “SpringBootApplication” and create a package for our source files “com.javainterviewpoint” under src/main/java
- Now add the following dependency in the POM.xml
<project xmlns="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/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.javainterviewpoint</groupId> <artifactId>SpringBootApplication</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>SpringBootApplication</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> </properties> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> </dependencies> </project>
- Create the Java classes Employee.java, EmployeeService.java, ErrorResponse.java, EmployeeController.java, RestExceptionHandler.java and Application.java under com.javainterviewpoint folder.
The spring-boot-starter-parent is a special starter, it provides useful Maven defaults. Since we are developing a web application, we also need to add spring-boot-starter-web dependency.This will add dependencies such Tomcat, Jackson, Spring boot etc which are required for our application.
Spring Boot Exception Handling – @RestControllerAdvice + @ExceptionHandler
Dependency Tree
[INFO] ------------------------------------------------------------------------ [INFO] Building SpringBootApplication 0.0.1-SNAPSHOT [INFO] ------------------------------------------------------------------------ [INFO] [INFO] --- maven-dependency-plugin:3.0.2:tree (default-cli) @ SpringBootApplication --- [INFO] com.javainterviewpoint:SpringBootApplication:jar:0.0.1-SNAPSHOT [INFO] \- org.springframework.boot:spring-boot-starter-web:jar:2.0.2.RELEASE:compile [INFO] +- org.springframework.boot:spring-boot-starter:jar:2.0.2.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot:jar:2.0.2.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.0.2.RELEASE:compile [INFO] | +- org.springframework.boot:spring-boot-starter-logging:jar:2.0.2.RELEASE:compile [INFO] | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile [INFO] | | | +- ch.qos.logback:logback-core:jar:1.2.3:compile [INFO] | | | \- org.slf4j:slf4j-api:jar:1.7.25:compile [INFO] | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.10.0:compile [INFO] | | | \- org.apache.logging.log4j:log4j-api:jar:2.10.0:compile [INFO] | | \- org.slf4j:jul-to-slf4j:jar:1.7.25:compile [INFO] | +- javax.annotation:javax.annotation-api:jar:1.3.2:compile [INFO] | +- org.springframework:spring-core:jar:5.0.6.RELEASE:compile [INFO] | | \- org.springframework:spring-jcl:jar:5.0.6.RELEASE:compile [INFO] | \- org.yaml:snakeyaml:jar:1.19:runtime [INFO] +- org.springframework.boot:spring-boot-starter-json:jar:2.0.2.RELEASE:compile [INFO] | +- com.fasterxml.jackson.core:jackson-databind:jar:2.9.5:compile [INFO] | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.9.0:compile [INFO] | | \- com.fasterxml.jackson.core:jackson-core:jar:2.9.5:compile [INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.9.5:compile [INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.9.5:compile [INFO] | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.9.5:compile [INFO] +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.0.2.RELEASE:compile [INFO] | +- org.apache.tomcat.embed:tomcat-embed-core:jar:8.5.31:compile [INFO] | +- org.apache.tomcat.embed:tomcat-embed-el:jar:8.5.31:compile [INFO] | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:8.5.31:compile [INFO] +- org.hibernate.validator:hibernate-validator:jar:6.0.9.Final:compile [INFO] | +- javax.validation:validation-api:jar:2.0.1.Final:compile [INFO] | +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile [INFO] | \- com.fasterxml:classmate:jar:1.3.4:compile [INFO] +- org.springframework:spring-web:jar:5.0.6.RELEASE:compile [INFO] | \- org.springframework:spring-beans:jar:5.0.6.RELEASE:compile [INFO] \- org.springframework:spring-webmvc:jar:5.0.6.RELEASE:compile [INFO] +- org.springframework:spring-aop:jar:5.0.6.RELEASE:compile [INFO] +- org.springframework:spring-context:jar:5.0.6.RELEASE:compile [INFO] \- org.springframework:spring-expression:jar:5.0.6.RELEASE:compile [INFO] ------------------------------------------------------------------------
Global Exception Handler
package com.javainterviewpoint; import java.io.IOException; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class RestExceptionHandler { @ExceptionHandler(value = { IOException.class }) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse badRequest(Exception ex) { return new ErrorResponse(400, "Bad Request"); } @ExceptionHandler(value = { Exception.class }) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse unKnownException(Exception ex) { return new ErrorResponse(404, "Employee Not Found"); } }
Here you can see that the RestExceptionHandler class is annotated with @RestControllerAdvice, which tells the Spring to treat this class as the global exception handler. The @ExceptionHandler handles each of the exception separately and returns the corresponding error message.
Custom Error Message
Global exception handler RestExceptionHandler will return the custom ErrorResponse with the error message and status code.
package com.javainterviewpoint; public class ErrorResponse { private int status; private String message; public ErrorResponse() { super(); } public ErrorResponse(int status, String message) { super(); this.status = status; this.message = message; } public int getStatus() { return status; } public void setStatus(int status) { this.status = status; } public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } @Override public String toString() { return "ErrorResponse [status=" + status + ", message=" + message + "]"; } }
EmployeeController.java
package com.javainterviewpoint; import java.io.IOException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class EmployeeController { @Autowired EmployeeService employeeService; @Autowired Employee employee; @RequestMapping("/employee/{employeeName}") public Employee hello(@PathVariable("employeeName") String employeeName) throws Exception { if (employeeName.length() < 4) throw new IOException(); employee = employeeService.getEmployee(employeeName); if(employee == null) throw new Exception(); return employee; } }
Our EmployeeController class will throw two exceptions
- When the employeeName is lesser than 4 characters it will throw IOException
- When the employee object is null then it will throw Exception
Both the exceptions will be handled globally by our RestExceptionHandler class.
EmployeeService.java
package com.javainterviewpoint; import java.util.HashMap; import java.util.Map; import javax.annotation.PostConstruct; import org.springframework.stereotype.Service; @Service public class EmployeeService { Map<String, Employee> employeeMap = new HashMap<String, Employee>(); @PostConstruct void initialize() { Employee emp1 = new Employee("John", 11); Employee emp2 = new Employee("James", 22); employeeMap.put("John", emp1); employeeMap.put("James", emp2); } public Employee getEmployee (String name) throws Exception { return employeeMap.get(name); } }
Employee.java
Employee class is a simple POJO consisting the getters and setters for name and age.
package com.javainterviewpoint; import org.springframework.stereotype.Component; @Component public class Employee { private String name; private int age; public Employee() { super(); } public Employee(String name, int age) { this.setName(name); this.setAge(age); } public String getName() { return name; } public void setName(String name) { this.name = name; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public String toString() { return "Employee [name=" + name + ", age=" + age + "]"; } }
Application.java
package com.javainterviewpoint; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
The Application class main() method is the triggering point of our application. Inside the main method we will be calling the SpringApplication class run() method which bootstraps our Application and starts the tomcat server. We will be passing our class name [Applicaion.class] as an argument to the run() method.
Output
Pass the employee name less than 4 characters
In POSTMAN, select GET method and give the url as “http://localhost:8080/employee/asd”. You will be getting 400 – Bad Request
Pass the employee name which is not in the map
In POSTMAN, select GET method and give the url as “http://localhost:8080/employee/javainterviewpoint”. You will be getting 500 – Internal Server Error
Pass the correct Employee name and you should the Employee details
In POSTMAN, select GET method and give the url as “http://localhost:8080/employee/John”.
sukh says
Nice article. Could you please let me know if we have MVC architecture where we should throw exceptions exactly (DAO or services or just like controller you mentioned)? Thanks
javainterviewpoint says
It is always a good practice throwing an exception from the DAO layer. Just for simplicity of the code, I have not followed that architecture.