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):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
xsi:schemaLocation="http://www.springframework.org/schema/beans
<!-- 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:
1
2
3
4
5
6
7
8
9
10
11
12
|
@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.