Spring请求scoped bean

时间:2017-02-09 09:51:10

标签: java spring spring-mvc lambda spring-social

我有一个spring应用程序,根据请求上下文注入某些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());
    }

}

此代码将正常运行,但每次都会失败并出现以下错误:

  

2017-02-09 01:39:59.133 ERROR 40802 --- [o-auto-1-exec-2]   o.a.c.c.C。[。[。[/]。[dispatcherServlet]:Servlet.service()for   在path []的上下文中的servlet [dispatcherServlet]引发了异常   [请求处理失败;嵌套异常是   org.springframework.beans.factory.BeanCreationException:错误   创建名称为“scopedTarget.facebook&#39;:范围&#39;请求&#39;是   对当前线程不活跃;考虑定义范围代理   对于这个bean,如果你打算从单身中引用它;嵌套   异常是java.lang.IllegalStateException:没有线程绑定请求   发现:您是指实际的请求属性吗?   Web请求,或处理原始请求之外的请求   接收线程?如果您实际在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)

我尝试了多种解决方案(包括Spring MVC: How to use a request-scoped bean inside a spawned thread?)但没有一种方法可行。

有没有办法将请求范围的bean传递给lambda或另一个线程?

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();
}

控制器现在看起来像:

@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();
    }

}

我也有以下配置:

@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");
        }
    }
}

即使这样,它有时也会出错:

{
    "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"
}

所以看起来我仍然缺少激活范围并将线程本地上下文复制到它的部分

4 个答案:

答案 0 :(得分:1)

发生了两件事:

1)Java流使用公共的Fork / Join池并行执行。这些线程不是由Spring框架(或由您)创建的。

2)使用ThreadLocal支持请求范围的bean。

这意味着如果一个不是由Spring创建的线程试图访问一个请求范围的bean,它就不会被找到,因为线程不知道它(它不在ThreadLocal中)。

为了解决此问题,您需要控制哪些线程用于您的流。一旦实现了这一点,就可以制作请求范围bean的副本以用于子线程。在线程完成任务之后,您还需要再次清理它们,否则您可能会将bean留在后面,这可能会被该线程上执行的下一个任务看到。

要更改并行流使用的线程,请参阅:Custom thread pool in Java 8 parallel stream

如何正确配置Spring以将请求范围的bean传播到我认为已经找到的子线程。

答案 1 :(得分:0)

是否需要并行处理流?这导致lambda可能在另一个线程中执行。

  

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

答案 2 :(得分:0)

我遇到了同样的问题,我试图使用并行流从Kubernetes REST API获取作业信息,因为并行流使用了新的线程,如John16384所述,我的代码无法获得“ scopedTarget.oauth2ClientContext”,因为范围是Spring中的请求,并且并行流创建的线程无法访问它。所以我不得不像下面这样更改它;

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());

在createJobObject方法中,我正在调用REST服务

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

答案 3 :(得分:0)

这对我在 fork-joined 线程中传输请求 bean 很有用。示例仅用于说明。

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());

    }
}