在实际 Web 请求之外使用请求范围的 bean

Posted

技术标签:

【中文标题】在实际 Web 请求之外使用请求范围的 bean【英文标题】:Using a request scoped bean outside of an actual web request 【发布时间】:2013-06-29 18:29:37 【问题描述】:

我有一个 Web 应用程序,它在一个单独的线程中运行一个 Spring Integration 逻辑。问题是在某些时候我的 Spring Integration 逻辑尝试使用请求范围的 bean,然后我得到以下错误:

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'scopedTarget.tenantContext': 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.


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.

我设置了 ContextLoaderListener:

<listener>
    <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
</listener>

我的 Scoped Bean 是这样注释的(因为我听说代理我的 bean 会有所帮助):

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)  
public class TenantContext  implements Serializable 

我正在做的事情可能吗? 如果是,我在这里错过了什么? 如果没有,关于我如何实现这一目标的任何其他建议?

【问题讨论】:

您是在请求服务后尝试运行代码,还是让请求等待一些异步处理? @OrangeDog 问题已得到回答,并且已被接受。早在 2013 年,我对很多事情一无所知,现在我明白这是一个菜鸟的错误,但还是谢谢。 我应该在那里阅读您的 cmets 以获得答案。你不希望我回答你如何实际做到这一点? 【参考方案1】:

您可以像这样在新线程中发布请求:

import org.springframework.web.context.request.RequestContextListener;
 ...
ServletRequestEvent requestEvent = new ServletRequestEvent(req.getServletContext(), req);
RequestContextListener requestContextListener = new RequestContextListener();
requestContextListener.requestInitialized(requestEvent);
 ...
requestContextListener.requestDestroyed(requestEvent);

如果您查看requestInitialized()-method 内部,您会发现一个保存请求的ThreadLocal-variable。现在您可以成功地自动装配您的请求了。

【讨论】:

【参考方案2】:

对于 spring-boot 2.4 和 spring framework 5,RequestContextFilterRequestContextListener 都不适用于我。

深入代码后发现DispatcherServlet会覆盖RequestContextHolderRequestContextFilter或其他任何一个设置的inheritable,见DispatcherServlet.processRequestDispatcherServlet.initContextHolders

所以解决方案很简单,不需要任何其他组件:

@Configuration
class whateverNameYouLike 
   @Bean
   DispatcherServlet dispatcherServlet() 
       DispatcherServlet srvl = new DispatcherServlet();
       srvl.setThreadContextInheritable(true);
       return srvl;
   

但请注意,该解决方案仅适用于当前请求线程创建的新线程,而不适用于任何线程池。

对于线程池的情况,可以依赖一个额外的包装类:

public class InheritableRequestContextTaskWrapper 
    private Map parentMDC = MDC.getCopyOfContextMap();
    private RequestAttributes parentAttrs = RequestContextHolder.currentRequestAttributes();

    public <T, R> Function<T, R> lambda1(Function<T, R> runnable) 
        return t -> 
            Map orinMDC = MDC.getCopyOfContextMap();
            if (parentMDC == null) 
                MDC.clear();
             else 
                MDC.setContextMap(parentMDC);
            

            RequestAttributes orinAttrs = null;
            try 
                orinAttrs = RequestContextHolder.currentRequestAttributes();
             catch (IllegalStateException e) 
            
            RequestContextHolder.setRequestAttributes(parentAttrs, true);
            try 
                return runnable.apply(t);
             finally 
                if (orinMDC == null) 
                    MDC.clear();
                 else 
                    MDC.setContextMap(orinMDC);
                
                if (orinAttrs == null) 
                    RequestContextHolder.resetRequestAttributes();
                 else 
                    RequestContextHolder.setRequestAttributes(orinAttrs, true);
                
            
        ;
    

然后这样使用:

InheritableRequestContextTaskWrapper wrapper = new InheritableRequestContextTaskWrapper();
List<String> res = pool.submit(() -> ids.parallelStream().map(
    wrapper.lambda1((String id) -> 
        try 
           // do something and return the result string
         catch (Exception e) 
            e.printStackTrace();
            throw new RuntimeException("Error occurred in async tasks", e);
        
    )).collect(Collectors.toList())).get();

【讨论】:

【参考方案3】:

使用 RequestContextFilter 并将属性 threadContextInheritable 设置为 true。这使得子线程继承父的上下文,其中包含请求对象本身。还要确保执行程序不会重用池中的线程,因为请求对象非常特定于该请求并且不能在各种请求之间共享。 SimpleAsyncTaskExecutor 就是一个这样的执行器。

更多信息请参考Scope 'session' is not active for the current thread; IllegalStateException: No thread-bound request found。

【讨论】:

【参考方案4】:

对于 Spring 4 框架添加 servletContext.addListener(new RequestContextListener());

public class WebApplicationInitializer extends AbstractAnnotationConfigDispatcherServletInitializer 
    @Override
    protected Class<?>[] getRootConfigClasses() 
        return new Class[]  RootConfiguration.class ;
    

    @Override
    protected Class<?>[] getServletConfigClasses() 
        return new Class[]  WebMvcConfiguration.class ;
    

    @Override
    protected String[] getServletMappings() 
        return new String[]  "/" ;
    

    @Override
    protected Filter[] getServletFilters() 
        return new Filter[]  new HiddenHttpMethodFilter() ;
    

    **@Override
    public void onStartup(ServletContext servletContext) throws ServletException 
        super.onStartup(servletContext);
        servletContext.addListener(new RequestContextListener());
    **

【讨论】:

对于 Spring 3 在你的 web.xml 添加这个 org.springframework.web.context.request.RequestContextListener 答案被引用了好几次......我没有尝试过,但我认为它没有理由适用于衍生线程,因为我完全同意here提供的论点。【参考方案5】:

您只能在运行请求的 Web 容器线程上使用请求(和会话)范围的 bean。

我认为线程正在等待来自您的 SI 流的异步回复?

如果是这样,您可以将请求范围的 bean 绑定到消息,可能在标头中,或者在负载中的某个位置。

【讨论】:

感谢您的回答。实际上我不认为该线程正在等待回复,因为我只是想保留一个对象并且我需要来自该 TenantContext bean 的信息才能这样做。 那么你根本不能使用请求范围的bean,因为根据定义,请求不再存在。 非常感谢 Gary,那我会另找办法的。 文档将RequestContextListener 描述为[A] listener that exposes the request to the current thread。这似乎与您在回答中所描述的相矛盾。你能澄清一下吗? 在这种情况下,“当前线程”是指正在处理 HTTP 请求的 servlet 容器线程,而不是您将请求交给的任意线程。

以上是关于在实际 Web 请求之外使用请求范围的 bean的主要内容,如果未能解决你的问题,请参考以下文章

为啥我可以在没有请求的情况下注入请求范围的 bean?

如何在 JSF 中正确使用组件绑定? (会话范围 bean 中的请求范围组件)

JSF 请求范围的 bean 不断在每个请求上重新创建新的有状态会话 bean?

将请求范围的 bean 自动装配到应用程序范围的 bean 中

春季请求和会话范围有啥区别?

刷新组件绑定到请求范围的 bean