当前位置: 首页 > 面试题库 >

CompletableFuture / ForkJoinPool设置类加载器

汪建白
2023-03-14
问题内容

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

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

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

因此,AppClassLoader尽管必须访问,但无法访问任何类,因为所有外部库类都驻留在该类中。

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

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

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

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

  • 部署到哪里? 是否在码头/ tomcat /任何JEE容器中运行?

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

    • 确切的问题是: java.lang.IllegalArgumentException:org.keycloak.admin.client.resource.Realms从类加载器看不到方法引用的资源

    • 您提交给supplyAsync()的作业是从AppClassLoader创建的,不是吗?

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

    • 当我在中创建自己的执行程序MainThread并将该执行程序传递给supplyAsync所有可用的程序时,我可以在调试过程中看到确实现在AppClassLoader是我的执行程序的父级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.<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]

问题答案:

因此,这是一个非常肮脏的解决方案,我不为此感到骄傲,如果您继续使用它, 可能会为您 带来麻烦:

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

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

    • 就我而言(Spring Boot环境),这将是ApplicationReadyEvent
    • 要收听此事件,您需要以下组件
          @Component
      

      class ForkJoinCommonPoolFix : ApplicationListener {
      override fun onApplicationEvent(event: ApplicationReadyEvent?) {
      }
      }

  2. 在挂钩中,您需要将ForkJoinWorkerThreadFactorycommonPool 设置为自定义实现(因此此自定义实现将使用应用程序类加载器)

    • 在科特林
          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)

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

如前所述:这是一个非常肮脏的解决方案
如果使用此解决方案,可能会发生有害的副作用。使用这样的反射不是一个好主意。如果您可以使用没有反思的解决方案(并在此处发布答案!)。

编辑:单个呼叫的替代

就像问题本身所指出的那样:如果您仅在少数几个地方遇到此问题(即自行解决此问题本身就没有问题),则可以使用自己的Executor。从此处复制的一个简单示例:

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


 类似资料:
  • 我处理了一个非常具体的问题,其解决办法似乎是基本的: 我的(Spring)应用程序的类加载器层次结构如下所示: 因此,我不能访问中的任何类,尽管我必须访问,因为所有外部库类都驻留在那里。 源库相当大,所以我不想/不能将所有线程相关部分重写为其他内容(例如,向每个调用传递一个自定义执行器)。 所以我的问题是:如何使例如创建的线程使用作为父级?(而不是) 我发现ForkJoinPool用于创建线程。但

  • 问题内容: 我是Java的新手,我不确定如何从单独的程序jar中访问位于特定目录中的类文件。 例如,我在/中有一个第三方jar文件,该文件应该加载/mylib/MyClass.class中的MyClass,因此我尝试运行: 但我得到了错误: 我究竟做错了什么? 问题答案: 当您使用,然后 只 在中定义的属性文件里面的文件会影响到类路径。 它还将忽略该参数(或更具体地说:将其解释为)中定义的主类的参

  • 我有IBMWebSphere8.5.5.10和hibernate-jpa-2.0-api-1.0.0。Final.jar是默认的jar。然而,我想用一个更新的版本- 看起来发生此错误是因为先加载了旧的/父级的jar。而且请注意,尽管我定义了不同的委托模式,但委托模式首先是PARENT_。我多次检查我的类加载器定义,重新启动应用程序甚至服务器,并且始终保持不变:( 有人能帮忙吗?

  • 代码: 我有上面的代码来并行执行一些任务。考虑到已经让调用线程等待完成,不知道它是否应该是而不是块中的。 注意:仅从输入列表中读取。

  • 以下是错误消息 java.lang.IllegalStateException:无法加载ApplicationContext 一个使用elasticSearch、mysql、redis等的Spingboot项目,谷歌有很多,但他只是一个新的Spingboot。网上的东西不管用。我不知道怎么改。 application-local.yml 应与ES的配置相关 控制器

  • 框架中所有的类都是通过类加载器(ClassLoader)加载的,通过Loader我们可以实现类的统一管理。下面我们一起来看看Loader提供了哪些加载方法: 1. Loader::import 加载一个类或者加载一个包 方法原型 import( $classPath, $type = IMPORT_APP, $extension=EXT_PHP ) 参数名称 参数说明 $classPath 文件的