CompletableFuture / ForkJoinPool Set Class Loader

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

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

如果我使用Java来运行线程。线程的值为:CompleteableFutureContextClassLoaderSystemClassLoader -> PlatformClassLoader -> ThreadClassLoader

因此,尽管我必须访问中的任何类,因为我必须这样做,因为所有外部库类都驻留在其中。AppClassLoader

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

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

我发现ForkJoinPool用于创建线程。但在我看来,那里的一切都是静态的和最终的。因此,我怀疑在这种情况下,即使使用系统属性设置自定义ForkJoinWorkerThreadFactory也会有所帮助。还是会?

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

  • 部署到哪里?这是在码头/雄猫/任何JEE容器内运行的吗?

    • 我使用的是默认的Spring Boot设置,因此使用了内部tomcat容器。
  • 您遇到的确切问题是什么?

    • 确切的问题是:java.lang.IllegalArgumentException: org.keycloak.admin.client.resource.Realms从方法引用的资源在类装入器中不可见
  • 您提交给 supplyAsync() 的作业是从 AppClassLoader 创建的,不是吗?

    • 从 调用,它使用 .但是,调试应用程序表明,所有这些线程都作为其父线程。至于我的理解,发生这种情况是因为ForkJoinPool.commonPool()是在应用程序启动期间构造的(因为它是静态的),因此使用默认类装入器作为父级,即.因此,此池中的所有线程都作为 ContextClassLoader 的父线程(而不是 )。supplyAsyncMainThreadAppClassLoaderPlatformClassLoaderPlatformClassLoaderPlatformClassLoaderAppClassLoader

    • 当我在内部创建自己的执行器并将此执行器传递给一切工作时 - 我可以在调试期间看到现在确实是我的父级。这似乎证实了我在第一种情况下的假设,即公共池不是由至少不是在使用自身时创建的。MainThreadsupplyAsyncAppClassLoaderThreadClassLoaderMainThreadAppClassLoader

全栈跟踪:

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]

答案 1

我遇到了类似的事情,并提出了一个不使用反射的解决方案,似乎与JDK9-JDK11配合得很好。

以下是javadocs所说的:

用于构造公共池的参数可以通过设置以下系统属性来控制:

  • java.util.concurrent.ForkJoinPool.common.threadFactory - ForkJoinPool.ForkJoinWorkerThreadFactory的类名。系统类装入器用于装入此类。

因此,如果您推出自己的版本,并将其设置为使用正确的使用系统属性,这应该有效。ForkJoinWorkerThreadFactoryClassLoader

这是我的自定义:ForkJoinWorkerThreadFactory

package foo;

public class MyForkJoinWorkerThreadFactory implements ForkJoinWorkerThreadFactory {

    @Override
    public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
        return new MyForkJoinWorkerThread(pool);
    }

    private static class MyForkJoinWorkerThread extends ForkJoinWorkerThread {

        private MyForkJoinWorkerThread(final ForkJoinPool pool) {
            super(pool);
            // set the correct classloader here
            setContextClassLoader(Thread.currentThread().getContextClassLoader());
        }
    }
} 

,然后在应用启动脚本中设置系统属性

-Djava.util.concurrent.ForkJoinPool.common.threadFactory=foo.MyForkJoinWorkerThreadFactory

上面的解决方案是假设第一次引用 ForkJoinPool 类并初始化 ,则此线程的上下文 ClassLoader 是您需要的正确上下文 ClassLoader(而不是 System 类加载器)。commonPool

以下是一些可能有帮助的背景知识:

Fork/Join 公共池线程返回系统类装入器作为其线程上下文类装入器

在 Java SE 9 中,属于 fork/join 公共池的线程将始终返回系统类装入器作为其线程上下文类装入器。在以前的发行版中,线程上下文类装入器可能继承自导致创建 fork/join 公共池线程的任何线程,例如通过提交任务。应用程序不能可靠地依赖于 fork/join 公共池何时或如何创建线程,因此不能可靠地依赖于要设置为线程上下文类装入器的自定义类装入器。

由于上述向后不兼容更改,使用用于在 JDK8 中工作的的东西在 JDK9+ 中可能不起作用。ForkJoinPool


答案 2

所以,这是一个非常肮脏的解决方案,我对此并不感到自豪,如果你同意它,可能会为你打破一些东西

问题是应用程序的类装入器未用于 。由于commonPool的设置是静态的,因此在应用程序启动期间,以后没有容易进行更改的可能性(至少据我所知)。因此,我们需要依赖 Java 反射 APIForkJoinPool.commonPool()

  1. 在应用程序成功启动后创建挂钩

    • 在我的情况下(Spring Boot环境),这将是应用程序ReadyEvent
    • 要侦听此事件,您需要一个如下所示的组件

      @Component
      class ForkJoinCommonPoolFix : ApplicationListener<ApplicationReadyEvent> {
          override fun onApplicationEvent(event: ApplicationReadyEvent?) {
        }
      }
      
  2. 在你的钩子里面,你需要将commonPool设置为自定义实现(所以这个自定义实现将使用应用程序类加载器)ForkJoinWorkerThreadFactory

    • (科特林)

      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

    • (科特林)

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

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

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

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

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

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

推荐