Where exactly is a model object created in Spring MVC?
Asked Answered
H

2

11

After going through some tutorials and initial document reading from the docs.spring.org reference I understood that it is created in the controller of a POJO class created by the developer. But while reading this I came across the paragraph below:

An @ModelAttribute on a method argument indicates the argument should be retrieved from the model. If not present in the model, the argument should be instantiated first and then added to the model. Once present in the model, the argument's fields should be populated from all request parameters that have matching names. This is known as data binding in Spring MVC, a very useful mechanism that saves you from having to parse each form field individually.

@RequestMapping(value="/owners/{ownerId}/pets/{petId}/edit", method = RequestMethod.POST)
public String processSubmit(@ModelAttribute Pet pet) {
   
}

Spring Documentation

In the paragraph what is most disturbing is the line:

"If not present in the model ... "

How can the data be there in the model? (Because we have not created a model - it will be created by us.)

Also, I have seen a few controller methods accepting the Model type as an argument. What does that mean? Is it getting the Model created somewhere? If so who is creating it for us?

Herbal answered 1/9, 2018 at 8:57 Comment(0)
H
9

If not present in the model, the argument should be instantiated first and then added to the model.

The paragraph describes the following piece of code:

if (mavContainer.containsAttribute(name)) {
    attribute = mavContainer.getModel().get(name);
} else {
    // Create attribute instance
    try {
        attribute = createAttribute(name, parameter, binderFactory, webRequest);
    }
    catch (BindException ex) {
        ...
    }
}
...
mavContainer.addAllAttributes(attribute);

(taken from ModelAttributeMethodProcessor#resolveArgument)

For every request, Spring initialises a ModelAndViewContainer instance which records model and view-related decisions made by HandlerMethodArgumentResolvers and HandlerMethodReturnValueHandlers during the course of invocation of a controller method.

A newly-created ModelAndViewContainer object is initially populated with flash attributes (if any):

ModelAndViewContainer mavContainer = new ModelAndViewContainer();
mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));

It means that the argument won't be initialised if it already exists in the model.

To prove it, let's move to a practical example.

The Pet class:

public class Pet {
    private String petId;
    private String ownerId;
    private String hiddenField;

    public Pet() {
         System.out.println("A new Pet instance was created!");
    }

    // setters and toString
}

The PetController class:

@RestController
public class PetController {

    @GetMapping(value = "/internal")
    public void invokeInternal(@ModelAttribute Pet pet) {
        System.out.println(pet);
    }

    @PostMapping(value = "/owners/{ownerId}/pets/{petId}/edit")
    public RedirectView editPet(@ModelAttribute Pet pet, RedirectAttributes attributes) {
        System.out.println(pet);
        pet.setHiddenField("XXX");

        attributes.addFlashAttribute("pet", pet);
        return new RedirectView("/internal");
    }

}

Let's make a POST request to the URI /owners/123/pets/456/edit and see the results:

A new Pet instance was created!
Pet[456,123,null]
Pet[456,123,XXX]

A new Pet instance was created!

Spring created a ModelAndViewContainer and didn't find anything to fill the instance with (it's a request from a client; there weren't any redirects). Since the model is empty, Spring had to create a new Pet object by invoking the default constructor which printed the line.

Pet[456,123,null]

Once present in the model, the argument's fields should be populated from all request parameters that have matching names.

We printed the given Pet to make sure all the fields petId and ownerId had been bound correctly.

Pet[456,123,XXX]

We set hiddenField to check our theory and redirected to the method invokeInternal which also expects a @ModelAttribute. As we see, the second method received the instance (with own hidden value) which was created for the first method.

Helper answered 1/9, 2018 at 15:7 Comment(9)
That's really interesting. But about 'didn't find anything to fill the instance with': under what circumstances would it have found something? Doesn't it always need to create a new instance?Bombycid
@daniu, if å redirect happens, a new instance of ModelAndViewContainer might be populated by the attributes extracted from RequestContextUtils.getInputFlashMap(request) that were set by the initialiser of redirectionHelper
@Bombycid Spring doesn't consider redirects to be game-changing enough to clear out the whole "context" for them. And it makes sense: in invokeInternal, I do expect the pet that was created for editPet if the redirect from it happened.Helper
Oh, that's why you even added the internal() one, got it. Thanks :)Bombycid
@AndrewTobilko Could you please answer for this as well "Also, I have seen a few controller methods accepting the Model type as an argument. What does that mean? Is it getting the Model created somewhere? If so who is creating it for us?"Herbal
How do you conclude for my question that where actually model object is created ?In Controller? In some other class?Or in both places Controller and that class ?Herbal
@Herbal I don't understand the question, it can't be created in the controller since it's an argumentHelper
@Herbal could you clarify it?Helper
So model object(not referring form backing model object) is not created in controller ? It is created somewhere and passed as argument to which we are adding attributes ?Herbal
H
0

To answer the question i found few snippets of code with the help of @andrew answer. Which justify a ModelMap instance[a model object] is created well before our controller/handler is called for specific URL

 public class ModelAndViewContainer {

    private boolean ignoreDefaultModelOnRedirect = false;

    @Nullable
    private Object view;

    private final ModelMap defaultModel = new BindingAwareModelMap();
      ....
      .....
   }

If we see the above snippet code (taken from spring-webmvc-5.0.8 jar). BindingAwareModelMap model object is created well before.

For better Understanding adding the comments for the class BindingAwareModelMap

   /**
     * Subclass of {@link org.springframework.ui.ExtendedModelMap} that automatically removes
     * a {@link org.springframework.validation.BindingResult} object if the corresponding
     * target attribute gets replaced through regular {@link Map} operations.
     *
     * <p>This is the class exposed to handler methods by Spring MVC, typically consumed through
     * a declaration of the {@link org.springframework.ui.Model} interface. There is no need to
     * build it within user code; a plain {@link org.springframework.ui.ModelMap} or even a just
     * a regular {@link Map} with String keys will be good enough to return a user model.
     *
     @SuppressWarnings("serial")
      public class BindingAwareModelMap extends ExtendedModelMap {
      ....
      ....
     }
Herbal answered 3/9, 2018 at 18:25 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.