Thursday, October 18, 2012

Power of Spring's @ModelAttribute and @SessionAttributes

The Spring Framework is an open source application framework that aims to make J2EE development easier. Unlike single-tier frameworks, such as Struts or Hibernate, Spring aims to help structure whole applications in a consistent, productive manner, pulling together best-of-breed single-tier frameworks to create a coherent architecture.

How does @ModelAttribute work?
@ModelAttribute is a Spring-MVC specific annotation used for preparing the model data. It is also used to define the command object that would be bound with the HTTP request data. The annotation works only if the class is a Controller class (i.e. annotated with @Controller).

ModelAttribute can be applied on both methods as well as method-parameters. It accepts an optional "value", which indicates the name of the attribute. If no value is supplied to the annotation, then the value would be defaulted to the return type name in case of methods and parameter type name in case of method-parameters.

The way Spring processes this annotation is:
  1. Before invoking the handler method, Spring invokes all the methods that have @ModelAttribute annotation. It adds the data returned by these methods to a temporary Map object. The data from this Map would be added to the final Model after the execution of the handler method. 
  2. Then it prepares to invoke the the handler method. To invoke this method, it has to resolve the arguments. If the method has a parameter with @ModelAttribute, then it would search in the temporary Map object with the value of @ModelAttribute. 
  3. If it finds, then the value from the Map is used for the handler method parameter. It it doesn't find it in the Map, then it checks if there is a SessionAttributes annotation applied on the controller with the given value. If the annotation is present, then the object is retrieved from the session and used for the handler method parameter. If the session doesn't contain the object despite of the @SessionAttributes, then an error is raised. 
  4. If the object is not resolved through Map or @SessionAttribute, then it creates an instance of the parameter-type and passes it as the handler method parameter. Therefore, for it to create the instance, the parameter type should be a concrete-class-type (interfaces or abstract class types would again raise an error). 
  5. Once the handler is executed, the parameters marked with @ModelAttributes are added to the Model.
Let's show examples:

@Controller
public class MyController {
    @Autowired
    private ServiceRegistry serviceRegistry;

    @ModelAttribute("myobject")
    public MyObject getInitializedMyObject() {
        return serviceRegistry.myService.getInitializedObject();
    }

    @RequestMapping(value="/handle.htm", method=RequestMethod.GET)
    public ModelAndView handleRequest() {
        return new ModelAndView("myView");
    }

}
In this example, the value returned by getInitializedMyObject is added to the Model. The View would be able to retrieve this object using the key "myobject" from the request attributes.
@Controller
public class MyController {
    @Autowired
    private ServiceRegistry serviceRegistry;

    @ModelAttribute("myobject")
    public MyObject getInitializeMyObject() {
        return serviceRegistry.myService.getInitializedObject();
    }

    @RequestMapping(value="/handle.htm", method=RequestMethod.GET)
    public ModelAndView handleRequest(@ModelAttribute("myobject") MyObject myObject) {
        myObject.setValue("test");
        return new ModelAndView("myView");
    }

}
In this case, the getInitializeMyObject is executed first and the result is stored in a temporary map. This value is then passed as a parameter to the handler method. And finally myObject is added to the model for the views.
@Controller
@SessionAttributes("myobject")
public class MyController {

    @RequestMapping(value="/handle.htm", method=RequestMethod.GET)
    public ModelAndView handleRequest(@ModelAttribute("myobject") MyObject myObject) {
        myObject.setValue("test");
        return new ModelAndView("myView");
    }

}
In this case, Spring searches for "myobject" in the session and pass it as the parameter to the handler method. If "myobject" is not found in the session, then HttpSessionRequiredException is raised. Let's explain @SessionAttributes lifecycle:
  1. @SessionAttribute is initialized when you put the corresponding attribute into model (either explicitly or using @ModelAttribute-annotated method). 
  2. @SessionAttribute is updated by the data from HTTP parameters when controller method with the corresponding model attribute in its signature is invoked. 
  3. @SessionAttributes are cleared when you call setComplete() on SessionStatus object passed into controller method as an argument.

@Controller
public class MyController {

    @RequestMapping(value="/handle.htm", method=RequestMethod.GET)
    public ModelAndView handleRequest(@ModelAttribute("myobject") MyObject myObject)   {
        myObject.setValue("test");
        return new ModelAndView("myView");
    }

}
In this case, a new instance of MyObject is created and then passed to handler method. If MyObject is an interface or an abstract class, then a BeanInstantiationException is raised.

Let's look another important point:
SessionStatus.setComplete() method will trigger cleaning of Session Attributes, but only those which Spring will find "actual session attribute". Suppose that you declare 3 session attributes, but use only 1 of them in your handler method parameters, like in following example:

@SessionAttributes({ "abc", "def", "ghi" })
public class BindingTestController {

    @ModelAttribute("abc")
    public String createABC() {
        return "abc";
    }

    @RequestMapping(method = RequestMethod.GET)
    public void onGet(@ModelAttribute("abc") String something) {
        // do nothing :)
    }

    @RequestMapping(method = RequestMethod.POST)
    public void onPost(@ModelAttribute("abc") String something, BindingResult bindingResult, SessionStatus sessionStatus) {
        sessionStatus.setComplete();
    }

}
Only the "abc" attribute will be considered as "actual session attribute", and removed on POST request.

There are many powerful things in spring. That is why I'm in love with Spring:) If you are also a big fan of Spring and wanna be master on it and earn big money I strongly recommend to take one of two most popular Spring courses:

10 comments:

  1. Thanks a lot for this article!

    Could you add the following case as well and explain steps:

    @Controller
    @SessionAttributes("myobject")
    public class MyController {

    @ModelAttribute("myobject")
    public MyObject getInitializeMyObject() {
    return serviceRegistry.myService.getInitializedObject();
    }

    @RequestMapping(value="/handle.htm", method=RequestMethod.GET)
    public ModelAndView handleRequest(@ModelAttribute("myobject") MyObject myObject) {
    myObject.setValue("test");
    return new ModelAndView("myView");
    }

    }

    ReplyDelete
    Replies
    1. So what Spring does when dispatching to your @RequestMapping method to handle the GET is:
      1. Find the appropriate @RequestMapping method
      2. Examine each of the method parameters to see if the required data is there
      3. If a @ModelAttribute parameter is found and that attribute does not already exist in the model, then find the method in the controller that populates that @ModelAttribute
      4. Execute the method found in 3
      5. Finally execute the @RequestMapping method


      so in this case if "myobject" is in sessionAttribute so
      @ModelAttribute("myobject")
      public MyObject getInitializeMyObject() {
      return serviceRegistry.myService.getInitializedObject();
      }
      will do nothing :)

      Delete
    2. you said before , the modelattribute is first checked in the controller method and then the session atribute , so in above case (mentioned by Vladimirs) , session attribute "myobject" will do nothing , right ? but here you have mentioned the :
      @ModelAttribute("myobject")
      public MyObject getInitializeMyObject() {
      return serviceRegistry.myService.getInitializedObject();
      }
      will do nothing :(

      Delete
  2. These 2 seems contradict to each other:
    3.If it finds, then the value from the Map is used for the handler method parameter. It it doesn't find it in the Map, then it checks if there is a SessionAttributes annotation applied on the controller with the given value. If the annotation is present, then the object is retrieved from the session and used for the handler method parameter. If the session doesn't contain the object despite of the @SessionAttributes, then an error is raised.
    4.If the object is not resolved through Map or @SessionAttribute, then it creates an instance of the parameter-type and passes it as the handler method parameter. Therefore, for it to create the instance, the parameter type should be a concrete-class-type (interfaces or abstract class types would again raise an error).
    When a method contains param with @ModelAtrribute and it cannot find either in temp Map or @SessionAtrribute. What will spring do? Create a new instance base on the type of the parameter or throw exception?
    Thanks

    ReplyDelete
  3. Hi,

    May i know how thw @ModelAttribute parameter myobject get refered by view page? where we have to specify our reference?

    ReplyDelete
  4. Hi.

    I need to do something similar, but using a composite key object and get values from a form using @ModelAttribute for a Composite key class.
    Is this possible or do I need use another approach??

    For example:
    @Embeddable
    public class CarPK implements Serializable {

    @Column
    private int serial;
    @Column
    private String brand;

    public CarPK() {

    }

    public CarPK(int serial, String brand) {
    this.serial = serial;
    this.brand = brand;
    }

    //cut
    }

    @Entity
    @Table(name=”Car”)
    public class Car {

    @EmbeddedId
    private CarPK id;

    @Column(name = “name”)
    private String name;

    //cut
    }

    For a simple example in controller:
    @RequestMapping(value = “/addcar”)
    public String addCar(@ModelAttribute Car car) {

    //Data in car are no correctly populated

    carSvc.add(car);
    return “redirect:/cars”;
    }

    and In service:
    @Transactional
    public void add(Car c) {

    em.persist(c);

    }

    Any idea?

    Thanks

    ReplyDelete
  5. Hey,

    Indeed a nice article cleared a lot of murkiness.

    However while trying some hands on i came across a peculiar problem.

    As soon as a method having @ModelAtrribute in its signature is invoked the @SessionAttribute with the same name is updated.

    however when i 'REDIRECT' to a jsp view from this method the session Object is lost even though the request now contains the modified/updated model attribute. By lost I don't mean unupdated (cause that won't be the case as no method is invoked to update session attribute) but completely lost, as in it isn't there in the list of session attributes available to the jsp view. (i checked it using a debugger).

    ReplyDelete
  6. Contradict : The data from this Map would be added to the final Model after the execution of the handler method.

    It gets added to model before the execution of the handler method.

    It gets added to request aft erthe execution of the handler method.

    ReplyDelete