ScopedProxy 如何决定使用什么会话?

How does a ScopedProxy decide what Session to use?

Singleton 无法自动装配 SessionBean,但 ScopedProxy 可以。

假设 100 个用户在同一个应用程序中同时有一个有效的 Session,ScopedProxy 如何决定会话是什么意思?

我认为 ScopedProxy 不会选择任何随机会话,在我看来这是无稽之谈。

  1. ScopedProxy 如何决定使用哪个会话?
  2. 0个用户有Session怎么办? NullPointerException 会发生吗?
  3. @Async 是与调用请求处理线程不同的线程如何将 HttpRequest-Context 注入异步任务?

ThreadLocal 几乎就是您要找的答案。

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.

Spring 有 RequestContextHolder

Holder class to expose the web request in the form of a thread-bound RequestAttributes object. The request will be inherited by any child threads spawned by the current thread if the inheritable flag is set to true.

Inside the class 您将看到以下内容:

private static final ThreadLocal<RequestAttributes> requestAttributesHolder =
            new NamedThreadLocal<RequestAttributes>("Request attributes");

这里是实际的 setter(注意它是静态的):

/**
     * Bind the given RequestAttributes to the current thread.
     * @param attributes the RequestAttributes to expose,
     * or {@code null} to reset the thread-bound context
     * @param inheritable whether to expose the RequestAttributes as inheritable
     * for child threads (using an {@link InheritableThreadLocal})
     */
    public static void setRequestAttributes(RequestAttributes attributes, boolean inheritable) {}

因此,如您所见,那里没有魔法,只有 ThreadLocal 提供的线程特定变量。

如果您足够好奇,这里是 ThreadLocal.get 实现(returns 当前线程的此线程局部变量副本中的值):

/**
 * Returns the value in the current thread's copy of this
 * thread-local variable.  If the variable has no value for the
 * current thread, it is first initialized to the value returned
 * by an invocation of the {@link #initialValue} method.
 *
 * @return the current thread's value of this thread-local
 */
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null)
            return (T)e.value;
    }
    return setInitialValue();
}

如您所见,它仅依赖于 ThreadLocalMap:

/**
 * ThreadLocalMap is a customized hash map suitable only for
 * maintaining thread local values. No operations are exported
 * outside of the ThreadLocal class. The class is package private to
 * allow declaration of fields in class Thread.  To help deal with
 * very large and long-lived usages, the hash table entries use
 * WeakReferences for keys. However, since reference queues are not
 * used, stale entries are guaranteed to be removed only when
 * the table starts running out of space.
 */
static class ThreadLocalMap {

getEntry() 在地图中执行查找。我希望你现在看到了整个画面。

关于潜在的 NullPointerException

基本上,您只能在范围处于活动状态时调用代理的方法,这意味着执行线程应该是一个servlet 请求。因此,任何异步作业、命令等都将通过这种方法失败。

我想说,这在ScopedProxy背后是一个很大的问题。它确实透明地解决了一些问题(例如简化了调用链),但如果你不遵守规则,你可能会得到 java.lang.IllegalStateException: No thread-bound request found

(Spring Framework Reference Documentation) 表示如下:

DispatcherServlet, RequestContextListener and RequestContextFilter all do exactly the same thing, namely bind the HTTP request object to the Thread that is servicing that request. This makes beans that are request- and session-scoped available further down the call chain.

您还可以查看以下问题:Accessing request scoped beans in a multi-threaded web application

@异步和请求属性注入

一般来说,没有直接解决问题的方法。如前所述,我们有线程绑定的 RequestAttributes。

可能的解决方案是手动传递所需的对象并确保 @Async 背后的逻辑考虑到这一点。

一个更聪明的解决方案(Eugene Kuleshov 建议)是透明地执行此操作。我将复制代码以简化阅读并将 link 放在代码块下。

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;

/**
 * @author Eugene Kuleshov
 */
public abstract class RequestAwareRunnable implements Runnable {
  private final RequestAttributes requestAttributes;
  private Thread thread;

  public RequestAwareRunnable() {
    this.requestAttributes = RequestContextHolder.getRequestAttributes();
    this.thread = Thread.currentThread();
  }

  public void run() {
    try {
      RequestContextHolder.setRequestAttributes(requestAttributes);
      onRun();
    } finally {
      if (Thread.currentThread() != thread) {
        RequestContextHolder.resetRequestAttributes();
      }
      thread = null;
    }
  }

  protected abstract void onRun();
} 

这是问题:Accessing scoped proxy beans within Threads of

如您所见,此解决方案依赖于构造函数将在适当的上下文中执行这一事实,因此可以缓存适当的上下文并稍后注入它。

这是另一个非常有趣的话题 @Async annotated method hanging on session-scoped bean

A Singleton can not autowire a SessionBean but a ScopedProxy can.

这个说法有点令人费解。它应该改写为

Spring cannot inject session-scoped beans into singleton-scoped beans unless the former are defined as (scoped)proxies.

换句话说,Spring 无法将非代理会话范围的 bean 注入单例范围的 bean。它将成功地将代理会话范围的 bean 注入到单例范围的 bean 中。

Assuming 100 users have a valid Session at the same time in the same application, how does the ScopedProxy decide what session is meant?

首先要澄清的是,session 是 Servlet 容器的一个组件,由 HttpSession 表示。 Spring(和 Spring MVC)用会话范围的 bean(以及其他东西,如 flash 属性)将其抽象出来。

HttpSession 对象通常通过适当的 cookie 与用户相关联。 HTTP 请求提供具有用户标识值的 cookie,Servlet 容器检索或创建关联的 HttpSession。换句话说,会话可以从请求中的信息中识别出来。您或 Spring 需要访问该请求。

Spring MVC 显然可以通过 DispatcherServlet 访问请求,即使它通常不会将其暴露给处理程序方法(请记住 Spring MVC 试图隐藏来自您的 Servlet API)。

以下是大致的实现细节。 Spring MVC 不会将请求对象 (HttpServletRequest) 一直传播到调用堆栈,而是将其存储在 RequestContextHolder.

Holder class to expose the web request in the form of a thread-bound RequestAttributes object.

之所以能够做到这一点,是因为 Servlet 容器通常(即非异步)在单个线程中处理请求。如果您在该请求处理程序线程中执行代码,您就可以访问该请求。如果您有权访问该请求,you have access to the HttpSession.

实际执行时间相当长。如果您想深入了解,请从 SessionScope 开始,然后逐步完成。

所有这一切都表明 Spring 没有注入具体 bean 类型的对象,它注入了一个代理。以下示例使用 JDK proxies(仅接口),是会话范围代理的行为。鉴于

interface SessionScopedBean {...}
class SessionScopedBeanImpl implements SessionScopedBean {...}

Spring 会创建一个代理 SessionScopedBean

类似(但更复杂)
SessionScopedBean proxy = (SessionScopedBean) Proxy.newProxyInstance(Sample.class.getClassLoader(),
        new Class<?>[] { SessionScopedBean.class }, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                HttpSession session = ...;// get session through RequestContextHolder
                SessionScopedBean actual = session.getAttribute("some.bean.identifier");
                if (actual == null) {
                    // if absent, set it
                    session.setAttribute("some.bean.identifier", actual = new SessionScopedBeanImpl());
                }
                return method.invoke(actual, args); // delegate to actual object
            }
        });

并将此 proxy 对象注入到您的单例 bean(大概是控制器)中。当您的单例 bean 将要使用会话范围的 bean 时,它实际上正在通过代理并且代理正在检索并将调用委托给实际对象。

What if 0 users have a Session? Will a NullPointerException occur?

Spring 注入代理。代理不是 null。它是一个对象。它知道如何检索和使用实际目标。对于会话范围,如果目标不存在,则会在会话中创建并保存(然后使用)。


这里的危险在于尝试在会话上下文之外使用会话范围的代理。如前所述,整个技巧之所以有效,是因为 Servlet 容器通过在单个线程中处理单个请求来工作。如果您尝试在未绑定请求的线程中访问会话范围的 bean,则会出现异常。

因此,不要尝试跨线程边界传递会话范围的 bean。 Servlet 规范允许您使用 Async Processing and Spring MVC supports it with DefferedResult and Callable. There's a blog series about it, here. You still can't pass around the session-scoped bean. However, if you have a reference to the AsyncContext,您可以自己检索 HttpServletRequest 并访问 HttpSession

如果您正在控制如何分派线程(或者更确切地说 Runnables),可以使用一些技术来复制请求上下文,like the one described here


这里有一些关于(会话)范围和代理的相关帖子:

我会做一个很简单的解释

@Component
@Scope(value="prototype", proxyMode=ScopedProxyMode.TARGET_CLASS)
class YourScopedProxy {

public String dosomething() {
        return "Hello"; 
     }

}


@Component
class YourSingleton {
 @Autowired private YourScopedProxy meScopedProxy;

 public String usedosomething(){
  return this.meScopedProxy.dosomething();
 }
}


   1. How does the ScopedProxy decide what session to use?

we have 100 users

1 user (http session) call YourSingleton.usedosomething => call meScopedProxy :  
=> meScopedProxy  is not the YourScopedProxy  (original)  but a proxy to the YourScopedProxy
   and this proxy understands the scope 
=>   in this case : proxy get real 'YourScopedProxy' object from  HTTP Session  


   2. What if 0 users have a Session? Will a NullPointerException occur?
No because meScopedProxy is a proxy , when u use it 
=> proxy get real 'YourScopedProxy' object from  HTTP Session