Завершенный комплект FutureJoinPool Set Loader

Я рассмотрел очень специфическую проблему, решение которой кажется чем-то простым:

Моя (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]

Ответ 1

Итак, вот очень грязное решение, которым я не горжусь, и может сломать для вас вещи, если вы согласитесь с ним:

Проблема заключалась в том, что загрузчик классов приложения не использовался для ForkJoinPool.commonPool(). Поскольку настройка commonPool является статической и поэтому во время запуска приложения нет легкой возможности (по крайней мере, насколько мне известно) внести изменения позже. Поэтому нам нужно полагаться на API отражения Java.

  1. создать ловушку после успешного запуска вашего приложения

    • в моем случае (среда Spring Boot) это будет ApplicationReadyEvent
    • чтобы прослушать это событие, вам нужен компонент, подобный следующему

      @Component
      class ForkJoinCommonPoolFix : ApplicationListener<ApplicationReadyEvent> {
          override fun onApplicationEvent(event: ApplicationReadyEvent?) {
        }
      }
      
  2. Внутри вашего хука вам нужно установить ForkJoinWorkerThreadFactory commonPool в пользовательскую реализацию (так что эта пользовательская реализация будет использовать загрузчик классов приложения)

    • в Котлине

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

Ответ 2

Кажется, что 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())

Ответ 3

Я столкнулся с чем-то похожим и нашел решение, которое не использует отражение и, кажется, хорошо работает с 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 для этого потока является правильным, который вам нужен (и не является загрузчиком класса System).

Вот некоторая справочная информация, которая может помочь:

Потоки общего пула Fork/Join возвращают загрузчик системного класса в качестве загрузчика класса контекста потока.

В Java SE 9 потоки, являющиеся частью общего пула fork/join, всегда возвращают загрузчик системного класса в качестве загрузчика класса контекста потока. В предыдущих выпусках загрузчик класса контекста потока мог быть унаследован от любого потока, вызывающего создание общего потока пула fork/join, например, отправив задачу. Приложение не может надежно зависеть от того, когда или как потоки создаются общим пулом fork/join, и поэтому не может надежно зависеть от пользовательского загрузчика классов, который будет установлен в качестве загрузчика класса контекста потока.

В результате вышеуказанного изменения обратной несовместимости вещи, которые используют ForkJoinPool, который работал в JDK8, могут не работать в JDK9+.