In Spring Boot applications, graceful error handling is crucial for providing a seamless user experience and informative feedback. The @ControllerAdvice
annotation offers a powerful mechanism to centrally handle exceptions thrown by your controllers. This approach promotes code reusability, consistency, and better error management.
Understanding @ControllerAdvice
The @ControllerAdvice
annotation designates a class as an aspect that advises controllers. It allows you to define methods that will be executed before or after controller methods, or handle exceptions thrown by controllers.
Before getting into the details of a @ControllerAdvice, let’s see how Exceptions flow from a Spring Boot Controller to the Client.
Throwing Exceptions from the Endpoint
Let’s create a resource that throws an exception, and send a GET request to it in order to understand how the application reacts to runtime exceptions.
Check the following code snippet:
@RequestMapping("/list") public List < Customer > findAll() { throw new RuntimeException("Some Exception Occured"); }
Let’s fire a GET request to the preceding service at http://localhost:8080/list/
The response is as shown in the following code:
curl -s http://localhost:8080/list | jq { "timestamp": "2020-04-01T13:15:11.091+0000", "status": 500, "error": "Internal Server Error", "message": "Some Exception Occured", "path": "/list" }
Some important things to note are as follows:
- The response header has an HTTP status of 500 ; Internal server error
- Spring Boot also returns the message which contains the error thown.
Using Controller Advice
Create a class annotated with @ControllerAdvice
and define methods with @ExceptionHandler
to handle specific exceptions.
For example:
@ControllerAdvice public class ControllerAdvisor { @ExceptionHandler(CustomerNotFoundException.class) public ResponseEntity<Object> handleCustomerNotFoundException( CustomerNotFoundException ex) { Map<String, Object> body = new LinkedHashMap<>(); body.put("timestamp", LocalDateTime.now()); body.put("message", ex.getMessage()); return new ResponseEntity<>(body, HttpStatus.NOT_FOUND); } @ExceptionHandler(Exception.class) public ResponseEntity<String> handleException(Exception ex) { return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("An error occurred: " + ex.getMessage()); } }
Explanation:
- @ControllerAdvice: This annotation indicates that the class provides global advice for controllers.
- @ExceptionHandler: This annotation marks a method as an exception handler for a specific exception type.
- @ResponseStatus: Sets the HTTP status code for the response.
In our example, the @ControllerAdvice captures both a generic Exception and a custom Exception such as CustomerNotFoundException.
Therefore, the following method, in case of CustomerException, will be captured by our ControllerAdvice:
public Customer getCustomerById(Long id) { // Use stream and filter with lambda expression to find customer by id return customerList.stream() .filter(customer -> customer.getId() == id) .findFirst() // Return the first matching element or null if none found .orElseThrow(() -> new CustomerNotFoundException("Customer not found!")); // Return null if no customer found with the id }
As you can see, in this case we are throwing a CustomerNotFoundException if your search returns no data.
In this case, if we return a 500 Internal Server Error it would not be so meaningful for the Client, which cannot guess how to fix the error.
And here is the CustomerNotFoundException class:
public class CustomerNotFoundException extends RuntimeException { public CustomerNotFoundException() { } @Override public String getMessage() { return message; } public void setMessage(String message) { this.message = message; } String message; public CustomerNotFoundException(String s) { this.message = s; } }
How to customize all Exception Responses
Finally, to override the default JSON error response for all exceptions, simply create a Class which extends DefaultErrorAttributes. Let’s see how we could add in the list of error attribures the application version:
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; import org.springframework.stereotype.Component; import org.springframework.web.context.request.WebRequest; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Map; @Component public class MyErrorAttributes extends DefaultErrorAttributes { private static final DateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); @Override public Map < String, Object > getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) { Map < String, Object > errorAttributes = super.getErrorAttributes(webRequest, includeStackTrace); Object timestamp = errorAttributes.get("timestamp"); if (timestamp == null) { errorAttributes.put("timestamp", dateFormat.format(new Date())); } else { errorAttributes.put("timestamp", dateFormat.format((Date) timestamp)); } // Custom Attribute errorAttributes.put("app.version", "2.2"); return errorAttributes; } }
In this case, the returned JSON will be:
curl -s http://localhost:8080/query/9 | jq { "timestamp": "2020/04/01 16:25:29", "status": 404, "error": "Not Found", "message": "No message available", "path": "/query/9", "app.version": "2.2" }
Source code for this tutorial: https://github.com/fmarchioni/masterspringboot/tree/master/exception
Found the article helpful? if so please follow us on Socials