Using @ControllerAdvice to handle @Controller exceptions globally

Using @ControllerAdvice to handle @Controller exceptions globally

ControllerAdviceWhen there are multiple API endpoints in an application, a problem of boilerplate code related to returning HTTP error codes becomes noticeable. For example an endpoint that adds a user returns HttpStatus.OK when the operation finishes successfully. But when such user already exists in the system, it is expected that the endpoint should return an error code. When the controller internally uses a service to handle the operation, exceptions are often used and this is when @ControllerAdvice becomes handy.

 

Service and controller

To show you how useful this is, I created a service with two methods: addUser and changeUser.

@Service
public class UserService {

public Long addUser(User user) throws UserAlreadyExistsException, EmptyFieldException {
...
}

public void changeUser(User user) throws EmptyFieldException, UserDoesNotExistException {
...
}

}

Each method can throw an exception of two types when something goes wrong.

The service is used in a controller.

@RestController
@RequestMapping(value = "/user")
public class UserController {

private UserService userService;

@Autowired
protected UserController(UserService userService) {
this.userService = userService;
}

@RequestMapping(value = "/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Long> addUser(@RequestBody User user) {
try {
return new ResponseEntity<>(userService.addUser(user), HttpStatus.OK);
} catch (UserAlreadyExistsException e) {
return new ResponseEntity<>(HttpStatus.CONFLICT);
} catch (EmptyFieldException e) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
}
}

@RequestMapping(value = "/", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity changeUser(@RequestBody User user) {
try {
userService.changeUser(user);
} catch (EmptyFieldException e) {
return new ResponseEntity(HttpStatus.BAD_REQUEST);
} catch (UserDoesNotExistException e) {
return new ResponseEntity(HttpStatus.NOT_FOUND);
}
return new ResponseEntity(HttpStatus.OK);
}

}

There are two endpoints: one that adds a user and the second one changes a user. As you can see, using the service methods forces me to handle exceptions to translate them to proper HTTP response codes. I could leave them unhandled but then a browser would receive exception details with a stacktrace which I do not want as it reveals internals of the system. The try-catch code is definitely a boilerplate and it is worth to try avoiding it.

Would you like to learn db performance? Enroll to my course on Udemy.

Promo code: PERF_OPT_0522

liquibase course promo

 

@ControllerAdvice usage example

ControllerAdvice is a Spring annotation to mark a global exceptions handler for Spring controllers. To use it, I create a class and annotate it with @ControllerAdvice.

@ControllerAdvice
@Log4j
public class GlobalExceptionHandler {

@ResponseStatus(value= HttpStatus.BAD_REQUEST, reason="Not all mandatory fields are filled")
@ExceptionHandler(EmptyFieldException.class)
public void handleEmptyFieldException(EmptyFieldException e){
log.error("Not all mandatory fields are filled", e);
}

@ResponseStatus(value= HttpStatus.CONFLICT, reason="User already exists")
@ExceptionHandler(UserAlreadyExistsException.class)
public void handleUserAlreadyExistsException(UserAlreadyExistsException e){
log.error("User already exists", e);
}

@ResponseStatus(value= HttpStatus.NOT_FOUND, reason="User does not exist")
@ExceptionHandler(UserDoesNotExistException.class)
public void handleUserDoesNotExistException(UserDoesNotExistException e){
log.error("User does not exist", e);
}

}

When any exception is thrown from a Spring controller, it goes through an instance of this class looking for a method that can handle the exception. For example when EmptyFieldException is handled by handleEmptyFieldException method. Its body is executed. In this case an error is logged. If you do not know how the log variable got here, see how Lombok can help eliminating some boilerplate code. At the end HttpStatus.BAD_REQUEST is returned instead of the exception.

This mapping is valid in the whole web application so handling an exception class once in the controller advice takes care of all endpoints created with Spring.

 

Changes in controller to take advantage of @ControllerAdvice

If my controller still needed all this try-catch code, it would not be worth of using the global handler at all. It can be reduced to the following form.

@RestController
@RequestMapping(value = "/user")
public class UserController {

private UserService userService;

@Autowired
protected UserController(UserService userService) {
this.userService = userService;
}

@RequestMapping(value = "/", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Long> addUser(@RequestBody User user) throws UserAlreadyExistsException, EmptyFieldException {
return new ResponseEntity<>(userService.addUser(user), HttpStatus.OK);
}

@RequestMapping(value = "/", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity changeUser(@RequestBody User user) throws EmptyFieldException, UserDoesNotExistException {
userService.changeUser(user);
return new ResponseEntity(HttpStatus.OK);
}

}

I removed exceptions handling from the controller but I had to extend methods declaration with exception classes.

This is just a simple example but benefits of using a controller advice are meaningful when a project contains many Spring endpoints.

We use cookies

We use cookies on our website. Some of them are essential for the operation of the site, while others help us to improve this site and the user experience (tracking cookies). You can decide for yourself whether you want to allow cookies or not. Please note that if you reject them, you may not be able to use all the functionalities of the site.