1. 概要

実行時に使用可能なすべてのクラスを見つけるなど、アプリケーションの実行時の動作に関する情報を取得したい場合があります。

このチュートリアルでは、実行時にJavaパッケージ内のすべてのクラスを検索する方法のいくつかの例を検討します。

2. クラスローダー

まず、 JavaクラスローダーJavaクラスローダーは、JavaクラスをJava仮想マシン(JVM)に動的にロードするJavaランタイム環境(JRE)の一部です。 Javaクラスローダーは、JREをファイルおよびファイルシステムについての知識から切り離します。 すべてのクラスが単一のクラスローダーによってロードされるわけではありません。

Javaで利用可能なクラスローダーを図で理解しましょう。

Java 9では、クラスローダーにいくつかの大きな変更が加えられました。 モジュールの導入により、クラスパスと一緒にモジュールパスを提供するオプションがあります。 システムクラスローダーは、モジュールパスに存在するクラスをロードします。

クラスローダーは動的です。 実行時に提供できるクラスをJVMに通知する必要はありません。 したがって、パッケージ内のクラスの検索は、 Java Reflection を使用して実行される操作ではなく、基本的にファイルシステム操作です。

ただし、独自のクラスローダーを作成するか、クラスパスを調べてパッケージ内のクラスを見つけることができます。

3. Javaパッケージでのクラスの検索

説明のために、パッケージを作成しましょう com.baeldung.reflection.access.packages.search.

それでは、サンプルクラスを定義しましょう。

public class ClassExample {
    class NestedClass {
    }
}

次に、インターフェースを定義しましょう。

public interface InterfaceExample {
}

次のセクションでは、システムクラスローダーといくつかのサードパーティライブラリを使用してクラスを見つける方法を見ていきます。

3.1. システムクラスローダー

まず、組み込みのシステムクラスローダーを使用します。システムクラスローダーは、クラスパスにあるすべてのクラスをロードします。 これは、JVMの初期初期化中に発生します。

public class AccessingAllClassesInPackage {

    public Set<Class> findAllClassesUsingClassLoader(String packageName) {
        InputStream stream = ClassLoader.getSystemClassLoader()
          .getResourceAsStream(packageName.replaceAll("[.]", "/"));
        BufferedReader reader = new BufferedReader(new InputStreamReader(stream));
        return reader.lines()
          .filter(line -> line.endsWith(".class"))
          .map(line -> getClass(line, packageName))
          .collect(Collectors.toSet());
    }
 
    private Class getClass(String className, String packageName) {
        try {
            return Class.forName(packageName + "."
              + className.substring(0, className.lastIndexOf('.')));
        } catch (ClassNotFoundException e) {
            // handle the exception
        }
        return null;
    }
}

上記の例では、静的 getSystemClassLoader()メソッドを使用してシステムクラスローダーをロードしています。

次に、指定されたパッケージ内のリソースを見つけます。 getResourceAsStream メソッドを使用して、リソースをURLのストリームとして読み取ります。 パッケージの下のリソースをフェッチするには、パッケージ名をURL文字列に変換する必要があります。 したがって、すべてのドット(。)をパス区切り文字(「/」)に置き換える必要があります。

その後、ストリームを BufferedReader に入力し、.class拡張子を持つすべてのURLをフィルタリングします。 必要なリソースを取得したら、クラスを作成し、すべての結果をSetに収集します。 Javaはラムダが例外をスローすることを許可しないため、getClassメソッドで処理する必要があります。

このメソッドをテストしてみましょう。

@Test
public void when_findAllClassesUsingClassLoader_thenSuccess() {
    AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
 
    Set<Class> classes = instance.findAllClassesUsingClassLoader(
      "com.baeldung.reflection.access.packages.search");
 
    Assertions.assertEquals(3, classes.size());
}

パッケージには2つのJavaファイルしかありません。 ただし、ネストされたクラスNestedExampleを含む3つのクラスが宣言されています。 その結果、私たちのテストは3つのクラスになりました。

検索パッケージは現在の作業パッケージとは異なることに注意してください。

3.2. リフレクションライブラリ

Reflections は、現在のクラスパスをスキャンし、実行時にクエリできるようにする人気のあるライブラリです。

リフレクション依存関係をMavenプロジェクトに追加することから始めましょう。

<dependency>
    <groupId>org.reflections</groupId>
    <artifactId>reflections</artifactId> 
    <version>0.9.12</version>
</dependency>

それでは、コードサンプルを詳しく見ていきましょう。

public Set<Class> findAllClassesUsingReflectionsLibrary(String packageName) {
    Reflections reflections = new Reflections(packageName, new SubTypesScanner(false));
    return reflections.getSubTypesOf(Object.class)
      .stream()
      .collect(Collectors.toSet());
}

このメソッドでは、 SubTypesScanner クラスを開始し、Objectクラスのすべてのサブタイプをフェッチしています。 このアプローチにより、クラスをフェッチする際の粒度が向上します。

繰り返しますが、テストしてみましょう。

@Test
public void when_findAllClassesUsingReflectionsLibrary_thenSuccess() {
    AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
 
    Set<Class> classes = instance.findAllClassesUsingReflectionsLibrary(
      "com.baeldung.reflection.access.packages.search");
 
    Assertions.assertEquals(3, classes.size());
}

以前のテストと同様に、このテストでは、指定されたパッケージで宣言されたクラスが検出されます。

それでは、次の例に移りましょう。

3.3. GoogleGuavaライブラリ

このセクションでは、GoogleGuavaライブラリを使用してクラスを検索する方法を説明します。 Google Guavaは、クラスローダーのソースをスキャンしてすべてのロード可能なクラスとリソースを見つけるClassPathユーティリティクラスを提供します。

まず、guava依存関係をプロジェクトに追加しましょう。

<dependency>
      <groupId>com.google.guava</groupId>
      <artifactId>guava</artifactId>
      <version>31.0.1-jre</version>
</dependency>

コードを詳しく見ていきましょう。

public Set<Class> findAllClassesUsingGoogleGuice(String packageName) throws IOException {
    return ClassPath.from(ClassLoader.getSystemClassLoader())
      .getAllClasses()
      .stream()
      .filter(clazz -> clazz.getPackageName()
        .equalsIgnoreCase(packageName))
      .map(clazz -> clazz.load())
      .collect(Collectors.toSet());
}

上記のメソッドでは、 ClassPath#fromメソッドへの入力としてシステムクラスローダーを提供しています。 ClassPath によってスキャンされたすべてのクラスは、パッケージ名に基づいてフィルタリングされます。 次に、フィルタリングされたクラスがロードされ(ただし、リンクまたは初期化されません)、Setに収集されます。

このメソッドをテストしてみましょう。

@Test
public void when_findAllClassesUsingGoogleGuice_thenSuccess() throws IOException {
    AccessingAllClassesInPackage instance = new AccessingAllClassesInPackage();
 
    Set<Class> classes = instance.findAllClassesUsingGoogleGuice(
      "com.baeldung.reflection.access.packages.search");
 
    Assertions.assertEquals(3, classes.size());
}

さらに、Google Guavaライブラリは、 getTopLevelClasses()および getTopLevelClassesRecursive()メソッドを提供します。

上記のすべての例で、 package-infoは、パッケージの下に存在し、1つ以上のパッケージレベルの注釈で注釈が付けられている場合、使用可能なクラスのリストに含まれることに注意してください。

次のセクションでは、モジュラーアプリケーションでクラスを見つける方法について説明します。

4. モジュラーアプリケーションでのクラスの検索

Javaプラットフォームモジュールシステム(JPMS)は、モジュールを介した新しいレベルのアクセス制御を紹介しました。 モジュールの外部にアクセスするには、各パッケージを明示的にエクスポートする必要があります。

モジュラーアプリケーションでは、各モジュールは、名前付き、名前なし、または自動モジュールのいずれかになります。

名前付きモジュールと自動モジュールの場合、組み込みのシステムクラスローダーにはクラスパスがありません。 システムクラスローダーは、アプリケーションモジュールパスを使用してクラスとリソースを検索します。

名前のないモジュールの場合、クラスパスが現在の作業ディレクトリに設定されます。

4.1. モジュール内

モジュール内のすべてのパッケージは、モジュール内の他のパッケージを可視化できます。 モジュール内のコードは、すべてのタイプとそのすべてのメンバーへのリフレクティブアクセスを楽しんでいます。

4.2. モジュールの外

Javaは最も制限の厳しいアクセスを強制するため、モジュール内のクラスへのリフレクティブアクセスを取得するには、exportまたはopenモジュール宣言を使用してパッケージを明示的に宣言する必要があります。

通常のモジュールの場合、エクスポートされたパッケージ(開いているパッケージは除く)へのリフレクティブアクセスは、publicおよびprotectedタイプと宣言されたパッケージのすべてのメンバーへのアクセスのみを提供します。

検索する必要のあるパッケージをエクスポートするモジュールを作成できます。

module my.module {
    exports com.baeldung.reflection.access.packages.search;
}

通常のモジュールの場合、開いているパッケージへのリフレクティブアクセスにより、宣言されたパッケージのすべてのタイプとそのメンバーへのアクセスが提供されます。

module my.module {
    opens com.baeldung.reflection.access.packages.search;
}

同様に、オープンモジュールは、すべてのパッケージが開かれたかのように、すべてのタイプとそのメンバーにリフレクティブアクセスを許可します。 モジュール全体を開いて、リフレクティブアクセスを行いましょう。

open module my.module{
}

最後に、パッケージにアクセスするための適切なモジュラー記述がモジュールに提供されていることを確認した後、前のセクションのメソッドのいずれかを使用して、パッケージ内の使用可能なすべてのクラスを見つけることができます。

5. 結論

結論として、クラスローダーとパッケージ内のすべてのクラスを見つけるさまざまな方法について学びました。 また、モジュラーアプリケーションでパッケージにアクセスする方法についても説明しました。

いつものように、すべてのコードはGitHub利用できます。