CompletableFuture / ForkJoinPool设置类加载器

时间:2018-03-05 14:55:37

标签: java spring classloader completable-future

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

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

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

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

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

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

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

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

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

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

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

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

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

完整的堆栈跟踪:

AppClassLoader

4 个答案:

答案 0 :(得分:2)

我遇到了类似的情况,并提出了一个不使用反射的解决方案,并且似乎可以与JDK9-JDK11一起很好地工作。

javadocs的意思是:

  

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

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

因此,如果您推出自己的ForkJoinWorkerThreadFactory版本,并使用system属性将其设置为使用正确的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是您所需的正确类(而不是System类加载器)

以下background可能会有所帮助:

  

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

     

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

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

答案 1 :(得分:1)

似乎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())

答案 2 :(得分:1)

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

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

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

    • 就我而言(Spring Boot环境),这将是ApplicationReadyEvent
    • 要收听此活动,您需要一个类似以下内容的组件

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

    • 在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)
      
  4. 如果您需要有关反思的更多信息以及更改最终字段please refer to herehere的原因。简短摘要:由于优化,更新的最终字段可能对其他对象不可见,并且可能发生其他未知副作用。

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

    编辑:单次通话的替代方法

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

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

答案 3 :(得分:1)

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

中的新构造函数。

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

历史悠久
在jdk9 ForkJoinPool.common()之前,在Java 9 this behave changes中返回带有主线程的ClassLoader的执行程序,并返回带有系统jdk系统类加载器的执行程序。 因此,由于此更改,在从Java 8升级到Java 9/10/11时,很容易在CompletableFutures代码中找到ClassNotFoundExceptions。

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

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();