Session Scoped Beans - Initial Design

Work Item:

type key summary assignee reporter priority status resolution created updated due

Unable to locate Jira server for this macro. It may be due to Application Link configuration.

Problem

uPortal 3 has a set of objects whose instances are scoped to the HttpSessions of particular portal users. The desire is to have these objects scoped transparently to the session so they may be used like any other spring bean in the code. That is, they will appear to be singleton beans from the perspective of code dependent upon the bean, but the object instance handling any particular call will be the appropriate instance for the portal user being served.
Note: in practice, this approach should be applied to spring subfactories, and not to a multitude of the individual beans. For instance, entire uP2 portal context is a session-scoped entity. It's described by a single factory (/uP2_context). When uP2_context factory is a session-scoped bean, other parts of spring config (such as portlet bean configuration) should be able to reference beans within uP2_context using standard syntax (i.e. <bean ref="../uP2_context/portletWindowManager"/>).

Solution

The solution has to deal with the following issues:

  1. The HttpSession may not be available via any available API at many points in the code.
  2. ThreadLocals require extra care as a thread pool is used which can disrupt their functionality.

Thread Local Servlet Filter

A filter like the following will be added as the first filter in the chain for all requests to the uPortal 3 web app context.

  1. A modification to this class to wrap the HttpSession in a WeakReference before putting it in the ThreadLocal. This would ensure no memory is leaked on the off chance that a ThreadLocal isn't cleaned appropriate.

Updated with actual code being used in uP3

SessionLocalBindingFilter.java
/* 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
 */

package org.jasig.portal.core;

import java.io.IOException;
import java.lang.ref.WeakReference;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

/**
 * This filter should be the first filter in the chain for this context.<br><br>
 * 
 * It looks for a HttpServletRequest to get a HttpSession from. If found it is put in
 * a ThreadLocal accessible via the static {@link #getSession()} method. A finally block
 * in after the request is passed up the chain is used to remove the session from the 
 * ThreadLocal.<br><br>
 * 
 * The HttpSession is also wrapped in a WeakReference. This ensures that a failure to remove
 * the session from the ThreadLocal for some reason won't result in a memory leak.
 * 
 * @author Eric Dalquist <a href="mailto:edalquist@unicon.net">edalquist@unicon.net</a>
 * @version $Revision$
 */
public class SessionLocalBindingFilter implements Filter {
    private static final ThreadLocal SESSION_LOCAL = new ThreadLocal();

    /**
     * Retrieves the HttpSession stored for the thread calling this method.
     * 
     * @return The HttpSession stored for the thread calling this method.
     */
    public static HttpSession getSession() {
        final WeakReference ref = (WeakReference)SESSION_LOCAL.get();
        if (ref != null) {
            return (HttpSession)ref.get();
        }
        else {
            return null;
        }
    }

    /**
     * Sets the HttpSession for this thread. Only accessible by package classes for security.
     * 
     * @param session The HttpSession for this thread.
     */
    static void setSession(HttpSession session) {
        if (session != null) {
            SESSION_LOCAL.set(new WeakReference(session));
        }
        else {
            SESSION_LOCAL.set(null);
        }
    }

    /**
     * @see javax.servlet.Filter#doFilter(javax.servlet.ServletRequest, javax.servlet.ServletResponse, javax.servlet.FilterChain)
     */
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        try {
            //Only HttpServletRequests have sessions
            if (request instanceof HttpServletRequest) {
                //Cast the request
                final HttpServletRequest httpRequest = (HttpServletRequest)request;
                //Get the session (creating it if it doesn't exist)
                final HttpSession session = httpRequest.getSession();
                //Bind the session to this thread
                setSession(session);
            }
            else {
                //No session, clear the thread local
                setSession(null);
            }

            //Pass the request up the chain
            chain.doFilter(request, response);
        }
        finally {
            //Clear the thread local after the request is processed
            setSession(null);
        }
    }

    /**
     * @see javax.servlet.Filter#init(javax.servlet.FilterConfig)
     */
    public void init(FilterConfig filterConfig) throws ServletException {
    }

    /**
     * @see javax.servlet.Filter#destroy()
     */
    public void destroy() {
    }
}

Custom Spring TargetSource

This custom TargetSource implementation looks for the instance of a bean in the HttpSession stored in the ThreadLocal by SessionLocalTargetSource. This combination ensures that no matter where the proxied bean is called from the TargetSource can get the appropriate instance.

SessionLocalTargetSource.java
/* 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
 */

package org.jasig.portal.spring;

import javax.servlet.http.HttpSession;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.portal.core.SessionLocalBindingFilter;
import org.springframework.aop.target.AbstractPrototypeBasedTargetSource;

/**
 * This target source is to be used in collaberation with the {@link SessionLocalBindingFilter}.
 * The target source binds the target bean to the HttpSession retrieved from 
 * {@link SessionLocalBindingFilter#getSession()}. 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>
 * @version $Revision$
 */
public class SessionLocalTargetSource extends AbstractPrototypeBasedTargetSource {

    private final static Log LOG = LogFactory.getLog(SessionLocalTargetSource.class);

    private String sessionKey = null;

    /**
     * @return Returns the sessionKey.
     */
    public String getSessionKey() {
        return this.sessionKey;
    }

    /**
     * @param sessionKey The sessionKey to set.
     */
    public void setSessionKey(String sessionKey) {
        this.sessionKey = sessionKey;
    }

    /**
     * @see org.springframework.aop.TargetSource#getTarget()
     */
    public Object getTarget() throws Exception {
        final HttpSession session = SessionLocalBindingFilter.getSession();
        if (session == null) {
            LOG.warn("No HttpSession found for thread '" + Thread.currentThread().getName()
                    + "'. Creating new prototype bean instance of '" + this.getTargetBeanName()
                    + "'.");
            return this.newPrototypeInstance();
        }
        else {
            final String beanKey = this.getClass().getName() + "_"
                    + (this.sessionKey != null ? this.sessionKey : this.getTargetBeanName());

            Object instance = session.getAttribute(beanKey);
            if (instance == null) {
                instance = this.newPrototypeInstance();
                session.setAttribute(beanKey, instance);

                if (LOG.isDebugEnabled()) {
                    LOG.debug("Created instance of '" + this.getTargetBeanName()
                            + "', bound to HttpSession for '" + Thread.currentThread().getName()
                            + "' using key '" + beanKey + "'.");
                }
            }
            else if (LOG.isDebugEnabled()) {
                LOG.debug("Found instance of '" + this.getTargetBeanName()
                        + "' bean in HttpSession for '" + Thread.currentThread().getName()
                        + "' using key '" + beanKey + "'.");
            }

            return instance;
        }
    }
}

Here is an example on how to configure this class:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN//EN" 
    "http://www.springframework.org/dtd/spring-beans.dtd">

<beans default-lazy-init="true">
    <bean id="safeBean" class="beans.ThreadSafeBean" singleton="true">
        <property name="sessionSafeBean">
            <ref bean="sessionBean"/>
        </property>
    </bean>
    
    <bean id="sessionBean" class="org.springframework.aop.framework.ProxyFactoryBean">
        <property name="targetSource" ref="sessionBeanTargetSource"/>
        <property name="proxyInterfaces">
            <value>beans.IdentifiableBean</value>
        </property>
    </bean>
    
    <bean id="sessionBeanTargetSource" class="spring.SessionLocalTargetSource">
        <property name="targetBeanName">
            <value>sessionBeanTarget</value>
        </property>
    </bean>
    
    <bean id="sessionBeanTarget" class="beans.SessionSafeBean" singleton="false">
    </bean>
</beans>  

Thread Pool Wrapper

uPortal 3 is currently using the backport-util-conncurrent threading library. Specifically the ExecutorService is being used to delegate work to a thread pool. The following wrapper can be used around the desired ExecutorService implementation to ensure the HttpSession object placed in the main request thread's ThreadLocal is copied into the pooled thread's ThreadLocal and then cleaned up when the execution is complete.

ThreadLocalDelegatingExecutorServiceWrapper.java
public class ThreadLocalDelegatingExecutorServiceWrapper 
    extends AbstractExecutorService {
    private final ExecutorService service;
    
    public ThreadLocalDelegatingExecutorServiceWrapper(ExecutorService service) {
        this.service = service;
    }

    public boolean awaitTermination(long arg0, TimeUnit arg1) throws InterruptedException {
        return this.service.awaitTermination(arg0, arg1);
    }

    public boolean isShutdown() {
        return this.service.isShutdown();
    }

    public boolean isTerminated() {
        return this.service.isTerminated();
    }

    public void shutdown() {
        this.service.shutdown();
    }

    public List shutdownNow() {
        return this.service.shutdownNow();
    }

    public void execute(Runnable arg0) {
        this.service.execute(new ThreadLocalMovingRunnableWrapper(arg0));
    }
    
    private class ThreadLocalMovingRunnableWrapper implements Runnable {
        private final Runnable delegate;
        private HttpSession local;
        
        public ThreadLocalMovingRunnableWrapper(Runnable delegate) {
            this.delegate = delegate;
            this.local = SessionLocalBindingFilter.getSession();
        }

        public void run() {
            try {
                SessionLocalBindingFilter.setSession(this.local);
                this.delegate.run();
            }
            finally {
                SessionLocalBindingFilter.setSession(null);
            }
        }
    }
}