简体   繁体   中英

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] oaccC[.[.[/].[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

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.

Is it required, that the stream is processed in parallel? That causes, that the lambda may be executed in another thread.

Stream stream = StreamSupport.stream(userRepository.findAll().spliterator(), false );

This is what worked for me to transfer request beans in fork-joined threads. The example is only for illustration.

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
// org.slf4j:slf4j-api:1.7.30
import org.slf4j.MDC;
// org.springframework:spring-web:5.2.12.RELEASE
import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

class Scratch {

    public static void main(String[] args) {
        RequestAttributes context = RequestContextHolder.currentRequestAttributes();
        Map<String, String> contextMap = MDC.getCopyOfContextMap();
        List<String> list = new ArrayList<>();
        list.parallelStream().map(id -> {
            try {
                // copy all required for spring beans
                RequestContextHolder.setRequestAttributes(context);
                MDC.setContextMap(contextMap);

                // ************************************
                // Spring request beans usage goes here
                // ************************************
                return 1;
            } finally {
                // clean all from thread local
                MDC.clear();
                RequestContextHolder.resetRequestAttributes();
            }
        })
                .collect(Collectors.toList());

    }
} 

I had the same issue, I was trying to use the parallel stream to fetch job information from Kubernetes REST API since the parallel stream uses new Threads as John16384 explained, my code couldn't get the 'scopedTarget.oauth2ClientContext' because it's scope is request in Spring and the thread created by parallel stream couldn't access it. So I had to change it like below;

old version: items.parallelStream().map(jobItem -> createJobObject(jobItem, createJobTrigger(jobItem))).collect(Collectors.toList());

fixed version: items.stream().map(jobItem -> createJobObject(jobItem, createJobTrigger(jobItem))).collect(Collectors.toList());

and inside the createJobObject method, I was calling a REST service

restTemplate.getForEntity(url, KubernetesJob.class).getBody().getItems();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM