【问题标题】:CompletableFuture / ForkJoinPool Set Class LoaderCompletableFuture / ForkJoinPool 集合类加载器
【发布时间】:2018-03-05 14:55:36
【问题描述】:

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

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

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

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

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

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

我发现ForkJoinPool 用于创建线程。但在我看来,一切都是staticfinal。所以我怀疑即使设置一个带有系统属性的自定义ForkJoinWorkerThreadFactory 在这种情况下也会有所帮助。还是会?

编辑回答 cmets 的问题:

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

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

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

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

    • 1234563 .这似乎证实了我在第一种情况下的假设,即公共池不是由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.<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]

【问题讨论】:

  • 你部署到哪里?这是否在 jetty / tomcat / 任何 JEE 容器中运行?
  • 您遇到的具体问题是什么?您提交给supplyAsync() 的作业是从AppClassLoader 创建的,不是吗?所以他们应该可以访问它的类。这对我来说就像一个XY problem
  • 感谢你们两位的cmets!请在上面编辑过的问题中找到答案。谢谢!
  • 您应该提供异常的完整堆栈跟踪。此外,这可能是 keycloak 特有的——因此相应的标签可能是相关的。
  • 所以这与您在Spring Boot / Security classloader issues with Keycloak run from terminal 中遇到的问题完全相同。你最后解决了吗?我还注意到此错误消息特定于 Java 9。您是否尝试过 8?这可能是一种回归。

标签: java spring classloader completable-future


【解决方案1】:

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

javadocs 是这样说的:

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

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

因此,如果您推出自己的 ForkJoinWorkerThreadFactory 版本并将其设置为使用系统属性使用正确的 ClassLoader,这应该可以工作。

这是我的自定义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 类并初始化 commonPool 时,此线程的上下文 ClassLoader 是您需要的正确的类加载器(而不是系统类加载器)。

这里有一些background 可能会有所帮助:

Fork/Join 公共池线程返回系统类加载器作为它们的线程上下文类加载器

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

由于上述向后不兼容的变化,使用 ForkJoinPool 的东西在 JDK8 中可能无法在 JDK9+ 中使用。

【讨论】:

  • 如您所说,此解决方案在 JDK9+ 中不起作用,因此我将根据您的解决方案在下面发布一个新的完整解决方案。谢谢
  • MyForkJoinWorkerThread构造函数中的类加载器不应该这样设置吗? setContextClassLoader(MyForkJoinWorkerThreadFactory.class.getClassLoader());
【解决方案2】:

所以,这是一个非常肮脏的解决方案,我并不为此感到自豪,如果你接受它,可能会破坏你的事情

问题是应用程序的类加载器没有用于ForkJoinPool.commonPool()。因为 commonPool 的设置是静态的,因此在应用程序启动期间(至少据我所知)很难在以后进行更改。所以我们需要依赖Java反射API

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

    • 在我的情况下(Spring Boot 环境),这将是 ApplicationReadyEvent
    • 要收听此事件,您需要如下所示的组件

      @Component
      class ForkJoinCommonPoolFix : ApplicationListener<ApplicationReadyEvent> {
          override fun onApplicationEvent(event: ApplicationReadyEvent?) {
        }
      }
      
  2. 在您的钩子中,您需要将 commonPool 的 ForkJoinWorkerThreadFactory 设置为自定义实现(因此此自定义实现将使用应用类加载器)

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

如果您需要更多关于反射的信息,以及为什么更改 final 字段 please refer to herehere 不好。简短摘要:由于优化,更新后的 final 字段可能对其他对象不可见,并且可能会出现其他未知的副作用。

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

编辑:单次调用的替代方案

就像问题本身所述:如果您只在少数地方遇到此问题(即修复呼叫本身没有问题),您可以使用自己的Executor。一个简单的例子copied from here

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

【讨论】:

  • 添加了一个没有反射的工作解决方案。鉴于您已经连接了具有正确类加载器的 CustomForkJoinWorkerThreadFactory,这也应该对您有用。
【解决方案3】:

在 jdk11 中有效的一种可能的解决方案(使用 Spring Boot 2.2 测试)是利用 ForkJoinPool 中的新构造函数

主要思想是使用自定义的 ThreadFactory 创建一个自定义的ForkJoinPool,该线程工厂使用我们自己的 ClassLoader(不是系统一 - 这种行为始于 jdk9-)

一点历史
在 jdk9 ForkJoinPool.common() 返回一个带有你主线程的 ClassLoader 的 Executor 之前,在 Java 9 this behave changes 中,并返回一个带有系统 jdk 系统类加载器的 executor。 所以在从 Java 8 升级到 Java 9 / 10 / 11 时,很容易在 CompletableFutures 代码中找到 ClassNotFoundExceptions,因为这个变化。

解决方案 创建我们自己的工厂,如Neo said in an anwser before 并使用这个工厂创建一个 ForkJoinPool 和一个 Executor

MyForkJoinWorkerThreadFactory factory = new MyForkJoinWorkerThreadFactory();

ForkJoinPool myCommonPool = new ForkJoinPool(Math.min(32767, Runtime.getRuntime().availableProcessors()), factory, null, false);

这样使用

CompletableFuture.runAsync(() -> {
   log.info(Thread.currentThread().getName()+" "+Thread.currentThread().getContextClassLoader().toString());  
   // will print the classloader from the Main Thread, not the jdk system one :)
}, myCommonPool).join();

外球
如果您使用基于 Spring 的应用程序,则需要将您的 Spring Security Context 添加到新的自定义线程池中

@Bean(name = "customExecutor")
public Executor customExecutor() {
    MyForkJoinWorkerThreadFactory factory = new MyForkJoinWorkerThreadFactory();
    ForkJoinPool myCommonPool = new ForkJoinPool(Math.min(32767, Runtime.getRuntime().availableProcessors()), factory, null, false);

    DelegatingSecurityContextExecutor delegatingExecutorCustom = new DelegatingSecurityContextExecutor(myCommonPool, SecurityContextHolder.getContext());
    return delegatingExecutorCustom;
}

并像使用任何其他资源一样使用它自动装配

@Autowired private Executor customExecutor;

CompletableFuture.runAsync(() -> {
    ....
}, customExecutor).join();

【讨论】:

    【解决方案4】:

    似乎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 的子级)。

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

    【讨论】:

    • 感谢您的建议。这基本上是我已经知道的,但我仍然感谢任何建设性的评论:) 我赞成你的回答,所以你因为能够发布 cmets 而获得了一些声誉;)
    • 感谢您的支持 :) 是的,我想将其作为评论发布,但由于我不能,我不得不将其作为答案。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-06-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多