I have a controller that I'd like to be unique per session. According to the spring documentation there are two details to the implementation:
1. Initial web configuration
To support the scoping of beans at the request, session, and global session levels (web-scoped beans), some minor initial configuration is required before you define your beans.
I've added the following to my web.xml
as shown in the documentation:
<listener>
<listener-class>
org.springframework.web.context.request.RequestContextListener
</listener-class>
</listener>
2. Scoped beans as dependencies
If you want to inject (for example) an HTTP request scoped bean into another bean, you must inject an AOP proxy in place of the scoped bean.
I've annotated the bean with @Scope
providing the proxyMode
as shown below:
@Controller
@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public class ReportBuilder implements Serializable {
...
...
}
Problem
In spite of the above configuration, I get the following exception:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.reportBuilder': 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.
Update 1
Below is my component scan. I have the following in web.xml
:
<context-param>
<param-name>contextClass</param-name>
<param-value>
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
</param-value>
</context-param>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>org.example.AppConfig</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
And the following in AppConfig.java
:
@Configuration
@EnableAsync
@EnableCaching
@ComponentScan("org.example")
@ImportResource("classpath:applicationContext.xml")
public class AppConfig implements AsyncConfigurer {
...
...
}
Update 2
I've created a reproducible test case. This is a much smaller project, so there are differences, but the same error happens. There's quite a few files, so I've uploaded it as a tar.gz
to megafileupload.
I'm answering my own question because it provides a better overview of the cause and possible solutions. I've awarded the bonus to @Martin because he pin pointed the cause.
Cause
As suggested by @Martin the cause is the use of multiple threads. The request object is not available in these threads, as mentioned in the Spring Guide:
DispatcherServlet
,RequestContextListener
andRequestContextFilter
all do exactly the same thing, namely bind the HTTP request object to the Thread that is servicing that request. This makes beans that are request- and session-scoped available further down the call chain.
Solution 1
It is possible to make the request object available to other threads, but it places a couple of limitations on the system, which may not be workable in all projects. I got this solution from Accessing request scoped beans in a multi-threaded web application:
I managed to get around this issue. I started using
SimpleAsyncTaskExecutor
instead ofWorkManagerTaskExecutor
/ThreadPoolExecutorFactoryBean
. The benefit is thatSimpleAsyncTaskExecutor
will never re-use threads. That's only half the solution. The other half of the solution is to use aRequestContextFilter
instead ofRequestContextListener
.RequestContextFilter
(as well asDispatcherServlet
) has athreadContextInheritable
property which basically allows child threads to inherit the parent context.
Solution 2
The only other option is to use the session scoped bean inside the request thread. In my case this wasn't possible because:
@Async
;