0

I have a controller that works, but I do not understand why it works. Here it is:

    @Controller
    @RequestMapping("/someUrl")
    public class MyController {
        @Autowired
        private SomeService someService;

        @RequestMapping(method = RequestMethod.GET)
        public String page() throws ApplicationException {
            return "page";
        }

        @ModelAttribute("modelAttribute")
        public PageModel loadPageModel() throws ApplicationException {
            return someService.createPageModel();
        }
    }

Visiting "/someUrl" will get me to the view of the name "page", as it is expected. What is puzzling, is that despite the model attribute "modelAttribute", or an object of type "PageModel" is not referenced anywhere near the method page, the modelAttribute is still visible for the view. I am happy that this works, but I do not understand why. Any thoughts?

Daniel David Kovacs
  • 226
  • 1
  • 2
  • 13
  • Please elaborate, it is, to me, unclear what you are asking. `@ModelAttribute` methods are always executed before request handling methods, as explained [here](http://docs.spring.io/spring/docs/current/spring-framework-reference/html/mvc.html#mvc-ann-modelattrib-methods). – M. Deinum May 09 '14 at 13:19
  • that is one piece of the puzzle. what perplexes me is that in some magical way the object returned by the @ModelAttribute method ends up passed to the view, with seemingly no explicit reference to it. – Daniel David Kovacs May 09 '14 at 13:29

3 Answers3

0

As the contract for the ModelAttribute annotation puts it:

Annotation that binds a (..) method return value to a named model attribute, exposed to a web view. Supported for controller classes with @RequestMapping methods.

In other words you are adding an attribute implicitely by returning it.

Why this works?

I don't know how internally Spring is doing it but what is enough for us to know is how we can make it eligible for Spring MVC to identify and make it available to the view which is well documented:

  1. having @RequestMapping Controller methods (in your case implicitly defined at the class level)
  2. having one of the supported return types that are combined with the ModelAttribute annotation.
  3. of course being within the component-scan area
dimitrisli
  • 20,895
  • 12
  • 59
  • 63
  • Thx, so do you mean to say, that if I had two methods with '@RequestMapping', the return value of '@ModelAttribute' would be added to both? – Daniel David Kovacs May 09 '14 at 13:47
  • you have RequestMapping method implicitely because you define it at your class level. But the @ModelAttribute is per method and you need to define it with the name you want it to appear as in the view, so no it's not applied by default. – dimitrisli May 09 '14 at 13:49
  • Than we are back to square one, aren't we, because with the code that I had to work with does exactly just that... hence my confusion. – Daniel David Kovacs May 09 '14 at 13:54
  • isn't your PageModel returned object visible to the view under the name modelAttribute? Where's the confusion? That's how it's documented for @ModelAttribute to be working – dimitrisli May 09 '14 at 13:57
  • sorry, why? I read them and can't find them contradicting. Which part is contradicting to you? – dimitrisli May 09 '14 at 14:49
  • I am sure I am the one not getting something, and in the future I will not understand how that happened, but at the moment I am puzzled. These are the facts of the project that I am working on: 1. It is for some reason unknown to me working. 2. There is a PageModel object produced by the method loadPageModel, that is passed to the view via the method page() of the controller. 3. Yet the PageModel object is referenced nowhere in the definition of the method page(). – Daniel David Kovacs May 09 '14 at 18:27
  • Now as for the contradicting part: You are saying that 'isn't your PageModel returned object visible to the view under the name modelAttribute?' implying that with the given code it should be visible to the view. You are also saying that 'But the @ModelAttribute is per method and you need to define it with the name you want it to appear as in the view', which to me implies that in fact this should not work. In short: 1. it should work. 2. it shouldn't work. – Daniel David Kovacs May 09 '14 at 18:29
  • Your `3.` should state *being declared in a controller*. If you add a `@ModelAttribute` annotated method inside a non controller type it won't be executed. – Bart May 09 '14 at 19:13
0

Have a good look at ModelAttributeMethodProcessor and lookup the handleReturnValue method. This method will be executed on controller methods annotated with @ModelAttribute.

/**
 * Add non-null return values to the {@link ModelAndViewContainer}.
 */
@Override
public void handleReturnValue(
        Object returnValue, MethodParameter returnType,
        ModelAndViewContainer mavContainer, NativeWebRequest webRequest)
        throws Exception {

    if (returnValue != null) {
        String name = ModelFactory.getNameForReturnValue(returnValue, returnType);
        mavContainer.addAttribute(name, returnValue);
    }
}

You can see it simply adds it's return value to the model and is invoked with every request concerning that particular controller.

P.s. The starting point of most "magic" operations can be found in RequestMappingHandlerAdapter which is the default HandlerAdapter for modern Spring MVC applications.

Bart
  • 17,070
  • 5
  • 61
  • 80
0

@ModelAttribute is somewhat overloaded, and serves two main functions depending on where it's declared.

With method parameter:

@RequestMapping(value="/create", method=POST)
public String createPage(@ModelAttribute("pageModel") PageModel pageModel) ...

For example, when @ModelAttribute is declared with a method parameter, and you submit parameters to that method, Spring will try to bind the submitted params to an object of type PageModel. Where does it get the PageModel: 1) if there's one currently in Spring managed model, use that 2) otherwise it will create a PageModel using the default constructor of PageModel object

At the method declaration (how you're using it):

@ModelAttribute("modelAttribute")
public PageModel loadPageModel() throws ApplicationException {
   return someService.createPageModel();
}

In your case, when@ModelAttribute is declared at the method level, it tells Spring that the method will provide a PageModel before any RequestMapping is called for a particular request. The created PageModel instance is put into Springs managed model under a "modelAttribute" key. Then from your views, you should be able to reference that PageModel.

So the request goes something like this (i'm only showing relevant events):

  1. request for /someUrl
  2. Spring maps request to page() method
  3. Spring see's that page() method is annotated with @RequestMapping so it calls all methods annotated with @ModelAttribute (at the method level) before executing page()
  4. redirect to "page" view, where a PageModel under "modelAttribute" key is available for you to use

Note:

  • if your loadPageModel() method did not return a PageModel, it would still be called.
  • if you had multiple methods declared with @ModelAttribute at the method level, they would all be called each time a method with @RequestMapping is executed
ikumen
  • 11,275
  • 4
  • 41
  • 41