In this tutorial, 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 that is 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 in JSON/XML format.
Spring Boot Exception Handling Workflow
Tools and Technologies Used
- Spring Boot
- JDK - 1.8 or later
- Spring MVC
- Hibernate
- Maven
- Spring Data JPA
- IDE - Eclipse or Spring Tool Suite (STS) or Intellij IDEA // Any IDE works
- MYSQL
1. Create Spring boot application
Spring Boot provides a web tool called Spring Initializer to bootstrap an application quickly. Just go to https://start.spring.io/ and generate a new spring boot project.
Use the below details in the Spring boot creation:
Project Name: springboot-blog-rest-api
Project Type: Maven
Choose dependencies: Spring Web, Lombok, Spring Data JPA, Spring Security, Dev Tools, and MySQL Driver
Package name: net.javaguides.springboot
Packaging: Jar
Download the Spring Boot project as a zip file, unzip it and import it in your favorite IDE.
2. Maven Dependencies
Here is the pom.xml file 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 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.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springboot.blog</groupId>
<artifactId>springboot-blog-rest-api</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-blog-rest-api</name>
<description>Spring boot blog application rest api's</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-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</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>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
3. Configure MySQL Database
create database myblog
spring.datasource.url = jdbc:mysql://localhost:3306/myblog?useSSL=false&serverTimezone=UTC
spring.datasource.username = root
spring.datasource.password = root
# hibernate properties
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect
# Hibernate ddl auto (create, create-drop, validate, update)
spring.jpa.hibernate.ddl-auto = update
logging.level.org.springframework.security=DEBUG
4. Model Layer - Create JPA Entity
package com.springboot.blog.entity;
import lombok.*;
import jakarta.persistence.*;
import java.util.HashSet;
import java.util.Set;
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Entity
@Table(
name = "posts", uniqueConstraints = {@UniqueConstraint(columnNames = {"title"})}
)
public class Post {
@Id
@GeneratedValue(
strategy = GenerationType.IDENTITY
)
private Long id;
@Column(name = "title", nullable = false)
private String title;
@Column(name = "description", nullable = false)
private String description;
@Column(name = "content", nullable = false)
private String content;
}
5. Repository Layer
package com.springboot.blog.repository;
import com.springboot.blog.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostRepository extends JpaRepository<Post, Long> {
}
6. Exception(Error) Handling for RESTful Services
{
"timestamp": "2021-02-28T14:15:18.250+00:00",
"status": 404,
"error": "Not Found",
"trace": "com.springboot.blog.exception.ResourceNotFoundEx
ception: Post not found with id : ’6’……...
"message": "Post not found with id : '6'",
"path": "/api/posts/6"
}
{
"timestamp": "2021-02-28T14:13:47.572+00:00",
"message": "Post not found with id : '6'",
"details": "uri=/api/posts/6"
}
ErrorDetails
package com.springboot.blog.payload;
import java.util.Date;
public class ErrorDetails {
private Date timestamp;
private String message;
private String details;
public ErrorDetails(Date timestamp, String message, String details) {
this.timestamp = timestamp;
this.message = message;
this.details = details;
}
public Date getTimestamp() {
return timestamp;
}
public String getMessage() {
return message;
}
public String getDetails() {
return details;
}
}
ResourceNotFoundException
package com.springboot.blog.exception;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException{
private String resourceName;
private String fieldName;
private long fieldValue;
public ResourceNotFoundException(String resourceName, String fieldName, long fieldValue) {
super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue)); // Post not found with id : 1
this.resourceName = resourceName;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
}
public String getResourceName() {
return resourceName;
}
public String getFieldName() {
return fieldName;
}
public long getFieldValue() {
return fieldValue;
}
}
Create GlobalExceptionHandler class
package com.springboot.blog.exception;
import com.springboot.blog.payload.ErrorDetails;
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;
import java.util.Date;
@ControllerAdvice
public class GlobalExceptionHandler {
// handle specific exceptions
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorDetails> handleResourceNotFoundException(ResourceNotFoundException exception,
WebRequest webRequest){
ErrorDetails errorDetails = new ErrorDetails(new Date(), exception.getMessage(),
webRequest.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(BlogAPIException.class)
public ResponseEntity<ErrorDetails> handleBlogAPIException(BlogAPIException exception,
WebRequest webRequest){
ErrorDetails errorDetails = new ErrorDetails(new Date(), exception.getMessage(),
webRequest.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.BAD_REQUEST);
}
// global exceptions
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorDetails> handleGlobalException(Exception exception,
WebRequest webRequest){
ErrorDetails errorDetails = new ErrorDetails(new Date(), exception.getMessage(),
webRequest.getDescription(false));
return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
7. Service Layer
PostService
package com.springboot.blog.service;
import com.springboot.blog.payload.PostDto;
import com.springboot.blog.payload.PostResponse;
import java.util.List;
public interface PostService {
PostDto createPost(PostDto postDto);
PostResponse getAllPosts(int pageNo, int pageSize, String sortBy, String sortDir);
PostDto getPostById(long id);
PostDto updatePost(PostDto postDto, long id);
void deletePostById(long id);
}
PostServiceImpl
package com.springboot.blog.service.impl;
import com.springboot.blog.entity.Post;
import com.springboot.blog.exception.ResourceNotFoundException;
import com.springboot.blog.payload.PostDto;
import com.springboot.blog.payload.PostResponse;
import com.springboot.blog.repository.PostRepository;
import com.springboot.blog.service.PostService;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.stream.Collectors;
@Service
public class PostServiceImpl implements PostService {
private PostRepository postRepository;
public PostServiceImpl(PostRepository postRepository) {
this.postRepository = postRepository;
}
@Override
public PostDto createPost(PostDto postDto) {
// convert DTO to entity
Post post = mapToEntity(postDto);
Post newPost = postRepository.save(post);
// convert entity to DTO
PostDto postResponse = mapToDTO(newPost);
return postResponse;
}
@Override
public PostResponse getAllPosts(int pageNo, int pageSize, String sortBy, String sortDir) {
Sort sort = sortDir.equalsIgnoreCase(Sort.Direction.ASC.name()) ? Sort.by(sortBy).ascending()
: Sort.by(sortBy).descending();
// create Pageable instance
Pageable pageable = PageRequest.of(pageNo, pageSize, sort);
Page<Post> posts = postRepository.findAll(pageable);
// get content for page object
List<Post> listOfPosts = posts.getContent();
List<PostDto> content= listOfPosts.stream().map(post -> mapToDTO(post)).collect(Collectors.toList());
PostResponse postResponse = new PostResponse();
postResponse.setContent(content);
postResponse.setPageNo(posts.getNumber());
postResponse.setPageSize(posts.getSize());
postResponse.setTotalElements(posts.getTotalElements());
postResponse.setTotalPages(posts.getTotalPages());
postResponse.setLast(posts.isLast());
return postResponse;
}
@Override
public PostDto getPostById(long id) {
Post post = postRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Post", "id", id));
return mapToDTO(post);
}
@Override
public PostDto updatePost(PostDto postDto, long id) {
// get post by id from the database
Post post = postRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Post", "id", id));
post.setTitle(postDto.getTitle());
post.setDescription(postDto.getDescription());
post.setContent(postDto.getContent());
Post updatedPost = postRepository.save(post);
return mapToDTO(updatedPost);
}
@Override
public void deletePostById(long id) {
// get post by id from the database
Post post = postRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Post", "id", id));
postRepository.delete(post);
}
// convert Entity into DTO
private PostDto mapToDTO(Post post){
PostDto postDto = new PostDto();
postDto.setId(post.getId());
postDto.setTitle(post.getTitle());
postDto.setDescription(post.getDescription());
postDto.setContent(post.getContent());
return postDto;
}
// convert DTO to entity
private Post mapToEntity(PostDto postDto){
Post post = new Post();
post.setTitle(postDto.getTitle());
post.setDescription(postDto.getDescription());
post.setContent(postDto.getContent());
return post;
}
}
8. Controller Layer
package com.springboot.blog.controller;
import com.springboot.blog.payload.PostDto;
import com.springboot.blog.payload.PostResponse;
import com.springboot.blog.service.PostService;
import com.springboot.blog.utils.AppConstants;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/posts")
public class PostController {
private PostService postService;
public PostController(PostService postService) {
this.postService = postService;
}
// create blog post rest api
@PostMapping
public ResponseEntity<PostDto> createPost(@RequestBody PostDto postDto){
return new ResponseEntity<>(postService.createPost(postDto), HttpStatus.CREATED);
}
// get all posts rest api
@GetMapping
public PostResponse getAllPosts(
@RequestParam(value = "pageNo", defaultValue = AppConstants.DEFAULT_PAGE_NUMBER, required = false) int pageNo,
@RequestParam(value = "pageSize", defaultValue = AppConstants.DEFAULT_PAGE_SIZE, required = false) int pageSize,
@RequestParam(value = "sortBy", defaultValue = AppConstants.DEFAULT_SORT_BY, required = false) String sortBy,
@RequestParam(value = "sortDir", defaultValue = AppConstants.DEFAULT_SORT_DIRECTION, required = false) String sortDir
){
return postService.getAllPosts(pageNo, pageSize, sortBy, sortDir);
}
// get post by id
@GetMapping("/{id}")
public ResponseEntity<PostDto> getPostById(@PathVariable(name = "id") long id){
return ResponseEntity.ok(postService.getPostById(id));
}
// update post by id rest api
@PutMapping("/{id}")
public ResponseEntity<PostDto> updatePost(@RequestBody PostDto postDto, @PathVariable(name = "id") long id){
PostDto postResponse = postService.updatePost(postDto, id);
return new ResponseEntity<>(postResponse, HttpStatus.OK);
}
// delete post rest api
@DeleteMapping("/{id}")
public ResponseEntity<String> deletePost(@PathVariable(name = "id") long id){
postService.deletePostById(id);
return new ResponseEntity<>("Post entity deleted successfully.", HttpStatus.OK);
}
}
Create a class AppConstants and add the following code to it:
package com.springboot.blog.utils;
public class AppConstants {
public static final String DEFAULT_PAGE_NUMBER = "0";
public static final String DEFAULT_PAGE_SIZE = "10";
public static final String DEFAULT_SORT_BY = "id";
public static final String DEFAULT_SORT_DIRECTION = "asc";
}
Comments
Post a Comment
Leave Comment