Spring/JSF Support For Multiple Browser Tabs
In January, I wrote a blog post about the need to partition HttpSession across multiple browser tabs or windows. In that blog, the client was using JSF 2.0, but not Spring. Now, nine months later, I find myself at a different client with the same problem, but with a different technology stack. They are using Spring 3.0.5 but are back a level on JSF using 1.0. This update represents the pitch I made to this client for the same challenge of trying to support “Open in new Tab” built into most browsers today.
Web applications will often need to store information across requests. For example, saving search results so that once the user returns from viewing details about one result in the search then they can select another. UI beans that need this behavior are often annotated as Session scope. This means that information needed across HTTP requests are stored in the user’s HTTP Session. This session object is maintained on the server and normally has an expiration of 30 minutes. Normally, each time a user opens a new browser instance, they get a new session. However, most browsers use the same session when a user just opens another tab in the same web browser.
The problem with reusing session across two browser tabs can be illustrated with a simple example. Suppose a user opens a browser, enters the URL for an application and then specifies three pieces of information for search criteria and gets back 24 rows of results. The user then selects one of the rows to see the details of that item. The same user opens another tab, navigates to the same application and does a new search this time with different criteria that brings back 3 results. What happens when the user goes back to the first tab and returns to the search results? If the application uses HTTP session to store the search results, then the search results from the first tab have been overridden by the second tab’s search. Rather than seeing the original search’s 24 results in the first tab, the user will see the 3 results from the second tab’s search.
This blog post describes a common approach to both preventing multi-tab usage, as well as how to support it.
DETECTING MULTI-TAB USAGE
The first step in handling a problem is detecting when the situation that causes the problem occurs. Application servers are designed to know when to create a new HTTP session and to pass a unique ID back to the web browser so that subsequent requests from that instance of that browser can be re-associated to that session. However, normal browser window processing does not give the server application any indication that there are two separate browser windows trying to use the same session. As far as the application server is concerned, the requests from two different tabs within the same browser will appear as the same user, just making multiple requests from the same window.
A common approach to overcoming this limitation is for the application to create an ID (let’s call it a window ID) and pass it to the browser. The trick is how to get the browser to return that ID on all subsequent requests from that tab but not on requests from new tabs. The answer to this problem is in understanding that a subsequent related request should only happen from URLs that rendered on the initial response. In other words, the only way to make a subsequent related request to this response is for this response to give you a button or a link for you to press. All buttons and links (as well as AJAX calls) in HTML have to specify a URL to which the user is directed when the action occurs. With this in mind, the answer is simply to provide the window ID as a URL query string parameter on every URL which will be seen by the application as an HTTP request parameter.
For example, the search button in myApp may have a URL on the buttons form that was “/myApp/search.jsf”. Rendering that URL as “/myapp/search.jsf?windowID=1” will give the server application a request parameter of “windowID” that is equal to “1”. Seeing this window ID on a subsequent request lets the application know that this request is associated with a window from which a request to this application was made earlier. If the user opens a new tab and navigates to the application without specifying a window ID, as is normal, then the application knows a brand new request is coming in.
At this point the application can easily distinguish requests from different windows and, if this scenario is not supported, it can simply return an error page with a message stating that multiple windows is not supported. Of course this also assumes that that the there is an initial request, such as login, that initiates the window ID and that subsequent requests to login can be recognized as such so that requests sent without a window ID can be returned as errors.
SUPPORTING MULTI-TAB USAGE
Once your application can distinguish between results from different tabs, the next problem is how to store information in HTTP session without corrupting information from one window with information from another window with information. The answer is that each window needs its own partition of session space.
To attack that problem, you first need to understand that for the purposes of this topic, HTTP session is really just a java.util.Map. It allows the application to store a piece of information using a unique key. For instance, a singleton UI -bean such as a user search bean is typically stored in session with a key equal to its bean name, e.g. userSearchBean. This is the crux of our problem. If the UserSearchBean is session scope, then both windows will access the same bean as they are both sharing session. The fix to our problem is to have a separate map per window id. These per-window maps are then kept in session. Each request for an item out of session, assuming the application knows the “current” window ID, is then just two mapping look-ups instead of one.
IMPLEMENTATION IN SPRING 3.0.5
So far, this discussion has been theoretical with no discussion of how to add a query parameter to every URL or how to intercept requests for session. A quick search of the internet will show that there are two semi-common implementations of this.
The first is in an open source framework called Orchestra. Its documentation of this issue and the solution is superior to all others. See this link for instance. However, as is normally the case with frameworks, this is not its main problem space so incorporating Orchestra brings you a lot more functionality and overhead.
Since the Spring Framework also has a solution for this, if you are already using Spring, this is the preferred approach. However, if your application is limited in the version of frameworks supported and you are still on Spring 3.0.5, then you will be unable to use the built-in ConversationScope that is available in version 3.1. Therefore “reuse” of future framework code is required. The following design is not fool-proof but will suffice until Spring 3.1 can replace it.
The first design requirement is that every request must have a window ID. There are two scenarios to cover; either this must be the first request and a new window ID is needed or this must be a subsequent request from a previously established window. Which scenario we process is determined once for each request by looking for a prescribed request parameter that is holding the window ID. There are three distinct responsibilities here that must be implemented:
- Detecting a request parameter for each request can be handled with a Servlet Filter. For Spring 3.0.5, a subclass of the GenericFilterBean, WindowIdFilter, can be used to easily check the request parameters for the existence of the window ID.
- Establishing a new, unique ID for a new window can be handled using an java.util.concurrent.atomic.AtomicLong that serves up the next long available when explicitly requested. Encapsulating this value is the first responsibility of a new WindowIdManager class.
- Holding onto the window ID for this request and making it available without having to pass it through every filter and context can be handled by using the WindowIdManager class that has a static TheadLocal field for holding onto the ID for this request’s window.
The WindowIdFilter and WindowIdManager classes work together to gather and keep the window ID for the request. Putting the ID on subsequent requests is handled via URL ENCODING.
The real magic of this solution is how to ensure that each subsequent related request passes in the appropriate window ID. To understand how this is possible requires you to understand that every HTML renderer should always encode any URL that is returned on a response. The task of URL encoding is needed to prevent any text in the URL from confusing the browser when rendering the HTML page. The conventional way to handle URL encoding is for the renderer to delegate the task to the HTTP Response object passed into the servlet and/or filter to ensure a consistent encoding happen regardless of which renderer is rendering the response.
This design depends on this approach by providing a response wrapper that intercepts all encoding requests. The wrapper class, WindowIdResponseWrapper, checks each URL passed in to determine if a query string is already in effect. If so, an ampersand is appended to the URL; otherwise a question mark is appended. In both cases, the window ID parm name is appended along with an equals sign and the actual window ID value.
To activate this wrapper, the aforementioned WindowIdFilter constructs the wrapper using the actual response object as an argument and then supplies that to downstream filters. This is a common scheme used to override servlet behavior. The final piece of the puzzle is to ensure that any information that would have been stored in Session Scope is instead related to a window.
The scope of a bean in Spring is determined by the @Scope annotation. Normal scopes are Request, Session, or Prototype. Spring gives the ability to provide a custom scope also. The class used to implement the custom scope is called each time Spring needs to resolve a bean that is annotated with that scope. For our situation, we want to take most of the beans that were annotated as Session scope and change them to Window scope in order to have them behave correctly when using multiple tabs. You must also introduce the new scope in the Spring configuration as follows:
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer"> <property name="scopes"> <map> <entry key="window"> <bean class="org.myorg.WindowIdScope" /> </entry> </map> </property> </bean>
The WindowIdScope class is called each time a bean with Window scope is referenced to resolve the one instance of that bean. This scope class depends on the WindowIdManager class to return the window id from its value stored in a ThreadLocal. That value is used to determine which HashMap in Session should contain the actual instance of the bean.
The whole process fits together as follows:
The user opens a browser and hits the initial URL for the application.
The WindowIdFilter first checks the request parameters for a window ID; if one is not found, the WindowIdManager, is asked to create one. If one is found it is set on the WindowIdManager and stored in its ThreadLocal.
The WindowIdFilter then wrappers the HTTP Response and passes it into the downstream servlet filters.
The normal application functionality is invoked changing the state of beans. Each reference to a bean with window scope is resolved by calling the WindowIdScope to look for the map in session corresponding to the appropriate window ID and getting the bean from there.
Once rendering is begun on the response, every URL is encoded using the wrappered Response object which adds the window ID as a query string.
While this approach is used in various places, it is not without its flaws. Three of these limitations are listed below:
- Any link containing the window id that is opened in a new window will fool the application into thinking that requests from the new window are still part of the original window which will cause you to be right back to where you started with session info being shared across windows. This is the most serious of the flaws. One approach for overcoming this is to use buttons styled as links so that the browser won’t offer “open in new tab/window” on its context menu.
- An unavoidable nuisance is that every URL that displays in the URL Address field of the browser or that is shown in the window footer when you hover over it will show the window ID value.
- If a user bookmarks a page, the window ID will be included in the bookmark.
This design provides an inexpensive way to achieve a partitioned session approach prior to the official, full-blown approach found in Spring 3.1.0. There is a relatively small amount of code needed to implement this design, but also all pages of the application are impacted so any side effects from this design could appear anywhere in your application.
Regardless, the avoidance of any solution would yield, at best, unpredictable results. In this case, the risks are outweighed by the pervasiveness of the solution and the maintainability of the design. Also, since the ability to use Spring 3.1 is fairly close, this solution does not need to live for long.
– Keith Shakib, firstname.lastname@example.org