Monday, 25 March 2013

Exception Handling with the Spring 3.2 @ControllerAdvice Annotation

A short time ago, I wrote a blog outlining how I upgraded my Spring sample code to version 3.2 and demonstrating a few of the little 'gotchas' that arose. Since that I've been perusing Spring 3.2's new feature list and whilst it doesn't contain any revolutionary new changes, which I suspect the Guys at Spring are saving for version 4, it does contain a few neat upgrades. The first one that grabbed my attention was the new @ControllerAdvice annotation, which seems to neatly plug a gap in Spring 3 functionality. Let me explain…

If you take a look at my blog on Spring 3 MVC Exception Handlers you'll see that the sample code contains a flaky controller with a request handler method that throws an IOException. The IOException is then handled by another method in the same controller that's annotated with @ExceptionHandler(IOException.class). The problem is that your method that's annotated with @ExceptionHandler(IOException.class) can only handle IOExceptions thrown by its containing controller. If you want to create a global exception handler that handles exceptions thrown by all controllers then you have to revert to something like Spring 2's SimpleMapingExceptionHandler and some XML configuration. Now things are different. To demonstrate the use of @ControllerAdvice I've created a simple Spring 3.2 MVC application that you can find on github. The application's home page ostensively allows the user to display either their address or credit card details,


...except that when the user attempt to do this, the associated controllers will throw an IOException and the application displays the following error page:


The controllers that generate the exceptions are fairly straightforward and listed below:

@Controller
public class UserCreditCardController {

 
private static final Logger logger = LoggerFactory.getLogger(UserCreditCardController.class);

 
/**
   * Whoops, throw an IOException
   */
 
@RequestMapping(value = "userdetails", method = RequestMethod.GET)
 
public String getCardDetails(Model model) throws IOException {

   
logger.info("This will throw an IOException");

   
boolean throwException = true;

   
if (throwException) {
     
throw new IOException("This is my IOException");
   
}

   
return "home";
 
}

}

@Controller
public class UserAddressController {

 
private static final Logger logger = LoggerFactory.getLogger(UserAddressController.class);

 
/**
   * Whoops, throw an IOException
   */
 
@RequestMapping(value = "useraddress", method = RequestMethod.GET)
 
public String getUserAddress(Model model) throws IOException {

   
logger.info("This will throw an IOException");

   
boolean throwException = true;

   
if (throwException) {
     
throw new IOException("This is my IOException");
   
}

   
return "home";
 
}

}

As you can see, all that this code does is to map userdetails and useraddress to the getCardDetails(...) and getUserAddress(...) methods respectively. When either of these methods throw an IOException, then the exception is caught by the following class:

@ControllerAdvice
public class MyControllerAdviceDemo {

 
private static final Logger logger = LoggerFactory.getLogger(MyControllerAdviceDemo.class);

 
@Autowired
 
private UserDao userDao;

 
/**
   * Catch IOException and redirect to a 'personal' page.
   */
 
@ExceptionHandler(IOException.class)
 
public ModelAndView handleIOException(IOException ex) {

   
logger.info("handleIOException - Catching: " + ex.getClass().getSimpleName());
   
return errorModelAndView(ex);
 
}

 
/**
   * Get the users details for the 'personal' page
   */
 
private ModelAndView errorModelAndView(Exception ex) {
   
ModelAndView modelAndView = new ModelAndView();
    modelAndView.setViewName
("error");
    modelAndView.addObject
("name", ex.getClass().getSimpleName());
    modelAndView.addObject
("user", userDao.readUserName());

   
return modelAndView;
 
}
}

The class above is annotated by the new @ControllerAdvice annotation and contains a single public method handleIOException(IOException.class). This method catches all IOExceptions thrown by the controllers above, generates a model containing some relevant user information and then displays and error page. The nice thing about this is that,no matter how many controllers your application contains, when any of them throws an IOException, then it'll be handled by the MyControllerAdviceDemo exception handler.

@ModelAttribute and @InitBinder

One final thing to remember is that the although the ControllerAdvice annotation is useful for handling exceptions, it can also be used the globally handle the @ModelAttribute and @InitBinder annotations. The combination of ControllerAdvice and @ModelAttribute gives you the facility to setup model objects for all controllers in one place and likewise the combination of ControllerAdvice and @InitBinder allows you to attach the same custom validator to all your controllers, again, in one place.

5 comments:

pakman said...

How did you manage to make the @Autowired work in your @ControllerAdvice?

I can't make it work. The @ControllerAdvice gets called alright, but the @Autowired dependency is not satisfied (it is alwas null).

Roger Hughes said...

Pakman
It's difficult to say what's wrong without looking at your code, mine worked okay.

Things to check are:
1) that you have the correct annotation on the object you're injecting via @Autowired. This means using @Service or @Component

2) that you have the correct tags in your Spring context xml file.

You can also switch on DEBUG logging and take a look at all the messages Spring throws out whilst it's loading the ApplicationContext.

Ina Galer said...

Hi, I have one question: this controller handles exceptions thrown by ohter controller or it handles all the exception, from service let say.
Thanks

Roger Hughes said...

Ina, this should handle any exceptions that are specified in the @ExceptionHandler annotation irrespective of wherever they originate in your code, so long as they get thrown out of the controller method.

Unknown said...

Thanks for sharing. The 'ControllerAdvice' is really a useful annotation to abstract exception handling behaviors!