Spring Boot ControllerAdvise example

ControllerAdvice

ControllerAdvice is an annotation provided by Spring. It is used to manage Exception handling in a Spring application. It allows us to handle any exception from any Classes in the Application and not just a single Class or one method. This article explains the advantages of using ControllerAdvice and some important points to be considered while using it.
In this article, we will walk through a few examples to demonstrate the functionality of ControllerAdvice annotation. To start will we will use the Spring Boot Rest application. This application has many endpoints specifically to drive a blog backed which can serve APIs for CRUD operations on Blog, Tags, Categories, etc.
Let's examine the following endpoints from the application which allows us to retrieve a specific Blog or Comments by its id.
Example REST API
GET /blogapi/blogs/:Blog_ID
GET /blogapi/comments/:Comments_ID
GET /blogapi/categories/:CAT_ID
All these endpoints return the respective Objects if they exist with the specified ID or throws NotFoundException. If we think about any other Resource that has a GET endpoint will have a similar use case, that is throwing NotFoundException if Object is not found with the specified id.
So basically for any Resource Controller will need to deal with common exceptions that can occur while invoking the API. ControllerAdvice helps in handling all these exceptions from all the controllers globally in a separate class.
We will see the implementation of a Blog controller without Controlleradvice and with Controlleradvice
Let's see the Controller for the GET /blogs/id endpoint without Controlleradvice.

Example 1

Controller for Blog
This controller method implements the GET blog by id. The controller uses BlogService to get the BlogStory object with the specified id. If the service can't find any blog with the specified id, it throws NotFoundException. In the controller, we catch that exception and return ResponseEntity with HTTP Status Not Found. The service can also throw AppExceptions which is also needed to be handled at the controller.
@RequestMapping(value = {"/v1/blogs/{id}"}, method = RequestMethod.GET,
      produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity getBlogStory(@PathVariable(value = "") String id) {
    ResponseEntity apiResponse;
      BlogStory blogStory;
      try {
        blogStory = blogService.getBlogStory(id);
        apiResponse = new ResponseEntity(blogStory, HttpStatus.OK);
      } catch (NotFoundException e) {
        //Handle NotFoundException
        apiResponse = new ResponseEntity(HttpStatus.NOT_FOUND);
        e.printStackTrace();
      }
      catch (AppException e) {
        //Handle AppException
        apiResponse = new ResponseEntity(HttpStatus.INTERNAL_SERVER_ERROR);
        log.error(e.getMessage());
      }
    return apiResponse;
  }
Nothing is wrong with this controller but, we are doing the same kind of stuff probably in all the controller methods, that are handling NotFoundException or AppException. For instance think about the implementations of other endpoints GET /categories/id and GET /comments/id.
That's where ControllerAdvice comes into the picture. We will create a Global Exception handing class that can handle the specific type of Exceptions for example in our case it is NotFoundException. We mark the class with @ControllerAdvice so that Spring can apply the exception handling implemented in this class globally to all the controllers in this application.

Example 2

GlobalExceptionHandler
package com.bootng.handler.exception;

import java.util.Collections;
import java.util.List;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpMediaTypeNotSupportedException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.context.request.WebRequest;
import com.bootng.beans.AppException;

@ControllerAdvice
public class GlobalExceptionHandler {

  private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/** Customize the response for NotFoundException. */
  @ExceptionHandler({NotFoundException.class})
  protected ResponseEntity<Object> handleNotFoundException(Exception ex, WebRequest request) {
    log.info("global exception handler " + ex.getMessage());
    List<String> errors = Collections.singletonList(ex.getMessage());
    ResponseEntity<Object> ret = ResponseEntity.status(HttpStatus.NOT_FOUND).body(errors);
    return ret;
  }
  
  /** Customize the response for AppException. */
  @ExceptionHandler({AppException.class})
  protected ResponseEntity<Object> handleException(Exception ex, WebRequest request) {
    log.info("global exception handler " + ex.getMessage());
    String errors = "We cannot serve the request now, please try later";
    ResponseEntity<Object> ret = ResponseEntity.status(HttpStatus.FORBIDDEN).body(errors);
    return ret;
  }
}
In this ControllerAdvice implementations, we are handing two exceptions, NotFoundException and AppException. If you see the source code in git, this ControllerAdvice has actually more methods to handle more Exceptions, but here we are just showing handling just these two Exceptions.
Now let us write the same controller taking advantage of the Global Exception Handler class we defined above. Now we don't have to catch the exception in our controller, we can simply work with the core business logic and delegate the exception handling to the Global Exception Handler Class.
Get Blog By ID v2
@RequestMapping(value = {"/v2/blogs/{id}"}, method = RequestMethod.GET,
      produces = MediaType.APPLICATION_JSON_VALUE)
  public ResponseEntity<BlogStory> getBlogStoryV2(@PathVariable(value = "") String id)
      throws AppException, NotFoundException {
    BlogStory blogStory = blogService.getBlogStory(id);
    return new ResponseEntity<BlogStory>(blogStory, HttpStatus.OK);
  }
For the end-user both the controller works exactly the same. But the implementation is different with regards to handling exceptions. If we have a ControllerAdvice and any of the controllers throws exception handled by ControllerAdvice then, Spring will invoke the respective method from the ControllerAdvice to handle the exception


Summary
  • The @ControllerAdvice annotation was first introduced in Spring 3.2
  • We can have a single or multiple controller advice class per application. Having a single controller advice class is recommended per application.
  • ControllerAdvice should contain methods to handle specific errors in the application.
  • Using ControllerAdvice results in more clean and maintainable codes.
  • We can handle both custom Exceptions as well as Spring Exceptions like HttpMediaTypeNotSupportedException, HttpRequestMethodNotSupportedException etc.
Git Source Code
  • git clone https://github.com/siddharthagit/spring-boot-references
  • cd spring-rest
  • mvn clean install
  • mvn spring-boot:run

    No comments :

    Post a Comment

    Please leave your message queries or suggetions.