Не удается получить bean-компонент с областью сеанса по тайм-ауту сеанса

Мне нужно получить проксируемый bean-компонент с областью действия сеанса на HttpSessionListener.sessionDestroyed(). Цель состоит в том, чтобы очистить сеанс, когда он будет уничтожен (либо по invalidate(), либо по тайм-ауту). Я добавил ContextLoaderListener, чтобы раскрыть контекст, и получил bean-компонент через WebApplicationContextUtils.getWebApplicationContext().

Все работает нормально, если я сам аннулирую сеанс в сервлете, но когда сеанс истекает, я получаю Scope 'session' is not active for the current thread;. Я понимаю, что проблема возникает из-за того, что очистка выполняется внутренним потоком механизма сервлетов, но мне все еще нужно иметь возможность получить этот компонент из файла HttpSessionListener.

Кажется, у меня много таких же вопросов, но никто не нашел решения, поэтому я спрашиваю снова.

В моем applicationContext.xml нет объявления bean-компонента, так как я использую аннотации.

Это bean, к которому мне нужно получить доступ, когда время сеанса истекает:

@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;
    }
}

Это контроллер, который либо create, либо destroy сеанс вручную.

@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;
    }

}

А это HttpSessionListener, который прослушивает уничтожение сеанса, где мне нужно получить доступ к содержимому Access компонента с областью действия сеанса.

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*/}

}

Исключение ниже возникает в 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

Наконец, вот мой 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>

Как я уже сказал, все идет хорошо, когда я объявляю сеанс недействительным в методе контроллера destroy.

ОБНОВЛЕНИЕ 1: НАЙДЕНЫ ВОЗМОЖНЫЕ РЕШЕНИЯ

Проблема возникает из-за того, что для доступа к сеансовым компонентам Spring требуется запрос. Событие, хотя у нас есть контекст, связанный с потоком, запроса нет.

Здесь есть несколько возможных вариантов:

  1. Реализуйте интерфейс DisposableBean, как предложил alexwen. Это подразумевает перемещение бизнес-логики в объект модели[здесь];
  2. Реализуйте DestructionAwareBeanPostProcessor, также предложенный alexwen. Это будет означать, что вам нужно будет проверить, является ли удаляемый компонент Access или нет, прежде чем выполнять какое-либо удаление[здесь];
  3. Получить bean-компонент непосредственно из сеанса. Этот способ не очень хорош, так как вы используете недокументированное поведение для достижения результата, но работает[здесь] ;
  4. Смоделируйте запрос сервлета и привяжите его атрибуты к потоку через RequestContextHolder. Это также приводит к недокументированному поведению, которое может быть изменено в будущих выпусках[здесь];

Я не выбрал последние два, потому что они не задокументированы. Кроме того, мне не нравилась идея очищать каждый боб после определенного. Поскольку я также не хочу смешивать бизнес-логику с компонентами моей модели, в итоге я создал @Service, который создает компонент, а также имеет метод destroy.

Этот метод отвечает за удаление bean-компонента доступа. Я реализовал интерфейс DisposableBean на Access, внедрил службу AccessManager в bean-компонент Access и вызвал метод службы destroy. Сервис выглядит так:

@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
    }

}

person Caio Cunha    schedule 15.08.2012    source источник
comment
Как указано в исключении, в web.xml вы должны использовать либо DispatcherServlet, либо RequestContextListener/RequestContextFilter, чтобы выставить все HTTP-запросы и привязать запрос к потоку.   -  person yorkw    schedule 15.08.2012
comment
@yorkw слушатель уже зарегистрирован, и все будет хорошо, если я аннулирую сеанс. Проблема в таймауте. Я обновил сообщение, включив в него файл web.xml.   -  person Caio Cunha    schedule 15.08.2012


Ответы (2)


Если вы реализуете интерфейс, DisposableBean в вашем классе Access Spring вызовет метод "destroy" при уничтожении сеанса.

Кроме того, вы можете зарегистрировать в Spring DestructionAwareBeanPostProcessor, который будет иметь возможность обрабатывать все bean-компоненты в области сеанса, когда область будет уничтожена. Документы и инструкции здесь.

Если вам интересно, как Spring делает все это, я бы посоветовал вам взглянуть на DisposableBeanAdapter, который Spring регистрирует как HttpSessionBindingListener с сеансом.

person alexwen    schedule 15.08.2012
comment
Спасибо. Метод уничтожения работает, и я уже пробовал это. Но это будет означать перенос бизнес-логики на мою модель bean-компонентов. Я хотел бы избежать этого и сделать сервисы, которые могли бы навести порядок. Я посмотрю на DestructionAwareBeanPostProcessor и HttpSessionBidingListener. - person Caio Cunha; 15.08.2012
comment
Это работало с DestructionAwareBeanPostProcessor, но тогда мне нужно будет следить за каждым bean-компонентом в приложении и искать экземпляры Access? Нет возможности посмотреть конкретные экземпляры? Собираюсь посмотреть и поискать осознание масштаба разрушения. Спасибо. - person Caio Cunha; 15.08.2012
comment
Как я вижу, Spring не предлагает средств для фильтрации bean-компонентов, передаваемых в постпроцессор. Как правило, я видел с интерфейсами *Aware в Spring, что они работают со всеми bean-компонентами в контексте. Одним из возможных способов обойти это (поскольку вы, похоже, не хотите выполнять проверки instanceof) было бы создание определенного контекста для bean-компонентов, которые вы хотите знать об их уничтожении. - person alexwen; 16.08.2012

Проблема возникает из-за того, что к слушателю не привязан запрос, хотя к нему привязан контекст. Область сеанса нуждается в разрешении запроса.

Почитав вокруг, я обнаружил, что компоненты области видимости хранятся в самом HttpSession. Если все, что мне нужно, это захватить bean-компонент с областью действия сеанса, мне просто нужно получить атрибут сеанса с именем bean-компонента (session.getAttribute('beanName')).

Единственная хитрость здесь заключается в том, что проксированные компоненты, как и в случае выше, имеют префикс строки scopedTarget., поэтому, чтобы получить компонент Access, мне просто нужно вызвать атрибут с именем scopedTarget.access. Также нет необходимости в ContextLoaderListener.

Все, что мне нужно сделать, чтобы получить bean-компонент выше, это реализовать HttpSessionListener и написать следующий метод:

@Override
public void sessionDestroyed(HttpSessionEvent ev) {

    // Get the session
    HttpSession session = ev.getSession();

    // Get the access bean
    Access access = (Access) session.getAttribute("scopedTarget.access");

    // Print the serial
    System.out.println(access.getSerial());

}

ВАЖНО: я не уверен, что это лучший подход. Я считаю, что это рискованно, поскольку нет никаких гарантий, что имя компонента будет соответствовать тем же правилам, что и сейчас.

person Caio Cunha    schedule 16.08.2012
comment
Ваше последнее заявление я считаю самым важным. Поскольку это недокументированное поведение и просто побочный эффект их реализации в Spring, он может не поддерживаться в будущих выпусках. - person alexwen; 16.08.2012
comment
Я попытаюсь исследовать лучший способ получить эти атрибуты, и если это рекомендуется для этого. - person Caio Cunha; 16.08.2012
comment
Еще полезная информация в 2021 году - person mjj1409; 30.06.2021