Friday 7 September 2018

Global Exception Handling in Spring Boot

Spring Boot Exception Handling

Question: Why we should use Exception Handling?
Answer: 


  • We should present error or exception messages in user understandable manner, which will help to resolve these errors easily.
  • We should provide proper response status.
  • We should not present sensitive information in the response.

org.springframework.web.bind.annotation
Annotation Type ControllerAdvice

@Target(value=TYPE)
 @Retention(value=RUNTIME)
 @Documented
 @Component
public @interface ControllerAdvice

Specialization of @Component for classes that declare @ExceptionHandler, @InitBinder, or @ModelAttribute methods to be shared across multiple @Controller classes.

Classes with @ControllerAdvice can be declared explicitly as Spring beans or auto-detected via classpath scanning. All such beans are sorted via AnnotationAwareOrderComparator, i.e. based on @Order and Ordered, and applied in that order at runtime. 

For handling exceptions the first @ExceptionHandler to match the exception is used. For model attributes and InitBinder initialization, @ModelAttribute and @InitBinder methods will also follow @ControllerAdvice order.

By default the methods in an @ControllerAdvice apply globally to all Controllers. Use selectors annotations(), basePackageClasses(), and basePackages() (or its alias value()) to define a more narrow subset of targeted Controllers. 

If multiple selectors are declared, OR logic is applied, meaning selected Controllers should match 
at least one selector. Note that selector checks are performed at runtime and so adding many selectors may negatively impact performance and add complexity.

Since:
3.2



Spring introduces @ControllerAdvice annotation which allows you to write global exception handling code that can be applied to a all controllers or depending on controllers to a chosen package or even a specific annotation. 

We can handle global exception using @ControllerAdvice and @ExceptionHandler.
@ControllerAdvice annotation will be applied to all classes that use the @Controller annotation (which extends to classes using @RestController). 

We can use @ControllerAdvice in below formats:

  • @ControllerAdvice("com.gaurav.springboot")
  • @ControllerAdvice(value = "com.gaurav.springboot")
  • @ControllerAdvice(basePackages = "com.gaurav.springboot")

We have another way to define a specific package using basePackageClasses property, which will enable @ControllerAdvice to all controllers inside that package where that class resides.
@ControllerAdvice(basePackageClasses = Application.class)


To apply to specific classes use assignableTypes.
@ControllerAdvice(assignableTypes = UserController.class)


Spring Boot default Exception Handling


Suppose a resource is not available then the response thrown will be look alike below:
when we fire a request which does not exist like: http://localhost:8080/users/test
Then the response presented by Spring boot is like below

{
    "timestamp": "2018-09-07T05:54:32.828+0000",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/users/test"
}

Demo example to configure Global Exception Handling in Spring Boot


1. 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.gaurav.springboot</groupId>
<artifactId>GlobalExceptionHandlingInSpringBoot</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>Global-Exception-Handling-In-SpringBoot</name>
<description>GlobalExceptionHandlingInSpringBoot - Sample Project</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</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>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<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-hateoas</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
  <groupId>com.fasterxml.jackson.datatype</groupId>
  <artifactId>jackson-datatype-jsr310</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>
</plugin>
</plugins>
</build>

<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>


</project>

2. User.java


package com.gaurav.springboot.bean;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "user")
public class User {

public User() {
}

public User(int id, String firstName, String lastName, String gender, int age) {
super();
this.id = id;
this.firstName = firstName;
this.lastName = lastName;
this.gender = gender;
this.age = age;
}

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;

@Column(name = "first_name")
private String firstName;

@Column(name = "last_name")
private String lastName;

@Column(name = "gender")
private String gender;

@Column(name = "age")
private int age;

public long getId() {
return id;
}

public void setId(long id) {
this.id = id;
}

public String getFirstName() {
return firstName;
}

public void setFirstName(String firstName) {
this.firstName = firstName;
}

public String getLastName() {
return lastName;
}

public void setLastName(String lastName) {
this.lastName = lastName;
}

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;
}

@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + (int) (id ^ (id >>> 32));
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
User other = (User) obj;
if (id != other.id)
return false;
return true;
}

}

3. ErrorDetails.java

package com.gaurav.springboot.exception;
import java.util.Date;

import org.springframework.http.HttpStatus;

import com.fasterxml.jackson.annotation.JsonFormat;

public class ErrorDetails {

  @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd MMM yyyy hh:mm:ss aaa")
  private Date timestamp;
  private String message;
  private String description;

public Date getTimestamp() {
return timestamp;
}

public String getMessage() {
    return message;
  }

public String getDescription() {
return description;
}

public ErrorDetails(Date timestamp, String message, String description) {
    super();
    this.timestamp = timestamp;
    this.message = message;
    this.description = description;
  }

}

4. UserNotFoundException.java


package com.gaurav.springboot.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class UserNotFoundException extends RuntimeException {

/**
*/
private static final long serialVersionUID = -8403805750611410023L;

public UserNotFoundException(String exception) {
super(exception);
}

}

5. CustomizedResponseEntityExceptionHandler.java

package com.gaurav.springboot.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.bind.annotation.RestController;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;

@ControllerAdvice
@RestController
public class CustomizedResponseEntityExceptionHandler extends ResponseEntityExceptionHandler {

  @ExceptionHandler(Exception.class)
  public final ResponseEntity<ErrorDetails> handleExceptionsGlobally(Exception ex, WebRequest request) {
    ErrorDetails exceptionResponse = new ErrorDetails(new Date(), ex.getMessage(), 
        request.getDescription(false));
    return new ResponseEntity<>(exceptionResponse, HttpStatus.INTERNAL_SERVER_ERROR);
  }

  @ExceptionHandler(UserNotFoundException.class)
  public final ResponseEntity<ErrorDetails> handleUserNotFoundException(UserNotFoundException ex, WebRequest request) {
    ErrorDetails exceptionResponse = new ErrorDetails(new Date(), ex.getMessage(), 
        request.getDescription(false));
    return new ResponseEntity<>(exceptionResponse, HttpStatus.NOT_FOUND);
  }

}

6. UserRepository.java

package com.gaurav.springboot.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import com.gaurav.springboot.bean.User;

@Repository
public interface UserRepository extends JpaRepository<User, Long>{

}


7. UserController.java

package com.gaurav.springboot.controller;

import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.methodOn;

import java.net.URI;
import java.util.List;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.Resource;
import org.springframework.hateoas.mvc.ControllerLinkBuilder;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
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.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import com.gaurav.springboot.bean.User;
import com.gaurav.springboot.exception.UserNotFoundException;
import com.gaurav.springboot.repository.UserRepository;

@RestController
@RequestMapping(value = { "/users" })
public class UserController {

@Autowired
private UserRepository userRepository;

/**
* To create a user
* @param user
* @return
*/
@PostMapping("/new")
public ResponseEntity<Object> createUser(@RequestBody User user) {
User savedUser = userRepository.save(user);

URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}")
.buildAndExpand(savedUser.getId()).toUri();

return ResponseEntity.created(location).build();

}
/**
* To retrieve all users
* @return
*/
@GetMapping("/get/all")
public List<User> getAllUsers() {
return userRepository.findAll();
}

/**
* To retrieve specific users
* @param id
* @return
*/
@GetMapping("/get/{id}")
public Resource<User> getUser(@PathVariable long id) {
Optional<User> user = userRepository.findById(id);

if (!user.isPresent())
throw new UserNotFoundException("id-" + id+" is not available");

Resource<User> resource = new Resource<User>(user.get());

ControllerLinkBuilder linkTo = linkTo(methodOn(this.getClass()).getAllUsers());

resource.add(linkTo.withRel("get-all-users"));

return resource;
}

/**
* To update specific user
* @param user
* @param id
* @return
*/
@PutMapping("/update/{id}")
public ResponseEntity<Object> updateUser(@RequestBody User user, @PathVariable long id) {

Optional<User> userDet = userRepository.findById(id);

if (!userDet.isPresent())
return ResponseEntity.notFound().build();

user.setId(id);
userRepository.save(user);

// return ResponseEntity.noContent().build();
return ResponseEntity.ok().build();
}
/**
* To Delete specific user
* @param id
*/
@DeleteMapping("/delete/{id}")
public void deleteUser(@PathVariable long id) {
userRepository.deleteById(id);
}
}

8. GlobalExceptionHandlingApplication.java


package com.gaurav.springboot;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GlobalExceptionHandlingApplication {

public static void main(String[] args) {
SpringApplication.run(GlobalExceptionHandlingApplication.class, args);
}
}

9. application.properties


logging.level.org.springframework.web=INFO
spring.datasource.driver: com.mysql.jdbc.Driver
spring.datasource.url: jdbc:mysql://localhost:3306/experimentdemo
spring.datasource.username:root
spring.datasource.password:root

spring.jpa.hibernate.dialect:org.hibernate.dialect.MySQL5Dialect
spring.jpa.show-sql:true
spring.jpa.hibernate.ddl-auto :update
spring.jpa.entitymanager.packagesToScan:com.gaurav.springboot

Get All Users: http://localhost:8080/users/get/all


Get Specific User: http://localhost:8080/users/get/1



POST(Create a new User):http://localhost:8080/users/new


Check the Created User in DB:



PUT(Updated the User): http://localhost:8080/users/update/11



Delete(Delete User): http://localhost:8080/users/delete/11


No comments:

Post a Comment