web-dev-qa-db-ja.com

CompletableFuture / ForkJoinPoolセットクラスローダー

私は非常に具体的な問題に取り組みました。その解決策は基本的なもののようです。

私の(春の)アプリケーションのクラスローダー階層は次のようなものです:_SystemClassLoader -> PlatformClassLoader -> AppClassLoader_

Java CompleteableFutureを使用してスレッドを実行する場合。スレッドのContextClassLoaderは次のとおりです。_SystemClassLoader -> PlatformClassLoader -> ThreadClassLoader_

したがって、AppClassLoaderのクラスにはアクセスできませんが、すべての外部ライブラリクラスがそこにあるためアクセスする必要があります。

ソースベースは非常に大きいので、スレッドに関連するすべての部分を別のものに書き直したくない/書き直せません(たとえば、各呼び出しにカスタムエグゼキューターを渡します)。

だから私の質問は:どうすれば例えばによって作成されたスレッドを作ることができますか? CompleteableFuture.supplyAsync()AppClassLoaderを親として使用しますか?PlatformClassloaderの代わりに)

ForkJoinPool がスレッドの作成に使用されていることがわかりました。しかし、私には思えますが、そこにあるものはすべてstaticfinalです。したがって、この場合、システムプロパティを使用してカスタム ForkJoinWorkerThreadFactory を設定しても役立つとは思えません。それともそれでしょうか?

コメントからの質問に答えるために編集してください:

  • どこにデプロイしますか?これはjetty/Tomcat /任意のJEEコンテナ内で実行されていますか?

    • デフォルトのSpringBootセットアップを使用しているため、内部のTomcatコンテナーが使用されます。
  • あなたが抱えている正確な問題は何ですか?

    • 正確な問題は次のとおりです。Java.lang.IllegalArgumentException:メソッドから参照されるorg.keycloak.admin.client.resource.RealmsResourceがクラスローダーから表示されません
  • supplierAsync()に送信するジョブは、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]
_
17
Moe Pad

だから、これは私が誇りに思っていない非常に汚い解決策ですあなたのために物事を壊すかもしれませんあなたがそれに沿って行けば:

問題は、アプリケーションのクラスローダーが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)
      

必要な場合リフレクションの詳細そして最終フィールドを変更するのが良くない理由 ここを参照してください および ここ 。簡単な要約:最適化により、更新された最終フィールドが他のオブジェクトに表示されない場合があり、他の未知の副作用が発生する場合があります。

前に述べたように:これは非常に汚い解決策です。このソリューションを使用すると、望ましくない副作用が発生する可能性があります。このような反射を使用することはお勧めできません。リフレクションなしでソリューションを使用できる場合(そしてここに回答として投稿してください!)

編集:シングルコールの代替

質問自体で述べられているように、この問題が少数の場所でのみ発生する場合(つまり、呼び出し自体を修正することは問題ありません)、独自の Executor を使用できます。簡単な例 ここからコピー

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

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

要求されたクラスを再度ロードしようとすると、スレッドクラスローダーにそれを探してロードするように要求します。要求されたクラスがクラスローダーから見えないクラスパスにある場合、操作は失敗します。

そして、まさにそれがあなたのアプリケーションに起こることです:ThreadClassLoaderは、アプリケーションのクラスパスにあるリソースを読み込もうとしました。このクラスパスのリソースはAppClassLoaderとその子の場合、ThreadClassLoaderはロードに失敗しました(ThreadClassLoaderAppClassLoader)の子ではありません。

考えられる解決策は、アプリのClassLoaderによってスレッドコンテキストClassLoaderをオーバーライドすることです。thread.setContextClassLoader(appClass.class.getClassLoader())

4
Hamdi

私は似たようなものに出くわし、リフレクションを使用せず、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クラスローダーではない)と想定して機能します。

ここにいくつかの 背景 が役立つかもしれません:

フォーク/結合共通プールスレッドは、システムクラスローダーをスレッドコンテキストクラスローダーとして返します

Java SE 9では、フォーク/結合共通プールの一部であるスレッドは、常にシステムクラスローダーをスレッドコンテキストクラスローダーとして返します。以前のリリースでは、スレッドコンテキストクラスローダーにタスクの送信などによって、fork/join共通プールスレッドの作成を引き起こすスレッドから継承されます。アプリケーションは、fork/join共通プールによってスレッドがいつ、どのように作成されるかに確実に依存できないため、確実に作成できません。スレッドコンテキストクラスローダーとして設定されるカスタム定義のクラスローダーに依存します。

上記の後方互換性の変更の結果として、JDK8で機能していたForkJoinPoolを使用するものがJDK9 +で機能しない場合があります。

3
Neo

Jdk11(Spring Boot 2.2を使用してテスト)で有効な1つの可能な解決策は、ForkJoinPoolの新しいコンストラクターを利用することです。

主なアイデアは、独自のClassLoaderを使用するカスタムThreadFactoryを使用してカスタムForkJoinPoolを作成することです(システムのものではありません-この動作はjdk9で始まります-)

ちょっとした歴史
jdk9の前にForkJoinPool.common()は、メインスレッドのClassLoaderを使用してエグゼキュータを返します。 Java 9では、これは変更 を実行し、システムjdkシステムクラスローダー。したがって、この変更により、Java 8からJava 9/10/11にアップグレードするときに、CompletableFuturesコード内でClassNotFoundExceptionsを簡単に見つけることができます。

Solutionのような独自のファクトリを作成する前にAnwserでNeoが言った そしてこのファクトリを使用して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セキュリティコンテキストを新しいカスタムスレッドプールに追加する必要があります。

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

1
David Canós