In software development, exception handling is a way or mechanism to handle any abnormality in the code at runtime in order to maintain the normal flow of the program. The most common way to apply exception handling in our code is by using try catch blocks. Suppose we are designing a simple microservice with a controller, service and DAO class, where all the exceptions are being handled in the controller itself, whether it occurs in the service, DAO, or the controller itself.
Exception Handling in the Controller:
In the sample application, we are having only a controller in which we are manually throwing an exception to replicate the flow of any normal microservice. Let's start with the example:
@RestController
public class DemoController {
@RequestMapping("exception/arithmetic")
public String controllerForArithmeticException()
{
throw new ArithmeticException("Divide by zero error");
}
}
Response in postman: (http://localhost:8080/exception/arithmetic)
Status: 500 Internal Server Error
Body:
{
"timestamp": "2020–02–28T17:21:50.860+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Divide by zero error",
"path": "/exception/arithmetic"
}
Now let's add a try-catch block to handle the exception as we don't want that our browser receives the exception details. It is not a good practice to leave the exceptions unhandled as it reveals the details of our internal system to the browser (client).
@RestController
public class DemoController {
@RequestMapping("exception/arithmetic")
public String controllerForArithmeticException()
{
try {
throw new ArithmeticException("Divide by zero error");
}
catch(ArithmeticException ae){
return ae.getMessage();
}
}
}
Response in postman: (http://localhost:8080/exception/arithmetic)
Status: 200 Ok
Body:
Divide by zero error
Now suppose we are exposing two endpoints in our microservice. In this case, we need to handle exceptions in both the controller methods separately using try-catch blocks as it can be seen below:
@RestController
public class DemoController {
@RequestMapping("exception/arithmetic")
public String controllerForArithmeticException()
{
try {
throw new ArithmeticException("Divide by zero error");
}
catch(ArithmeticException ae){
return ae.getMessage();
}
}
@RequestMapping("exception/arithmetic2")
public String controllerForArithmeticException2()
{
try {
throw new ArithmeticException("Divide by zero error");
}
catch(ArithmeticException ae){
return ae.getMessage();
}
}
}
This adds a lot of boilerplate codes. One more reason not to add exception handling code in our controller is to facilitate the separation of concerns. Since exception handling is not the part of our main business or/and it doesn't belong to the controller, it should be done in a separate class. So here comes the concept of global exception handling in spring boot in which any exception occurring in the controllers will be handled inside a separate single class globally.
Using @ControllerAdvice and @ExceptionHandler
The @ControllerAdvice
is a Spring Boot annotation to handle exceptions globally and it was introduced in Spring 3.2. A class annotated with it consists of the Exception handlers, annotated with @ExceptionHandler
handling the exceptions occurring in any controller of the application. In simple terms, it intercepts all the exceptions occurring in any of the controllers. The following strategy may be followed while adding global exception handling in our spring boot application:
-
Create and use data class for the exception: Instead of using the built-in exception classes, we must always create our own exception data class to represent the exceptions and use it.
-
Single @ControllerAdvice
annotated class per application: A single class must be annotated with @ControllerAdvice
annotation and all the exception handlers annotated with @ExceptionHandler
must be added in the same instead of having multiple @ControllerAdvice
annotated classes.
-
Single @ExceptionHandler
annotated handler method per exception: There should be one @ExceptionHandler
annotated handler method per exception defined in the @ControllerAdvice
annotated class. All the logic handling that exception must be performed there.
Let’s understand the above theory with an example application. Create a spring boot application.
Main class:
It's a usual and simple main class of a spring boot application.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
Data class for the exception:
It will be used to send the exception message to the client instead of the actual occurred exception object.
@Getter
@Setter
public class ApiError {
String exception;
}
Controller:
A simple REST controller class with two endpoints which are intentionally throwing exceptions to replicate such case in real life.
@RestController
public class DemoController {
@RequestMapping("exception/arithmetic")
public String controllerForArithmeticException()
{
throw new ArithmeticException("Divide by zero error");
}
@RequestMapping("exception/null-pointer")
public String controllerForException() throws Exception
{
throw new NullPointerException("Null reference");
}
}
Controller Advice:
The main logic of the global exception for a REST API. It has exception handlers for all the possible exceptions that can be thrown by the controller and also an exception handler for "Exception" class in order to handle any other exception that wouldn't be caught by other exception handlers.
@ControllerAdvice
public class GlobalExceptionHandler{
@ExceptionHandler(value = ArithmeticException.class)
public ResponseEntity<ApiError> handleArithmeticException(ArithmeticException e)
{
ApiError error = new ApiError();
error.setException("Arithmetic exception: " + e.getMessage());
HttpStatus status = HttpStatus.BAD_REQUEST;
return new ResponseEntity<ApiError>(error, status);
}
@ExceptionHandler(value = NullPointerException.class)
public ResponseEntity<ApiError> handleNullPointerException(NullPointerException e)
{
ApiError error = new ApiError();
error.setException("Null pointer exception: " + e.getMessage());
HttpStatus status = HttpStatus.NOT_ACCEPTABLE;
return new ResponseEntity<ApiError>(error, status);
}
@ExceptionHandler(value = Exception.class)
public ResponseEntity<ApiError> handleNullPointerException(Exception e)
{
ApiError error = new ApiError();
error.setException("Exception: " + e.getMessage());
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return new ResponseEntity<ApiError>(error, status);
}
}
Now, @ControllerAdvice
is intercepting all the exceptions occurring in the controller and is ready for global exception handling.
Response in postman: (http://localhost:8080/exception/arithmetic)
Status: 400 Bad Request
Body:
{
"exception": "Arithmetic exception: Divide by zero error"
}
Conclusion:
Using @ControllerAdvice
and @ExceptionHandler
annotations for global exception handling in spring boot allows us to keep all our exception handling logic in a single place and removes the boilerplate codes that exist in the case of try-catch blocks.
You may also like: