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.我测试过,完美无缺。
我有一个多租户 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.我测试过,完美无缺。