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.

 

@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.