Best Practice for Exception Handling In Spring Boot
Keep your exception handling at a central place in your application using @ControllerAdvice
Join the DZone community and get the full member experience.
Join For FreeWhenever we think of handling exceptions at any level of our code, we fall under writing everywhere try catch block in our code, and then after some days when we try to go through our code, we find most of the code is filled with handling exceptions. This degrades the readability of our code and also duplication of a lot of logger messages, which we can easily avoid. Here, we will try to learn the powerful feature provided by Spring Boot to avoid these duplications and improve the readability of code while handling exceptions in our application.
As we all know, exception handling is the most important and a crucial thing in securing Spring Boot Rest APIs, which helps us to perform conditional and unconditional checkings for our code and handle any kind of exception in a proper way. Along with its benefits, it also complicates the code and makes the code not easily readable to unknown users. Using try catch blocks anywhere in your code is not recommended because we are not able to read the code properly and also it increases more unwanted lines in your class. So we need to have a common place where we can manage and handle all kinds of exceptions and send the respective error code for the API response depending on the exception types. In this blog, we will try to know a simple way that will make our code be in a better format related to the handling of exceptions provided in SpringBoot. Review other best practices for securing API access tokens.
Description
SpringBoot provides a very powerful annotation called @ControllerAdvide under package org.springframework.web.bind.annotation. This annotation makes our life easy to handle all kinds of exceptions at a central place in our application. We don't need to catch any exception at each method or class separately instead you can just throw the exception from the method and then it will be caught under the central exception handler class annotated by @ControllerAdvide. Any class annotated with @ControllerAdvice will become a controller-advice class which will be responsible for handling exceptions. Under this class, we make use of annotations provided as @ExceptionHandler, @ModelAttribute, @InitBinder. Review a guide covering all Spring Boot Annotations.
Exception handling methods annotated with @ExceptionHandler will catch the exception thrown by the declared class and we can perform various things whenever we come through the related type exceptions.
@ControllerAdvice constructor comes with some special arguments, which allows you to scan only the related portion of your application and handle only those exceptions thrown by the respective classes mentioned in the constructor. By default, it will scan and handle all the classes in your application. Below are some types which we can use to restrict only specific classes to handle exceptions. Related: Learn more about Java Exceptions.
1) annotations - Controllers that are annotated with the mentioned annotations will be assisted by the @ControllerAdvice
annotated class and are eligible for exception of those classes
eg. @ControllerAdvice(annotations = RestController.class)
- Here the exception helper annotated by @ControllerAdvice will catch all the exceptions thrown by the @RestController
annotation classes.
2) basePackages - By Specifying the packages that we want to scan and handling exceptions for the same.
eg. @ControllerAdvice(basePackages = "org.example.controllers")
- This will only scan call the mentioned package and handle the exceptions for the same.
3) assignableTypes - This argument will make sure to scan and handle the exceptions from the mentioned classes
eg. @ControllerAdvice(assignableTypes = {ControllerInterface.class,
AbstractController.class})
Before Using @ControllerAdvice
In the below code snippet, we see there are many duplications of lines, and the controller code is not easily readable because of multiple try and catch blocks in each API.
xxxxxxxxxx
path = "/employees") (
public class EmployeeController {
private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class);
private EmployeeDao employeeDao;
(path="/{employeeId}", produces = "application/json")
public ResponseEntity<Employee> getEmployees( Long employeeId) {
ResponseEntity<Employee> response = null;
try {
if(null==employeeId || positionId.equals(0L)) {
throw new InvalidInputException("Employee Id is not valid");
}
employee = employeeDao.getEmployeeDetails(employeeId);
response = new ResponseEntity<Employee>(employee,HttpStatus.OK);
}
catch(InvalidInputException e) {
Logger.error("Invalid Input:",e.getMessage());
response = new ResponseEntity<Employee>(employee,HttpStatus.BAD_REQUEST);
}
catch(BusinessException e) {
Logger.error("Business Exception:",e.getMessage());
response = new ResponseEntity<Employee>(employee,HttpStatus.INTERNAL_SERVER_ERROR);
}
catch(Exception e) {
Logger.error("System Error:",e.getMessage());
response = new ResponseEntity<Employee>(employee,HttpStatus.INTERNAL_SERVER_ERROR);
}
return response;
}
(path="/address/{employeeId}", produces = "application/json")
public ResponseEntity<Address> getEmployeeAddress( Long employeeId, Long userId) {
ResponseEntity<Address> response = null;
try {
if(null==employeeId || positionId.equals(0L)) {
throw new InvalidInputException("Employee Id is not valid");
}
if(null==userId || userId.equals(0L)) {
throw new UnauthorizedException("Unauthorized user");
}
address = employeeDao.getEmployeeAddress(employeeId);
response = new ResponseEntity<Address>(address,HttpStatus.OK);
}
catch(UnauthorizedException e) {
Logger.error("Unauthorized:",e.getMessage());
response = new ResponseEntity<Address>(address,HttpStatus.BAD_REQUEST);
}
catch(InvalidInputException e) {
Logger.error("Invalid Input:",e.getMessage());
response = new ResponseEntity<Address>(address,HttpStatus.BAD_REQUEST);
}
catch(Exception e) {
Logger.error("System Error:",e.getMessage());
response = new ResponseEntity<Address>(address,HttpStatus.INTERNAL_SERVER_ERROR);
}
return response;
}
}
After Using @ControllerAdvice
The below code snippet makes the code easily readable and also reduces duplications of lines.
path = "/employees") (
public class EmployeeController {
private static final Logger logger = LoggerFactory.getLogger(EmployeeController.class);
(path="/{employeeId}", produces = "application/json")
public ResponseEntity<Employee> getEmployees( Long employeeId) {
if(null==employeeId || positionId.equals(0L)) {
throw new InvalidInputException("Employee Id is not valid");
}
Employee employee = employeeDao.getEmployeeDetails(employeeId);
return new ResponseEntity<Employee>(employee,HttpStatus.OK);;
}
(path="/address/{employeeId}", produces = "application/json")
public ResponseEntity<Address> getEmployeeAddress( Long employeeId, Long userId) {
if(null==employeeId || employeeId.equals(0L)) {
throw new InvalidInputException("Employee Id is not valid");
}
if(null==userId || userId.equals(0L)) {
throw new UnauthorizedException("Unauthorized user");
}
Address address = employeeDao.getEmployeeAddress(employeeId,userId);
return new ResponseEntity<Address>(address,HttpStatus.OK);
}
}
public class ExceptionHelper {
private static final Logger logger = LoggerFactory.getLogger(ExceptionHelper.class);
(value = { InvalidInputException.class })
public ResponseEntity<Object> handleInvalidInputException(InvalidInputException ex) {
LOGGER.error("Invalid Input Exception: ",ex.getMessage());
return new ResponseEntity<Object>(ex.getMessage(),HttpStatus.BAD_REQUEST);
}
(value = { Unauthorized.class })
public ResponseEntity<Object> handleUnauthorizedException(Unauthorized ex) {
LOGGER.error("Unauthorized Exception: ",ex.getMessage());
return new ResponseEntity<Object>(ex.getMessage(),HttpStatus.BAD_REQUEST);
}
(value = { BusinessException.class })
public ResponseEntity<Object> handleBusinessException(BusinessException ex) {
LOGGER.error("Business Exception: ",ex.getMessage());
return new ResponseEntity<Object>(ex.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR);
}
(value = { Exception.class })
public ResponseEntity<Object> handleException(Exception ex) {
LOGGER.error("Exception: ",ex.getMessage());
return new ResponseEntity<Object>(ex.getMessage(),HttpStatus.INTERNAL_SERVER_ERROR);
}
}
By going through the above example, we find that using @ControllerAdvice will solve our many issues of maintaining code quality, as well as increase readability of the code. This will help everyone to debug all the errors at a common place of your application. We can also easily update loggers for any kind of errors and maintain the uniformity for error messages using this approach.
Opinions expressed by DZone contributors are their own.
Comments