【问题标题】:Threadlocal not working in Intellij within an AspectThreadlocal 在 Aspect 中的 Intellij 中不起作用
【发布时间】:2021-09-07 19:20:16
【问题描述】:

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

Link to springboot and multitenancy

当一个 HTTP 请求进来时,流,在高层次上,是 servletfilter->TenantFilterAspect(事务开始时)-> REST Api。 servletFiler 设置tenantId,由TenantFilterAspect 访问,然后在REST api 中运行查询时,hibernate 应用租户过滤器。

如果我从命令行运行应用程序,一切都会按预期运行。 但是,如果我从 intellij(最终 2021.1)运行它,则 threadlocal 变量在 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。现在一切正常。

【问题讨论】:

  • 我启用了 spring secutity - 我尝试访问 SecurityContextHolder.getContext() - 这也显示为 null。我试过这个,因为 SecurityContextHolder 也使用 ThreadLocal。所以看起来 threadlocal 在方面之前和之后都可用,但在方面内不可用
  • 有趣的问题,但我想重现这个问题。请在 GitHub 上提供一个最小的 Maven 项目。我也使用 IntelliJ IDEA,所以我应该能够重现命令行和 IDEA 的情况。我的假设是您解释的一件或多件事情与现实有所不同。
  • 在原帖中添加了maven项目的链接
  • 那不是一个可点击的链接,它是一个克隆 URL。我为你修好了。请下次小心点。

标签: spring-boot intellij-idea aspectj thread-local hibernate-filters


【解决方案1】:

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

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

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

  • 在前一种情况下,ApsectJ weaver 在LaunchedURLClassLoader 上只注册了一次,而
  • 在后一种情况下,它注册了 3x,首先在 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 提供父级最后加载。

Parent last loading!这不是我们想要的,因为这意味着每个子类加载器都会重新加载父类之前已经加载过的类,这就解释了我们上面看到的问题。为了获得您期望的一致行为,只需在您的 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。我对其进行了测试,它完美无缺。

【讨论】:

  • 如果您之前阅读过答案,还请注意我的两个更新。第二次更新解决了你的问题。这是一个棘手但有趣的谜题,哇!
  • 哇 - 谢谢。我永远不会想到这是一个类加载器问题 - 我会假设一个线程是一个线程,并且它的行为将与线程本地相同,无论类加载器如何。我对类加载器知之甚少,无法完全欣赏和理解您的解决方案,但它确实有效 - 我想我需要阅读更多内容。
  • 与线程完全无关。任何其他静态字段都会以同样的方式受到影响。但是类似于您的陈述“线程就是线程”的想法让我想到了其他原因。它必须与应用程序的启动方式有关,线程不是问题,因为 3 条日志行是写在同一个线程中的。 ?
  • 这是一个非常有启发性的练习。无论如何,现在,我将禁用插件,因为我只需 4 秒即可重新加载。如果我确实找到了更好的方法,我会在这里发布给其他人。
  • 我已经更新了原帖,描述了使用 spring dev-tools 设置需要做些什么来避免这个问题
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2016-07-05
  • 1970-01-01
  • 2018-05-19
  • 1970-01-01
  • 1970-01-01
  • 2014-06-07
  • 2017-05-20
相关资源
最近更新 更多