In this article, we will learn how to handle exceptions for RESTful Web Services developed using Spring Boot.
We can handle exceptions in REST API in the same way we handle them in the Spring MVC-based web
application—by using the @ExceptionHandler
and @ControllerAdvice
annotations.
Instead of rendering a view, you can return ResponseEntity
with the
appropriate HTTP status
code and exception details.
By default, Spring Boot provides a /error
mapping that handles all errors in
a sensible way, and
it is registered as a “global” error page in the servlet container. Rest clients, it produces a JSON
response with details of the error, the HTTP status, and the exception message.
Instead of simply throwing an exception with the HTTP status code, it is better to provide more details about
the issue, such as the error code, message, cause, etc. In this example, we define a class annotated with
@ControllerAdvice
to customize the JSON document to return for a particular
controller and/or
exception type.
Let's develop CRUD REST APIs for the Employee
resource using Spring Boot,
Spring Data JPA (Hibernate), and
MySQL database and we will look into exception handling for these REST services.
We will develop a simple Spring Boot RESTful CRUD APIs for Employee
resource
and we will implement
Exception(Error) Handling for these RESTful Services.
There are many ways to create a Spring Boot application. The simplest way is to use Spring Initializr at http://start.spring.io/, which is an online Spring Boot application generator.
Refer to the below pom.xml
for your reference.
<?xml version="1.0" encoding="UTF-8"?> <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>net.guides.springboot2</groupId> <artifactId>springboot2-jpa-crud-example</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot2-jpa-crud-example</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.0.4</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.Size; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Table(name = "employees") @Data @Builder @AllArgsConstructor @NoArgsConstructor public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(name = "first_name", nullable = false) @NotEmpty(message = "First name should not be empty") private String firstName; @Column(name = "last_name", nullable = false) @NotEmpty(message = "Last name should not be empty") private String lastName; @Column(name = "email", nullable = false, unique = true) @NotEmpty(message = "Email should not be empty") @Size(max = 100, message = "Email should not be more than 100 characters") private String email; @Column(name = "phone_number") private String phoneNumber; @Column(name = "salary") private double salary; @Column(name = "address") private String address; @Column(name = "designation") private String designation; @Column(name = "status") private String status; }
Next, we will create a Spring Data JPA repository - EmployeeRepository.java
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface EmployeeRepository extends JpaRepository<Employee, Long> { }
Once the entity and repository are in place then we will create Spring Rest Controller - EmployeeController.java
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PutMapping; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import jakarta.validation.Valid; import java.util.List; @RestController @RequestMapping("/api/employees") @Validated public class EmployeeController { @Autowired private EmployeeRepository employeeRepository; @GetMapping public List<Employee> getAllEmployees() { return employeeRepository.findAll(); } @GetMapping("/{id}") public ResponseEntity<Employee> getEmployeeById(@PathVariable(value = "id") Long employeeId) { Employee employee = employeeRepository.findById(employeeId) .orElseThrow(() -> new ResourceNotFoundException("Employee", "id", employeeId)); return ResponseEntity.ok().body(employee); } @PostMapping public Employee createEmployee(@Valid @RequestBody Employee employee) { return employeeRepository.save(employee); } @PutMapping("/{id}") public ResponseEntity<Employee> updateEmployee( @PathVariable(value = "id") Long employeeId, @Valid @RequestBody Employee employeeDetails) { Employee employee = employeeRepository.findById(employeeId) .orElseThrow(() -> new ResourceNotFoundException("Employee", "id", employeeId)); employee.setFirstName(employeeDetails.getFirstName()); employee.setLastName(employeeDetails.getLastName()); employee.setEmail(employeeDetails.getEmail()); employee.setPhoneNumber(employeeDetails.getPhoneNumber()); employee.setSalary(employeeDetails.getSalary()); employee.setAddress(employeeDetails.getAddress()); employee.setDesignation(employeeDetails.getDesignation()); employee.setStatus(employeeDetails.getStatus()); Employee updatedEmployee = employeeRepository.save(employee); return ResponseEntity.ok(updatedEmployee); } @DeleteMapping("/{id}") public ResponseEntity<Void> deleteEmployee(@PathVariable(value = "id") Long employeeId) { Employee employee = employeeRepository.findById(employeeId) .orElseThrow(() -> new ResourceNotFoundException("Employee", "id", employeeId)); employeeRepository.delete(employee); return ResponseEntity.noContent().build(); } }
So far we have developed CRUD RESTful APIs for the Employee
resource. Now we
will look into how to handle
exceptions or errors for the above RESTFul APIs.
Spring Boot provides a good default implementation for exception handling for RESTful Services.
Let’s quickly look at the default Exception Handling features provided by Spring Boot.
Resource Not Present
{ "timestamp": 1512713804164, "status": 404, "error": "Not Found", "message": "No message available", "path": "/some-dummy-url" }
That's a cool error response. It contains all the details that are typically needed.
Let’s see what Spring Boot does when an exception is thrown from a Resource. we can specify the Response
Status for a specific exception along with the definition of the Exception with @ResponseStatus
annotation.
Let's create a ResourceNotFoundException.java
class.
Define a class for the error response:
package com.companyname.springbootcrudrest.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ResponseStatus; @ResponseStatus(value = HttpStatus.NOT_FOUND) public class ResourceNotFoundException extends Exception{ private static final long serialVersionUID = 1L; public ResourceNotFoundException(String message){ super(message); } }
Customizing Error Response Structure
The default error response provided by Spring Boot contains all the details that are typically needed.
However, you might want to create a framework-independent response structure for your organization. In that case, you can define a specific error response structure.
Let’s define a simple error response bean.
package com.companyname.springbootcrudrest.exception;
import java.util.Date;
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
super();
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
}
}
Create GlobalExceptionHandler class
To use ErrorDetails
to return the error response, let’s create a GlobalExceptionHandler
class annotated
with @ControllerAdvice
annotation. This class handles exception-specific
and global exceptions in a
single place.
package com.companyname.springbootcrudrest.exception;
import java.util.Date;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<?> globleExcpetionHandler(Exception ex, WebRequest request) {
ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
We are done with developing CRUD REST APIs and exception handling for REST APIs. Now it's time to run this Spring boot application.
This spring boot application has an entry point Java class called Application.java
with the public static
void main(String[] args) method, which you can run to start the application.
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); } }
Once the application is up and running, you can use Postman to test the REST APIs. You can perform operations
such as GET, POST, PUT, and DELETE on the /api/employees
endpoint to verify the functionality
and the exception handling.