Motivation
Spring currently does not support session scoped beans/components out of the box. You can decide between singleton or prototype lifecycle, but you cannot have your beans bound to the session lifecycle of web applications. There are plans for integrating such a feature in the Spring 2.0 release.
We will try to point out some possible workarounds for your WebWork based applications. First we look at the general solutions found among the Spring community, dealing with HTTPSession and all that. After that we will discuss the special conditions and requirements found in XWork/WebWork and how that might affect possible solutions. We will show some XWork/WebWork specific solutions for the given problem.
Info |
---|
The first milestone of Spring 2.0 (formerly 1.3) will be released the second week of December 2005. It is confirmed that it does contain Session Scope components using the Proxy (CGLIB or JDK) approach. |
General Solutions for Webapplications
The Spring 2.0 Way
Interface21 added support for session (and request) scoped beans in Spring 2.0. This approach creates a CGLIB or JDK Dynamic proxy of the session scoped bean using the org.springframework.aop.target.scope.ScopedProxyFactoryBean and setting the scopeMap to org.springframework.web.context.scope.SessionScopeMap.
Since the jars are backwards compatible simply build Spring and replace the jars shipped with WebWork. (Spring 2.0 M1 should be out by the time you read this.).
There are 2 ways to set this up depending upon whether or not XML simplification is used. The first method uses the traditional bean definitions and is useful to understand what is happening under the covers.
A modified applicationContext.xml for the shopping cart example using the traditional XML DTD is below.
Code Block | ||||
---|---|---|---|---|
| ||||
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans> <bean id="shoppingCart" class="org.springframework.aop.target.scope.ScopedProxyFactoryBean" singleton="false"> <property name="scopeKey" value="shoppingCart"/> <property name="targetBeanName" value="__shoppingCart"/> <property name="scopeMap"> <bean class="org.springframework.web.context.scope.SessionScopeMap"/> </property> </bean> <bean id="__shoppingCart" class="com.opensymphony.webwork.example.ajax.cart.DefaultCart" singleton="false"/> </beans> |
A modified applicationContext.xml for the shopping cart example using the XML simplification is below.
Code Block | ||||
---|---|---|---|---|
| ||||
<?xml version="1.0" encoding="UTF-8"?> <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.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <bean id="catalog" class="com.opensymphony.webwork.example.ajax.catalog.TestCatalog" singleton="true"/> <bean id="shoppingCart" class="com.opensymphony.webwork.example.ajax.cart.DefaultCart" singleton="true"> <aop:scope type="session"/> </bean> </beans> |
You will also need to modify the web.xml to include the following filter.
Code Block | ||||
---|---|---|---|---|
| ||||
<filter> <filter-name>springFilter</filter-name> <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class> </filter> <filter-mapping> <filter-name>springFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> |
Custom TargetSource with a ServletFilter
A quite "clean" solution for web applications in general can be found at JA-SIG. The solution is well documented and can be found here. Below you will find a WebWork adoption of this solution.
XWork/WebWork specific solutions
Preface
WebWork is based on XWork, and XWork is not tied to the web layer. So when dealing with session scoped objects, WW users might want to use XWork's session abstraction features to keep their application independent from the web context. This is why we will discuss some XW/WW specific solutions below.
Custom TargetSource, the WebWork way
Here is a modified version of the TargetSource solution pointed out above that integrates with the existing WebWork session and doesn't require an additional filter or listener. Usage is pretty much the same, create an interface for your object and make sure that you always use that interface and not the underlying implementation or autowiring will fail. You can find more information on how to make this work by looking at the WebWorkTargetSource Shopping Cart Example.
Code Block | ||||
---|---|---|---|---|
| ||||
package org.tuxbot.webwork.spring; /* Portions Copyright 2005 The JA-SIG Collaborative. All rights reserved. * See license distributed with this file and * available online at http://www.uportal.org/license.html */ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.aop.target.AbstractPrototypeBasedTargetSource; import org.springframework.beans.factory.DisposableBean; import com.opensymphony.xwork.ActionContext; import java.util.Map; /** * This target source is to be used in collaberation with WebWork. * The target source binds the target bean to the Session retrieved from * WebWork. By default the bean is bound to the session * using the name of the target bean as part of the key. This can be overridden by setting * the <code>sessionKey</code> property to a not null value. * * @author Eric Dalquist <a href="mailto:edalquist@unicon.net">edalquist@unicon.net</a> * @author Eric Molitor <a href="mailto:eric@tuxbot.com">eric@tuxbot.com</a> * @version 1.0 */ public class WebWorkTargetSource extends AbstractPrototypeBasedTargetSource implements DisposableBean { private final static Log LOG = LogFactory.getLog(WebWorkTargetSource.class); private transient Object noSessionInstance = null; private String sessionKey = null; private String compiledSessionKey = null; public WebWorkTargetSource() { this.updateBeanKey(); } /** * @return Returns the sessionKey. */ public String getSessionKey() { return this.sessionKey; } /** * @param sessionKey The sessionKey to set. */ public void setSessionKey(String sessionKey) { this.sessionKey = sessionKey; this.updateBeanKey(); } /** * @see org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource#setTargetBeanName(java.lang.String) */ public void setTargetBeanName(String targetBeanName) { super.setTargetBeanName(targetBeanName); this.updateBeanKey(); } /** * @see org.springframework.aop.TargetSource#getTarget() */ public Object getTarget() throws Exception { final Map session = ActionContext.getContext().getSession(); if (session == null) { LOG.warn("No Session found for thread '" + Thread.currentThread().getName() + "'"); if (this.noSessionInstance == null) { this.noSessionInstance = this.newPrototypeInstance(); if (LOG.isDebugEnabled()) { LOG.debug("Created instance of '" + this.getTargetBeanName() + "', not bound to any webWorkSession."); } } else { if (LOG.isDebugEnabled()) { LOG.debug("Found instance of '" + this.getTargetBeanName() + "', not bound to any webWorkSession."); } } return this.noSessionInstance; } else { String beanKey = this.compiledSessionKey; Object instance = session.get(beanKey); if (instance == null) { instance = this.newPrototypeInstance(); session.put(beanKey, instance); if (LOG.isDebugEnabled()) { LOG.debug("Created instance of '" + this.getTargetBeanName() + "', bound to webWorkSession for '" + Thread.currentThread().getName() + "' using key '" + beanKey + "'."); } } else if (LOG.isDebugEnabled()) { LOG.debug("Found instance of '" + this.getTargetBeanName() + "', bound to webWorkSession for '" + Thread.currentThread().getName() + "' using key '" + beanKey + "'."); } return instance; } } /** * @see org.springframework.beans.factory.DisposableBean#destroy() */ public void destroy() throws Exception { if (this.noSessionInstance != null && this.noSessionInstance instanceof DisposableBean) { if (LOG.isDebugEnabled()) { LOG.debug("Destroying sessionless bean instance '" + this.noSessionInstance + "'"); } ((DisposableBean)this.noSessionInstance).destroy(); } } /** * Generates the key to store the bean in the session with. */ private void updateBeanKey() { if (this.sessionKey == null) { final StringBuffer buff = new StringBuffer(); buff.append(this.getClass().getName()); buff.append("_"); buff.append(this.getTargetBeanName()); this.compiledSessionKey = buff.toString(); } else { this.compiledSessionKey = this.sessionKey; } } } |
Customized ApplicationContext Implementation
TODO: Document
Customized WW/XW ObjectFactory
TODO: Document
Session backed Bean Factory
The idea is to simply create a retrieve-or-create bean factory:
Code Block | ||||
---|---|---|---|---|
| ||||
package net.itneering.core.spring.session; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import com.opensymphony.xwork.ActionContext; import java.util.Map; import java.io.Serializable; /** * SessionBackedBeanFactory tries to lookup beans by name in XWork session. If not found, * it tries to instantiate new bean and attaches it to said session. * * @author <a href="mailto:gielen@it-neering.net">Rene Gielen</a> */ public class SessionBackedBeanFactory implements Serializable, BeanFactoryAware { BeanFactory beanFactory = null; /** * Find a component by name in session scoped storage implementation. If not found, try to instantiate new one by * {@link org.springframework.beans.factory.BeanFactory#getBean(String)}. Then found component will be attached * to session store implementation. * * @param componentName * @return The requested component, if found. */ public Object getSessionComponent( String componentName ) { Object result = getSession().get(componentName); if ( result == null ) { result = beanFactory.getBean(componentName); storeComponent(componentName, result); } return result; } public void storeComponent(String componentName, Object component ) { getSession().put(componentName, component); } /** * Actual implementation of the session scoped storage Map. * Lookup {@link com.opensymphony.xwork.ActionContext#getSession()}. * * @return The Map for keeping session objects. */ public Map getSession() { return ActionContext.getContext().getSession(); } /** * Callback that supplies the owning factory to a bean instance. * <p>Invoked after population of normal bean properties but before an init * callback like InitializingBean's afterPropertiesSet or a custom init-method. * * @param beanFactory owning BeanFactory (may not be null). * The bean can immediately call methods on the factory. * * @throws org.springframework.beans.BeansException * in case of initialization errors * @see org.springframework.beans.factory.BeanInitializationException */ public void setBeanFactory( BeanFactory beanFactory ) throws BeansException { this.beanFactory = beanFactory; } } |
Example applicationContext setup (note that the session scoped bean has to be setup with singleton="false"):
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans default-autowire="byName"> <bean id="sessionBeanProxy" class="net.itneering.core.spring.session.SessionBackedBeanFactory" singleton="true"/> <bean id="securityContextComponent" class="net.itneering.security.component.DefaultSecurityContextComponent" singleton="false" /> </beans> |
Example action use:
Code Block | ||||
---|---|---|---|---|
| ||||
package net.itneering.xwork.action; import com.opensymphony.xwork.ActionSupport; import net.itneering.core.spring.session.SessionBackedBeanFactory; import net.itneering.security.component.DefaultSecurityContextComponent; /** * Simple sessionBeanProxy aware action. * * @author <a href="mailto:gielen@it-neering.net">Rene Gielen</a> * @version $Revision: 1.1 $ */ public class SecurityAwareAction extends ActionSupport implements PrincipalAware { private static final Logger log = Logger.getLogger(SecurityAwareAction.class); protected SessionBackedBeanFactory sessionBeanProxy; /** * For Spring wiring usage. * * @param sessionBeanProxy The sessionBeanProxyto use. */ public void setSessionBeanProxy( SessionBackedBeanFactory sessionBeanProxy ) { this.sessionBeanProxy = sessionBeanProxy; } /** * Getter for actions security context. * * @return The securityContextComponent set by IoC. */ public SecurityContextComponent getSecurityContextComponent() { return sessionBeanProxy!= null ? sessionBeanProxy.getSessionComponent("securityContextComponent") : null; } /** * Get the current User Principal for this session. * * @return The User Principal. */ public UserEntity getPrincipal() { try { return getSecurityContextComponent().getPrincipal(); } catch ( NullPointerException e ) { return null; } } ... } |
For well known session scoped components, you might get more convenience by subclassing SessionBackedBeanFactory:
Code Block | ||||
---|---|---|---|---|
| ||||
package net.itneering.security.component; import net.itneering.core.spring.session.SessionBackedBeanFactory; /** * SecurityAwareSessionBeanProxy. * * @author <a href="mailto:gielen@it-neering.net">Rene Gielen</a> */ public class SecurityAwareSessionBeanProxy extends SessionBackedBeanFactory { String securityContextComponentName = "securityContextComponent"; /** * Make component name configurable by spring setup */ public void setSecurityContextComponentName( String securityContextComponentName ) { this.securityContextComponentName = securityContextComponentName; } public SecurityContextComponent getSecurityContextComponent() { return (SecurityContextComponent) getSessionComponent(securityContextComponentName); } } |
Again example applicationContext setup:
Code Block | ||||||
---|---|---|---|---|---|---|
| ||||||
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" "http://www.springframework.org/dtd/spring-beans.dtd"> <beans default-autowire="byName"> <bean id="mySecurityContextComponent" class="net.itneering.security.component.DefaultSecurityContextComponent" singleton="false" /> <bean id="sessionBeanProxy" class="net.itneering.security.component.SecurityAwareSessionBeanProxy" singleton="true"> <property name="securityContextComponentName" value="mySecurityContextComponent" /> </bean> </beans> |
Example action use:
Code Block | ||||
---|---|---|---|---|
| ||||
package net.itneering.xwork.action; import com.opensymphony.xwork.ActionSupport; import net.itneering.security.component.SecurityAwareSessionBeanProxy; import net.itneering.security.component.DefaultSecurityContextComponent; /** * Simple sessionBeanProxy aware action. * * @author <a href="mailto:gielen@it-neering.net">Rene Gielen</a> * @version $Revision: 1.1 $ */ public class SecurityAwareAction extends ActionSupport implements PrincipalAware { private static final Logger log = Logger.getLogger(SecurityAwareAction.class); protected SecurityAwareSessionBeanProxy sessionBeanProxy; /** * For Spring wiring usage. * * @param sessionBeanProxy The sessionBeanProxy to use. */ public void setSessionBeanProxy( SecurityAwareSessionBeanProxy sessionBeanProxy ) { this.sessionBeanProxy = sessionBeanProxy; } /** * Get the current User Principal for this session. * * @return The User Principal. */ public UserEntity getPrincipal() { try { return sessionBeanProxy.getSecurityContextComponent().getPrincipal(); } catch ( NullPointerException e ) { return null; } } ... } |
As said, the solution is very simple. You will get no ties to web layer, and the configuration is really simple, there is no need for proxy definitions in applicationContext.xml etc.
The main disadvantage is that you will not be able to wire session scoped beans directly into your actions, you will have to use the indirection via the session backed bean factory. And, as always when dealing with XWork session abstraction, you have to take care for a action context to be setup.
Auto proxied Session backed Component Factory
Does anyone have an implementation of this? (Eric Molitor)
The intention was a bit different for this one, so I tried to clarify headings. Nice idea, though ... (Rene Gielen).
The theory here is to create a custom Pointcut class that utilizes the ComponentConfiguration retrieved from the DefaultComponentManager (which loads the Component list from components.xml). The getClassFilter() matches anything that implements one of the Components in the ComponentConfiguration. The Pointcut is then registered as an advisor for all beans (AutoProxy via Springs DefaultAdvisorAutoProxyCreator). The Advice implementation looks at which Component is implmented and fetches the apporiate value out of the Session and calls the Components setter method.
TODO: Document, create example