【问题标题】:Spring 5.x - How to cleanup a ThreadLocal entrySpring 5.x - 如何清理 ThreadLocal 条目
【发布时间】:2022-01-03 23:28:34
【问题描述】:

为这个冗长的问题道歉..

我对 Spring 还很陌生,还不完全了解其内部工作原理。

所以,我当前的 java 项目早在 2015 年就编写了 Spring 4.x 代码,它使用 ThreadLocal 变量来存储一些用户权限数据。

流程从 REST 控制器中的 REST 调用开始,然后调用后端代码并检查数据库中的用户权限。

有一个 @Repository 类,它有一个 ThreadLocal 的静态实例,这个用户权限被存储在其中。 ThreadLocal 变量由调用线程更新。 因此,如果线程在已经存在的 ThreadLocal 实例中找到数据,它只会从 ThreadLocal 变量中读取该数据并工作。如果没有,它会转到 DB 表并获取新的权限数据并更新 ThreadLocal 变量。

所以我的理解是使用了 ThreadLocal 变量,因为在同一个 REST 调用中多次需要这些用户权限。所以这个想法是针对给定的 REST 请求,因为线程是相同的,它不需要从数据库中获取用户权限,而是可以在同一个 REST 请求中引用其在 ThreadLocal 变量中的条目。

现在,这似乎在 Spring 4.3.29.RELEASE 中运行良好,因为每个 REST 调用都由不同的线程提供服务。(我打印了线程 ID 来确认。)

Spring 4.x ThreadStack 到 Controller 方法调用:

com.xxx.myRESTController.getDoc(MyRESTController.java),
org.springframework.web.context.request.async.WebAsyncManager$5.run(WebAsyncManager.java:332),
java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511),
java.util.concurrent.FutureTask.run(FutureTask.java:266),
java.lang.Thread.run(Thread.java:748)]

但是,当我升级到 Spring 5.2.15.RELEASE 时,会在调用尝试从后端获取用户权限的不同 REST 端点时中断。

在后端打印 Stacktrace 时,我看到 Spring 5.x 中使用了一个 ThreadPoolExecutor。

Spring 5.x 线程堆栈:

com.xxx.myRESTController.getDoc(MyRESTController.java),
org.springframework.web.context.request.async.WebAsyncManager.lambda$startCallableProcessing$4(WebAsyncManager.java:337),
java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511),
java.util.concurrent.FutureTask.run(FutureTask.java:266),
java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149),
java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624),
java.lang.Thread.run(Thread.java:748)]

所以在 Spring 5.x 中,看起来同一个线程被放回 ThreadPool 中,然后被多个不同的 REST 调用调用。 当该线程查找 ThreadLocal 实例时,它会发现它为之前的不相关 REST 调用存储的陈旧数据。因此,我的很多测试用例都因读取过时的数据权限而失败。

我读到调用 ThreadLocal 的 remove() 会从变量中清除调用线程的条目(当时没有实现)。

我想以通用方式执行此操作,以便所有 REST 调用在 REST 响应发回之前调用 remove()。

现在,为了清除 ThreadLocal 条目,我尝试了

  1. 通过实现 HandlerInterceptor 来编写拦截器,但这不起作用。

  2. 我还写了另一个Interceptor扩展了HandlerInterceptorAdapter,并在它的afterCompletion()中调用了ThreadLocal的remove()。

  3. 然后我尝试实现 ServletRequestListener 并从它的 requestDestroyed() 方法中调用 ThreadLocal 的 remove()。

  4. 另外,我实现了一个Filter,在doFilter()方法中调用了remove()。

所有这 4 个实现都失败了,因为当我在它们的方法中打印线程 ID 时,它们彼此完全相同,但与在 RestController 方法中打印的线程 ID 不同。

因此,调用 REST 端点的线程与上述 4 个类调用的线程不同。因此,上述类中的 remove() 调用永远不会从 ThreadLocal 变量中清除任何内容。

有人可以提供一些关于如何在 Spring 中以通用方式清除给定线程的 ThreadLocal 条目的指针吗?

【问题讨论】:

  • 我建议将实现从 ThreadLocal 更改为 LRUCache 之类的东西,您可以在其中使用您将在查询中使用的参数作为键。就个人而言,如果我需要调用堆栈中的值,我会尽量避免使用ThreadLocal。我尝试仅在调用堆栈中需要它们时才使用它们,因此我可以在 finally 块中安全地调用 remove()
  • 简而言之:remove()?更准确地说:不要滥用ThreadLocal(存储请求状态)......这是一个古老而糟糕的黑客攻击,它利用了这个事实(事实不是约定/没有“最终规范”),servlet 容器处理 1 个请求有 1 个线程... Http Session 是更好的“地方”。

标签: java spring spring-boot tomcat9 thread-local


【解决方案1】:

如您所见,HandlerInterceptorServletRequestListener 都在接收请求的原始 servlet 容器线程中执行。由于您是在进行异步处理,因此您需要一个CallableProcessingInterceptor

它的preProcesspostProcess 方法在将发生异步处理的线程上执行。

因此你需要这样的东西:

WebAsyncUtils.getAsyncManager(request)//
             .registerCallableInterceptor("some_unique_key", new CallableProcessingInterceptor() {

                 @Override
                 public <T> void postProcess(NativeWebRequest request, Callable<T> task,
                         Object concurrentResult) throws Exception {
                     // remove the ThreadLocal
                 }

             });

在可以访问 ServletRequest 并在 original servlet 容器线程中执行的方法中,例如在HandlerInterceptor#preHandle 方法中。

备注:不用注册自己的ThreadLocal,可以使用Spring的RequestAttributes。使用静态方法:

RequestContextHolder.currentRequestAttributes()

检索当前实例。在后台使用了 ThreadLocal,但 Spring 负责在处理您的请求的每个线程上设置和删除它(包括异步处理)。

【讨论】:

  • 成功了,谢谢。感谢所有其他关于 LRUCache 和 RequestAttributes 的建议,我也会尝试一下。
猜你喜欢
  • 2013-03-01
  • 2015-02-23
  • 2021-01-07
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-04-10
  • 2020-02-24
相关资源
最近更新 更多