Tuesday, 1 March 2011

Using Sessions in Spring-MVC (including "scoped-proxies")

On the Spring-MVC video training course, I described three different approaches to handling sessions in Spring.

On the video, I mention that there is also a fourth way, but since the course was getting a bit long I said that I would cover this in a blog post, and here it is.

Thankyou to Bob Casazza for reminding me to do this.

First, a recap of the three approaches described on the video:

1: Use HttpSession directly.

With this approach, you declare HttpSession as a parameter to your controller method. The example on the course looks like this:

public ModelAndView addToCart(@RequestParam("id") int id, HttpSession session)
{
   ShoppingCart cart = (ShoppingCart)session.getAttribute("cart");
   // etc, continue with the cart
}
Pros: it's simple, very much like you would do it in older Spring-MVC and other less capable frameworks.

Cons: it's messy, exposes your clean controller to the Servlet API and needs null checking after you've called getAttribute. Unit testing of your controller is now much harder to do (you need to Mock the HttpSession object).

I don't like this approach, I would avoid it unless necessary (later in the post, I'll explain when I would use it).

2: Scope the Controller

Make your controller session scoped. You can then simply instantiate the object you want to store in session scope as a member variable of the controller...

@Controller
@Scope("session")
public class CartManagementController
{
   private ShoppingCart cart = new ShoppingCart();

   @RequestMapping("/addToCart")
   public ModelAndView addToCart(@RequestParam("id") int id)
   {
      // now just use the cart
   }
}
Pros: A very clean controller, very unit testable.

Cons: A new controller is created for each session, the controller object must be stored in HttpSession. This could bloat the session and in particular could mean replication problems on a large scale system. (Replication: where your web application is hosted on multiple servers. Then the session has to be copied from one server to another. Whilst this is automatic, big sessions cause serious performance problems)

3: Scope the Objects in the Session

This is a narrowing of the session scope, and we session scope just the object we want to store in the session.

@Component
@Scope("session")
public class ShoppingCart
{
   // just a plain java class - member variables and methods as usual
}
Note that the class is now a Spring Bean.

Then, we inject instances of the class into the controller:

@Controller
@Scope("request")
public class CartManagementController
{
   @Autowired
   private ShoppingCart cart;

   @RequestMapping("/addToCart")
   public ModelAndView addToCart(@RequestParam("id") int id)
   {
      // now just use the cart
   }  
}
So, for each request, Spring creates an instance of the controller and then finds the shopping cart from the session.

Crucually, the controller in this approach MUST be request scoped. The default is for Spring to create a global singleton instance of the controller, and this would not work as a singleton is shared by all requests (you can't injection session scoped objects into singleton scoped objects anyway).

Pros: Clean testable controller as in approach two, with the added benefit of the session now only holds the relevant session data.

Cons: A new instance of the controller is created for each request. This is fine if the controller is "small", but if it is expensive to create (ie the constructor is slow for some reason), scalability would be a problem. Also, this approach is harder to understand because of the request scoped controller.

Ok, so they're the three approaches on the course. The problem is they all have drawbacks. I personally almost always use approach 3 where possible, but if I have a "heavy weight" controller, I'd consider using approach 1.

But the fourth approach removes all of the downsides of the previous. The only "con" of this approach is that it is much more complicated. It relies on Spring's best friend: proxies...

4: Use a <aop:scoped-proxy/>

This is covered in full in the Spring Reference manual (at the time of writing, at http://static.springsource.org/spring/docs/3.1.0.M1/spring-framework-reference/html/beans.html#beans-factory-scopes-other-injection)

The general idea of this approach is that you declare your session data as a regular spring bean, with a special tag applied to it (<scoped-proxy>). Your controller will remain a regaular Spring bean, as singleton scope.

With the scoped-proxy tag, your controller looks like it is holding a reference to the session data, but it is actually holding a reference to a proxy which Spring has generated at run time. This proxy's responsibility is to find a session each time it is accessed.

If this is a bit complicated, you might need to check out our AOP session in the Spring Fundamentals video. Or, you can just copy what's here:

Sadly, they haven't created an annotation for scoped-proxy, so your session data (the shopping cart) has to be declared in old-school XML. You add this to your Spring wiring (eg Dispatcher-servlet.xml on the course):

<beans xmlns="http://www.springframework.org/schema/beans"
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xmlns:aop="http://www.springframework.org/schema/aop"
     xsi:schemaLocation="http://www.springframework.org/schema/beans
         http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
         http://www.springframework.org/schema/aop
         http://www.springframework.org/schema/aop/spring-aop-3.0.xsd">

   <!-- an HTTP Session-scoped bean exposed as a proxy -->
   <bean id="shoppingCart" class="com.virtualpairprogrammers.ShoppingCart" scope="session">
      <!-- this next element effects the proxying of the surrounding bean -->
      <aop:scoped-proxy/>
   </bean>
</beans>

Now, your controller looks very simple:
@Controller
public class CartManagementController
{
   @Autowired
   private ShoppingCart cart;

   @RequestMapping("/addToCart")
   public ModelAndView addToCart(@RequestParam("id") int id)
   {
      // now just use the cart
   }  
}
Pros: unit testable and clean as before, only session data is stored in the HttpSession, and the controller is a single-instance global singleton, so no issues with performance of creating them.

Cons: it's much harder to understand (I've taken hours over this post!) and you have to fall back to old fashioned XML wiring.

Conclusion:

The "fourth approach" is probably the most elegant in that it solves all of the technical problems identified earlier. But really, in most situations, approach 3 will be just fine. I have never felt the need to use this fourth approach, it seems like a really heavy solution to the session "problem".

In real life, I've always been happy to use approach 3, and when I'm worried about performance (in the rare case where I'm doing heavy work in the constructor), I'll use approach 1 instead.

I hope no-one minds me omitting approach four from the course, I felt that we had more than enough information on sessions - but I'm glad I've been able to cover it here.

37 comments:

  1. The following example from spring 2.5 documentation will fall into approach 1? (Please correct me if I'm wrong.)

    @Controller
    @RequestMapping("/editPet.do")
    @SessionAttributes("pet")
    public class EditPetForm {

    private final Clinic clinic;

    @Autowired
    public EditPetForm(Clinic clinic) {
    this.clinic = clinic;
    }

    @ModelAttribute("types")
    public Collection populatePetTypes() {
    return this.clinic.getPetTypes();
    }

    @RequestMapping(method = RequestMethod.GET)
    public String setupForm(@RequestParam("petId") int petId, ModelMap model) {
    Pet pet = this.clinic.loadPet(petId);
    model.addAttribute("pet", pet);
    return "petForm";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String processSubmit(
    @ModelAttribute("pet") Pet pet, BindingResult result, SessionStatus status) {

    new PetValidator().validate(pet, result);
    if (result.hasErrors()) {
    return "petForm";
    }
    else {
    this.clinic.storePet(pet);
    status.setComplete();
    return "redirect:owner.do?ownerId=" + pet.getOwner().getId();
    }
    }
    }

    ReplyDelete
  2. Hi Neethan,

    Wow, this is yet another approach that has a slightly different end result.

    This isn't quite a session, it's more a "conversation scope", where data is held for several requests, until setComplete() is called on the SessionStatus object, where the data is then discarded.

    So you could use this if you wanted to strap several forms together into a "wizard".

    (The "old" Spring-MVC has a perfectly good Wizard controller which was much more intuitive than this - I wish they hadn't deprecated all of the old controllers).

    So this is a slightly different subject, sorry it wasn't on the course (we couldn't get everything in), so I think another blog post is needed for this!

    ReplyDelete
  3. Thanks a lot for the very clear explanation on this subject which were missed out in documentation and books.

    ReplyDelete
  4. Hi Richard,

    I used the fourth approach in my project. My session scoped POJO contains attributes of type Set and Map. Those attributes are not getting differentiated based on session. Last session value is getting reflected in all sessions.

    e.g., Session 1 contains POJO Set[1,2]
    Session 2 contains POJO Set[3,4]

    Later when i come back to session1, POJO contains Set[1,2]

    ReplyDelete
  5. If there's something faulty with Sets or Maps, the first thing to check is that you've implemented equals() and hashCode() properly in the POJO.

    It may not be the problem but it's the first thing to check. Have you done this?

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Thanks Richard for your reply. Later i realized that the POJO contains accessor/mutator methods with final access specifier for those attributes and that restricts the Proxy functionality.

    ReplyDelete
  8. Hi Richard,
    I am using method in my Spring 3 portlets + Liferay environment. My controller has session scoped POJO contains attributes of type String and int.Those attributes are not getting differentaited based on session.
    e.g in one tab T1 search results are R1
    and in another tab T2 search results are R2
    if I refresh tab T1 it displays results R2.
    Do i need to make controller session scoped?

    ReplyDelete
  9. Sorry I meant i am using 4th method

    ReplyDelete
  10. This comment has been removed by a blog administrator.

    ReplyDelete
  11. Hi Richard,

    You said "Sadly, they haven't created an annotation for scoped-proxy, so your session data (the shopping cart) has to be declared in old-school XML.".

    But, in the Spring Reference manual (at http://static.springsource.org/spring/docs/3.1.0.RC1/spring-framework-reference/html/beans.html#beans-java-specifying-bean-scope) says that the @Scope annotation replaces the "old-school XML" approach, by passing the proxyMode attribute with the right value of "ScopedProxyMode".

    I used this approach here, and after I added the required libs worked nicely.

    I did as follows:

    @Component
    @Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
    public class UserSession { ... }

    Cheers.

    * Forgive me my English, I'm trying to improve it.

    ReplyDelete
  12. Hi Felipe,

    That's wonderful information -thanks, I'll add it to the post. Your English is excellent!

    ReplyDelete
  13. Thanks Richard, your post save my life!, it was so easy to put a session in my controller. Also Im trying the Felipe Brito way to use an AOP Proxy. Just for the case, if we use an AOP Proxy, it's only mean that our POJO will be lightweighted until we use it? Or there is another benefict for using an AOP Proxy Session?

    ReplyDelete
  14. Thanks for posting this. Very helpful.

    ReplyDelete
  15. with spring 3.1.1, i also used this following approach to achieve "Scoped beans as dependencies”:



    by adding the scoped-proxy in component-scan element, and using "interface" not the "targetClass", no CGLib is required. But of course, you would need interface created for everything that you want proxied... But my question is, does this mean everything in the defined package will be injected as proxy? and if true, any performance impact?

    ReplyDelete
    Replies
    1. Be careful with using interface-based proxies.

      If you are using Spring Security or Spring Transactions you might experience oddities when using interface-based proxies.

      E.g. if you have a bean T and that bean has methods a() and b() that are both annotated transactional. Calls from other beans directly to a() or b() will behave properly (as configured). However if you introduce an internal call - where a() calls b() then b's transactional metadata will have no effect. The reason is that when you are using interface-based proxies the internal call will not go through the proxy - and thus the transactional interceptor will not have a chance to start a new transaction.

      The same goes for Security. If method a() only requires USER-role but calls b() that requires ADMIN-role, then the internal call from a to b will be performed for any USER with no warnings. Same reason as above, internal calls do not go through the proxy and thus the security interceptor does not have a chance to act upon the call to b() from a().

      To solve issues like these use targetClass.

      PS: In Spring 3.2 you no longer need to depend on cglib as extra dependency - it has been embedded in the Spring libs.

      Delete
  16. This comment has been removed by the author.

    ReplyDelete
  17. for some reason i can't put in xml tag in this comment... so here is my last try...
    context:component-scan base-package="abc.xyz" scoped-proxy="interfaces"

    ReplyDelete
  18. Great post!! For those interested, the next link also talks about this topic: http://tedyoung.me/2011/10/19/practical-spring-mvc-part-5-sessions/

    ReplyDelete
  19. Curious to know if the above code can handle concurrency issues

    ReplyDelete
  20. Hi Sunil, as with all session scoped objects, you will have to make sure that the objects you store in the session are thread safe, as it would be possible for two requests from the same user to access the object concurrently.

    ReplyDelete
  21. Hi Richard,

    I am facing an issue in my application, when 2 or more users access the application same session is shared among all the users. How to solve this concurrency issue? Would be great if you help me out to solve this issue.
    Thanks in advance

    ReplyDelete
  22. Hi Dinesh, a session shouldn't be shared among all the users, how are you managing the session?

    ReplyDelete
  23. Richard, we're using the aop:scoped-proxy approach and we're having the following problem (i'll use your objects above).

    1) Open browser one and log in. Session A is created and ShoppingCart created and has session A.
    2) Open browser two and log in. Session B is created and ShoppingCart created and has session B.
    3) Try to do something in browser 1. New ShoppingCart created and has session C.
    4) ShoppingCart bombs because it's looking for an object that was added to session A after initial login.

    Any thoughts as to why Spring might be creating a new ShoopingCart for browser one with a new session after I log in from browser two?

    Thanks!

    ReplyDelete
    Replies
    1. Figured it out. Session A was being invalidated somewhere and therefore Spring created a new ShoppingCart and a new session. Nevermind!

      Delete
  24. Excellent summary on the approaches. Thanks a lot for taking the time to write it.

    ReplyDelete
  25. Awesome article, it gave me the perfect entry point for enabling session's on our project. Thank you very much Richard C.

    Approach #3 did the trick.

    Greetings from Mexico.

    ReplyDelete
  26. Good article!, I was able to implemented the "fourth approach", and I can access to that session from another Controllers, but How can I print directly in my web page the mybean.attribute?
    For example I created:

    @Component
    @Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
    public class UserBean implements Serializable{


    private long myproperty ... //get setter}

    I charged that session in my controllers and I can put/read that session and attributes (only in controllers)

    But I was trying to print directly in my web page:

    ${userBean.userName}
    or
    ${sessionScope.userBean.userName}

    But it doesn't work.

    Any idea?

    Thanks!!
    Great article!


    ReplyDelete
  27. It looks ok to me at first glance. You don't have <%@ page session="false" %> enabled by any chance do you?

    Something to try: output the whole session using ${sessionScope} and check the contents there.

    Failing that, raise a question at StackOverflow.com, reply to this with the question URL and I'll check it out.

    ReplyDelete
  28. Hi Richard, I solved that, I put in my page:

    ${sessionScope['scopedTarget.userBean'].myproperty}

    Thanks so much, your article helped me a lot!

    ReplyDelete
  29. I prefer...

    @Controller
    public class CartManagementController {

    @Inject (or @Autowired)
    private javax.inject.Provider shoppingCartProvider;


    }

    ReplyDelete
  30. @Controller
    @RequestMapping("/barlexamquize")
    public class BarlExamQuizeController {
    @Autowired private Constants constants;
    @RequestMapping(value = "/giveAnswer", method = RequestMethod.GET)
    public String practiceQuestions(HttpServletRequest request, ModelMap model) {
    PersistenceManager pm=null;
    Query q=null;
    List barlQuestion = null;
    pm = PMF.get().getPersistenceManager();
    q= pm.newQuery(BarlQuestion.class, "id == idParameter");
    q.declareParameters("long idParameter");
    try {
    barlQuestion = (List) q.execute(constants.getId());
    if (barlQuestion.isEmpty()) {
    model.addAttribute("barlQuestion", null);
    } else {
    model.addAttribute("barlQuestion", barlQuestion.get(0));
    }

    } finally {
    q.closeAll();
    pm.close();
    }
    return "giveAnswer";
    }

    @RequestMapping(value = "/nextQuestion", method = RequestMethod.POST)
    public String nextQuestion(HttpServletRequest request, ModelMap model) {

    PersistenceManager pm = PMF.get().getPersistenceManager();
    Query q = pm.newQuery(BarlQuestion.class, "id == idParameter");
    q.declareParameters("long idParameter");
    List barlQuestion = null;

    if (constants.getId() < totalQuestion) {
    constants.incrementId();
    try {
    barlQuestion = (List) q.execute(constants.getId());
    if (barlQuestion.isEmpty()) {
    model.addAttribute("barlQuestion", null);
    } else {
    model.addAttribute("barlQuestion", barlQuestion.get(0));
    }

    } finally {
    q.closeAll();
    pm.close();
    }
    return "giveAnswer";
    } else {
    String score = ((constants.getRightAns() * 100) / totalQuestion) + "%";
    model.addAttribute("score", score);
    constants.setRightAns(0);
    constants.setId(1);
    return "examScore";
    }
    }
    }

    and in dispatcher-servlet.xml:





    The controller can not keep the session. It behaves like request scope. the constants.getId() remains in initiazed value for each http session. please help me

    ReplyDelete
  31. Hi, I'm sorry it's hard to read your post (the blogger software doesn't keep formatting), and your XML is lost completely - please send in a support call through virtual pair programmers and we can handle the case properly there. (If you bought the course through Amazon include the barcode from the DVD in your call). I assume your constants bean has been created in request scope?

    ReplyDelete
  32. Hi Richard,
    Nice post !
    We are using forth apporach (AOP session scoped proxy) and have one issue.
    In same session we need two copies of same session scoped bean. But even if we inject two objects, while accessing it returns data for the first one.

    How to use AOP session scoped proxies if we need to put two different copies of bean in session ?

    ReplyDelete
  33. Hi Richard,
    I have a controller which sets few values to a model and sends to jsp. In jsp i need to show those values(as labels) along with additional values from user as input values. When i submit the jsp i only get valid values that user has entered and the values set earlier by controller is null.

    Will this issue be solved if I follow any of the above approach???

    ReplyDelete
  34. Hi Richard,

    I am trying to get option (4) to work with annotations - as Felipe suggested - and in truth I have googled to the text he mentions independently.

    However, the code invariably fails in the wiring, saying that the scope does not exist.

    Do you know of a simple example project that shows this working? Maybe I should give up on @... and accept that XML has not died yet :-)

    ReplyDelete