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

Threadlocal not working in Intellij within an Aspect

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

Link to springboot and multitenancy

当 HTTP 请求进入时,在较高级别的流程是 servletfilter->TenantFilterAspect(事务开始时)->RESTApi。 servletFiler设置tenantId,通过TenantFilterAspect访问,然后在RESTapi中查询运行时,hibernate应用租户过滤器

如果我 运行 从命令行运行应用程序,一切都会按预期进行。 但是,如果我 运行 来自 intellij(最终 2021.1),threadlocal 变量在 Aspect 中为 null 但在 REST API.

中正确

即我在过滤器中设置它并立即打印 tenantId - 它是正确的 - 当在 aspect 中打印时,它是不正确的,当在 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 returns NULL

**


更新 2

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


更新 3

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

Link to Spring's Customizing restart loader

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

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

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

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

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

我不是 Spring 专家,所以我不知道 Spring Boot 在后一种情况下是如何启动应用程序的,但我认为 class 中的这种差异-加载是问题的根本原因。解决方法是在 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是在两个不同的classloader中加载的,也就是说有两个不同的thread-locals,这也解释了为什么aspect中的那个是未初始化的。


更新 2: 好的,我查看了 RestartClassLoader 的 javadoc,发现了这句话:

Disposable ClassLoader used to support application restarting. Provides parent last loading for the specified URLs.

Parent last loading!这不是我们想要的,因为这意味着每个child classloader都会重新加载classes the parent already has加载之前,这解释了我们上面看到的问题。为了获得您期望的一致行为,只需 在您的 POM 中停用此依赖项

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

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

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