作用域“session”对于当前线程无效;非法状态异常:未找到绑定请求。

72

我有一个控制器,希望每个会话都是唯一的。根据Spring文档,有两个实现细节:

1. 初始Web配置

为了支持在请求、会话和全局会话级别上对bean进行范围限制(Web Scoped Beans),在定义bean之前需要进行一些微小的初始配置。

我已经按照文档中的说明将以下内容添加到我的web.xml中:

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

2. 作为依赖项的作用域限定 Bean

如果您想将(例如)一个 HTTP 请求作用域的 Bean 注入到另一个 Bean 中,必须在作用域 Bean 的位置注入 AOP 代理。

我已经使用 @Scope 注释了该 Bean,并提供了以下的 proxyMode

@Controller
@Scope(value="session", proxyMode=ScopedProxyMode.TARGET_CLASS)
public class ReportBuilder implements Serializable {
    ...
    ...
}

问题

尽管我进行了上述配置,但我收到以下异常:

org.springframework.beans.factory.BeanCreationException: 创建bean时出错,bean名称为'scopedTarget.reportBuilder':当前线程未激活范围“session”;如果您打算从单例中引用它,请考虑定义一个作用域代理bean;嵌套异常是java.lang.IllegalStateException:未找到绑定请求的线程:您是否在实际网络请求之外引用请求属性或处理原始接收线程之外的请求?如果您确实在网络请求内操作并仍然收到此消息,则您的代码可能正在DispatcherServlet / DispatcherPortlet之外运行:在这种情况下,请使用RequestContextListener或RequestContextFilter公开当前请求。

更新1

以下是我的组件扫描。 我在 web.xml 中有以下内容:

<context-param>
  <param-name>contextClass</param-name>
  <param-value>
    org.springframework.web.context.support.AnnotationConfigWebApplicationContext
  </param-value>
</context-param>

<context-param>
  <param-name>contextConfigLocation</param-name>
  <param-value>org.example.AppConfig</param-value>
</context-param>

<listener>
  <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

接下来是 AppConfig.java 的内容:

@Configuration
@EnableAsync
@EnableCaching
@ComponentScan("org.example")
@ImportResource("classpath:applicationContext.xml")
public class AppConfig implements AsyncConfigurer {
  ...
  ...
}

更新 2

我已经创建了一个可重现的测试案例。这是一个较小的项目,因此存在一些差异,但是发生了相同的错误。由于有相当多的文件,因此我已将其上传为 tar.gz 文件到megafileupload


1
请发布您的组件扫描配置(可能会发现控制器位于错误的上下文中)。如果您有两个组件扫描(ContextLoaderListener和DispatcherServlet(请参见https://dev59.com/Hmox5IYBdhLWcg3wtmnh)),那么请同时发布。 - Ralph
1
控制器应该是单例作用域。 - Sotirios Delimanolis
1
@BorisTreukhov 没有任何限制它们成为其他范围。然而,在我看来,创建一个不同的会话作用域 bean,其中包含您会话所需的任何状态,将更加合理。 - Sotirios Delimanolis
@BorisTreukhov,你有关于Wicket在会话范围内声明所有内容的信息来源吗? - Jon
我下载了它并运行了maven install,但是出现了这个错误:“No WebApplicationContext found: no ContextLoaderListener registered?”,但这不是你提到的错误。使用mvn clean install jetty:run-war跳过测试时会出现相同的错误。你如何运行以重现此错误?你能修复这个问题并提供另一个演示版本或提供运行它的说明吗? - Angular University
显示剩余8条评论
9个回答

75
问题不在于你的Spring注解,而是你的设计模式。你混合了不同的范围和线程:
  • 单例
  • 会话(或请求)
  • 作业线程池
单例在任何地方都可用,这很好。但是会话/请求作用域在附加到请求的线程之外不可用。
异步任务可以在请求或会话不存在的情况下运行,因此无法使用请求/会话依赖性bean。此外,如果您在单独的线程中运行作业,则无法知道起始请求的线程是哪个(这意味着此时aop:proxy无法提供帮助)。
我认为你的代码看起来像是想在ReportController,ReportBuilder,UselessTask和ReportPage之间建立一个合同。是否有一种方法可以只使用简单的类(POJO)从UselessTask中存储数据,在ReportController或ReportPage中读取它,并且不再使用ReportBuilder?

3
RequestContextListener的目的不是使请求可用于其他线程吗?报表生成器逐步构建报表。随着报表的构建,页面通过AJAX进行更新。 - Jon
不,不是的,我在深入研究你的代码之前就尝试过了。我还尝试了一些小的代码修改,但是由于我的回答中描述的原因,它们都没有起作用。 - Martin Strejc
@Jon,这种情况下你没有使用Dispatcher Servlet,所以你应该使用org.springframework.web.context.request.RequestContextListener(请参阅其javadoc),但无论如何它都不会起作用,因为你的工作是在线程池中完成的,正如Martin所指出的那样。所以这个答案是正确的。 - Boris Treukhov
在我的情况下,我有一个具有请求范围OAuth2ClientContextOAuth2RestTemplate,当我在新线程(异步调用)中使用oauth2 rest模板时,我会收到此错误。现在的问题是,将作用域更改为“prototype”是否可以解决我的问题?我实际上不需要将其绑定到请求,我只想发送一个请求到另一个微服务以响应传入的请求,但我确实需要我的OAuth2RestTemplate在发送第二个请求之前发出访问令牌。 - xbmono
@xbmono 这是很久以前的事了。不过你在这里找到解决方案了吗? - sushi
1
@sushi 是的,那是很久以前的事了,我不记得我当时具体做了什么,也没有准备好那个源代码。但是我依稀记得,我最终通过调试OAuth2RestTemplate找出了令牌何时被请求和存储,然后我手动构建了REST模板,并将该类设置为请求范围,其余部分则为会话范围...这样,只有该类被实例化,因为新实例化的类没有令牌,所以每个请求都会请求一个新的令牌。我还添加了缓存机制,以避免这种情况发生得太频繁(例如对于同一用户)。 - xbmono

38

我回答自己的问题是为了更好地概述原因和可能的解决方案。我将奖励给@Martin,因为他准确指出了原因。

原因

正如@Martin所建议的那样,原因是使用了多个线程。请求对象在这些线程中不可用,如Spring Guide中所述:

DispatcherServletRequestContextListenerRequestContextFilter都做同样的事情,即将HTTP请求对象绑定到服务该请求的线程上。这使得请求和会话范围内的bean在调用链下面也能使用。

解决方案1

可以将请求对象提供给其他线程使用,但这会对系统产生一些限制,在某些项目中可能无法实现。我从Accessing request scoped beans in a multi-threaded web application中获得了这个解决方案:

我成功解决了这个问题。我开始使用SimpleAsyncTaskExecutor而不是WorkManagerTaskExecutor/ThreadPoolExecutorFactoryBean。好处是SimpleAsyncTaskExecutor永远不会重复使用线程。这只是解决方案的一半。解决方案的另一半是使用RequestContextFilter而不是RequestContextListenerRequestContextFilter(以及DispatcherServlet)具有threadContextInheritable属性,允许子线程继承父上下文。 解决方案2 唯一的其他选择是在请求线程中使用会话范围的bean。在我的情况下,这不可能,因为:
  1. 控制器方法带有@Async注释;
  2. 控制器方法启动使用线程进行并行作业步骤的批处理作业。

2
我尝试了这些...没有一个起作用。我不知道为什么到处都建议使用这些解决方案,但它们都无效。对于那些仍然有此问题的人,请查看此链接:https://medium.com/@pranav_maniar/spring-accessing-request-scope-beans-outside-of-web-request-faad27b5ed57 - xbmono

37
如果其他人遇到同样的问题,以下方法解决了我的问题。
在web.xml中:
 <listener>
            <listener-class>
                    org.springframework.web.context.request.RequestContextListener 
            </listener-class>
  </listener>

在 Session 组件中

@Component
@Scope(value = "session",  proxyMode = ScopedProxyMode.TARGET_CLASS)

在pom.xml文件中

    <dependency>
        <groupId>cglib</groupId>
        <artifactId>cglib</artifactId>
        <version>3.1</version>
    </dependency>

1
你还可以使用ScopedProxyMode.INTERFACE而不需要cglib依赖。在这种情况下,当然需要一个接口。 - Gaël J
太棒了,这也救了我的命!!我在web.xml中缺少org.springframework.web.context.request.RequestContextListener监听器类。 - gene b.
从Spring框架版本3.2开始,您的项目不再需要显式添加CGLIB依赖。https://docs.spring.io/spring/docs/3.2.x/spring-framework-reference/html/migration-3.2.html#migration-3.2-inline-cglib - Reva

10

我通过添加以下代码解决了这个问题。

@Component
@Scope(value = "session",  proxyMode = ScopedProxyMode.TARGET_CLASS)

XML配置 -

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

我们可以使用Java配置来完成以上操作 -

@Configuration
@WebListener
public class MyRequestContextListener extends RequestContextListener {
}

如何在没有XML配置的情况下添加RequestContextListener?

我正在使用Spring版本5.1.4.RELEASE,并且不需要在pom中添加以下更改。

<dependency>
    <groupId>cglib</groupId>
    <artifactId>cglib</artifactId>
    <version>3.2.10</version>
</dependency>

1
我按照答案中描述的方法使用了注释,对我很有效。谢谢 :) - Dhayal

7
根据文档规定:

如果您在Spring Web MVC内访问作用域bean,即在由Spring DispatcherServlet或DispatcherPortlet处理的请求中,则无需进行特殊设置:DispatcherServlet和DispatcherPortlet已经公开了所有相关状态。

如果您运行在Spring MVC之外(未由DispatchServlet处理),则必须使用RequestContextListener而不仅仅是ContextLoaderListener.

请将以下内容添加到您的web.xml文件中

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

这将为Spring提供会话,以便在其中维护这些范围内的bean。

更新: 根据其他答案,@Controller只有在Spring MVC上下文中才有意义,所以在您的代码中,@Controller没有发挥实际作用。尽管如此,您仍然可以将bean注入到任何具有session scope / request scope的地方(您不需要Spring MVC / Controller来注入特定scope的bean)。

更新: RequestContextListener仅将请求公开给当前线程。
您已经在两个地方注入了ReportBuilder

1. ReportPage - 您可以看到Spring在此正确注入了Report Builder,因为我们仍在同一个Web线程中。我更改了您代码的顺序,以确保ReportBuilder像这样注入ReportPage。

log.info("ReportBuilder name: {}", reportBuilder.getName());
reportController.getReportData();

我知道日志应该在按照您的逻辑之后,只是为了调试目的我添加了它。


2. UselessTasklet - 我们在这里得到了异常,因为这是由Spring Batch创建的不同线程,在那里Request没有被RequestContextListener暴露。


您应该有不同的逻辑来创建和注入ReportBuilder实例到Spring Batch中(可以使用Spring Batch参数和Future<ReportBuilder>返回未来引用)


我已经在我的web.xml中有RequestContextListener。您可以在原始问题和示例项目的源代码中看到这一点。 - Jon
你在web.xml中使用的监听器是ContextLoaderListener,它加载Spring根上下文,而不是RequestContextListener将请求数据绑定到线程。尝试添加RequestContextListener(并保留ContextLoaderListener),并通过将参数传递给作业来删除@Async的使用,请参阅我的答案以获取更多详细信息。如果您将RequestContextListener添加到web.xml并删除Async的使用,可以告诉我们会发生什么吗? - Angular University
即使我们添加了RequestContextListener,它也不起作用。RequestContextListener将请求/会话属性添加到原始线程的threadLocal中(即Web请求引发的线程),而不会将属性放入由Spring Batch创建的线程中。 - Mani
这是正确的,添加RequestContextListener只能解决部分问题,但这是必要的 - 请参考我的答案,我会解释为什么以及如何修复它。导致“请求未绑定到线程问题”的两个问题,尝试从单独的Sprign批处理线程访问请求数据只是其中之一。 - Angular University

1
我的回答是针对问题描述者所描述的一般问题的特殊情况,但我还是会提供答案以帮助其他人。
当使用@EnableOAuth2Sso时,Spring会在应用程序上下文中放置一个OAuth2RestTemplate,并且此组件恰好假定与线程绑定的与servlet相关的东西。
我的代码有一个定时异步方法,它使用了一个自动装配的RestTemplate。这不是在DispatcherServlet内运行的,但Spring正在注入OAuth2RestTemplate,这导致了问题的出现。
解决方案是进行基于名称的注入。在Java配置中:
@Bean
public RestTemplate pingRestTemplate() {
    return new RestTemplate();
}

并且在使用它的类中:

@Autowired
@Qualifier("pingRestTemplate")
private RestTemplate restTemplate;

现在Spring注入了预期的、无需servlet的RestTemplate

0

1
请在你的回答中添加更多细节。 - Abdulla Nilam

0

当我在过滤器类上使用@Order注解时,遇到了相同的错误。即使我通过HttpSecurity链添加了过滤器。

移除@Order后,问题得到解决。


-1

如果您需要与默认的单例作用域不同的作用域(除了原型),则只需在bean中定义即可。例如:

<bean id="shoppingCart" 
   class="com.xxxxx.xxxx.ShoppingCartBean" scope="session">
   <aop:scoped-proxy/> 
</bean>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接