We had an interesting scenario in our code. It all started out when we had this complex controller that included methods specific to a certain flow. In case of any exception, we used an error handler to redirect the flow to an error page. The code would be along the lines of
This was simple. With Spring's fantastic annotations we simply had to remove the ModelAndView
and introduce the @ResponseBody annotation. Pretty cool.
Until one fine day. OK thats a lie!! Until 5 minutes later. The UI dude asked "What happens when the server fails ?"
We could not redirect to the JSP page in the middle of an AJAX. In fact we were not to touch the error JSP nor could we to move the AJAX logic. (Don't ask why.)
So we had to change our resolveException method to generate two different views -
The Error class is a simple POJO:
@Controller public class SimpleController { // various view methods @RequestMapping(value = "/home.do", method = RequestMethod.GET) public ModelAndView display() { // complex logic executed here // in case of error throw new RuntimeException("There was a failure in Page call"); } @ExceptionHandler(Exception.class) public ModelAndView resolveException(final HttpServletRequest request, final HttpServletResponse response, final Exception ex) { final ModelAndView mv = new ModelAndView("error/500"); // add details of error to the model return mv; } }As seen in case of any failure, the flow was redirected to the ExceptionHandler method. The code simply built the error objects and redirected to the error page. All was fine. Until we got a change. One of the flows had to be changed to an AJAX method.
This was simple. With Spring's fantastic annotations we simply had to remove the ModelAndView
and introduce the @ResponseBody annotation. Pretty cool.
@RequestMapping(method = RequestMethod.GET, value = "/call") public @ResponseBody Object getResource() { // complex logic... // build the object // if failure handle the exception throw new RuntimeException("There was a failure in Service call"); // return the object }The flow was integrated and all worked fine.
We could not redirect to the JSP page in the middle of an AJAX. In fact we were not to touch the error JSP nor could we to move the AJAX logic. (Don't ask why.)
So we had to change our resolveException method to generate two different views -
- A JSP when our controller methods failed and
- a JSON when our AJAX call failed.
@ExceptionHandler(Exception.class) public ModelAndView resolveException(final HttpServletRequest request, final HttpServletResponse response, final Exception ex) { final ModelAndView mv = new ModelAndView(); final String header = request.getHeader("X-Requested-With"); if ("XMLHttpRequest".equalsIgnoreCase(header)) { // an AJAX request response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); final MappingJacksonJsonView mappingJacksonJsonView = new MappingJacksonJsonView(); mappingJacksonJsonView.setExtractValueFromSingleKeyModel(true); mv.setView(mappingJacksonJsonView); // the details of the error final Error error = new Error(); error.setMsg("A 500 error was encountered !"); mv.addObject(error); } else { // a JSP style error mv.setViewName("error/500"); } return mv; }As seen the View need not simply have to be a string. (In reality the string too resolves to an instance of InternalResourceView). The View could be a MappingJacksonJsonView which ensures that the response is written back as JSON.
The Error class is a simple POJO:
class Error { private String msg; //setter getters }If I were to hit the URL "http://<application>/home.do" the the logs indicate:
ExceptionHandlerExceptionResolver:132 - Resolving exception from handler [public org... ModelAndView com..SimpleController.display()]:java.lang.RuntimeException: There was a failure in Page call ExceptionHandlerExceptionResolver:319 - Invoking @ExceptionHandler method: public org...ModelAndView com...SimpleController.resolveException(javax...HttpServletRequest,javax...HttpServletResponse, java.lang.Exception) HandlerMethod:129 - Invoking [resolveException] method with arguments [ org.apache.catalina.connector.RequestFacade@109b5ca, org.apache.catalina.connector.ResponseFacade@9bcf06, java.lang.RuntimeException: There was a failure in Page call] HandlerMethod:135 - Method [resolveException] returned [ModelAndView: reference to view with name 'error/500'; model is null] DispatcherServlet:1206 - Rendering view [org.springframework.web.servlet.view.InternalResourceView: name 'error/500'; URL [/jsp/error/500.jsp]] in DispatcherServlet with name 'DispatcherServlet'To test the AJAX flow involved a little more code. I created a simple HTML page for the same:
<!DOCTYPE html> <html> <head> <script type="text/javascript"> function run() { var xmlhttp = new XMLHttpRequest(); xmlhttp.onreadystatechange = function() { if (xmlhttp.readyState == 4 ) { document.getElementById("myDiv").innerHTML = xmlhttp.responseText; } }; xmlhttp.open("GET", "api/call", true); xmlhttp.setRequestHeader("X-Requested-With", "XMLHttpRequest"); xmlhttp.send(); } </script> </head> <body> <h2>Testing the AJAX call</h2> <button type="button" onclick="run()">Make Call</button> <div id="myDiv"></div> </body> </html>The code simply makes an AJAX call. The logs indicate the failure:
Resolving exception from handler [public java.lang.Object com...SimpleController.getResource()]: java.lang.RuntimeException: There was a failure in Service call DEBUG ExceptionHandlerExceptionResolver:319 - Invoking @ExceptionHandler method: public org....ModelAndView com....SimpleController.resolveException(javax...HttpServletRequest, javax...HttpServletResponse,java.lang.Exception) DEBUG HandlerMethod:129 - Invoking [resolveException] method with arguments [ org.apache.catalina.connector.RequestFacade@109b5ca, org.apache.catalina.connector.ResponseFacade@9bcf06, java.lang.RuntimeException: There was a failure in Service call] DEBUG HandlerMethod:135 - Method [resolveException] returned [ModelAndView: materialized View is [org.springframework.web.servlet.view.json.MappingJacksonJsonView: unnamed]; model is {error= com.controller.SimpleController$Error@27d489}] ... DispatcherServlet:1206 - Rendering view [org.springframework.web.servlet.view.json.MappingJacksonJsonView: unnamed] in DispatcherServlet with name 'DispatcherServlet'As seen the View object in ModelandView can be replaced with any custom view that suits our application. If we simply look in the spring packages we can find a large number of views indicating support for Velocity, Jasper, PDF, Excel and so many more..
Hey man! Great post, it helped me a lot!
ReplyDelete