CompletableFuture/ForkJoinPool设置类加载器 [英] CompletableFuture / ForkJoinPool Set Class Loader
问题描述
我解决了一个非常具体的问题,该问题的解决方案似乎是基本的:
I tackled down a very specific problem, whose solution seems to be something basic:
我(Spring)应用程序的类加载器层次结构是这样的:SystemClassLoader -> PlatformClassLoader -> AppClassLoader
My (Spring) application's classloader hierarchy is something like this: SystemClassLoader -> PlatformClassLoader -> AppClassLoader
如果我使用Java CompleteableFuture
运行线程.线程的ContextClassLoader
是:SystemClassLoader -> PlatformClassLoader -> ThreadClassLoader
If I use Java CompleteableFuture
to run threads. the ContextClassLoader
of the threads is: SystemClassLoader -> PlatformClassLoader -> ThreadClassLoader
因此,尽管必须这样做,但我无法访问AppClassLoader
中的任何类,因为所有外部库类都驻留在其中.
Thus, I cannot access any class in AppClassLoader
although I have to because all external library classes reside there.
源库很大,因此我不想/不能将所有与线程相关的部分重写为其他内容(例如,将自定义执行程序传递给每个调用).
The source base is quite large so I don't want to/can't rewrite all the thread related pieces to something else (e.g. pass a custom executor to each call).
所以我的问题是:如何制作由例如CompleteableFuture.supplyAsync()
使用AppClassLoader
作为父母吗?(而不是PlatformClassloader
)
So my question is: How can I make the threads created by e.g. CompleteableFuture.supplyAsync()
use the AppClassLoader
as a parent? (instead of the PlatformClassloader
)
我发现 ForkJoinPool 用于创建线程.但是在我看来,所有内容都是 static 和 final .因此,我怀疑即使设置自定义 ForkJoinWorkerThreadFactory带有系统属性的在这种情况下会有所帮助.还是会?
I found out that ForkJoinPool is used to create the threads. But as it seems to me, everything there is static and final. So I doubt that even setting a custom ForkJoinWorkerThreadFactory with a system property will help in this case. Or would it?
编辑以回答评论中的问题:
-
部署到哪里?这是在jetty/tomcat/任何JEE容器中运行吗?
- 我使用的是默认的Spring Boot设置,因此使用了内部的tomcat容器.
您遇到的确切问题是什么?
- 确切的问题是: java.lang.IllegalArgumentException:从方法引用的org.keycloak.admin.client.resource.RealmsResource在类加载器中不可见
您提交到supplyAsync()的作业是从AppClassLoader创建的,不是吗?
-
supplyAsync
是从使用AppClassLoader
的MainThread
调用的.但是,调试应用程序表明,所有此类线程均以PlatformClassLoader
作为其父级.据我了解,这是因为 ForkJoinPool.commonPool()是在应用程序启动期间构造的(因为它是静态的),因此使用默认的类加载器作为父级,即PlatformClassLoader
.因此,该池中的所有线程都将PlatformClassLoader
作为其 ContextClassLoader (而不是AppClassLoader
).
The
supplyAsync
is called from theMainThread
which uses theAppClassLoader
. But, debugging the applications shows that all such threads havePlatformClassLoader
as their parent. As to my understanding, this happens because ForkJoinPool.commonPool() is constructed during the application startup (because it's static) and so uses the default class loader as the parent which isPlatformClassLoader
. So, all threads from this pool getPlatformClassLoader
as their parent for ContextClassLoader (instead ofAppClassLoader
).
当我在MainThread
中创建自己的执行程序并将该执行程序传递给supplyAsync
时,一切正常-我可以在调试过程中看到,实际上AppClassLoader
是我的ThreadClassLoader
的父级.在第一种情况下,这似乎肯定了我的假设:公用池不是由MainThread
创建的,至少不是在使用AppClassLoader
本身时创建的.
When I'm creating my own executor inside the MainThread
and pass this executor to supplyAsync
everything works - and I can see during debugging that indeed now AppClassLoader
is the parent of my ThreadClassLoader
. Which seems to affirm my assumption in the first case that the common pool is not created by MainThread
at least not when it's using AppClassLoader
itself.
完整的堆栈跟踪:
java.lang.IllegalArgumentException: org.keycloak.admin.client.resource.RealmsResource referenced from a method is not visible from class loader
at java.base/java.lang.reflect.Proxy$ProxyBuilder.ensureVisible(Proxy.java:851) ~[na:na]
at java.base/java.lang.reflect.Proxy$ProxyBuilder.validateProxyInterfaces(Proxy.java:682) ~[na:na]
at java.base/java.lang.reflect.Proxy$ProxyBuilder.<init>(Proxy.java:628) ~[na:na]
at java.base/java.lang.reflect.Proxy.lambda$getProxyConstructor$1(Proxy.java:426) ~[na:na]
at java.base/jdk.internal.loader.AbstractClassLoaderValue$Memoizer.get(AbstractClassLoaderValue.java:327) ~[na:na]
at java.base/jdk.internal.loader.AbstractClassLoaderValue.computeIfAbsent(AbstractClassLoaderValue.java:203) ~[na:na]
at java.base/java.lang.reflect.Proxy.getProxyConstructor(Proxy.java:424) ~[na:na]
at java.base/java.lang.reflect.Proxy.newProxyInstance(Proxy.java:999) ~[na:na]
at org.jboss.resteasy.client.jaxrs.ProxyBuilder.proxy(ProxyBuilder.java:79) ~[resteasy-client-3.1.4.Final.jar!/:3.1.4.Final]
at org.jboss.resteasy.client.jaxrs.ProxyBuilder.build(ProxyBuilder.java:131) ~[resteasy-client-3.1.4.Final.jar!/:3.1.4.Final]
at org.jboss.resteasy.client.jaxrs.internal.ClientWebTarget.proxy(ClientWebTarget.java:93) ~[resteasy-client-3.1.4.Final.jar!/:3.1.4.Final]
at org.keycloak.admin.client.Keycloak.realms(Keycloak.java:114) ~[keycloak-admin-client-3.4.3.Final.jar!/:3.4.3.Final]
at org.keycloak.admin.client.Keycloak.realm(Keycloak.java:118) ~[keycloak-admin-client-3.4.3.Final.jar!/:3.4.3.Final]
推荐答案
因此,这是一个非常肮脏的解决方案,我对此并不感到骄傲,如果您继续尝试,可能会为您带来麻烦带有它:
So, here is a very dirty solution of which I'm not proud of and may break things for you if you go along with it:
问题是该应用程序的类加载器未用于ForkJoinPool.commonPool()
.由于commonPool的设置是静态的,因此在应用程序启动期间,没有轻易的机会(至少据我所知)稍后进行更改.因此,我们需要依赖 Java反射API .
The problem was that the classloader of the application was not used for ForkJoinPool.commonPool()
. Because the setup of commonPool is static and therefor during the application start up there is no easy possibility (at least to my knowledge) to make changes later. So we need to rely on Java reflection API.
-
在应用程序成功启动后创建一个钩子
create a hook after your application successfully started
- in my case (Spring Boot environment) this will be the ApplicationReadyEvent
to listen to this event you need a component like the following
@Component
class ForkJoinCommonPoolFix : ApplicationListener<ApplicationReadyEvent> {
override fun onApplicationEvent(event: ApplicationReadyEvent?) {
}
}
在您的钩子中,您需要将commonPool的ForkJoinWorkerThreadFactory
设置为自定义实现(因此该自定义实现将使用应用程序类加载器)
Inside your hook you need to set ForkJoinWorkerThreadFactory
of commonPool to a custom implementation (so this custom implementation will use the app classloader)
-
在科特林
in Kotlin
val javaClass = ForkJoinPool.commonPool()::class.java
val field = javaClass.getDeclaredField("factory")
field.isAccessible = true
val modifiers = field::class.java.getDeclaredField("modifiers")
modifiers.isAccessible = true
modifiers.setInt(field, field.modifiers and Modifier.FINAL.inv())
field.set(ForkJoinPool.commonPool(), CustomForkJoinWorkerThreadFactory())
field.isAccessible = false
CustomForkJoinWorkerThreadFactory
-
在科特林
in Kotlin
//Custom class
class CustomForkJoinWorkerThreadFactory : ForkJoinPool.ForkJoinWorkerThreadFactory {
override fun newThread(pool: ForkJoinPool?): ForkJoinWorkerThread {
return CustomForkJoinWorkerThread(pool)
}
}
// helper class (probably only needed in kotlin)
class CustomForkJoinWorkerThread(pool: ForkJoinPool?) : ForkJoinWorkerThread(pool)
如果您需要有关反射的更多信息,以及为什么更改最终字段不好此处.简短摘要:由于优化,更新的final字段可能对其他对象不可见,并且可能会发生其他未知的副作用.
If you need more information about reflection and why it's not good to change final fields please refer to here and here. Short summary: due to optimizations the updated final field may not be visible to other objects and other unknown side effects may occur.
如前所述:这是一个非常肮脏的解决方案.如果使用此解决方案,可能会发生不良的副作用.像这样使用反射并不是一个好主意.如果您可以不加思索地使用解决方案(并在此处将其发布为答案!).
修改:单次通话的替代方式
就像问题本身所指出的那样:如果您仅在少数几个地方遇到此问题(即,自行修复呼叫本身就没有问题),则可以使用自己的从此处复制:
Like stated in the question itself: if you only have this problem in a small number of places (i.e. it's no problem to fix the call itself) you can use your own Executor. A simple example copied from here:
ExecutorService pool = Executors.newFixedThreadPool(10);
final CompletableFuture<String> future =
CompletableFuture.supplyAsync(() -> { /* ... */ }, pool);
这篇关于CompletableFuture/ForkJoinPool设置类加载器的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!