我试图弄清楚spring如何在 Controller 组件中注入(inject)线程安全的请求/ session 作用域bean(这是单例和多个线程通过方法访问那些bean)。
作为示例,考虑标记有HttpServletRequest注释的 Controller 中的@Autowired字段(我知道将 Controller 耦合到servlet-api不好,但是出于学习目的,这是可以的)。我了解到此类 bean 是使用CGLib代理的,但仍无法弄清代理如何处理线程
安全性和作用域bean到当前线程


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

在同一实例上从请求到请求 Controller 字段点的

  • (即使在HttpServletRequest的情况下)
  • 来自 session 范围(代理)的传输对象方法调用的
  • 线程的堆栈跟踪
    java.lang.Thread.getStackTrace(Thread.java:1552)
    com.company.market.to.User.setUid(User.java:73)
    com.company.market.to.User$$FastClassBySpringCGLIB$$8eb69e9e.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 Enhancer在方法中生成的。


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

    任何建议或有趣的链接表示赞赏!
    谢谢。

    最佳答案

    只有应用程序上下文将Bean相互注入(inject),而没有其他人(不是DispatcherServlet或任何其他类)注入(inject)。并且所有注入(inject)仅发生一次-在应用程序启动时。

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



    让我们找出范围内的Bean是如何工作的。

    当Spring遇到有作用域的bean时,它将找到Scope接口(interface)的相应实现来获取bean实例。例如,当您像这样声明bean时:

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

    并注入(inject):
    @Autowired
    private RequestBean requestBean;
    

    spring将向RequstScope(默认情况下已注册此作用域类)询问RequestBean实例,并将其注入(inject)到另一个bean中。因此,只有Scope类知道如何获得作用域bean的实例,因此您不必关心它。顺便说一句,您可以编写自己的Scope实现,注册它并在bean声明中使用。

    现在,关于范围代理。

    如我所说,所有注入(inject)仅在应用程序启动时发生。这意味着,每次对完全相同的对象进行操作时,该对象最初都是在启动时注入(inject)的。例如,在注入(inject)请求范围的bean时,这不是您期望的行为。当然,您希望每个请求都获得此bean的新实例。为此,as said in documentation,将您的bean声明为proxified:
    @Bean
    @Scope(value="request")
    @ScopeProxy
    public RequestBean createBean(){
       return new RequsetBean();
    }
    

    在这种情况下,Spring会将您的bean包装到具有相同公共(public)接口(interface)的proxy-object中。现在,每次调用bean的方法时,proxy-object都会从Scope获得真实的对象,然后将它们委托(delegate)给方法调用。
    HttpServletRequest的工作方式几乎相同。 Spring注入(inject)了特殊的代理对象,而不是真正的HttpServletRequest。唯一的区别是此代理对象不使用Scope。当您调用其方法时,该代理对象将获得真实的HttpServletRequest对象,并将所有调用委派给该对象。顺便说一句,按照source codedocumentation看来,Spring确实将请求数据保留在ThreadLocal中。

    因此,每个请求线程都有其自己的HttpServletRequest实例,并且 Controller 的方法不需要同步。

    10-06 12:42
    查看更多