java替换forkjoinpool,CompletableFuture / ForkJoinPool设置类加载器

I tackled down a very specific problem, whose solution seems to be something basic:

My (Spring) application's classloader hierarchy is something like this: SystemClassLoader -> PlatformClassLoader -> AppClassLoader

If I use Java CompleteableFuture to run threads. the ContextClassLoader of the threads is: SystemClassLoader -> PlatformClassLoader -> ThreadClassLoader

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).

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)

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?

Edit to answer the questions from the comments:

where do you deploy to? Is this running within jetty / tomcat / any JEE container?

I'm using the default Spring Boot setup so an internal tomcat container is used.

What is the exact issue you have?

The exact issue is: java.lang.IllegalArgumentException: org.keycloak.admin.client.resource.RealmsResource referenced from a method is not visible from class loader

The jobs that you submit to supplyAsync() are created from the AppClassLoader, aren't they?

The supplyAsync is called from the MainThread which uses the AppClassLoader. But, debugging the applications shows that all such threads have PlatformClassLoader 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 is PlatformClassLoader. So, all threads from this pool get PlatformClassLoader as their parent for ContextClassLoader (instead of 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.

Full stacktrace:

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.(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:

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 {

override fun onApplicationEvent(event: ApplicationReadyEvent?) {

}

}

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

Simple implementation of 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)

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.

As stated before: this is a very dirty solution. Unwanted side effects may occur if you use this solution. Using reflections like this is not a good idea. If you can use a solution without reflection (and post it as an answer here!).

Edit: Alternative for single calls

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 future =

CompletableFuture.supplyAsync(() -> { /* ... */ }, pool);

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值