Spring Boot is built on the top of the spring and contains all the features of spring. And is becoming a favorite of developers these days because of its rapid production-ready environment which enables the developers to directly focus on the logic instead of struggling with the configuration and setup. Spring Boot is a microservice-based framework and making a production-ready application in it takes very little time. Exception Handling in Spring Boot helps to deal with errors and exceptions present in APIs so as to deliver a robust enterprise application. This article covers various ways in which exceptions can be handled in a Spring Boot Project. Let’s do the initial setup to explore each approach in more depth.
Initial Setup
In order to create a simple spring boot project using Spring Initializer, please refer to this article. Now let’s develop a Spring Boot Restful Webservice that performs CRUD operations on Customer Entity. We will be using MYSQL database for storing all necessary data.
Step 1: Creating a JPA Entity class Customer with three fields id, name, and address.
Java
// Step 1: Creating a JPA Entity // class Customer with three // fields id, name and address. package com.customer.model; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Entity @Data @AllArgsConstructor @NoArgsConstructor public class Customer { @Id @GeneratedValue (strategy = GenerationType.IDENTITY) private Long id; private String name; private String address; } |
The Customer class is annotated with @Entity annotation and defines getters, setters, and constructors for the fields.
Step 2: Creating a CustomerRepository Interface
Java
// Step 2: Creating a Repository // Interface extending // JpaRepository package com.customer.repository; import com.customer.model.Customer; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface CustomerRepository extends JpaRepository<Customer, Long> { } |
The CustomerRepository interface is annotated with @Repository annotation and extends the JpaRepository of Spring Data JPA.
Step 3: Creating Custom made Exceptions that can be thrown during necessary scenarios while performing CRUD.
CustomerAlreadyExistsException: This exception can be thrown when the user tries to add a customer that already exists in the database.
Java
// Step 3: Creating custom exception // that can be thrown when // user tries to add a customer // that already exists package com.customer.exception; public class CustomerAlreadyExistsException extends RuntimeException { private String message; public CustomerAlreadyExistsException() {} public CustomerAlreadyExistsException(String msg) { super (msg); this .message = msg; } } |
NoSuchCustomerExistsException: This exception can be thrown when the user tries to delete or update a customer record that doesn’t exist in the database.
Java
// Step 3: Creating custom exception // that can be thrown when // user tries to update/delete a // customer that doesn't exists package com.customer.exception; public class NoSuchCustomerExistsException extends RuntimeException { private String message; public NoSuchCustomerExistsException() {} public NoSuchCustomerExistsException(String msg) { super (msg); this .message = msg; } } |
Note: Both Custom Exception classes extend RuntimeException.
Step 4: Creating interface CustomerService and implementing class CustomerServiceImpl of service layer.
The CustomerService interface defines three different methods:
- Customer getCustomer(Long id): To get a customer record by its id. This method throws a NoSuchElementException exception when it doesn’t find a customer record with the given id.
- String addCustomer(Customer customer): To add details of a new Customer to the database. This method throws a CustomerAlreadyExistsException exception when the user tries to add a customer that already exists.
- String updateCustomer(Customer customer): To update details of Already existing Customers. This method throws a NoSuchCustomerExistsException exception when the user tries to update details of a customer that doesn’t exist in the database.
The Interface and service implementation class is as follows:
Java
// Step 4: Creating service interface package com.customer.service; import com.customer.model.Customer; public interface CustomerService { // Method to get customer by its Id Customer getCustomer(Long id); // Method to add a new Customer // into the database String addCustomer(Customer customer); // Method to update details of a Customer String updateCustomer(Customer customer); } |
Java
// Step 4: Service class Implementation // which defines the main logic package com.customer.service; // Importing required packages import com.customer.exception.CustomerAlreadyExistsException; import com.customer.exception.NoSuchCustomerExistsException; import com.customer.model.Customer; import com.customer.repository.CustomerRepository; import java.util.NoSuchElementException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; @Service public class CustomerServiceImpl implements CustomerService { @Autowired private CustomerRepository customerRespository; // Method to get customer by Id.Throws // NoSuchElementException for invalid Id public Customer getCustomer(Long id) { return customerRespository.findById(id).orElseThrow( () -> new NoSuchElementException( "NO CUSTOMER PRESENT WITH ID = " + id)); } // Method to add new customer details to database.Throws // CustomerAlreadyExistsException when customer detail // already exist public String addCustomer(Customer customer) { Customer existingCustomer = customerRespository.findById(customer.getId()) .orElse( null ); if (existingCustomer == null ) { customerRespository.save(customer); return "Customer added successfully" ; } else throw new CustomerAlreadyExistsException( "Customer already exists!!" ); } // Method to update customer details to database.Throws // NoSuchCustomerExistsException when customer doesn't // already exist in database public String updateCustomer(Customer customer) { Customer existingCustomer = customerRespository.findById(customer.getId()) .orElse( null ); if (existingCustomer == null ) throw new NoSuchCustomerExistsException( "No Such Customer exists!!" ); else { existingCustomer.setName(customer.getName()); existingCustomer.setAddress( customer.getAddress()); customerRespository.save(existingCustomer); return "Record updated Successfully" ; } } } |
Step 5: Creating Rest Controller CustomerController which defines various APIs.
Java
// Step 5: Creating Rest Controller CustomerController which // defines various API's. package com.customer.controller; // Importing required packages import com.customer.exception.CustomerAlreadyExistsException; import com.customer.exception.ErrorResponse; import com.customer.model.Customer; import com.customer.service.CustomerService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ExceptionHandler; 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.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.ResponseStatus; import org.springframework.web.bind.annotation.RestController; @RestController public class CustomerController { @Autowired private CustomerService customerService; // Get Customer by Id @GetMapping ( "/getCustomer/{id}" ) public Customer getCustomer( @PathVariable ( "id" ) Long id) { return customerService.getCustomer(id); } // Add new Customer @PostMapping ( "/addCustomer" ) public String addcustomer( @RequestBody Customer customer) { return customerService.addCustomer(customer); } // Update Customer details @PutMapping ( "/updateCustomer" ) public String updateCustomer( @RequestBody Customer customer) { return customerService.updateCustomer(customer); } } |
Now let’s go through the various ways in which we can handle the Exceptions thrown in this project.
Default Exception Handling by Spring Boot:
The getCustomer() method defined by CustomerController is used to get a customer with a given Id. It throws a NoSuchElementException when it doesn’t find a Customer record with the given id. On Running the Spring Boot Application and hitting the /getCustomer API with an Invalid Customer Id, we get a NoSuchElementException completely handled by Spring Boot as follows:
Spring Boot provides a systematic error response to the user with information such as timestamp, HTTP status code, error, message, and the path.
Using Spring Boot @ExceptionHandler Annotation:
@ExceptionHandler annotation provided by Spring Boot can be used to handle exceptions in particular Handler classes or Handler methods. Any method annotated with this is automatically recognized by Spring Configuration as an Exception Handler Method. An Exception Handler method handles all exceptions and their subclasses passed in the argument. It can also be configured to return a specific error response to the user. So let’s create a custom ErrorResponse class so that the exception is conveyed to the user in a clear and concise way as follows:
Java
// Custom Error Response Class package com.customer.exception; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @Data @AllArgsConstructor @NoArgsConstructor public class ErrorResponse { private int statusCode; private String message; public ErrorResponse(String message) { super (); this .message = message; } } |
The addCustomer() method defined by CustomerController throws a CustomerAlreadyExistsException when the user tries to add a Customer that already exists in the database else it saves the customer details.
To handle this exception let’s define a handler method handleCustomerAlreadyExistsException() in the CustomerController.So now when addCustomer() throws a CustomerAlreadyExistsException, the handler method gets invoked which returns a proper ErrorResponse to the user.
Java
// Exception Handler method added in CustomerController to // handle CustomerAlreadyExistsException exception @ExceptionHandler (value = CustomerAlreadyExistsException. class ) @ResponseStatus (HttpStatus.CONFLICT) public ErrorResponse handleCustomerAlreadyExistsException( CustomerAlreadyExistsException ex) { return new ErrorResponse(HttpStatus.CONFLICT.value(), ex.getMessage()); } |
Note: Spring Boot allows to annotate a method with @ResponseStatus to return the required Http Status Code.
On Running the Spring Boot Application and hitting the /addCustomer API with an existing Customer, CustomerAlreadyExistsException gets completely handled by handler method as follows:
Using @ControllerAdvice for Global Exception Handler:
In the previous approach, we can see that the @ExceptionHandler annotated method can only handle the exceptions thrown by that particular class. However, if we want to handle any exception thrown throughout the application we can define a global exception handler class and annotate it with @ControllerAdvice.This annotation helps to integrate multiple exception handlers into a single global unit.
The updateCustomer() method defined in CustomerController throws a NoSuchCustomerExistsException exception if the user tries to update details of a customer that doesn’t already exist in the database else it successfully saves the updated details for that particular customer.
To handle this exception, let’s define a GlobalExceptionHandler class annotated with @ControllerAdvice. This class defines the ExceptionHandler method for NoSuchCustomerExistsException exception as follows.
Java
// Class to handle exception Globally package com.customer.exception; import org.springframework.http.HttpStatus; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.bind.annotation.ResponseStatus; @ControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler (value = NoSuchCustomerExistsException. class ) @ResponseStatus (HttpStatus.BAD_REQUEST) public @ResponseBody ErrorResponse handleException(NoSuchCustomerExistsException ex) { return new ErrorResponse( HttpStatus.NOT_FOUND.value(), ex.getMessage()); } } |
On Running the Spring Boot Application and hitting the /updateCustomer API with invalid Customer details, NoSuchCustomerExistsException gets thrown which is completely handled by the handler method defined in GlobalExceptionHandler class as follows: