Spring MVC request/session 作用域 bean 线程安全

Spring MVC request/session scoped bean thread-safety

我想弄清楚 spring 如何设法在控制器组件中注入线程安全的 request/session 作用域 bean(这是单例和多线程通过方法访问这些 bean)。
例如,考虑控制器中标有 @Autowired 注释的 HttpServletRequest 字段(我知道将控制器耦合到 servlet-api 是不好的,但出于学习目的,它是可以的)。我了解到此类 bean 是使用 CGLib 代理的,但仍然无法弄清楚 代理如何处理线程 当前线程的安全和作用域 bean.

这就是我到目前为止学到的:

  1. 从请求到同一实例上的请求控制器字段点(即使在 HttpServletRequest 的情况下)
  2. 来自会话范围(代理)传输对象方法调用的线程堆栈跟踪

    java.lang.Thread.getStackTrace(Thread.java:1552)
    com.company.market.to.User.setUid(User.java:73)
    com.company.market.to.User$$FastClassBySpringCGLIB$eb69e9e.invoke(<generated>)
    org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:204)
    org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:720)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:157)
    org.springframework.aop.support.DelegatingIntroductionInterceptor.doProceed(DelegatingIntroductionInterceptor.java:133)
    org.springframework.aop.support.DelegatingIntroductionInterceptor.invoke(DelegatingIntroductionInterceptor.java:121)
    org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179)
    org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:655)
    com.company.market.to.User$$EnhancerBySpringCGLIB$$f4f46820.setUid(<generated>)
    com.company.market.controllers.UserController.signIn(UserController.java:97)
    

    如您所见,一些代码是在运行时由 CGLib 增强器在方法中生成的。


唯一想到的是ThreadLocalDispatcherServlet 持有对具有 ThreadLocal 字段的 MethodInterceptor 的引用,然后将实际对象(例如通过 getAttribute 从会话中检索或简单地 HttpServletRequest)注入该字段,然后拦截器使用实际bean(存储在本地线程中)并使用反射调用原始 bean 上的方法。

感谢任何建议或有趣的链接!
谢谢。

如果您使用的是请求范围的 bean,那么 Spring 只需为它处理的每个新 HTTP 请求创建一个全新的实例。所以当然在那种情况下不会有任何线程安全问题。

但是,由于您没有共享任何代码,所以我没有完全理解您所说的在 Controller 中有一个 HttpServletRequest 字段。您可以将 HttpServletRequest 作为控制器方法的方法参数,这完全没问题,但您永远不会将它作为控制器本身的字段。

对于会话范围的 bean,它类似于请求范围的 bean -- 为每个新的 HttpSession.

创建一个新实例

只有 应用程序上下文相互注入bean,而不是其他任何人(不是DispatcherServlet 或任何其他class)。所有注入 只发生一次 - 在应用程序启动时。

Spring 不关心组件的线程安全。你应该自己做。这就是为什么官方 documentation advice:

...As a rule, use the prototype scope for all stateful beans and the singleton scope for stateless beans.

让我们来看看作用域 bean 是如何工作的

当Spring遇到scoped bean时,找到对应的Scope接口实现,获取bean实例。例如,当你像这样声明 bean 时:

@Bean
@Scope(value="request")
public RequestBean createBean(){
   return new RequsetBean();
}

并注入它:

@Autowired
private RequestBean requestBean;

spring 将从 RequstScope(此范围 - class 默认注册)询问 RequestBean 的实例并将其注入另一个 bean。所以,只有 Scope class 知道,如何获取 scoped bean 的实例,你不应该关心它。顺便说一句,您可以编写自己的 Scope 实现,注册它,并在 bean 声明中使用。

现在,关于范围代理。

正如我所说,所有注入仅在应用程序启动时发生。这意味着,每次您将对完全相同的对象进行操作时,该对象最初是在启动时注入的。这不是您期望的行为,例如,在注入 request scoped bean 时。当然,您希望每个请求 都获得此 bean 的新实例。为此,as said in documentation,将您的 bean 声明为代理:

@Bean
@Scope(value="request")
@ScopeProxy
public RequestBean createBean(){
   return new RequsetBean();
}

在这种情况下,Spring 会将您的 bean 包装到具有相同 public 接口的代理对象中。现在,每次 你调用 bean 的方法时,proxy-object 都会从 Scope 获取真正的对象,然后将方法调用委托给它们。

HttpServletRequest 工作方式几乎相同。 Spring 不是真正的 HttpServletRequest,而是注入特殊的代理对象。唯一的区别是这个代理对象不使用 Scope。当您调用它的方法时,此代理对象会获得真实的 HttpServletRequest 对象并将所有调用委托给它。顺便说一句,根据source code and documentation,看来,Spring确实把请求数据保存在了ThreadLocal.

所以每个请求线程都有自己的HttpServletRequest实例,控制器的方法不需要同步。