Threadlocal 在一个方面内的 Intellij 中不起作用

问题描述

我有一个多租户 springboot (2.4.5) 应用程序 - 我将租户 ID 存储在 ThreadLocal 存储中。我为 Hibernate 过滤器启用了加载时编织。

Link to springboot and multitenancy

HTTP 请求进入时的流程是 servletfilter->TenantFilteraspect(当事务开始时)-> REST Api。 servletFiler 设置tenantId,由TenantFilteraspect 访问,然后当查询在REST api 中运行时,hibernate 应用租户过滤器。

如果我从命令行运行应用程序,一切都会按预期进行。 但是,如果我从 intellij(最终 2021.1)运行它,则线程局部变量在 Aspect 中为 null,但在 REST API 中是正确的。

即我在过滤器中设置它并立即打印tenantId - 它是正确的 - 在方面打印时,它是不正确的,当在 REST API 中打印时,它又是正确的。

public class JwtAuthTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request,HttpServletResponse response,FilterChain filterChain) throws servletexception,IOException {
        try {
            /* Tenant ID hardcoded in the example */
               TenantContext.setCurrentTenant(2);
               SecurityContextHolder.getContext().setAuthentication(authentication);
            /* Print ThreadID and value of thread local here */
            /* thread local value is 2 */

            }
        } 
        filterChain.doFilter(request,response);
    }
@Aspect
public class TenantFilteraspect {

    @pointcut("execution (* org.hibernate.internal.SessionFactoryImpl.SessionBuilderImpl.openSession(..))")
    public void openSession() {
    }

    @AfterReturning(pointcut = "openSession()",returning = "session")
    public void afterOpenSession(Object session) {
        if (session != null && Session.class.isinstance(session)) {
            Long currentTenant = TenantContext.getCurrentTenant(); 
            /* Thread ID is same as in the filter but thread local value is null */
            org.hibernate.Filter filter = ((Session) session).enableFilter("tenantFilter");
            filter.setParameter("tenantId",currentTenant);
        }
    }

}
/* @Transactional is at the class level - transaction is started before it gets here */ 
@PostMapping("/invoices/getPage")
public PagingDTO getInvoices(@RequestBody PagingDTO request) {
   Long currentTenant = TenantContext.getCurrentTenant();
   /* Thread ID is same as in the filter & aspect and thread local value is 2 */
   .....
}
public class TenantContext {
    private static ThreadLocal<Long> currentTenant = new InheritableThreadLocal<>();
    public static Long getCurrentTenant() {
        return currentTenant.get();
    }
    public static void setCurrentTenant(Long tenant) {
        currentTenant.set(tenant);
    }
}

命令行(带有额外的换行符以提高可读性)是

java
  -javaagent:/home/x/.m2/repository/org/aspectj/aspectjweaver/1.9.6/aspectjweaver-1.9.6.jar
  -jar target/webacc-0.0.1-SNAPSHOT.jar

在 Intellij 中,VM 选项已类似设置

-javaagent:/home/x/.m2/repository/org/aspectj/aspectjweaver/1.9.6/aspectjweaver-1.9.6.jar

对这种奇怪行为的任何帮助都将得到深深的赞赏 - 我在这里有点无能为力。在这两种情况下,加载时间编织似乎是正确的(在方面中记录正在工作),除了在 IntelliJ 情况下,访问 Threadlocal 在方面中返回 NULL

**


更新 2

** 我已根据要求添加了最少的代码以在 https://github.com/AnishJoseph/Threadlocal-Issue 重现该问题。说明在自述文件中。


更新 3

基于下面 kriegaex 出色的分析,我对与开发工具相关的 Spring 类加载进行了一些挖掘。

Link to Spring's Customizing restart loader

现在,修复是相当直接的 - 我将方面代码放在不同的模块中,并从重新加载中排除了该 jar。现在一切正常。

解决方法

我可以在 IDEA 中重现该问题。原因似乎是在两种情况下启动应用程序的方式不同,

  • 来自可执行 JAR(命令行)与
  • 来自 IDE 生成的类路径,由 Maven 导入确定。

如果比较两种情况下的控制台日志,您会看到

  • 在前一种情况下,ApsectJ weaver 在 LaunchedURLClassLoader 上只注册一次,而
  • 在后一种情况下,它注册了 3 次,首先在 AppClassLoader 上,然后是 RestartClassLoader,然后是 MethodUtil

我不是 Spring 专家,所以我不知道在后一种情况下 Spring Boot 如何启动应用程序,但我认为类加载的这种差异是问题的根本原因。一种解决方法是在 IntelliJ IDEA 中创建一个“JAR 应用程序”类型的运行配置并以这种方式运行应用程序。在这种情况下,它的行为就像在控制台中一样,但当然,您必须确保在启动之前确实构建了 JAR。

如果我发现更多,我会更新答案,但也许这已经对您有所帮助。


更新:如果在所有 3 个位置添加 System.out.println("### " + TenantContext.class.getClassLoader());,您将看到可执行 JAR 的控制台日志:

### org.springframework.boot.loader.LaunchedURLClassLoader@53e25b76
In Servlet Filter : Thread ID is 21  :: ThreadLocal Value is 10
### org.springframework.boot.loader.LaunchedURLClassLoader@53e25b76
In TenantFilterAspect :: Thread ID is 21  :: ThreadLocal Value is 10
### org.springframework.boot.loader.LaunchedURLClassLoader@53e25b76
In REST API :: Thread ID is 21  :: ThreadLocal Value is 10

然而,当从 IDE 启动应用程序时,您将看到:

### org.springframework.boot.devtools.restart.classloader.RestartClassLoader@1c5e93fb
In Servlet Filter : Thread ID is 36  :: ThreadLocal Value is 10
### sun.misc.Launcher$AppClassLoader@18b4aac2
In TenantFilterAspect :: Thread ID is 36  :: ThreadLocal Value is null
### org.springframework.boot.devtools.restart.classloader.RestartClassLoader@1c5e93fb
In REST API :: Thread ID is 36  :: ThreadLocal Value is 10

看到了吗? TenantContext 在两个不同的类加载器中加载,这意味着有两个不同的线程局部,这也解释了为什么方面中的一个未初始化。


更新 2: 好的,我查看了 RestartClassLoader 的 javadoc 并找到了这句话:

一次性 ClassLoader 用于支持应用程序重新启动。为指定的 URL 提供父级上次加载。

父类上次加载! 这不是我们想要的,因为这意味着每个子类加载器都会重新加载父类之前已经加载过的类,这就解释了我们上面看到的问题。为了获得您期望的一致行为,只需在您的 POM 中停用此依赖项

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-devtools</artifactId>
  <scope>runtime</scope>
  <optional>true</optional>
</dependency>

这样您就无法重新启动应用程序和动态刷新资源,但您的租户可以按预期工作。请自行决定,您喜欢哪种方式。也许有一种方法可以以更细粒度的方式配置类加载行为,例如从父级最后加载中排除 TenantContext。不是 Spring 用户,我不知道。

顺便说一句,您也可以停用 AspectJ Maven 插件,因为加载时编织器可以在加载方面完成方面,如果您不使用本机 AspectJ 语法,则 LTW 不需要编译器。您使用的旧插件版本将您限制为 JDK 8。如果您只是删除它,您还可以使用 JDK 9+ 构建您的应用程序,例如JDK 16。我测试过,它完美无缺。