问题
In a Spring MVC application, I have a request-scoped bean. I inject this bean somewhere. There, the HTTP-request serving thread could possibly spawn a new thread.
But whenever I try accessing the request-scoped bean from the newly spawned thread, I get a org.springframework.beans.factory.BeanCreationException
(see stack trace below).
Accessing the request-scoped bean from the HTTP request thread works fine.
How can I make a request-scoped bean available to threads spawned by the HTTP request thread?
Simple setup
Get the following code snippets running. Then start up a server, for instance at http://example.com:8080.
When accessing http://example.com:8080/scopetestnormal, each time a request is made to this address, counter
is incremented by 1 (noticeable via logger output). :) Super!
When accessing http://example.com:8080/scopetestthread, each time a request is made to this address, the mentioned exceptions are thrown. :(. No matter what chosen ScopedProxyMode
, this happens for both CGLIB-based and
JDK-dynamic-proxy-interface-based request-scoped beans
Configuration file
package com.example.config
@Configuration
@ComponentScan(basePackages = { "com.example.scopetest" })
public class ScopeConfig {
private Integer counter = new Integer(0);
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public Number counter() {
counter = new Integer(counter.intValue() + 1);
return counter;
}
/* Adding a org.springframework.social.facebook.api.Facebook request-scoped bean as a real-world example why all this matters
@Bean
@Scope(value = "request", proxyMode = ScopedProxyMode.INTERFACES)
public Facebook facebook() {
Connection<Facebook> facebook = connectionRepository()
.findPrimaryConnection(Facebook.class);
return facebook != null ? facebook.getApi() : new FacebookTemplate();
}
*/
...................
}
Controller file
package com.example.scopetest;
import javax.inject.Inject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.social.facebook.api.Facebook;
import org.springframework.social.facebook.api.FacebookProfile;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class ScopeTestController {
//@Inject
//private Facebook facebook;
@Inject
private Number counter;
private static final Logger logger = LoggerFactory
.getLogger(ScopeTestController.class);
@RequestMapping(value = "/scopetestnormal")
public void scopetestnormal() {
logger.debug("About to interact with a request-scoped bean from HTTP request thread");
logger.debug("counter is: {}", counter);
/*
* The following also works
* FacebookProfile profile = facebook.userOperations().getUserProfile();
* logger.debug("Facebook user ID is: {}", profile.getId());
*/
}
@RequestMapping(value = "/scopetestthread")
public void scopetestthread() {
logger.debug("About to spawn a new thread");
new Thread(new RequestScopedBeanAccessingThread()).start();
logger.debug("Spawned a new thread");
}
private class RequestScopedBeanAccessingThread implements Runnable {
@Override
public void run() {
logger.debug("About to interact with a request-scoped bean from another thread. Doomed to fail.");
logger.debug("counter is: {}", counter);
/*
* The following is also doomed to fail
* FacebookProfile profile = facebook.userOperations().getUserProfile();
* logger.debug("Facebook user ID is: {}", profile.getId());
*/
}
}
}
Stack trace for CGLIB-based request-scoped bean (proxyMode = ScopedProxyMode.TARGET_CLASS
)
SLF4J: Failed toString() invocation on an object of type [$java.lang.Number$$EnhancerByCGLIB$$45ffcde7]
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.counter': Scope 'request' 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 $java.lang.Number$$EnhancerByCGLIB$$45ffcde7.toString(<generated>)
at org.slf4j.helpers.MessageFormatter.safeObjectAppend(MessageFormatter.java:304)
at org.slf4j.helpers.MessageFormatter.deeplyAppendParameter(MessageFormatter.java:276)
at org.slf4j.helpers.MessageFormatter.arrayFormat(MessageFormatter.java:230)
at ch.qos.logback.classic.spi.LoggingEvent.<init>(LoggingEvent.java:114)
at ch.qos.logback.classic.Logger.buildLoggingEventAndAppend(Logger.java:447)18:09:48.276 container [Thread-16] DEBUG c.g.s.c.c.god.ScopeTestController - counter is: [FAILED toString()]
at ch.qos.logback.classic.Logger.filterAndLog_1(Logger.java:421)
at ch.qos.logback.classic.Logger.debug(Logger.java:514)
at com.example.scopetest.ScopeTestController$RequestScopedBeanAccessingThread.run(ScopeTestController.java:58)
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.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:40)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:328)
... 14 more
Stack trace for JDK-dynamic-proxy-interface-based request-scoped bean (proxyMode = ScopedProxyMode.INTERFACES
)
Exception in thread "Thread-16" org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.facebook': Scope 'request' 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.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:182)
at $Proxy28.userOperations(Unknown Source)
at com.example.scopetest.ScopeTestController$PrintingThread.run(ScopeTestController.java:61)
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.AbstractRequestAttributesScope.get(AbstractRequestAttributesScope.java:40)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:328)
... 6 more
回答1:
OK, by reading the code in SimpleThreadScope that comes with Spring I think you can create a SimpleInheritableThreadScope by using an InheritableThreadLocal instead.
Then just use a bit of xml to register your custom scope:
<bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
<property name="scopes">
<map>
<entry key="thread-inherited">
<bean class="org.mael.spring.context.support.SimpleInheritableThreadScope"/>
</entry>
</map>
</property>
</bean>
This means that when you create a bean with a thread-inherited
scope, you will have access to this bean with a copy per thread and that copy will be avaliable in threads spawned by your thread i.e. a request scoped bean that can be used in threads spawned in your request thread.
回答2:
The configuration below will propagate request context to your threads launched from within HTTP request:
<servlet>
<servlet-name>Spring MVC Dispatcher Servlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>threadContextInheritable</param-name>
<param-value>true</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
Disclaimer: I havent tested this specifically with request-scoped beans as I dont use any. I did test that RequestContextHolder returns valid context in child threads.
Disclaimer 2: there is a reason this setting defaults to false. There may be side-effects, especially if you reuse your threads (as in threadpools).
回答3:
If you have a look at AbstractRequestAttributesScope
you'll see that it's using the current RequestAttributes
in order to get the desired bean.
In your thread you'll probably want to do something like this:
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
final SecurityContext securityContext = SecurityContextHolder.getContext();
new Thread(
() -> {
boolean hasContext = RequestContextHolder.getRequestAttributes() == requestAttributes
&& SecurityContextHolder.getContext() == securityContext;
if (!hasContext) {
RequestContextHolder.setRequestAttributes(requestAttributes);
SecurityContextHolder.setContext(securityContext);
}
try {
// useful stuff goes here
} finally {
if (!hasContext) {
RequestContextHolder.resetRequestAttributes();
SecurityContextHolder.clearContext();
}
}
}
).start();
回答4:
Inspired by @mael's answer, here is my "custom-scope-out-of-the-box" solution. I am using a fully annotation-driven Spring configuration.
For my particular case, Spring's own org.springframework.context.support.SimpleThreadScope
already provides the behavior the question is looking for (right, that's weird, because SimpleThreadScope
doesn't use an InheritableThreadLocal
, but effectively a ThreadLocal
. But as it works, I'm already happy).
Correct behavior at concurrent user interaction has not been tested yet.
Steps
Register the SimpleThreadScope
type:
package com.example.config
public class MainConfig implements BeanFactoryAware {
private static final Logger logger = LoggerFactory.getLogger(MainConfig.class);
.......
@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
if (beanFactory instanceof ConfigurableBeanFactory) {
logger.info("MainConfig is backed by a ConfigurableBeanFactory");
ConfigurableBeanFactory cbf = (ConfigurableBeanFactory) beanFactory;
/*Notice:
*org.springframework.beans.factory.config.Scope
* !=
*org.springframework.context.annotation.Scope
*/
org.springframework.beans.factory.config.Scope simpleThreadScope = new SimpleThreadScope();
cbf.registerScope("simpleThreadScope", simpleThreadScope);
/*why the following? Because "Spring Social" gets the HTTP request's username from
*SecurityContextHolder.getContext().getAuthentication() ... and this
*by default only has a ThreadLocal strategy...
*also see http://stackoverflow.com/a/3468965/923560
*/
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
}
else {
logger.info("MainConfig is not backed by a ConfigurableBeanFactory");
}
}
}
Now for any bean that shall have request-scope and that shall be usable from any thread spawned by the HTTP request thread, set the newly defined scope accordingly:
package com.example.config
@Configuration
@ComponentScan(basePackages = { "com.example.scopetest" })
public class ScopeConfig {
private Integer counter = new Integer(0);
@Bean
@Scope(value = "simpleThreadScope", proxyMode = ScopedProxyMode.TARGET_CLASS)
public Number counter() {
counter = new Integer(counter.intValue() + 1);
return counter;
}
@Bean
@Scope(value = "simpleThreadScope", proxyMode = ScopedProxyMode.INTERFACES)
public ConnectionRepository connectionRepository() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
}
return usersConnectionRepository().createConnectionRepository(authentication.getName());
}
@Bean
@Scope(value = "simpleThreadScope", proxyMode = ScopedProxyMode.INTERFACES)
public Facebook facebook() {
Connection<Facebook> facebook = connectionRepository().findPrimaryConnection(Facebook.class);
return facebook != null ? facebook.getApi() : new FacebookTemplate();
}
...................
}
回答5:
https://stackoverflow.com/a/30640097/2569475
For This Issue check My answer at above given url
Using a request scoped bean outside of an actual web request. If you use a Servlet 2.5 web container, with requests processed outside of Spring’s DispatcherServlet (for example, when using JSF or Struts), you need to register the org.springframework.web.context.request.RequestContextListener ServletRequestListener. For Servlet 3.0+, this can done programmatically via the WebApplicationInitializer interface. Alternatively, or for older containers, add the following declaration to your web application’s web.xml file:
来源:https://stackoverflow.com/questions/14986519/spring-mvc-how-to-use-a-request-scoped-bean-inside-a-spawned-thread