Не удается получить bean-объект сессионной области по тайм-ауту
Мне нужно получить прокси-сессионный компонент на HttpSessionListener.sessionDestroyed()
, Целью является очистка сеанса, когда он уничтожается invalidate()
или тайм-аут). Я добавил ContextLoaderListener
раскрыть контекст и получить бин через 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. Событие, хотя у нас есть контекст, связанный с потоком, нет запроса.
Здесь есть несколько возможных вариантов:
- Реализуйте интерфейс DisposableBean, как предложено alexwen. Это подразумевает перемещение бизнес-логики к объекту модели [здесь];
- Реализовать
DestructionAwareBeanPostProcessor
также предложено alexwen. Это будет означать, что вам нужно будет проверить, является ли размещаемый компонентAccess
или нет, прежде чем делать какую-либо утилизацию [здесь]; - Получить бин прямо из сеанса. Этот способ не очень хорош, так как вы используете недокументированное поведение для достижения результата, но он работает [здесь];
- Смоделируйте запрос сервлета и привяжите его атрибуты к потоку через
RequestContextHolder
, Это также приводит к недокументированному поведению, которое может быть изменено в будущих выпусках [здесь];
Я не выбрал последние два, потому что они не задокументированы. Кроме того, мне не понравилась идея очищать каждый боб после определенного. Поскольку я также не хочу смешивать бизнес-логику с моими модельными компонентами, в итоге я создал @Service
который создает боб, а также имеет destroy
метод.
Этот метод отвечает за удаление компонента доступа. Я реализовал DisposableBean
интерфейс на Access
и ввел AccessManager
служба для 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
}
}
2 ответа
Если вы реализуете интерфейс, DisposableBean в вашем классе доступа Spring будет вызывать метод "уничтожить", когда сеанс уничтожается.
В качестве альтернативы вы можете зарегистрировать в Spring a DestructionAwareBeanPostProcessor, который будет иметь возможность обрабатывать все bean-компоненты в области Session, когда область будет уничтожена. Документы и инструкции здесь.
Если вам интересно, как Spring делает все это, я бы посоветовал вам взглянуть на DisposableBeanAdapter, который Spring регистрирует как HttpSessionBindingListener в Session.
Проблема возникает, потому что нет запроса, привязанного к слушателю, хотя есть контекст, связанный с ним. Область сеанса нуждается в разрешении запроса.
Читая вокруг, я обнаружил, что бины области видимости хранятся в HttpSession
сам. Если все, что мне нужно, это получить сессионный компонент, мне просто нужно получить атрибут сеанса с именем компонента (session.getAttribute('beanName')
).
Единственная хитрость здесь в том, что переданные бины, как и в предыдущем случае, имеют префикс строки scopedTarget.
чтобы захватить Access
бин, мне просто нужно вызвать атрибут с именем scopedTarget.access
, Нет необходимости в ContextLoaderListener также.
Все, что мне нужно сделать, чтобы получить бин выше, это реализовать 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());
}
ВАЖНО: хотя я не знаю, является ли это лучшим подходом. Я считаю, что это рискованно, поскольку нет никаких гарантий, что имя бина будет соответствовать тем же правилам, что и сейчас.