CompletableFuture / ForkJoinPool设置类加载器

我解决了一个非常具体的问题,其解决方案似乎是基本的:

我的(Spring)应用程序的类加载器层次结构是这样的: SystemClassLoader -> PlatformClassLoader -> AppClassLoader

如果我使用Java CompleteableFuture来运行线程。 线程的ContextClassLoader是: SystemClassLoader -> PlatformClassLoader -> ThreadClassLoader

因此,虽然我不得不访问AppClassLoader任何类,因为所有外部库类都驻留在那里。

源代码库非常大,所以我不希望/不能将所有与线程相关的部分重写为其他内容(例如,将自定义执行程序传递给每个调用)。

所以我的问题是: 如何使用例如CompleteableFuture.supplyAsync()创建的线程使用AppClassLoader作为父级? (而不是PlatformClassloader

我发现ForkJoinPool用于创建线程。 但在我看来,一切都是静态的最终的 。 所以我怀疑即使在系统属性中设置自定义ForkJoinWorkerThreadFactory也会有所帮助。 或者是吗?

编辑以回答评论中的问题:

  • 你在哪里部署? 这是在jetty / tomcat /任何JEE容器内运行吗?

    • 我正在使用默认的Spring Boot设置,因此使用内部tomcat容器。
  • 你有什么确切的问题?

    • 确切的问题是: java.lang.IllegalArgumentException:从类加载器中看不到从方法引用的org.keycloak.admin.client.resource.RealmsResource
  • 您提交给supplyAsync()的作业是从AppClassLoader创建的,不是吗?

    • MainThread调用supplyAsync ,它使用AppClassLoader 。 但是,调试应用程序显示所有此类线程都将PlatformClassLoader作为其父级。 至于我的理解,这是因为ForkJoinPool.commonPool()是在应用程序启动期间构建的(因为它是静态的),因此使用默认的类加载器作为父类PlatformClassLoader 。 因此,来自此池的所有线程都将PlatformClassLoader作为ContextClassLoader (而不是AppClassLoader )的父级。

    • 当我在MainThread创建自己的执行程序并将此执行程序传递给supplyAsync一切正常 – 我可以在调试期间看到,确实现在AppClassLoader是我的ThreadClassLoader的父ThreadClassLoader 。 这似乎证实了我在第一种情况下的假设,即公共池不是由MainThread创建的,至少不是在使用AppClassLoader本身时。

完整堆栈跟踪:

 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] 

似乎resteasy lib使用线程上下文类加载器来加载一些资源: http ://grepcode.com/file/repo1.maven.org/maven2/org.jboss.resteasy/resteasy-client/3.0-beta-1/org/ jboss / resteasy / client / jaxrs / ProxyBuilder.java#21 。

当resteasy尝试加载所请求的类时,它将要求线程类加载器查找并加载它(如果可能),当请求的类位于类加载器不可见的类路径中时,操作失败。

确切地说,您的应用程序会发生什么: ThreadClassLoader尝试加载位于应用程序类路径中的资源,因为此类路径中的资源只能从AppClassLoader及其子级访问,因此ThreadClassLoader无法加载它( ThreadClassLoader不是AppClassLoader )。

一个可能的解决方案是通过你的app ClassLoader覆盖线程上下文ClassLoader: thread.setContextClassLoader(appClass.class.getClassLoader())

所以,这是一个非常肮脏的解决方案,我并不为此感到骄傲,如果你同意它可能会为你解决问题:

问题是该应用程序的类加载器未用于ForkJoinPool.commonPool() 。 因为commonPool的设置是静态的,因此在应用程序启动期间,没有简单的可能性(至少据我所知)以后进行更改。 所以我们需要依赖JavareflectionAPI

  1. 应用程序成功启动后创建一个钩子

    • 在我的情况下(Spring Boot环境),这将是ApplicationReadyEvent
    • 要监听此事件,您需要一个类似以下@Component class ForkJoinCommonPoolFix : ApplicationListener { override fun onApplicationEvent(event: ApplicationReadyEvent?) { } }的组件@Component class ForkJoinCommonPoolFix : ApplicationListener { override fun onApplicationEvent(event: ApplicationReadyEvent?) { } }
  2. 在你的钩子里,你需要将commonPool的ForkJoinWorkerThreadFactory设置为自定义实现(所以这个自定义实现将使用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
  3. CustomForkJoinWorkerThreadFactory简单实现

    • 在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)

如果您需要有关reflection的更多信息以及更改最终字段的原因, 请参阅此处和此处 。 简短摘要:由于优化,更新的最终字段可能对其他对象不可见,并且可能发生其他未知副作用。

如前所述:这是一个非常脏的解决方案。 如果使用此解决方案,可能会出现不需要的副作用。 使用这样的reflection不是一个好主意。 如果您可以使用没有reflection的解决方案(并在此处将其作为答案发布!)。

编辑:单个呼叫的替代方案

就像在问题本身中所述:如果你只在少数地方遇到这个问题(即修复调用本身没问题),你可以使用自己的执行程序 。 从这里复制一个简单的例子:

 ExecutorService pool = Executors.newFixedThreadPool(10); final CompletableFuture future = CompletableFuture.supplyAsync(() -> { /* ... */ }, pool);