November 13, 2024

Spring Boot REST API Exception Handling Using @ControllerAdvice

In the article Spring Boot REST API, Data JPA, One-to-Many/Many-To-One Bidirectional Example we have seen how to create a Spring Boot REST API with Spring Data, JPA but one thing that is missing in that application is; exception handling. In this article we'll see how to handle exception in a Spring Boot REST application using @ControllerAdvice and @ExceptionHandler annotations.

If you don't handle exceptions that are thrown with in your application that results in stack trace being shown to the end user which is not considered a good practice. Here is an example of trying to get Customer data by passing an ID that doesn't exist.

Spring Boot Exception Handling

As you can see whole stack trace is shown to the end user which may not even make sense to the user. You should rather show a meaningful message to the user by handling the exceptions.

Exceptions in current application

The REST API which we developed throws a custom exception- ResourceNotFoundException in the methods where the resource should already exist. One such method is as given below.

@Override
public Customer getCustomerById(Long customerId) {
  Customer customer = customerRepository.findById(customerId)
                      .orElseThrow(() -> new ResourceNotFoundException("Customer not found for the given Id: " + customerId));
  return customer;
}

Application also throws RunTimeException where it converts a DB layer exception to a RunTimeException so the DB layer exception is not passed as it is to the presentation layer. One such method which is in CustomerController is as given below.

@PostMapping("/customer")
public ResponseEntity<Customer> createCustomer(@RequestBody Customer customer) {
  try {
    Customer savedCustomer = customerService.createCustomer(customer);
    final URI location = ServletUriComponentsBuilder.fromCurrentRequest().path("/{id}").build()
                .expand(savedCustomer.getCustomerId()).toUri();
    return ResponseEntity.created(location).body(savedCustomer);
  }catch(Exception e) {
    throw new RuntimeException("Error while creating customer " + e.getMessage());          
  }
}

Handling exception in Spring Boot REST application

In order to handle the application one strategy is to add method in each controller with @ExceptionHandler annotation to handle the exceptions that are thrown.

For example, in order to handle the RUnTimeException, you can add a method as given below in Customer Controller.

@ExceptionHandler(RuntimeException.class)
public ResponseEntity<ErrorResponse> handleRunTimeException(Exception ex) {
  String message = "Error while processing request";
  ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNPROCESSABLE_ENTITY, message, ex.getMessage());
  return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.UNPROCESSABLE_ENTITY);
  
}

But the problem with this approach is this handling of exception is limited to the exception thrown in CustomerController. For the same type of exception in AccountController you will have to add the similar method in that controller and so on meaning having a lot of duplicate code doing the same task.

Handling exception using @ControllerAdvice

Using @ControllerAdvice annotation you can create a single component which handles exceptions globally. Which means you can write a single method, in this component, annotated with @ExceptionHandler and that will handle exception thrown in any Controller.

Creating global exception handler

In order to create a global exception handler, you need to create a class annotated with @ControllerAdvice annotation.

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.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
import com.knpcode.customer.dto.ErrorResponse;

@ControllerAdvice
public class UniversalExceptionHandler extends ResponseEntityExceptionHandler{
  
  @ExceptionHandler(value = ResourceNotFoundException.class)
  public ResponseEntity<ErrorResponse> handleResourceNotFoundException(Exception ex) {
    String message = "Error while processing request";
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND, message, ex.getMessage());
    return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.NOT_FOUND);
    
  }
  
  @ExceptionHandler(RuntimeException.class)
  public ResponseEntity<ErrorResponse> handleRunTimeException(Exception ex) {
    String message = "Error while processing request";
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.UNPROCESSABLE_ENTITY, message, ex.getMessage());
    return new ResponseEntity<ErrorResponse>(errorResponse, HttpStatus.UNPROCESSABLE_ENTITY);
    
  }
}

Important points about this class-

  1. The component class extends ResponseEntityExceptionHandler which is an abstract class with method that handles all Spring MVC raised exceptions. You can extend it and then add extra methods to handle other exceptions.
  2. As you can see now you have to write a method only once which will work for any controller throwing the exception specified with the @ExceptionHandler annotation.
  3. You can specify more than one exception class with @ExceptionHandler annotation.
     @ExceptionHandler(value = {Exception1.class, Exception2.class})
    
    Then the method will handle all the exceptions specified with the @ExceptionHandler.
  4. In our component there are two separate methods on for handling ResourceNotFoundException and another for handling RuntimeException because the HttpStatus code which is sent is different.
  5. General practice is to create a class whose object is sent as the response body. Here that class is named ErrorResponse and it has the following fields-
    • statusCode- For HTTP status code
    • timestamp- Date and time when the exception is thrown
    • message- To store a generic user friendly message
    • exceptionMessage- To store exception message
    import java.time.LocalDateTime;
    import org.springframework.http.HttpStatus;
    import com.fasterxml.jackson.annotation.JsonFormat;
    
    public class ErrorResponse {
    	private HttpStatus statusCode;
    	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
    	private LocalDateTime timestamp;
    	private String message;
    	private String exceptionMessage;
    	ErrorResponse() {}
    	public ErrorResponse(HttpStatus statusCode, String message, String exceptionMessage) {
    		this.statusCode = statusCode;
    		this.message = message;
    		this.exceptionMessage = exceptionMessage;
    		this.timestamp = LocalDateTime.now();
    	}
    	
    	public HttpStatus getStatusCode() {
    		return statusCode;
    	}
    	public void setStatus(HttpStatus statusCode) {
    		this.statusCode = statusCode;
    	}
    	public LocalDateTime getTimestamp() {
    		return timestamp;
    	}
    	public void setTimestamp(LocalDateTime timestamp) {
    		this.timestamp = timestamp;
    	}
    	public String getMessage() {
    		return message;
    	}
    	public void setMessage(String message) {
    		this.message = message;
    	}
    	public String getExceptionMessage() {
    		return exceptionMessage;
    	}
    	public void setExceptionMessage(String exceptionMessage) {
    		this.exceptionMessage = exceptionMessage;
    	}
    	
    }
    

Messages after exception handling

Trying to get customer data where ID doesn't exist.

Spring Boot @ControllerAdvice Example

Trying to insert another customer with the same mobile number. Note that mobile number column is kept unique in the DB.

Exception handling POST mapping

That's all for the topic Spring Boot REST API Exception Handling Using @ControllerAdvice. If something is missing or you have something to share about the topic please write a comment.


You may also like

No comments:

Post a Comment