Spring MVC: How to use a request-scoped bean inside a spawned thread?
Asked Answered
R

5

31

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
Rubicon answered 20/2, 2013 at 17:47 Comment(11)
+1 interesting although I'm thinking that request-scoped beans are available to a single threat (the request itself) and sharing them among other threats will defeat its purpose.Chinaware
is there a way to make spring use an InheritableThreadLocal instead of a ThreadLocal for this scope?Kress
@mael Exploiting a InheritableThreadLocal inside the request-scope-backing implementation sounds like a viable solution to the question posed! If only there was a way to tell Spring to use a custom implementation for scope="request" annotated beans... maybe it is possible... any ideas?Rubicon
Yes, implement org.springframework.beans.factory.config.Scope. I am checking the documentation about itKress
Check this out: static.springsource.org/spring/docs/3.1.x/javadoc-api/org/… I do not know yet how to configure this parameter in a servlet environment, but it's definitely possibleAlvar
@rootkit007 that will expose the request to the threads but the request will contain the request-scoped beans?Kress
@mael, from my understanding Spring will then be able to utilize the request bound to thread and inject appropriate request-scoped beans (which are probably stored with request context). I have not tested this, howeverAlvar
Looks like you need to set up RequestContextFilter as Spring filter via DelegatingFilterProxy, and add RequestContextFilter bean to the chain. You will be able to set threadContextInheritable there. The custom scope approach by @mael should also work.Alvar
@rootkit007 I think you can use the filter as is by declaring it in the deployment descriptor and then using an init-param named threadContextInheritable set to true as the filter is a GenericFilterBeanKress
@mael I just tried adding RequestContextFilter to my web.xml. Unfortunately, it didn't work in my case. Maybe it doesn't work, as the DispatcherServlet already takes care of request-scoped bean handling (... as mentioned in RequestContextFilter's Javadoc).Rubicon
As a reference, here is the relevant snippet of my unsuccessful web.xml attempt: <filter> <filter-name>requestAttributesToAllSpawnedThreadsFilter</filter-name> <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class> <init-param> <param-name>threadContextInheritable</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>requestAttributesToAllSpawnedThreadsFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>Rubicon
K
12

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.

Kress answered 20/2, 2013 at 18:32 Comment(5)
That's good! I started and finished working on a solution inspired by this answer. I will post my solution as an answer shortly.Rubicon
Hey Abdull can you share your implementation which worked for you?Limelight
Hi, I've implemented in same way. Just a query, are there any side-effects of this approach? So far we didn't find one, but is there anything which one needs to take care of with this solution? Reference link - github.com/spring-by-example/spring-by-example/blob/master/…Sigh
This solution works for me. I'm still looking for side-effects on this :). Thanks!Julienne
The thread-inherited proposed here is not equal to “request-inherited”, after requests finished, those context will not be cleared.Unrealizable
A
8

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).

Alvar answered 20/2, 2013 at 19:50 Comment(0)
F
8

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();  
Furnishing answered 24/6, 2016 at 11:24 Comment(1)
This basically Spring's DelegatingSecurityContextRunnable but extended for RequestAttributes. Very nice example!Piscatory
N
4

https://mcmap.net/q/275519/-using-a-request-scoped-bean-outside-of-an-actual-web-request

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:

Neau answered 17/6, 2015 at 7:16 Comment(0)
R
3

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 https://mcmap.net/q/275428/-how-to-set-up-spring-security-securitycontextholder-strategy 
             */
            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();
    }


    ...................

}
Rubicon answered 20/2, 2013 at 23:32 Comment(1)
I might be wrong, but provided code solves the problem of authentication information accessibility (provided by SecurityContextHolder), not the HTTP request. For last one the threadContextInheritable property of DispatcherServlet or RequestContextFilter should be set to true.Lingo

© 2022 - 2024 — McMap. All rights reserved.