Threadlocal在特性内的Intellij中不起作用



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

链接到springboot和多租户

当HTTP请求传入时,在高级级别上的流是servlet筛选器->TenantFilter特性(当事务启动时)->REST Api。servletFiler设置tenantId,TenantFilterAspect访问该tenantId,然后当在RESTapi中运行查询时,hibernate应用租户筛选器。

如果我从命令行运行应用程序,一切都会按预期进行。然而,如果我从intellij(最终2021.1)运行这个,那么threadlocal变量在方面中为null,但在RESTneneneba 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类加载进行了一些挖掘。

链接到Spring的自定义重启加载程序

现在,修复相当简单——我把方面代码放在另一个模块中,并从重新加载中排除了那个jar。现在一切都很顺利。

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

  • 从可执行JAR(命令行)与
  • 根据Maven导入确定的IDE生成的类路径

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

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

我不是Spring专家,所以我不知道在后一种情况下Spring Boot是如何启动应用程序的,但我认为类加载的这种差异是问题的根本原因。一种变通方法是创建一个";JAR应用程序";在IntelliJ IDEA中键入run configuration并以这种方式运行应用程序。在这种情况下,它的行为与控制台类似,但当然,在启动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插件,因为加载时weaver可以在加载时完成方面,如果您不使用本机AspectJ语法,则LTW不需要编译器。您使用的旧插件版本将您限制为JDK8。如果您只是删除它,您还可以使用JDK 9+构建应用程序,例如JDK 16。我测试过了,它完美无瑕。

相关内容

最新更新