Sunteți pe pagina 1din 8

A Simple Conversation Scope

for Spring-based Applications


Jim Majure, Majure Consulting, LLC <jim@majureconsulting.com>

Table of Contents
Introduction ....................................................................................................................................... 1
An Example ...................................................................................................................................... 2
Making it Work ......................................................................................................................... 3
The ConversationFilter Servlet Filter ........................................................................................... 4
The Spring Scoping Mechanism ............................................................................................................ 5
Configuring the Spring Beans ............................................................................................................... 6
Configuring conversation-related beans .......................................................................................... 6
Configuring Application Beans ..................................................................................................... 7
Conversation Abandonment .................................................................................................................. 7
Handling Redirects ............................................................................................................................. 8
Warnings and Issues ........................................................................................................................... 8
Hibernate Session Size ................................................................................................................ 8
Hibernate Session Flushing Configuration ....................................................................................... 8
Clustering and Load Balancing ..................................................................................................... 8

A conversation scope for web applications can solve several problems that are commonly faced by web developers. A
conversation scope holds application state for a single logical conversation. It outlives the request scope, but is not as
broad as the session scope, and each user can start an arbitrary number of them. There are two main problems that this
solves: the ability to open multiple screens to operate on different instances of the same type of object; and reducing
the number of occurrences of the annoying HibernateLazyLoadingException (if you're using Hibernate for
your persistence mechanism).

Implementing this in Spring involves replacing the OpenSessionInView filter to resume conversations when
necessary and play nicely with the Spring transaction environment, programmatically beginning and ending
conversations when required, and updating views to include the current conversation identifier.

Introduction
There have been many times in my own applications and in others' applications that I have been editing an entity and
wanted to open a second window to look at another entity of the same type to see how something is configured. If I
were to open a second window, however, the first window would no longer usable because the application only stores
state for a single window. The reason for this is that a normal web application only provides two scopes: request and
session. So, typically, application state is stored at the session level - and there is only one session for each user of
an application.

The solution to this problem, of course, is the introduction of a new type of scope. One that lasts longer than a request
and one that allows a single user to open multiple instances. This idea is not new, of course, and has been solved
before. Spring Webflow provides a flow scope, which accomplishes the same basic goals, but requires you to create an
formal flow definition, which is not always necessary. JBoss Seam has solved the problem very elegantly and serves
as the inspiration for this work. But you have to take many parts of Seam that you might not be ready to use. (I must
admit, a very compelling argument can be made for Seam given the many problems it has solved.) For many of us that

1
A Simple Conversation Scope
for Spring-based Applications

have a commitment to Spring and an existing code base to support, it would be nice to make a few simple changes
and reap the benefits of a conversation scope.

A conversation scope solves another persistent problem for those that use Hibernate for object persistence: the infamous
HibernateLazyLoadingException. As Gavin King points out, this exception is usually due to the fact that
Hibernate is being used incorrectly. Hibernate sessions are intended to hang around as long as they are needed
(hibernate reference) and not be arbitrarily closed at the end of each request, which is what Spring does in the
OpenSessionInView servlet filter. With a conversation scope, the session can be opened at the beginning of the
conversation and kept open until the conversation ends.

The following sections will describe what is needed to create and use a simple conversation scope in a Spring-based
application.

An Example
To help motivate the discussion, let's consider a specific example. To keep things simple, let's consider CRUD
operations on a customer entity in a time tracking application. Lets assume that each customer has a collection of
projects against which time can be billed. I want to be able to change the customer's name and add/remove projects.
None of the changes should be persisted until I issue a save command and I can cancel the edits at any time before I
save. In addition to this, I want to be able to concurrently edit two instances of the same entity in two different screens.
Figure 1 shows what the main screen looks like.

Figure 1. Main screen of example application.

To use the application, the user navigates to the main screen, which presents a list of customers. The user can then click
on the edit icon to edit the customer's information. The user can edit another user by right-clicking and choosing to
open in a new window or tab. The application will operation by initiating a new conversation, retrieving the customer
entity and placing it in the new conversation scope, and presenting the information to the user for editing.

2
A Simple Conversation Scope
for Spring-based Applications

The example is written using JSF and Spring to wire together the collaborating beans and manage transactions.

Making it Work
Once all of the plumbing is in place (which is described below), we need to see how to start/end extended conversations,
and how to modify the view to ensure conversations are continued. Figure 1, “Main screen of example application.”
shows the application's main landing page, which displays a clickable list of customers. Example 1, “JSF code to
generate clickable customer table” shows the JSF code to generate the clickable table.

Example 1. JSF code to generate clickable customer table

<h:dataTable id="customerTable" value="#{customerUi.customers}"


var="customer" >
<h:column>
<h:commandLink action="#{customerBean.editCustomer}">
<f:setPropertyActionListener value="#{customer}"
target="#{customerBean.customer}" />
<h:outputText value="#{customer.name}" />
</h:commandLink>
</h:column>
</h:dataTable>

Clicking on a customer name, then, does two main things, shown in Example 2, “CustomerBean code snippet”:

1. Stores the customer to be edited into the conversation-scoped customerBean

2. Executes the customerBean.editCustomer method, which programmatically starts a conversation and


redirects to the edit page

Example 2. CustomerBean code snippet

public String editCustomer() {


conversationManager.startNewConversation(false);
return "editPage";
}
public void setCustomer(Customer newCustomer) {
customer = newCustomer;
}
public void saveCustomer() {
customerService.save(getCustomer());
}
public String cancelCustomerEdit() {
conversationManager.endConversation();
return null;
}

All editing of the customer must take place within the context of the conversation that has been created. To do this, the
edit page must include the current conversation id in any form submissions. Example 3, “JSF code to edit a customer”
shows the necessary JSF code for this.

3
A Simple Conversation Scope
for Spring-based Applications

Example 3. JSF code to edit a customer

...
<h:form>
<h:panelGrid columns="3">
<h:outputLabel value="Name" for="custName"/>
<h:inputText id="custName"
value="#{customerBean.customer.name}"/>
<h:message for="custName"/>
...
</h:panelGrid>
<h:commandButton action="#{customerBean.saveCustomer}"
value="Save"/>
<h:commandButton action="#{customerBean.cancelCustomerEdit}"
value="Cancel"/>
<input type="hidden" name="conversationId"
value="#{conversationManager.currentConversationId}"/>
</h:form>
...

In particular, note the hidden input field which includes the current conversation id in the form submission. This field
can also be put into a facelet so it doesn't have to be repeated in every form.

The save button invokes the saveCustomer method, which saves the customer via the customer service. This has the
effect of flushing the hibernate session, which persists the customer (and through cascade, the customer's projects, too).

The cancel button invokes the cancelCustomerEdit method, which ends the current conversation. This has the
effect of removing all beans in the current conversation, which includes all edits to the currently selected customer.

This ends the description of using conversation-scoped beans. The remainder of the document describes how the
scoping mechanism works and how it is configured.

The ConversationFilter Servlet Filter


The first part of supporting conversations is to create a servlet filter that handles suspending and resuming conversations
that span more than one request. This filter is responsible to check each request for a conversation identifier and resume
existing conversations when necessary. Because we are using Hibernate for persistence and Spring for transaction
management, this filter must also create and close Hibernate sessions and ensure that the Hibernate session is registered
with the Spring transaction synchronization mechanisms. Example 4, “ConversationFilter filter method.”
shows the main method for this filter.

4
A Simple Conversation Scope
for Spring-based Applications

Example 4. ConversationFilter filter method.

protected void doFilterInternal(HttpServletRequest request,


HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {

ConversationManager manager = lookupConversationManager();


String conversationId = getConversationParameter(request);
if (conversationId == null) {
manager.startNewConversation(true);
} else {
manager.resumeConversation(conversationId);
}
try {
filterChain.doFilter(request, response);
} finally {
manager.suspendConversation();
}
}

The ConversationFilter checks the request to see if it is a continuation of an existing conversation, indicated
by the presence of a request parameter identifying the conversation to be continued. It then delegates to the
ConversationManager to either start a new (temporary) conversation, or resume an existing one. The filter then
delegates to the filter chain and suspends the conversation when the request has completed.

Note that all requests operate within a conversation. By default, conversations are marked as temporary, which means
that they only last for a single request. This is identical to the more typical use of the Spring OpenSessionInView
servlet filter. Non-temporary conversations are started and ended programmatically at the beginning and end of logical
conversations.

Most of the conversation-related work is done by the ConversationManager class. This class is responsible for
starting and ending conversations, creating and closing Hibernate sessions, interfacing with the Spring transaction
synchronization mechanisms, and exposing the conversation context to the Spring scoping mechanism. The details of
this class will not be explained, but can be reviewed by the interested reader.

The Spring Scoping Mechanism


Beans in the conversation context are managed by Spring just like any other Spring-managed beans. They are placed
into the conversation scope using the Spring scoping mechanism. The Spring scoping mechanism, introduced in version
2, allows Spring beans to be placed in a specific scope. Spring provides the common scopes out-of-the-box, including:

request - one instance for each HTTP request


session - one instance for each web session
singleton - one instance for the spring context (i.e., global scope)

The Spring scoping mechanism was written genericly, so creating additional scope types is relatively easy. In our
case, a conversation scope is required. The ConversationManager class, introduced previously, is responsible
for making the current conversation context accessble. Our conversation scope needs to have access to the
ConversationManager so that it can retrieve beans from the conversation context.

Custom Spring scopes must implement the Spring Scope interface. The primary method of interest defined by this
interfaces is the get method. This method is responsible for reteieving an object from the scope, or, if it is not already

5
A Simple Conversation Scope
for Spring-based Applications

in the scope, creating a new instance from a supplied bean factory. The get method for the conversation scope is
shown in Example 5, “Conversation scope get method.”

Example 5. Conversation scope get method.

public Object get(String name, ObjectFactory objectFactory) {


Object result = ConversationManager.
getCurrentConversationContext().get(name);
if (result == null) {
result = objectFactory.getObject();
ConversationManager.
getCurrentConversationContext().put(name, result);
}
return result;
}

Configuring the Spring Beans


There are two areas of Spring bean configuration that are important

1. Configuring the conversation-related beans

2. Configuring the application beans, placing them in conversation scope as necessary

Configuring conversation-related beans


Before the conversation scope can be used, the conversation manager and custom scope must be registered with Spring
in the correct manner. Example 6, “Configuring Conversation-specific Beans” shows the configuration necessary for
this. There are several things to note in this configuration:

The conversationManager bean is initilized by executing the initialize method. This method starts a thread
that continuously monitors the conversation manager for abandoned conversations, and cleans them up.
The bean of class CustomScopeConfigurer registers the custom scope with Spring, so that it is aware of it at
runtime.

6
A Simple Conversation Scope
for Spring-based Applications

Example 6. Configuring Conversation-specific Beans

...
<bean id="conversationManager"
class="com.stryker.corp.scn.web.conversation.ConversationManager"
init-method="initialize">
<property name="sessionFactory" ref="sessionFactory"/>
<property name="conversationTimeout" value="3600000"/>
</bean>

<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="conversation">
<bean class="...conversation.ConversationScope"/>
</entry>
</map>
</property>
</bean>
...

Configuring Application Beans


Next, application beans must be configured to put necessary beans into the conversation scope. The beans placed into
conversation scope hold the conversational state for a user for a particular conversation. Example 7, “Configuring
application Beans” shows how the collaborating beans in our simple CRUD application are configured. There are
several things to note about this configuration:

The customerBean bean is configured in conversation scope. This bean holds the currently selected customer
instance. It also holds a reference to the customerService bean.
The customerService is configured in (default) singleton scope. This beans represent stateless services that
interact to provide persistence and execute business logic.

Example 7. Configuring application Beans

...
<bean id="customerBean" class="ui.customer.CustomerBean" scope="conversation">
<property name="service" ref="customerService"/>
<aop:scoped-proxy/>
</bean>

<bean id="customerService" class="customer.hbm.CustomerServiceHbm"


autowire="byType"/>
...

Conversation Abandonment
Explicitly ending conversations is done programatically, as shown in the cancel example. However, it is probably
more frequent for users to simply abandon a conversation by navigating away from it. Because, there is really no

7
A Simple Conversation Scope
for Spring-based Applications

viable way to keep this from happening, it is important to have a mechanism to clean up abandoned conversations to
keep them from bloating the session. The ConversationManager handles this for us by continuously checking
for conversations that have timed out, and removing them.

Handling Redirects
Navigation in web applications often occurs via browser redirects. JSF, in particular, handles many redirects
tranparently to the developer. If a redirect occurs in the context of a conversation, however, it must include the
conversation identifier in order to propogate the conversation. (NOTE: Seam has handled this very elegantly, almost
completely hiding the details from the developer.) To solve this problem, a custom JSF navigation handler is used to
propogate the conversation id on redirect.

Warnings and Issues


There are several issues to consider when using a conversation scoping mechanism like this one. The include:

1. Hibernate session size

2. Hibernate session flushing configuration

3. Clustering and load balancing

Hibernate Session Size


Because the hibernate session is kept open for the duration of the conversation, it is paramount that an extended
conversation be properly bounded. It should not be possible for a user to initiate conversations that have an arbitrary
time span and allow objects to be continually added to the session.

Hibernate Session Flushing Configuration


The hibernate session can be configured to flush in response to different types of events. Because our goal is to ensure
that the conversation is not persisted until the user requests it to be saved, it is best to configure flushing to occur on
transaction commit. This ensures that the session won't be flushed to the db until a transaction is committed.

Clustering and Load Balancing


The current implementation stores the conversation scope in memory, so distributing a single session across multiple
servers is problematic. It could easily be put into the user's session in order to take advantage of session persistence
provided in many server environments. I haven't tried this, so I don't know how well it would work. If the primary
goal of clustering is for failover, perhaps the best solution is to use sticky sessions. In this case, a conversation would
only be lost in the event the server it is on fails.

S-ar putea să vă placă și