It is very important to handle the exceptions, logical errors and provide meaningful message to the client to avoid abnormal termination of the Restful APIs in Springboot.
Today we will learn how to implement Global Exception Handling as well as Class Level Exception Handling.
Pre-requisite : You must have knowledge about Springboot and Restful API Creation, Jpa (Java Persistance API) and a bit about Exception Handling in Java core.
We will be using java-17 to create spring boot template project with Spring Initilizr
We need the Following dependencies for the Project.
pom.xml
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.4</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bukhari</groupId>
<artifactId>exceptionhandling-validation</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>exceptionhandling-validation</name>
<description>Demo project for Spring Boot</description>
<properties>
<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-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</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>
The application.properties File
#Dabase Configuration
spring.application.name=Exception-Handling-Application
server.port=8082
spring.datasource.url=jdbc:mysql://localhost:3306/libdb
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.testWhileIdle=true
spring.datasource.validationQuery=SELECT 1
spring.jpa.hibernate.naming-strategy=org.hibernate.dialect.MySQL8Dialect
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=create
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.formate_sql=true
Class Level Exception Handling
The Hierarchy of Code
Entity Book
package com.bukhari.exceptionhandlingvalidation.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String author;
private Double price;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getAuthor() {
return author;
}
public void setAuthor(String author) {
this.author = author;
}
public Double getPrice() {
return price;
}
public void setPrice(Double price) {
this.price = price;
}
}
The BookRepository
Make sure you import the Book from entity.Book package not from java.awt.print
package com.bukhari.exceptionhandlingvalidation.repository;
import com.bukhari.exceptionhandlingvalidation.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BookRepository extends JpaRepository<Book, Long> {
}
The Exceptions that we want to handle are
- Book name cannot be empty.
- Book Author name cannot be empty.
- Price of Book must be $10 or greater than $10. (assuming price is always non-empty).
The BookService
package com.bukhari.exceptionhandlingvalidation.service;
import com.bukhari.exceptionhandlingvalidation.classexception.LibBusinessException;
import com.bukhari.exceptionhandlingvalidation.entity.Book;
import com.bukhari.exceptionhandlingvalidation.repository.BookRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class BookService {
@Autowired
private BookRepository bookRepository;
public Book addBook(Book book) {
//If the Name of the book is Empty
if (book.getName().isEmpty()) {
throw new LibBusinessException("801", "The Name of the Book can't be Empty");
}
//If the Name of the Author is Empty
if (book.getAuthor().isEmpty()) {
throw new LibBusinessException("802", "The Author's Name can't be Empty");
}
//If the Price is less than 10
if (book.getPrice().intValue() < 10) {
throw new LibBusinessException("803", "The Price of the Book must be $10 or greater than $10");
}
return bookRepository.save(book);
}
}
The LibBusinessException
package com.bukhari.exceptionhandlingvalidation.classexception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(HttpStatus.BAD_REQUEST)
public class LibBusinessException extends RuntimeException {
private String errorCode;
private String errorMessage;
public LibBusinessException(String errorCode, String errorMessage) {
super();
this.errorCode = errorCode;
this.errorMessage = errorMessage;
}
public String getErrorCode() {
return errorCode;
}
public void setErrorCode(String errorCode) {
this.errorCode = errorCode;
}
public String getErrorMessage() {
return errorMessage;
}
public void setErrorMessage(String errorMessage) {
this.errorMessage = errorMessage;
}
}
The BookController
package com.bukhari.exceptionhandlingvalidation.controller;
import com.bukhari.exceptionhandlingvalidation.classexception.LibBusinessException;
import com.bukhari.exceptionhandlingvalidation.entity.Book;
import com.bukhari.exceptionhandlingvalidation.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("digital/lib")
public class LibraryController {
@Autowired
private BookService bookService;
@PostMapping("/book")
public ResponseEntity<?> saveBook(@RequestBody Book book) {
try {
Book savedBook = bookService.addBook(book);
return new ResponseEntity<Book>(savedBook, HttpStatus.CREATED);
} catch (LibBusinessException e) {
LibBusinessException libex = new LibBusinessException(e.getErrorCode(), e.getErrorMessage());
return new ResponseEntity<LibBusinessException>(libex, HttpStatus.BAD_REQUEST);
} catch (Exception e) {
LibBusinessException libex = new LibBusinessException("804", "Something went wrong in controller");
return new ResponseEntity<LibBusinessException>(libex, HttpStatus.BAD_REQUEST);
}
}
}
If the Book Name is Empty
PostMan Request and Response
If the Book Author Name is Empty
PostMan Request and Response
If the Book Price is less than $10
PostMan Request and Response
Validation and Global Exception Handling Using ControllerAdvice
Let's Understand now validation and Global Exception Handling
We will take AppUser Entity for this purpose. (pom.xml is same).
The AppUser Entity
package com.bukhari.exceptionhandlingvalidation.entity;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class AppUser {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int userId;
private String name;
private String email;
private String mobile;
private String gender;
private int age;
private String nationality;
public AppUser() {
}
public AppUser(int userId, String name, String email, String mobile, String gender, int age, String nationality) {
this.userId = userId;
this.name = name;
this.email = email;
this.mobile = mobile;
this.gender = gender;
this.age = age;
this.nationality = nationality;
}
public int getUserId() {
return userId;
}
public void setUserId(int userId) {
this.userId = userId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getNationality() {
return nationality;
}
public void setNationality(String nationality) {
this.nationality = nationality;
}
}
Request For AppUser
package com.bukhari.exceptionhandlingvalidation.request;
import jakarta.validation.constraints.*;
/**
* Global Exception Handling AppUserRequest
*/
public class AppUserRequest {
@NotNull(message = "username must not be null")
private String name;
@Email(message = "Invalid email address, Please provide a valid email")
private String email;
@Pattern(regexp = "^\\d{10}$", message = "Invalid mobile number, Please Provide a Valid Number")
private String mobile;
private String gender;
@Min(value = 18, message = "Age must be 18 or greater")
@Max(value = 60, message = "Age must be 60 or less")
private int age;
@NotBlank
private String nationality;
public AppUserRequest(String name, String email, String mobile, String gender, int age, String nationality) {
this.name = name;
this.email = email;
this.mobile = mobile;
this.gender = gender;
this.age = age;
this.nationality = nationality;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getMobile() {
return mobile;
}
public void setMobile(String mobile) {
this.mobile = mobile;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getNationality() {
return nationality;
}
public void setNationality(String nationality) {
this.nationality = nationality;
}
}
We are going to validate and handle the following exceptions
- Name must not be null.
- Email must be valid.
- Mobile Number must be valid.
- Age must be between 18 and 60.
- Nationality must not be blank (non-empty and non-null).
- Map Custom Class to ExceptionHandler if user is not Found.
For this purpose we are using @RestControllerAdvice
as Global Exception handler which is a better approach as compared to the Class Level approach. The @RestControllerAdvice
is mapped to all the Controllers and once the request is triggered the Advice checks for any mapping matches to it and sends the response accordingly. Since Spring Provides us @ExceptionHandler
annotation to intercept any java as well as logical exception so we can implement according to our own requirement.
The RestControllerAdvice
package com.bukhari.exceptionhandlingvalidation.globalexception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import java.util.HashMap;
import java.util.Map;
/**
* Global Exception Handling ControllerAdvice
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleInvalidArgument(MethodArgumentNotValidException ex) {
Map<String, String> errorMap = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errorMap.put(error.getField(), error.getDefaultMessage());
});
return errorMap;
}
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(UserNotFoundException.class)
public Map<String, String> handleBusinessException(UserNotFoundException ex) {
Map<String, String> errorMap = new HashMap<>();
errorMap.put("errorMessage", ex.getMessage());
return errorMap;
}
}
The Custom Class For UserNotFoundException
package com.bukhari.exceptionhandlingvalidation.globalexception;
/**
* Global Exception Handling UserNotFoundException
*/
public class UserNotFoundException extends Exception{
public UserNotFoundException(String message) {
super(message);
}
}
The AppUserRepository
package com.bukhari.exceptionhandlingvalidation.repository;
import com.bukhari.exceptionhandlingvalidation.entity.AppUser;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
/**
* Global Exception Handling AppUserRepository
*/
@Repository
public interface AppUserRepository extends JpaRepository<AppUser,Integer> {
}
The AppUserService
package com.bukhari.exceptionhandlingvalidation.service;
import com.bukhari.exceptionhandlingvalidation.entity.AppUser;
import com.bukhari.exceptionhandlingvalidation.globalexception.UserNotFoundException;
import com.bukhari.exceptionhandlingvalidation.repository.AppUserRepository;
import com.bukhari.exceptionhandlingvalidation.request.AppUserRequest;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
/**
* Global Exception Handling AppUserService
*/
@Service
public class AppUserService {
@Autowired
private AppUserRepository appUserRepository;
public AppUser saveUser(AppUserRequest userRequest) {
AppUser user = new AppUser(0, userRequest.getName(), userRequest.getEmail(),
userRequest.getMobile(), userRequest.getGender(), userRequest.getAge(), userRequest.getNationality());
return appUserRepository.save(user);
}
public List<AppUser> getALlUsers() {
return appUserRepository.findAll();
}
public AppUser getUser(int id) throws UserNotFoundException {
Optional<AppUser> optional = appUserRepository.findById(id);
if (optional.isPresent()) {
return optional.get();
} else {
throw new UserNotFoundException("user not found with id : " + id);
}
}
}
The AppUserController
package com.bukhari.exceptionhandlingvalidation.controller;
import com.bukhari.exceptionhandlingvalidation.entity.AppUser;
import com.bukhari.exceptionhandlingvalidation.globalexception.UserNotFoundException;
import com.bukhari.exceptionhandlingvalidation.request.AppUserRequest;
import com.bukhari.exceptionhandlingvalidation.service.AppUserService;
import jakarta.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/users")
public class AppUserController {
@Autowired
private AppUserService appUserService;
@PostMapping("/signup")
public ResponseEntity<AppUser> saveUser(@RequestBody @Valid AppUserRequest userRequest){
return new ResponseEntity<>(appUserService.saveUser(userRequest), HttpStatus.CREATED);
}
@GetMapping("/fetchAll")
public ResponseEntity<List<AppUser>> getAllUsers(){
return ResponseEntity.ok(appUserService.getALlUsers());
}
@GetMapping("/{id}")
public ResponseEntity<AppUser> getUser(@PathVariable int id) throws UserNotFoundException {
return ResponseEntity.ok(appUserService.getUser(id));
}
}
The @Valid
annotation is used to validate the payload
PostMan Request and Response are given below.
If the data is not valid
If the User is not Found.
Hope the Tutorial was helpful. You learnt something from it. If it was helpful make sure to follow and like.
Top comments (0)