Spring 5.x - 如何清理 ThreadLocal 条目

Spring 5.x - How to cleanup a ThreadLocal entry

很抱歉问题很长..

我是 Spring 的新手,还不完全了解内部工作原理。

因此,我当前的 java 项目有 Spring 4.x 代码早在 2015 年就使用 ThreadLocal 变量存储一些用户权限数据。

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

有一个@Repository class,它有一个ThreadLocal 的静态实例,用于存储此用户权限。 ThreadLocal 变量由调用线程更新。 因此,如果线程在 ThreadLocal 实例中找到已经存在的数据,它只是从 ThreadLocal 变量中读取该数据并开始工作。如果没有,它会转到数据库表并获取新的权限数据并更新 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.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(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. 我还写了另一个拦截器,扩展了HandlerInterceptorAdapter,并在它的afterCompletion()中调用了ThreadLocal的remove()。

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

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

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

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

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

如您所见,HandlerInterceptor and the ServletRequestListener are executed in the original servlet container thread, where the request is received. Since you are doing asynchronous processing, you need a 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 并在 原始 servlet 容器线程中执行的方法中,例如在 HandlerInterceptor#preHandle 方法中。

备注:您可以使用Spring的RequestAttributes代替注册您自己的ThreadLocal。使用静态方法:

RequestContextHolder.currentRequestAttributes()

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