Spring Framework
  1. Spring Framework
  2. SPR-5426

Allow for custom processing or result objects returned from handler/controller methods

    Details

    • Type: Refactoring Refactoring
    • Status: Closed
    • Priority: Major Major
    • Resolution: Fixed
    • Affects Version/s: 2.5.6
    • Fix Version/s: 3.0 M3
    • Component/s: Web
    • Labels:
      None

      Description

      See the Spring forum link for explanation why this can be useful.

      Generally, the case is following:
      I use the annotation-base controllers. I have a method, which takes some arguments, including MyTypeX, and returns MyTypeY:

      @Controller
      public class MyController {
      
       public MyTypeY myMethod(MyTypeX input, ...) {...}
      
      }
      

      I want to register some custom converters for them. I can use the setCustomArgumentResolver() from AnnotationMethodHandlerAdapter method, to register custom converter for input type MyTypeX. That's great. But it's seems impossible for return MyTypeY to be converted. The AnnotationMethodHandlerAdapter doesn't contain any extension point for this. The extension point should allow for two things:
      1. custom processing of returned value to add it to model, or create model from it, or create view/view name from it;
      2. custom processing of return value to write the response directly to output stream base on it, and return null to signal that response was already sent.

      This is a piece of code of invokeHandlerMethod() of this AnnotationMethodHandlerAdapter:

      Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
      ModelAndView mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
      

      One should be able to override the getModelAndView() method. Unfortunately, this method is defined in some private inner class, what prevents from subclassing and overriding it.

      Proposed changes are:

      1.Create the ModelAndViewResolver interface (the parameters for its getModelAndView method are taken directly from the code above, but it does not have to be exactly the same) :

      package org.springframework.web.servlet;
      
      import java.lang.reflect.Method;
      
      import org.springframework.ui.ExtendedModelMap;
      import org.springframework.web.context.request.ServletWebRequest;
      
      public interface ModelAndViewResolver {
      
         boolean supports(Class clazz);
         
         ModelAndView getModelAndView(Method handlerMethod, Class handlerType, Object returnValue,
               ExtendedModelMap implicitModel, ServletWebRequest webRequest);
      }
      

      2. Add setter methods and field into AnnotationMethodHandlerAdapter for custom implementation of this interface :

         private ModelAndViewResolver[] customModelAndViewResolvers;
      
         public void setCustomModelAndViewResolver(ModelAndViewResolver resolver) {
            this.customModelAndViewResolvers = new ModelAndViewResolver[] {resolver};
         }
         
         public void setModelAndViewResolvers(ModelAndViewResolver[] resolvers) {
            this.customModelAndViewResolvers = resolvers;
         }
      

      3. Change the two aforementioned two lines in the invokeHandlerMethod of AnnotationMethodHandlerAdapter class into following:

               Object result = methodInvoker.invokeHandlerMethod(handlerMethod, handler, webRequest, implicitModel);
               ModelAndViewResolver customModelAndViewResolver = null;
               if (customModelAndViewResolvers != null) {
                  for (ModelAndViewResolver mavr : customModelAndViewResolvers) {
                     if (mavr.supports(handlerMethod.getReturnType())) {
                        customModelAndViewResolver = mavr;
                     }
                  }
               }
               ModelAndView mav;
               if (customModelAndViewResolver != null) {
                  mav = customModelAndViewResolver.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
               } else {
                  mav = methodInvoker.getModelAndView(handlerMethod, handler.getClass(), result, implicitModel, webRequest);
               }
      

      Now user will be able to plug in own resolvers this way:

          <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter">
          	<property name="customArgumentResolver">
          		<bean class="org.me.MyTypeXArgumentResolver"/>
      		</property>
      		<property name="customModelAndViewResolver">
          		<bean class="org.me.MyTypeYResultResolver"/>
      		</property>
      </bean>
      

      This is how I use it to return directly JSONObject from my handler methods:

      import java.io.IOException;
      import java.lang.reflect.Method;
      
      import javax.servlet.ServletResponse;
      
      import net.sf.json.JSONObject;
      
      import org.springframework.ui.ExtendedModelMap;
      import org.springframework.web.context.request.ServletWebRequest;
      import org.springframework.web.servlet.ModelAndView;
      import org.springframework.web.servlet.ModelAndViewResolver;
      
      public class JsonResultResolver implements ModelAndViewResolver {
      
         public ModelAndView getModelAndView(Method handlerMethod, Class handlerType, Object returnValue, ExtendedModelMap implicitModel, ServletWebRequest webRequest) {
            ServletResponse out = webRequest.getResponse();
            out.setContentType("application/json; charset=utf-8");
            try {
               out.getWriter().write(returnValue.toString());
            } catch (IOException e) {
               throw new RuntimeException(e);
            }
            return null;
         }
      
         public boolean supports(Class clazz) {
            return JSONObject.class.equals(clazz);
         }
      
      }
      

        Issue Links

          Activity

          Hide
          Arjen Poutsma added a comment -

          Added code tags to make the Description more readable.

          Show
          Arjen Poutsma added a comment - Added code tags to make the Description more readable.
          Hide
          Grzegorz Borkowski added a comment -

          Thanks for fixing the formatting. When you post the issue, you don't always remember about it. And unfortunately, Spring's Jira setup doesn't let me edit my own comments and bug descriptions.

          Show
          Grzegorz Borkowski added a comment - Thanks for fixing the formatting. When you post the issue, you don't always remember about it. And unfortunately, Spring's Jira setup doesn't let me edit my own comments and bug descriptions.
          Hide
          Arjen Poutsma added a comment -

          I am thinking of adding another method-level annotation to indicate that a returned object should be handled by HttpMessageConverters. It will probable be named @ResponseBody, to show that it's related to @RequestBody.

          What do you think about this option? Perhaps in addition to custom return type handlers, as you describe above?

          Show
          Arjen Poutsma added a comment - I am thinking of adding another method-level annotation to indicate that a returned object should be handled by HttpMessageConverters. It will probable be named @ResponseBody, to show that it's related to @RequestBody. What do you think about this option? Perhaps in addition to custom return type handlers, as you describe above?
          Hide
          Grzegorz Borkowski added a comment -

          Yes, @ResponseBody makes sense as the counterpart of @RequestBody.
          On the other hand, you would have to define it on each method. If you'd like to define it globaly, "ModelAndViewResolver" defined in spring context would be better.
          Besides, if there is a method setCustomArgumentResolver(WebArgumentReslolver) /setCustomArgumentResolvers(WebArgumentReslolver[]), it makes sense to have its logical counterpart setCustomModelAndViewResolver(ModelAndViewResolver)/setCustomModelAndViewResolvers(ModelAndViewResolver[]).

          So I would say that both options would be useful.

          Show
          Grzegorz Borkowski added a comment - Yes, @ResponseBody makes sense as the counterpart of @RequestBody. On the other hand, you would have to define it on each method. If you'd like to define it globaly, "ModelAndViewResolver" defined in spring context would be better. Besides, if there is a method setCustomArgumentResolver(WebArgumentReslolver) /setCustomArgumentResolvers(WebArgumentReslolver[]), it makes sense to have its logical counterpart setCustomModelAndViewResolver(ModelAndViewResolver)/setCustomModelAndViewResolvers(ModelAndViewResolver[]). So I would say that both options would be useful.
          Hide
          Arjen Poutsma added a comment -

          Fixed in SVN, through the ModelAndViewResolver. Rather than having the supports() method, I opted for a special magic UNRESOLVED value, to make it more consistent with WebArgumentResolver.

          Show
          Arjen Poutsma added a comment - Fixed in SVN, through the ModelAndViewResolver. Rather than having the supports() method, I opted for a special magic UNRESOLVED value, to make it more consistent with WebArgumentResolver.

            People

            • Assignee:
              Arjen Poutsma
              Reporter:
              Grzegorz Borkowski
              Last updater:
              Trevor Marshall
            • Votes:
              0 Vote for this issue
              Watchers:
              1 Start watching this issue

              Dates

              • Created:
                Updated:
                Resolved:
                Days since last comment:
                4 years, 51 weeks, 1 day ago