简体   繁体   English

lambda 中的 Spring Request 作用域 bean

[英]Spring Request scoped beans in lambda

I have a spring application that injects certain beans are injexted based on the request context.我有一个 spring 应用程序,它根据请求上下文注入某些 bean。 In this example it is the Facebook bean.在本例中,它是 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; 2017-02-09 01:39:59.133 错误 40802 --- [o-auto-1-exec-2] oaccC[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in带有路径 [] 的上下文抛出异常 [请求处理失败; nested exception is org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.facebook': Scope 'request' is not active for the current thread;嵌套异常是 org.springframework.beans.factory.BeanCreationException:创建名为“scopedTarget.facebook”的 bean 时出错:当前线程的作用域“请求”未激活; consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;如果您打算从单例中引用它,请考虑为此 bean 定义一个范围代理; 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?嵌套异常是 java.lang.IllegalStateException: No thread-bound request found: 您是指实际 Web 请求之外的请求属性,还是在原始接收线程之外处理请求? 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如果您实际上是在 Web 请求中操作并且仍然收到此消息,则您的代码可能在 DispatcherServlet/DispatcherPortlet 之外运行:在这种情况下,使用 RequestContextListener 或 RequestContextFilter 来公开当前请求。] 与根本原因

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.我尝试了多种解决方案(包括Spring MVC: How to use a request-scoped bean inside a spawned thread? ),但都没有奏效。

Is there a way to pass a request scoped bean down to a lambda or another thread?有没有办法将请求范围的 bean 传递给 lambda 或另一个线程?

going of what https://stackoverflow.com/users/1262865/john16384 said i have changed my config to: https://stackoverflow.com/users/1262865/john16384说我已将配置更改为:

   @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. 1) Java 流使用通用的 Fork/Join 池来并行执行事物。 These threads are not created by the Spring framework (or by you).这些线程不是由 Spring 框架(或您)创建的。

2) Request scoped beans are supported by using a ThreadLocal. 2) 使用 ThreadLocal 支持请求范围的 bean。

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).这意味着如果一个不是由 Spring 创建的线程试图访问一个请求范围的 bean,它不会被找到,因为该线程不知道它(它不在 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.一旦你实现了这一点,你就可以制作一个请求范围 bean 的副本以用于子线程。 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.您还需要在线程完成其任务后再次清理它们,否则您可能会留下 bean,而在该线程上执行的下一个任务可能会看到它们。

To change which threads are used by parallel streams, see: Custom thread pool in Java 8 parallel stream要更改并行流使用的线程,请参阅: Java 8 并行流中的自定义线程池

How to configure Spring properly to propagate request scoped beans to child threads you already found I think.我认为如何正确配置 Spring 以将请求范围的 bean 传播到您已经找到的子线程。

Is it required, that the stream is processed in parallel?是否需要并行处理流? That causes, that the lambda may be executed in another thread.这导致 lambda 可能在另一个线程中执行。

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

This is what worked for me to transfer request beans in fork-joined threads.这就是我在 fork-joined 线程中传输请求 bean 的方法。 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.我遇到了同样的问题,我试图使用并行流从 Kubernetes REST API 获取作业信息,因为并行流使用新线程,如 John16384 所述,我的代码无法获得“scopedTarget.oauth2ClientContext”,因为它的范围是请求在 Spring 中,并行流创建的线程无法访问它。 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在 createJobObject 方法中,我正在调用 REST 服务

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

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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