restcontrolleradvice-example-spring-boot-feature-image

@RestControllerAdvice example in Spring Boot

In this tutorial, we’re gonna look at an Spring Boot example that uses @RestControllerAdvice for exception handling in Restful API. I also show you the comparison between @RestControllerAdvice and @ControllerAdvice along with the use of @ExceptionHandler annotation.

Related Posts:
Spring Boot, Spring Data JPA – Rest CRUD API example
Spring Boot Pagination & Filter example
Spring Boot Sort/Order by multiple Columns

More Practice:
Spring Boot Multipart File upload example
Spring Boot Token based Authentication with Spring Security & JWT


Rest API exception handling

We’ve created Rest Controller for CRUD Operations and finder method.
Let look at the code:
(step by step to build the Rest APIs is in:
Spring Boot Data JPA CRUD example
Spring Boot MongoDB CRUD example
Spring Boot Cassandra CRUD example)

@RestController
public class TutorialController {

  @Autowired
  TutorialRepository tutorialRepository;

  @GetMapping("/tutorials")
  public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) {
    try {
      ...
      return new ResponseEntity<>(tutorials, HttpStatus.OK);
    } catch (Exception e) {
      return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }
  
  @GetMapping("/tutorials/{id}")
  public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) {
    Optional<Tutorial> tutorialData = tutorialRepository.findById(id);

    if (tutorialData.isPresent()) {
      return new ResponseEntity<>(tutorialData.get(), HttpStatus.OK);
    } else {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
  }
  
  @PutMapping("/tutorials/{id}")
  public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
    Optional<Tutorial> tutorialData = tutorialRepository.findById(id);

    if (tutorialData.isPresent()) {
      ...
      return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK);
    } else {
      return new ResponseEntity<>(HttpStatus.NOT_FOUND);
    }
  }

  ...

  @DeleteMapping("/tutorials/{id}")
  public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") long id) {
    try {
      tutorialRepository.deleteById(id);
      return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    } catch (Exception e) {
      return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);
    }
  }

  @DeleteMapping("/tutorials")
  public ResponseEntity<HttpStatus> deleteAllTutorials() {
    // try and catch
  }

  @GetMapping("/tutorials/published")
  public ResponseEntity<List<Tutorial>> findByPublished() {
    // try and catch
  }
}

You can see that we use try/catch many times for similar exception (INTERNAL_SERVER_ERROR), and there are also many cases that return NOT_FOUND.

Is there any way to keep them simple, any way to attach the error response message smartly and flexibility?
Let’s solve the problem now.

Rest Exception Handler with Controller Advice in Spring

Spring supports exception handling by a global Exception Handler (@ExceptionHandler) with Controller Advice (@RestControllerAdvice).

@RestControllerAdvice
public class ControllerExceptionHandler {
  
  @ExceptionHandler(value = {ResourceNotFoundException.class, CertainException.class})
  @ResponseStatus(value = HttpStatus.NOT_FOUND)
  public ErrorMessage resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
    ErrorMessage message = new ErrorMessage(
        status,
        date,
        ex.getMessage(),
        description);
    
    return message;
  }
}

The @RestControllerAdvice annotation is specialization of @Component annotation so that it is auto-detected via classpath scanning. It is a kind of interceptor that surrounds the logic in our Controllers and allows us to apply some common logic to them.

Rest Controller Advice’s methods (annotated with @ExceptionHandler) are shared globally across multiple @Controller components to capture exceptions and translate them to HTTP responses. The @ExceptionHandler annotation indicates which type of Exception we want to handle. The exception instance and the request will be injected via method arguments.

By using two annotations together, we can:

  • control the body of the response along with status code
  • handle several exceptions in the same method

How about @ResponseStatus?

@RestControllerAdvice annotation tells a controller that the object returned is automatically serialized into JSON and passed it to the HttpResponse object. You only need to return Java body object instead of ResponseEntity object. But the status could be always OK (200) although the data corresponds to exception signal (404 – Not Found for example). @ResponseStatus can help to set the HTTP status code for the response:

@ResponseStatus(value = HttpStatus.NOT_FOUND)
public ErrorMessage resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
  // ...    
  return message;
}

@RestControllerAdvice with @ResponseEntity

If you use @RestControllerAdvice without @ResponseBody and @ResponseStatus, you can return ResponseEntity object instead.

@RestControllerAdvice
public class ControllerExceptionHandler {

  @ExceptionHandler(ResourceNotFoundException.class)
  @ResponseStatus(value = HttpStatus.NOT_FOUND)
  public ErrorMessage resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
    ErrorMessage message = new ErrorMessage(...);
    
    return message;
  }
}

@ControllerAdvice vs @RestControllerAdvice

@RestControllerAdvice is the combination of both @ControllerAdvice and @ResponseBody:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
  ...
}

You can just understand it as:

  • @RestControler = @Controller + @ResponseBody
  • @RestControllerAdvice = @ControllerAdvice + @ResponseBody

Let’s do the thing with @ControllerAdvice and @ResponseBody:


@ControllerAdvice
@ResponseBody
public class ControllerExceptionHandler {

  @ExceptionHandler(ResourceNotFoundException.class)
  @ResponseStatus(value = HttpStatus.NOT_FOUND)
  public ErrorMessage resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
    ErrorMessage message = new ErrorMessage(...);
    return message;
  }
}

If you use @ControllerAdvice without @ResponseBody and @ResponseStatus, you can return ResponseEntity object instead.

@ControllerAdvice
public class ControllerExceptionHandler {

  @ExceptionHandler(ResourceNotFoundException.class)
  public ResponseEntity<ErrorMessage> resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
    ErrorMessage message = new ErrorMessage...);
    
    return new ResponseEntity<ErrorMessage>(message, HttpStatus.NOT_FOUND);
  }
}

Step by step to use @ControllerAdvice can be found in the post:
Spring Boot @ControllerAdvice & @ExceptionHandler example

Setup Spring Boot Project

You can follow step by step, or get source code in one of following posts:
Spring Boot Data JPA CRUD example
Spring Boot MongoDB CRUD example
Spring Boot Cassandra CRUD example

The Spring Project contains structure that we only need to add some changes to make the pagination work well.

spring-boot-data-jpa-crud-example-project-structure

Or you can get the new Github source code at the end of this tutorial.

The final project structure will be like this:

restcontrolleradvice-example-spring-boot-project-structure

Define Error Response Structure

We want to create a our own message response structure instead of using default error response provided by Spring Boot.
Let’s define a specific error response structure.

exception/ErrorMessage.java

package com.bezkoder.spring.rest.exhandling.exception;

import java.util.Date;

public class ErrorMessage {
  private int statusCode;
  private Date timestamp;
  private String message;
  private String description;

  public ErrorMessage(int statusCode, Date timestamp, String message, String description) {
    this.statusCode = statusCode;
    this.timestamp = timestamp;
    this.message = message;
    this.description = description;
  }

  public int getStatusCode() {
    return statusCode;
  }

  public Date getTimestamp() {
    return timestamp;
  }

  public String getMessage() {
    return message;
  }

  public String getDescription() {
    return description;
  }
}

Create Custom Exception

We’re gonna throw an exception for Resource not found in Spring Boot controller.
Lets create a ResourceNotFoundException class that extends RuntimeException.

exception/ResourceNotFoundException.java

package com.bezkoder.spring.rest.exhandling.exception;

public class ResourceNotFoundException extends RuntimeException {

  private static final long serialVersionUID = 1L;

  public ResourceNotFoundException(String msg) {
    super(msg);
  }
}

Create @RestControllerAdvice with @ExceptionHandler

Now we’re gonna create a special class which is annotated by @RestControllerAdvice annotation. This class handles specific exception (ResoureNotFoundException) and global Exception in only one place.

exception/ControllerExceptionHandler.java

package com.bezkoder.spring.rest.exhandling.exception;

import java.util.Date;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;

import com.bezkoder.spring.rest.exhandling.exception.ErrorMessage;
import com.bezkoder.spring.rest.exhandling.exception.ResourceNotFoundException;

@RestControllerAdvice
public class ControllerExceptionHandler {

  @ExceptionHandler(ResourceNotFoundException.class)
  @ResponseStatus(value = HttpStatus.NOT_FOUND)
  public ErrorMessage resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
    ErrorMessage message = new ErrorMessage(
        HttpStatus.NOT_FOUND.value(),
        new Date(),
        ex.getMessage(),
        request.getDescription(false));
    
    return message;
  }
  
  @ExceptionHandler(Exception.class)
  @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
  public ErrorMessage globalExceptionHandler(Exception ex, WebRequest request) {
    ErrorMessage message = new ErrorMessage(
        HttpStatus.INTERNAL_SERVER_ERROR.value(),
        new Date(),
        ex.getMessage(),
        request.getDescription(false));
    
    return message;
  }
}

Modify Controller for using @RestControllerAdvice

Our Rest Controller now doesn’t have try/catch block, and it will throw ResourceNotFoundException where we want to send NOT_FOUND notification in response message.

controller/TutorialController.java

package com.bezkoder.spring.rest.exhandling.controller;

import java.util.ArrayList;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.CrossOrigin;
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.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.bezkoder.spring.rest.exhandling.exception.ResourceNotFoundException;
import com.bezkoder.spring.rest.exhandling.model.Tutorial;
import com.bezkoder.spring.rest.exhandling.repository.TutorialRepository;

@CrossOrigin(origins = "http://localhost:8081")
@RestController
@RequestMapping("/api")
public class TutorialController {

  @Autowired
  TutorialRepository tutorialRepository;

  @GetMapping("/tutorials")
  public ResponseEntity<List<Tutorial>> getAllTutorials(@RequestParam(required = false) String title) {
    List<Tutorial> tutorials = new ArrayList<Tutorial>();

    if (title == null)
      tutorialRepository.findAll().forEach(tutorials::add);
    else
      tutorialRepository.findByTitleContaining(title).forEach(tutorials::add);

    if (tutorials.isEmpty()) {
      return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

    return new ResponseEntity<>(tutorials, HttpStatus.OK);
  }

  @GetMapping("/tutorials/{id}")
  public ResponseEntity<Tutorial> getTutorialById(@PathVariable("id") long id) {
    Tutorial tutorial = tutorialRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));

    return new ResponseEntity<>(tutorial, HttpStatus.OK);
  }

  @PostMapping("/tutorials")
  public ResponseEntity<Tutorial> createTutorial(@RequestBody Tutorial tutorial) {
    Tutorial _tutorial = tutorialRepository.save(new Tutorial(tutorial.getTitle(), tutorial.getDescription(), false));
    return new ResponseEntity<>(_tutorial, HttpStatus.CREATED);
  }

  @PutMapping("/tutorials/{id}")
  public ResponseEntity<Tutorial> updateTutorial(@PathVariable("id") long id, @RequestBody Tutorial tutorial) {
    Tutorial _tutorial = tutorialRepository.findById(id)
        .orElseThrow(() -> new ResourceNotFoundException("Not found Tutorial with id = " + id));

    _tutorial.setTitle(tutorial.getTitle());
    _tutorial.setDescription(tutorial.getDescription());
    _tutorial.setPublished(tutorial.isPublished());
    
    return new ResponseEntity<>(tutorialRepository.save(_tutorial), HttpStatus.OK);
  }

  @DeleteMapping("/tutorials/{id}")
  public ResponseEntity<HttpStatus> deleteTutorial(@PathVariable("id") long id) {
    tutorialRepository.deleteById(id);
    
    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
  }

  @DeleteMapping("/tutorials")
  public ResponseEntity<HttpStatus> deleteAllTutorials() {
    tutorialRepository.deleteAll();
    
    return new ResponseEntity<>(HttpStatus.NO_CONTENT);
  }

  @GetMapping("/tutorials/published")
  public ResponseEntity<List<Tutorial>> findByPublished() {
    List<Tutorial> tutorials = tutorialRepository.findByPublished(true);

    if (tutorials.isEmpty()) {
      return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }
    
    return new ResponseEntity<>(tutorials, HttpStatus.OK);
  }
}

Run and Test

We finish implementing CRUD REST APIs and exception handling for it.
Run Spring Boot application with command: mvn spring-boot:run.

– Get a non-existent tutorial:

restcontrolleradvice-example-spring-boot-demo-get-not-found

– Update a non-existent tutorial:

restcontrolleradvice-example-spring-boot-demo-put-not-found

– Create tutorial with wrong fields:

restcontrolleradvice-example-spring-boot-demo-post-exception

– Delete a non-existent tutorial:

restcontrolleradvice-example-spring-boot-demo-delete-not-found

Conclusion

Today we’ve built a Exception Handling class for Spring Boot Rest APIs example using @RestControllerAdvice along with @ExceptionHandler. Now you can create your own custom exception handler class or handle global exception in single place at ease.

If you want to add Pagination to this Spring project, you can find the instruction at:
Spring Boot Pagination & Filter example | Spring JPA, Pageable

To sort/order by multiple fields:
Spring Data JPA Sort/Order by multiple Columns | Spring Boot

Or way to write Unit Test for the JPA Repository:
Spring Boot Unit Test for JPA Repositiory with @DataJpaTest

You can also know how to deploy this Spring Boot App on AWS (for free) with this tutorial.

Happy learning! See you again.

Further Reading

Source Code

You can find the complete source code for this tutorial on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *