web-dev-qa-db-ja.com

Java複数のクラスローダーを持つServiceLoader

複数のクラスローダーがある環境で ServiceLoader を使用するためのベストプラクティスは何ですか?ドキュメントでは、初期化時に単一のサービスインスタンスを作成して保存することを推奨しています。

private static ServiceLoader<CodecSet> codecSetLoader = ServiceLoader.load(CodecSet.class);

これにより、現在のコンテキストクラスローダーを使用してServiceLoaderが初期化されます。ここで、このスニペットがWebコンテナーの共有クラスローダーを使用してロードされたクラスに含まれ、複数のWebアプリケーションが独自のサービス実装を定義したいとします。これらは上記のコードでは取得されません。ローダーが最初のwebappsコンテキストクラスローダーを使用して初期化され、他のユーザーに誤った実装を提供する可能性さえあります。

常に新しいServiceLoaderを作成することは、毎回サービスファイルを列挙して解析する必要があるため、無駄なパフォーマンスのように見えます。 編集:JavaのXPath実装に関するこの回答 に示すように、これは大きなパフォーマンスの問題になる可能性もあります。

他のライブラリはこれをどのように処理しますか?クラスローダーごとの実装をキャッシュしますか、毎回設定を再解析しますか、それとも単にこの問題を無視して1つのクラスローダーでのみ機能しますか?

36
Jörn Horstmann

個人的には、どのような状況でもServiceLoaderは好きではありません。それは遅く、不必要に無駄であり、それを最適化するためにあなたができることはほとんどありません。

また、少し限定されていることもわかります。タイプだけで検索する以上のことをしたい場合は、本当に邪魔をする必要があります。

xbean-FinderのResourceFinder

  • ResourceFinder は自己完結型ですJava ServiceLoaderの使用を置き換えることができるファイルです。コピー/貼り付けの再利用は問題ありません。これは1つですJavaファイルであり、ASL 2.0のライセンスが付与され、Apacheから入手できます。

注意スパンが短くなりすぎる前に、ServiceLoaderを置き換える方法を次に示します。

_ResourceFinder Finder = new ResourceFinder("META-INF/services/");
List<Class<? extends Plugin>> impls = Finder.findAllImplementations(Plugin.class);
_

これにより、クラスパス内のすべての_META-INF/services/org.acme.Plugin_実装が見つかります。

実際にはすべてのインスタンスがインスタンス化されるわけではないことに注意してください。必要なものを1つ選択すれば、インスタンスを持つことなくnewInstance()を呼び出すことができます。

なんでこれがいいの?

  • 適切な例外処理でnewInstance()を呼び出すのはどれほど難しいですか?難しくない。
  • 必要なものだけをインスタンス化する自由があるのは素晴らしいことです。
  • これでコンストラクター引数をサポートできます!

検索範囲の絞り込み

特定のURLだけを確認したい場合は、簡単に確認できます。

_URL url = new File("some.jar").toURI().toURL();
ResourceFinder Finder = new ResourceFinder("META-INF/services/", url);
_

ここでは、このResourceFinderインスタンスの使用状況で「some.jar」のみが検索されます。

クラスパスからURLを簡単に選択できるUrlSetという便利なクラスもあります。

_ClassLoader webAppClassLoader = Thread.currentThread().getContextClassLoader(); 
UrlSet urlSet = new UrlSet(webAppClassLoader);
urlSet = urlSet.exclude(webAppClassLoader.getParent());
urlSet = urlSet.matching(".*acme-.*.jar");

List<URL> urls = urlSet.getUrls();
_

代替の「サービス」スタイル

ServiceLoaderタイプの概念を適用してURL処理を再設計し、特定のプロトコルの_Java.net.URLStreamHandler_を検索/ロードしたいとします。

クラスパスでサービスをレイアウトする方法は次のとおりです。

  • _META-INF/Java.net.URLStreamHandler/foo_
  • _META-INF/Java.net.URLStreamHandler/bar_
  • _META-INF/Java.net.URLStreamHandler/baz_

ここで、fooは、以前と同じようにサービス実装の名前を含むプレーンテキストファイルです。ここで、誰かが_foo://..._ URLを作成するとします。そのための実装をすばやく見つけることができます。

_ResourceFinder Finder = new ResourceFinder("META-INF/");
Map<String, Class<? extends URLStreamHandler>> handlers = Finder.mapAllImplementations(URLStreamHandler.class);
Class<? extends URLStreamHandler> fooHandler = handlers.get("foo");
_

代替の「サービス」スタイル2

サービスファイルに構成情報を入れたいので、クラス名以外の情報も含まれているとします。サービスをプロパティファイルに解決する別のスタイルを次に示します。慣例により、1つのキーはクラス名であり、他のキーは注入可能なプロパティです。

したがって、ここでredはプロパティファイルです

  • _META-INF/org.acme.Plugin/red_
  • _META-INF/org.acme.Plugin/blue_
  • _META-INF/org.acme.Plugin/green_

以前と同じように検索できます。

_ResourceFinder Finder = new ResourceFinder("META-INF/");

Map<String,Properties> plugins = Finder.mapAllProperties(Plugin.class.getName());
Properties redDefinition = plugins.get("red");
_

これらのプロパティを_xbean-reflect_で使用する方法は次のとおりです。これは、フレームワークのないIoCを提供できるもう1つの小さなライブラリです。あなたはそれにクラス名と名前と値のペアを与えるだけで、それが構築され注入されます。

_ObjectRecipe recipe = new ObjectRecipe(redDefinition.remove("className").toString());
recipe.setAllProperties(redDefinition);

Plugin red = (Plugin) recipe.create();
red.start();
_

これが長い形式で「スペル」と表示される方法は次のとおりです。

_ObjectRecipe recipe = new ObjectRecipe("com.example.plugins.RedPlugin");
recipe.setProperty("myDateField","2011-08-29");
recipe.setProperty("myIntField","100");
recipe.setProperty("myBooleanField","true");
recipe.setProperty("myUrlField","http://www.stackoverflow.com");
Plugin red = (Plugin) recipe.create();
red.start();
_

_xbean-reflect_ライブラリは組み込みのJavaBeans APIを超える一歩ですが、GuiceやSpringなどのフルオンのIoCフレームワークに完全に移動する必要がないため、少し優れています。ファクトリメソッドとコンストラクタの引数、セッター/フィールドインジェクションをサポートしています。

ServiceLoaderがそれほど制限されているのはなぜですか?

JVMで非推奨のコードはJava言語自体に損傷を与えます。後でトリミングできないため、JVMに追加される前に多くのものが骨までトリミングされます。ServiceLoaderはAPIは制限されており、OpenJDKの実装はjavadocを含めて約500行程度です。

特別なことは何もありません。それを置き換えるのは簡単です。うまくいかない場合は、使用しないでください。

クラスパスのスコープ

APIはさておき、実際には、検索されるURLの範囲を狭めることがこの問題の真の解決策です。アプリケーションサーバーには、アプリケーションにjarを含めず、それ自体でかなり多くのURLがあります。たとえば、OSX上のTomcat 7には、StandardClassLoaderだけで約40個のURLがあります(これはすべてのwebappクラスローダーの親です)。

アプリサーバーが大きいほど、単純な検索でも時間がかかります。

複数のエントリを検索する場合、キャッシュは役に立ちません。同様に、いくつかの悪いリークを追加する可能性があります。本当の負け負けのシナリオになる可能性があります。

URLを本当に気になる5または12に絞り込みます。そうすれば、あらゆる種類のサービスのロードを実行でき、ヒットに気付くことはありません。

61
David Blevins

使用するクラスローダーを指定できるように、2つの引数のバージョンを使用してみましたか?つまり、Java.util.ServiceLoader.load(Class, ClassLoader)

6
Peter

ムー。

1x WebContainer <-> Nx WebApplicationシステムでは、WebContainerでインスタンス化されたServiceLoaderは、WebApplicationsで定義されたクラスではなく、コンテナ内のクラスのみを取得します。 WebApplicationでインスタンス化されたServiceLoaderは、コンテナで定義されたクラスに加えて、アプリケーションで定義されたクラスを検出します。

WebApplicationは個別に保持する必要があり、そのように設計されている必要があることに注意してくださいwillそれを回避しようとすると失敗し、コンテナを拡張するために利用できるメソッドとシステムではありません-ライブラリの場合シンプルなJarです。コンテナの適切な拡張フォルダにドロップするだけです。

4

私はコメントに追加したリンクでニールの答えが本当に好きです。最近のプロジェクトでも同じ経験があります。

「ServiceLoaderでもう1つ注意すべきことは、ルックアップメカニズムを抽象化しようとすることです。パブリッシュメカニズムは非常に美しくクリーンで宣言的です。グローバルな可視性を持たない任意の環境(OSGiやJava EE)など)にコードを配置すると、ひどく壊れるスキャナー。コードがそれに巻き込まれると、 OSGiで後でそれを実行するのは大変です。時間が来たら置き換えられる抽象化を書く方が良いでしょう。」

私は実際にOSGi環境でこの問題に遭遇しましたが、実際には私たちのプロジェクトではEclipseにすぎません。しかし、幸運にもそれをタイムリーに修正しました。私の回避策は、ロードしたいプラグインから1つのクラスを使用し、そこからclassLoaderを取得することです。これは有効な修正です。標準のServiceLoaderは使用しませんでしたが、プロセスはかなり似ています。プロパティを使用して、ロードする必要のあるプラグインクラスを定義します。そして、各プラグインのクラスローダーを知る別の方法があることも知っています。しかし、少なくともそれを使用する必要はありません。

正直なところ、ServiceLoaderで使用されるジェネリックは好きではありません。 1つのServiceLoaderが1つのインターフェースのクラスしか処理できないという制限があるためです。まあそれは本当に便利ですか?私の実装では、この制限によって強制されることはありません。ローダーの1つの実装を使用して、すべてのプラグインクラスをロードします。 2つ以上使う理由がわかりません。消費者が原因で、インターフェースと実装の間の関係について設定ファイルから知ることができます。

3
Clark Bao

この質問は、私が最初に予想したよりも複雑なようです。私が見るように、ServiceLoadersを処理するための3つの可能な戦略があります。

  1. 静的ServiceLoaderインスタンスを使用し、ServiceLoader参照を保持するクラスローダーと同じクラスローダーからのクラスのロードのみをサポートします。これは次の場合に機能します

    • サービスの構成と実装は共有クラスローダーにあり、すべての子クラスローダーは同じ実装を使用しています。ドキュメントの例は、ユースケースを対象としています。

      または

    • 構成と実装は各子クラスローダーに入れられ、WEB-INF/libの各Webアプリに沿ってデプロイされます。

    このシナリオでは、サービスを共有クラスローダーにデプロイして、各webappに独自のサービス実装を選択させることはできません。

  2. 現在のスレッドのコンテキストクラスローダーを2番目のパラメーターとして渡して、アクセスごとにServiceLoaderを初期化します。このアプローチはJAXPおよびJAXB APIとして採用されていますが、ServiceLoaderの代わりに独自の FactoryFinder 実装を使用しています。したがって、XMLパーサーをWebアプリケーションにバンドルして、たとえばDocumentBuilderFactory#newInstanceによって自動的に取得されるようにすることができます。

    このルックアップには パフォーマンスへの影響 がありますが、xml解析の場合、実装をルックアップする時間は実際にxmlドキュメントを解析するのに必要な時間と比べて短いです。ライブラリでは、ファクトリー自体はかなり単純であるため、ルックアップ時間がパフォーマンスを支配することになると想定しています。

  3. どういうわけか、コンテキストクラスローダーをキーとして実装クラスをキャッシュします。上記のすべてのケースで、メモリリークを発生させることなくこれが可能かどうかは、完全にはわかりません。

結論として、私はおそらくこの問題を無視し、ライブラリを各webapp内にデプロイする必要があります(つまり、上記のオプション1b)。

1
Jörn Horstmann