Spring REST: Exception handling vol. 2
This is a second article from the series about REST Exception handling with Spring. In my previous post I have described how to organise the most simple exception handling in a REST service. This time I will go further and I will show you when you’d better to use exception handling on a @ControllerAdvice level.
INTRO
Before I will start the technical part of the post, I need to concider a situation when we would better to use an exception handling on a @ControllerAdvice level.
Usually one controller is responsible for entire logic related to one type of entities. That’s mean that if I have some EntityController class, it will contain all CRUD (create, read, update, delete) operations with the entity and maybe some extra logic if it is required. Let’s examine three operations: read, update, delete.
The read operation returns some particular entity depending on ID which we pass to it as an argument. if the entity doesn’t exist the read operation returns null. The update / delete operation updates / deletes a particular entity respectively. And each of these two operations include the read operation, because before an entity will be updated / deleted we need to ensure that it exist in a system.
It’s a normally practice when an entity is not found during the update / delete operation an application will throw an EntityNotFoundException. An exception handling in this case will be very straightforward. The application have to return an information to a client:
- Response Header: 404
- A link which caused the exception
- Error message: There is no Entity with id: N
This is a most simple response structure for a such exceptions. So regardless how many different entity classes you have in an application, because you can handle similar kind of exception (e.g. no such entity) in the same way. This became possible thanks @ControllerAdvice annotation.
Exception handling on @ControllerAdvice level
Practical part of the article will be based on the application form the last tutorial.
Firstly I need to add an error message into the messages.properties file:
error.no.smartphone.id = There is no Smartphone with id:
After this let’s look on the controller methods which are interesting for us in topic of this article.
... @RequestMapping(value="/edit/{id}", method=RequestMethod.GET) public ModelAndView editSmartphonePage(@PathVariable int id) { ModelAndView mav = new ModelAndView("phones/edit-phone"); Smartphone smartphone = smartphoneService.get(id); mav.addObject("sPhone", smartphone); return mav; } @RequestMapping(value="/edit/{id}", method=RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public Smartphone editSmartphone(@PathVariable int id, @Valid @RequestBody Smartphone smartphone) { smartphone.setId(id); return smartphoneService.update(smartphone); } ... @RequestMapping(value="/delete/{id}", method=RequestMethod.DELETE, produces = MediaType.APPLICATION_JSON_VALUE, consumes = MediaType.APPLICATION_JSON_VALUE) @ResponseBody public Smartphone deleteSmartphone(@PathVariable int id) { return smartphoneService.delete(id); } ...
These methods include calls of the SmartphoneService. And implementation of the SmartphoneService contains methods which can throw a SmartphoneNotFoundException.
@Service @Transactional(rollbackFor = { SmartphoneNotFoundException.class }) public class SmartphoneServiceImpl implements SmartphoneService { @Autowired private SmartphoneRepository smartphoneRepository; @Override public Smartphone create(Smartphone sp) { return smartphoneRepository.save(sp); } @Override public Smartphone get(Integer id) { Smartphone sp = null; if (id instanceof Integer) sp = smartphoneRepository.findOne(id); if (sp != null) return sp; throw new SmartphoneNotFoundException(id); } @Override public List getAll() { return smartphoneRepository.findAll(); } @Override public Smartphone update(Smartphone sp) { Smartphone sPhoneToUpdate = get(sp.getId()); sPhoneToUpdate.update(sp); return sPhoneToUpdate; } @Override public Smartphone delete(Integer id) { Smartphone sPhone = get(id); smartphoneRepository.delete(id); return sPhone; } }
Here is a code of SmartphoneNotFoundException:
public class SmartphoneNotFoundException extends RuntimeException { private static final long serialVersionUID = -2859292084648724403L; private final int smartphoneId; public SmartphoneNotFoundException(int id) { smartphoneId = id; } public int getSmartphoneId() { return smartphoneId; } }
And finally I can move to the @ControllerAdvice.
@ControllerAdvice public class RestExceptionProcessor { @Autowired private MessageSource messageSource; @ExceptionHandler(SmartphoneNotFoundException.class) @ResponseStatus(value=HttpStatus.NOT_FOUND) @ResponseBody public ErrorInfo smartphoneNotFound(HttpServletRequest req, SmartphoneNotFoundException ex) { Locale locale = LocaleContextHolder.getLocale(); String errorMessage = messageSource.getMessage("error.no.smartphone.id", null, locale); errorMessage += ex.getSmartphoneId(); String errorURL = req.getRequestURL().toString(); return new ErrorInfo(errorURL, errorMessage); } }
The exception handler method returns ErrorInfo object. You can read more about it in the previous post about Exception Handling on a @Controller level.
In this way we can collect all similar exceptions in one place, just by adding extra exception classes into @ExceptionHandler annotation. This approach makes a code maintenance more easier within an entire application.
Demonstration of the example:
Note: I made the request with the id value = 356, but there wasn’t any record in a database which correspond to this id value. This circumstance cause the exception.