Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Spring Request scoped beans in lambda

I have a spring application that injects certain beans are injexted based on the request context. In this example it is the Facebook bean.

@RestController
@RequestMapping("facebook")
public class FacebookInjectionController {

    @Autowired
    private Facebook facebook;

    @Autowired
    private UserRepository userRepository;

    @RequestMapping(method = RequestMethod.GET)
    public List<String> blah() {
        String firstName = facebook.userOperations().getUserProfile().getFirstName();
        return Arrays.asList(firstName);
    }

    @RequestMapping(method = RequestMethod.GET, value = "complex")
    public List<String> blah2() {
        UserJwt principal = (UserJwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        Stream<User> stream = StreamSupport.stream(userRepository.findAll().spliterator(), true);

        return stream.filter(u -> u.getUid().equals(principal.getUid()))
                .map(u ->
                        facebook.userOperations().getUserProfile().getFirstName()
                ).collect(Collectors.toList());
    }

}

This code will run normally but every so often it will fail with the following error:

2017-02-09 01:39:59.133 ERROR 40802 --- [o-auto-1-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed; nested exception is 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.] with root cause

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:41)
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:340)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
    at org.springframework.aop.target.SimpleBeanTargetSource.getTarget(SimpleBeanTargetSource.java:35)
    at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:187)
    at com.sun.proxy.$Proxy137.userOperations(Unknown Source)
    at com.roomsync.FacebookInjectionController.lambda$blah2$5(FacebookInjectionController.java:43)
    at com.roomsync.FacebookInjectionController$$Lambda$10/2024009478.apply(Unknown Source)
    at java.util.stream.ReferencePipeline$3$1.accept(ReferencePipeline.java:193)
    at java.util.stream.ReferencePipeline$2$1.accept(ReferencePipeline.java:175)
    at java.util.ArrayList$ArrayListSpliterator.forEachRemaining(ArrayList.java:1374)
    at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:512)
    at java.util.stream.AbstractPipeline.wrapAndCopyInto(AbstractPipeline.java:502)
    at java.util.stream.ReduceOps$ReduceTask.doLeaf(ReduceOps.java:747)
    at java.util.stream.ReduceOps$ReduceTask.doLeaf(ReduceOps.java:721)
    at java.util.stream.AbstractTask.compute(AbstractTask.java:316)
    at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731)
    at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289)
    at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:902)
    at java.util.concurrent.ForkJoinPool.scan(ForkJoinPool.java:1689)
    at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1644)
    at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)

I have tried multiple solutions (including Spring MVC: How to use a request-scoped bean inside a spawned thread?) but none have worked.

Is there a way to pass a request scoped bean down to a lambda or another thread?

going of what https://stackoverflow.com/users/1262865/john16384 said i have changed my config to:

   @Bean
@Scope(value = "inheritableThreadScope", proxyMode = ScopedProxyMode.INTERFACES)
public ConnectionRepository connectionRepository(ConnectionFactoryLocator connectionFactoryLocator) {
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication == null) {
        throw new IllegalStateException("Unable to get a ConnectionRepository: no user signed in");
    }
    return getUsersConnectionRepository(connectionFactoryLocator).createConnectionRepository(authentication.getName());
}

@Bean
@Scope(value="inheritableThreadScope", proxyMode=ScopedProxyMode.INTERFACES)
public Facebook facebook(ConnectionFactoryLocator connectionFactoryLocator) {
    Connection<Facebook> connection = connectionRepository(connectionFactoryLocator).findPrimaryConnection(Facebook.class);

    return connection != null ? connection.getApi() : null;
}

@Bean
@Scope(value = "inheritableThreadScope", proxyMode = ScopedProxyMode.INTERFACES)
public ExecutorService fbExecutor () {
    return Executors.newSingleThreadExecutor();
}

the controller now looks like:

@RestController
@RequestMapping("facebook")
public class FacebookInjectionController {

    @Autowired
    private Facebook facebook;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private ExecutorService fbExecutor;

    @RequestMapping(method = RequestMethod.GET)
    public List<String> blah() {
        String firstName = facebook.userOperations().getUserProfile().getFirstName();
        return Arrays.asList(firstName);
    }

    @RequestMapping(method = RequestMethod.GET, value = "complex")
    public List<String> blah2() throws ExecutionException, InterruptedException {
        UserJwt principal = (UserJwt) SecurityContextHolder.getContext().getAuthentication().getPrincipal();

        Stream<User> stream = StreamSupport.stream(userRepository.findAll().spliterator(), true);

        Future<List<String>> submit = fbExecutor.submit(() -> stream.filter(u -> u.getUid().equals(principal.getUid()))
                .map(u ->
                        facebook.userOperations().getUserProfile().getFirstName()
                )
                .collect(Collectors.toList()));

        return submit.get();
    }

}

i also have the following config:

@Configuration
public class BeanFactoryConfig implements BeanFactoryAware {
    private static final Logger LOGGER = Logger.getLogger(BeanFactoryConfig.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() {
                @Override
                public void registerDestructionCallback(String name, Runnable callback) {
                                        RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
                    attributes.registerDestructionCallback(name, callback, 3);
                }
            };
            cbf.registerScope("inheritableThreadScope", 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://stackoverflow.com/a/3468965/923560
             */
            SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

        }
        else {
//            logger.info("MainConfig is not backed by a ConfigurableBeanFactory");
        }
    }
}

even with this it sometimes get the error:

{
    "timestamp": 1486686875535,
    "status": 500,
    "error": "Internal Server Error",
    "exception": "java.util.concurrent.ExecutionException",
    "message": "org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.facebook' defined in class path resource [com/roomsync/config/SocialConfig.class]: Bean instantiation via factory method failed; nested exception is org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.springframework.social.facebook.api.Facebook]: Factory method 'facebook' threw exception; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.connectionRepository': Scope 'inheritableThreadScope' 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.",
    "path": "/facebook/complex"
}

so it seems that im still missing the piece to activate the scope and copying the thread local context to it

like image 943
arhimmel Avatar asked Sep 05 '25 23:09

arhimmel


1 Answers

There's two things going on:

1) Java streams use a common Fork/Join pool to execute things in parallel. These threads are not created by the Spring framework (or by you).

2) Request scoped beans are supported by using a ThreadLocal.

This means that if a thread, not created by Spring, tries to access a request scoped bean, it won't be found as the thread does not know about it (it is not in the ThreadLocal).

In order for you to resolve this issue you will need to take control of which threads are used for your streams. Once you achieved that, you can make a copy of the request scoped beans to use for the sub-threads. You'll also need to clean them up again after the thread has finished its task or you risk leaving beans behind that may be seen by the next task being executed on that thread.

To change which threads are used by parallel streams, see: Custom thread pool in Java 8 parallel stream

How to configure Spring properly to propagate request scoped beans to child threads you already found I think.

like image 66
john16384 Avatar answered Sep 08 '25 22:09

john16384