Monday 27 April 2015

[Spring] How to create global exception handler ?

Sometimes you may want to handle exceptions globally. Let's say you have a service which exposes several rest endpoints which are conntected to the same database. Your data access objects may throw DatabaseConnectionException (or something like that) when db is not available. In such case the response should contain proper HTTP status code and error message. Sample rest endpoint:
@RestController
public class TemperatureEndpoint {
 @Inject
 private WeatherService temperatureService;

 @RequestMapping(
  value = "/temperature/{city}",
  method = RequestMethod.GET,
  produces = "application/json"
 )
 @ResponseBody
 public Temperature currentTemperature(@PathVariable("city") String city) {
  return temperatureService.getCurrentTemperatureIn(city);
 }
}
And another one:
@RestController
public class HumidityEndpoint {
 @Inject
 private HumidityService humidityService;

 @RequestMapping(
  value = "/humidity/{city}",
  method = RequestMethod.GET,
  produces = "application/json"
 )
 @ResponseBody
 public Humidity currentHumidity(@PathVariable("city") String city) {
  return humidityService.getCurrentHumidityIn(city);
 }
}
Both HumidityService and TemperatureService use the same database so you may use some unified runtime exception like DatabaseConnectionException. Handling the exception in both services isn't a good idea because the result will in most cases be the same. By the way each try-catch block makes you to write additional unit test so global handler seems to be a natural choice. Spring allows you to create a global exception handler (will be applied to all the rest controllers) using @ControllerAdvice annotation. Typically class annotated with ControllerAdvice contains methods which map exceptions to HTTP status codes and messages. In well-designed applications it can be the only place where exception handling occurs. Let's create some mappings. Each mapper should also generate proper json response so the client can handle it. Let's create a class annotated with @ControllerAdvice (note that the class has to be in scope of component scan).
@ControllerAdvice
public class WeatherServiceExceptionHandler {
        private static final Logger LOG = LoggerFactory.getLogger(WeatherServiceExceptionHandler.class);
}
I've added a logger because we may want to log something. Let's get back to DatabaseCommunicationException. In case the exception is thrown I want my service to log proper information and return exception's message in json so the client can handle it properly.
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(DatabaseCommunicationException.class)
public void handleDatabaseConnectionException(DatabaseCommunicationException e) { 
 LOG.error("DatabaseCommunicationException occurred", e);
}
As you can see I've created a method which takes as a parameter DatabaseCommunicationException so I can log its content. @ExceptionHandler annotation tells Spring that the method handles DatabaseCommunicationException exception. @ResponseStatus allows you to specify which HTTP status will be assigned to the response when this particular handler is being fired. In this case WeatherService cannot work without database connection so I chose 500 which is internal server error. You can obviously use whichever code you want to. For now the handler only logs a message and returns http code but it should also return a message to the client. In all RestControllers I use jackson to map POJO cleassess to json. It can be used in ControllerAdvice as well. Jackson dependency:
<dependency>
 <groupId>com.fasterxml.jackson.core</groupId>
 <artifactId>jackson-databind</artifactId>
</dependency>
In this example json response will only contain a message but you can add here anything else. My response is just a sample immutable POJO. It looks as follows:
public class ErrorMessage {
 private final String message;

 public ErrorMessage(String message) {
  this.message = message;
 }

 public String getMessage() {
  return message;
 }
}
Now the handler should return ErrorMessage and indicate that response should be converted into json format. Complete method looks like this:
@ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(DatabaseCommunicationException.class)
@ResponseBody
public ErrorMessage handleDatabaseConnectionException(DatabaseCommunicationException e) {
 LOG.error("DatabaseCommunicationException occurred", e);
 return new ErrorMessage(e.getMessage());
}
Let's add another one. This time I want to tell the user that the service doesn't support requested city. Fo instance the user wants to get current temperature in Goszowice (which is very small village in southern Poland) but the database simply doesn't contain any data about this place. In such case TemperatureService throws UnsupportedCityException. I guess HttpStatus.NOT_FOUND (404) fits best.
@ResponseStatus(value = HttpStatus.NOT_FOUND)
@ExceptionHandler(UnsupportedCityException.class)
@ResponseBody
public ErrorMessage handleUnsupportedCityException(UnsupportedCityException e) {
 LOG.error("UnsupportedCityException occurred", e);
 return new ErrorMessage(e.getMessage());
}
As you can see bodies of both handlers are almost the same so it can be extracted to new method. It's just an example so let it be as it is. Complete code:
@ControllerAdvice
public class WeatherServiceExceptionHandler {
 private static final Logger LOG = LoggerFactory.getLogger(WeatherServiceExceptionHandler.class);

 @ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
 @ExceptionHandler(DatabaseCommunicationException.class)
 @ResponseBody
 public ErrorMessage handleDatabaseConnectionException(DatabaseCommunicationException e) {
  LOG.error("DatabaseCommunicationException occurred", e);
  return new ErrorMessage(e.getMessage());
 }

 @ResponseStatus(value = HttpStatus.NOT_FOUND)
 @ExceptionHandler(UnsupportedCityException.class)
 @ResponseBody
 public ErrorMessage handleUnsupportedCityException(UnsupportedCityException e) {
  LOG.error("UnsupportedCityException occurred", e);
  return new ErrorMessage(e.getMessage());
 }
}
You can obviously provide a better way of constructing error message because you may not want to expose your internal messages. Something like this looks a bit better:
private static final ImmutableMap<Class, String> EXCEPTION_TO_MESSAGE_MAP = ImmutableMap.<Class, String>of(
   DatabaseCommunicationException.class, "Service is not available. Please try again later.",
   UnsupportedCityException.class, "Service does not track temperature in requested city."
);
That would be all :) Cheers

No comments:

Post a Comment