Can't get session scoped bean on session timeout

Caio Cunha picture Caio Cunha · Aug 15, 2012 · Viewed 10.4k times · Source

I need to get a proxied session-scoped bean on the HttpSessionListener.sessionDestroyed(). The objective is doing a session cleanup when it gets destroyed (either by invalidate() or timeout). I added the ContextLoaderListener to expose the context and got the bean through WebApplicationContextUtils.getWebApplicationContext().

Everything works fine if I invalidate the session myself in a Servlet, but when session times out I get an Scope 'session' is not active for the current thread;. I understand that the problem happens for the cleanup is being done by a internal thread of the Servlet engine, but I still need to be able to get this bean from a HttpSessionListener.

I've seem many of the same question around, no one got a solution, this is why I'm asking again.

My applicationContext.xml has no bean declaration, as I'm using annotations.

This the bean I need to access when session times out:

@Component
@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public class Access {

    static private int SERIAL = 0;
private int serial;

    public Access() {
            serial = SERIAL++;
    }

    public int getSerial() {
            return serial;
    }
}

This is the controller that either create or destroy the session manually.

@Controller
public class Handler {

    @Autowired
    Access access;


    @RequestMapping("/create")
    public @ResponseBody String create() {
        return "Created "+access.getSerial();
    }

    @RequestMapping("/destroy")
    public @ResponseBody String destroy(HttpSession sess) {
        int val = access.getSerial();
        sess.invalidate();
        return "Destroyed "+val;
    }

}

And this is the HttpSessionListener that listen to the session destruction, where I need to access the the contents of the Access session scoped bean.

public class SessionCleanup implements HttpSessionListener {

@Override
public void sessionDestroyed(HttpSessionEvent ev) {

    // Get context exposed at ContextLoaderListener
    WebApplicationContext ctx = WebApplicationContextUtils
        .getWebApplicationContext(ev.getSession().getServletContext());

    // Get the beans
    Access v = (Access) ctx.getBean("access");

    // prints a not-null object
    System.out.println(v);

    // this line raise the exception
    System.out.println(v.getSerial());

    }


    @Override
    public void sessionCreated(HttpSessionEvent ev) {/*Nothing to do*/}

}

The exception below is raised at v.getSerial().

Ago 14, 2012 11:44:58 PM org.apache.catalina.session.StandardSession expire
SEVERE: Session event listener threw exception
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.access': Scope 'session' is not active for the current thread; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton; nested exception is java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:342)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:193)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:33)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.getTarget(Cglib2AopProxy.java:654)
    at org.springframework.aop.framework.Cglib2AopProxy$DynamicAdvisedInterceptor.intercept(Cglib2AopProxy.java:605)
    at model.Access$$EnhancerByCGLIB$$438f41a5.toString(<generated>)
    at java.lang.String.valueOf(String.java:2902)
    at java.io.PrintStream.println(PrintStream.java:821)
    at org.apache.tomcat.util.log.SystemLogHandler.println(SystemLogHandler.java:242)
    at service.SessionCleanup.sessionDestroyed(SessionCleanup.java:24)
    at org.apache.catalina.session.StandardSession.expire(StandardSession.java:709)
    at org.apache.catalina.session.StandardSession.isValid(StandardSession.java:576)
    at org.apache.catalina.session.ManagerBase.processExpires(ManagerBase.java:712)
    at org.apache.catalina.session.ManagerBase.backgroundProcess(ManagerBase.java:697)
    at org.apache.catalina.core.ContainerBase.backgroundProcess(ContainerBase.java:1364)
    at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1649)
    at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1658)
    at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.processChildren(ContainerBase.java:1658)
    at org.apache.catalina.core.ContainerBase$ContainerBackgroundProcessor.run(ContainerBase.java:1638)
    at java.lang.Thread.run(Thread.java:722)
Caused by: java.lang.IllegalStateException: No thread-bound request found: Are you referring to request attributes outside of an actual web request, or processing a request outside of the originally receiving thread? If you are actually operating within a web request and still receive this message, your code is probably running outside of DispatcherServlet/DispatcherPortlet: In this case, use RequestContextListener or RequestContextFilter to expose the current request.
    at org.springframework.web.context.request.RequestContextHolder.currentRequestAttributes(RequestContextHolder.java:131)
    at org.springframework.web.context.request.SessionScope.get(SessionScope.java:90)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:328)
    ... 19 more

Finally, here is my web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app 
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
    xmlns="http://java.sun.com/xml/ns/javaee" 
    xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
    xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" 
    id="WebApp_ID" version="2.5">

  <display-name>session-listener-cleanup</display-name>


    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/spring-config.xml</param-value>
    </context-param>


    <listener>
      <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>

    <listener>
      <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>


    <listener>
      <listener-class>service.SessionCleanup</listener-class>
    </listener>


    <servlet>
        <servlet-name>appServlet</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>/WEB-INF/spring-config.xml</param-value>
        </init-param>
        <load-on-startup>1</load-on-startup>
    </servlet>

    <servlet-mapping>
        <servlet-name>appServlet</servlet-name>
        <url-pattern>/*</url-pattern>
    </servlet-mapping>


    <session-config>
      <session-timeout>1</session-timeout>
    </session-config>


</web-app>

As I've said, everything goes nice when I invalidate the session at the controller's method destroy.

UPDATE 1: POSSIBLE SOLUTIONS FOUND

The problem happens because a request is needed in order to Spring access session beans. Event though we have a Context associated to the Thread, there is no request.

There are some possible choices here:

  1. Implement the interface DisposableBean, as suggested by alexwen. This would imply in moving business logic to the model object [here];
  2. Implement the DestructionAwareBeanPostProcessor also suggested by alexwen. This one would mean that you would need to check if the bean being disposed is a Access or not before doing any disposal [here];
  3. Retrieve the bean directly from the session. This way is not a very good one, as you use an undocumented behavior to achieve the result, but does work [here];
  4. Mock a servlet request and bind it's attributes to the Thread through RequestContextHolder. This also leads to undocumented behavior, able of being changed in future releases [here];

I didn't chose the last two, for they're not documented. Also, I didn't like the idea of scavenging every bean after a specific one. As I also don't want to mix the business logic into my model beans, I ended up by creating a @Service that creates the bean and also have a destroy method.

This method is responsible by the disposal of the access bean. I implemented the DisposableBean interface on the Access and injected the AccessManager service to the Access bean and call the service destroy method. The service look like this:

@Service
public class AccessManager {

    @Bean(name="access", destroyMethod="destroy")
    @Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
    @Autowired
    public Access create(HttpServletRequest request) {
        // creation logic
    }


    public void destroy(Access access) {
        // disposal logic
    }

}

Answer

alexwen picture alexwen · Aug 15, 2012

If you implement the interface, DisposableBean on your Access class Spring will invoke the method "destroy" when the Session is destroyed.

Alternatively, you can register with Spring a DestructionAwareBeanPostProcessor which will have the opportunity to process all beans in the Session scope when the scope is destroyed. Docs and instructions here.

If you are curious as to how Spring does all this I would encourage you to look at the DisposableBeanAdapter which Spring registers as a HttpSessionBindingListener with the Session.