CompletableFuture/ForkJoinPool设置类加载器

Moe*_*Pad 13 java spring classloader completable-future

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

我的(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创建的,不是吗?

    • 在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]
Run Code Online (Sandbox Code Playgroud)

Neo*_*Neo 10

我遇到了类似的问题,并提出了一个不使用反射并且似乎与 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());
        }
    }
} 
Run Code Online (Sandbox Code Playgroud)

然后在您的应用程序启动脚本中设置系统属性

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

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

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

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

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

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


Dav*_*nós 6

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

主要思想是ForkJoinPool使用自定义 ThreadFactory创建自定义,该自定义 ThreadFactory 使用我们自己的 ClassLoader(不是系统类加载器 - 此行为始于 jdk9-)

一点历史
在 jdk9ForkJoinPool.common()返回一个带有主线程类加载器的 Executor之前,在Java 9 中,这种行为发生了变化,并返回一个带有系统 jdk 系统类加载器的执行器。因此,由于此更改,在从 Java 8 升级到 Java 9 / 10 / 11 时,很容易在 CompletableFutures 代码中找到 ClassNotFoundExceptions。

解决方案 像Neo 之前在 anwser 中所说的那样创建我们自己的工厂,并使用这个工厂 yo 创建一个 ForkJoinPool 和一个 Executor

MyForkJoinWorkerThreadFactory factory = new MyForkJoinWorkerThreadFactory();

ForkJoinPool myCommonPool = new ForkJoinPool(Math.min(32767, Runtime.getRuntime().availableProcessors()), factory, null, false);
Run Code Online (Sandbox Code Playgroud)

像这样使用它

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();
Run Code Online (Sandbox Code Playgroud)

Extraball
如果您使用的是基于 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;
}
Run Code Online (Sandbox Code Playgroud)

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

@Autowired private Executor customExecutor;

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

Run Code Online (Sandbox Code Playgroud)


Ham*_*mdi 5

似乎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不是应用类加载器)。

一种可能的解决方案是通过您的应用程序类加载器覆盖线程上下文类加载器: thread.setContextClassLoader(appClass.class.getClassLoader())


Moe*_*Pad 5

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

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

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

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

      @Component
      class ForkJoinCommonPoolFix : ApplicationListener<ApplicationReadyEvent> {
          override fun onApplicationEvent(event: ApplicationReadyEvent?) {
        }
      }
      
      Run Code Online (Sandbox Code Playgroud)
  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
      
      Run Code Online (Sandbox Code Playgroud)
  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)
      
      Run Code Online (Sandbox Code Playgroud)

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

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

编辑:单个呼叫的替代

就像问题本身中所述:如果您仅在少数几个地方遇到此问题(即,自行修复呼叫本身就没有问题),则可以使用自己的Executor。从这里复制的一个简单示例:

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