Saturday, September 14, 2013

Spring 3.2 @ControllerAdvice to handle Controller Exceptions

My Java/Spring web application has controllers that either return ModelAndView to a jsp page or return json for ajax calls.  Exceptions can occur at any time, and I noticed that on ajax success the data returned was json, but on ajax error the data returned was regular text; all ajax calls should return the same kind of data, namely json.  There had to be a way to solve this problem generically!

Spring 3.2 to the rescue - it introduced a new annotation called @ControllerAdvice (http://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/new-in-3.2.html#new-in-3.2-webmvc-controller-advice)  that defines methods that applies to all @RequestMapping rest url methods, and in particular to help with exceptions with @ExceptionHandler.  We can use this to have a generic exception handling solution to have all ajax calls return json, and all ModelAndView return html.

  • @ControllerAdvice allows you to define ONE controller class to handle ALL exceptions that could occur (no specific exceptions defined needed)
  • You can control the HTTP status code returned
  • You can control the json message returned by grabbing the exception message from the specific exception that occurred

Here's how you do it:
  1. Your controllers won't change, the code will still continue to throw any number of specific exceptions:
  2. @Controller
    public class ExceptionController {
        
        @RequestMapping(value = "/randomException", method = RequestMethod.GET)
        public String randomException(Authentication auth, HttpServletRequest request) throws Exception {    
           
            throw new NumberFormatException(" " +
                    "Test ControllerAdvice randomException[" + "NumberFormatException" + "]");
    
            [...]
        }
    
        @RequestMapping(value = "/mavException", method = RequestMethod.GET)   
        public ModelAndView modelAndViewException(Authentication auth, HttpServletRequest request) throws Exception {  
         
            throw new UnexpectedRollbackException("Test ControllerAdvice Exception for mav");
            [...]
        }
    }
    
  3. Write ONE new Controller, this will act as your ControllerAdvice controller. Notice:
    • the class is annotated with @ControllerAdvice
    • @ExceptionHandler is annotated above your generic "handleException" function
    • handleException function takes generic Exception e as a parameter
    • it checks the accept header to see how the controller function was invoked (via ajax or via form submit; randomException or mavException, respectively).  *Update*  We have to check for null first in case the request doesn't specify a response type expected.  If the request doesn't specify it, then simply return text/html.
    • It returns json or html
    • @ControllerAdvice
      public class CherryShoeControllerAdvice {
      
          /*
           * Handles JSON and HTML
           */
          @ExceptionHandler
          @ResponseBody
          @ResponseStatus(HttpStatus.BAD_REQUEST)
          public String handleException(HttpServletRequest request, HttpServletResponse response, Exception e) throws IOException {    
              String acceptHeader = request.getHeader("Accept");
             
              // If Accept header exists, check if it expects a response of type json, otherwise just return text/html
              // Use apache commons lang3 to escape json values
              if(acceptHeader.contains("application/json")) {
                  // return as JSON
                  String jsonString = 
                          "{\"success\": false, \"message\": \"" + StringEscapeUtils.escapeJson(e.getMessage()) + "\" }";
              
                  System.out.println("In handleGeneric" + e.getMessage());
                  return jsonString;
              } else {
                  //return as HTML
                  response.setContentType("text/html");
                  return response.toString();
              }
          }
      
      }
      

BTW - if you wanted to specify each type of exception going to a separate @ExceptionHandler you could.

BTW - The main cons for using pre-Spring 3.2 @ExceptionHandler (http://docs.spring.io/spring/docs/3.1.x/spring-framework-reference/html/mvc.html#mvc-exceptionhandlersby itself are you have to:

  • Define an ExceptionHandler for EACH controller (or have each controller inherit from one common base)
  • Define EACH exception type to be handled – Or have each controller throw the specific type of exception expected


8 comments:

  1. super..really it helped me solving my issue with spring mvc and angularjs...thank you very much...

    ReplyDelete
  2. super..really it helped me solving my issue with spring mvc and angularjs...thank you very much...

    ReplyDelete
  3. Thanks for the tip on ConrollerAdvice. Works great!

    ReplyDelete
  4. Hello. Im using spring 3.2.7 and it's not working. I have Controllers in differents packages. It only works if the @ControllerAdvice class is in the same package with a particular Controller

    ReplyDelete

I appreciate your time in leaving a comment!