CLOVER🍀

That was when it all began.

Javaで特定のパッケージ配下のクラスを検索する

1度やってみたかったんですよね、これ。DIコンテナとかで、よく特定のパッケージ配下のクラスを検索するような機能がありますが、これを自分で書いてみようと思います。

検索対象のクラスが配置されているパターンとしては、

  1. ディレクトリ配下に.classファイルが配置してある
  2. JARファイル内にパッケージングしてある

というのがよく考えられると思います。今回は、とりあえず上記2つを対象に考えます。

まず、検索対象のスケープゴートとして、以下のようなディレクトリ構成のクラスを用意しました。
ソースコード

root/RootPackageClass1.java
root/RootPackageClass2.java
root/sub/SubPackageClass1.java
root/sub/SubPackageClass2.java

クラスファイル

root/RootPackageClass1.class
root/RootPackageClass2.class
root/sub/SubPackageClass1.class
root/sub/SubPackageClass1$InnerClass1.class
root/sub/SubPackageClass2.class

クラスファイルをパッケージングしたJARファイル

$ jar -tvf classes.jar 
     0 Sun Mar 11 18:54:34 JST 2012 META-INF/
    71 Sun Mar 11 18:54:34 JST 2012 META-INF/MANIFEST.MF
     0 Sun Mar 11 17:50:42 JST 2012 root/
     0 Sun Mar 11 18:54:12 JST 2012 root/sub/
   303 Sun Mar 11 18:54:12 JST 2012 root/sub/SubPackageClass1$InnerClass1.class
   307 Sun Mar 11 18:54:12 JST 2012 root/sub/SubPackageClass1.class
   215 Sun Mar 11 18:54:12 JST 2012 root/sub/SubPackageClass2.class
   213 Sun Mar 11 18:54:02 JST 2012 root/RootPackageClass1.class
   213 Sun Mar 11 18:54:02 JST 2012 root/RootPackageClass2.class

クラスの検索がしたいだけなので、中身はどれも空っぽのクラスです。

package root;

public class RootPackageClass1 {
}

------------------------------------------------------

package root;

public class RootPackageClass2 {
}

------------------------------------------------------

package root.sub;

public class SubPackageClass1 {
    public static class InnerClass1 {
    }
}

------------------------------------------------------

package root.sub;

public class SubPackageClass2 {
}

しれっと、インナークラスが混じっています。

これにクラスパスを通して実行する、こういうクラスを書いてみます。
*import文は省略しています

public class ClassFinder {
    private ClassLoader classLoader;

    public static void main(String[] args) throws Exception {
        ClassFinder classFinder = new ClassFinder();
        classFinder.printClasses(args[0]);
    }

    public ClassFinder() {
        classLoader = Thread.currentThread().getContextClassLoader();
    }

    public ClassFinder(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    public void printClasses(String rootPackageName) throws Exception {
        String resourceName = rootPackageName.replace('.', '/');
        URL url = classLoader.getResource(resourceName);
        System.out.println("URL = " + url);
        System.out.println("URLConnection = " + url.openConnection());
    }
}

ポイントは、パッケージ名を「.」つなぎから「/」つなぎに変換していることと、

        String resourceName = rootPackageName.replace('.', '/');

ClassLoader#getResourceメソッドを使ってURLを取得していることです。

        URL url = classLoader.getResource(resourceName);

最後に、取得したURLを表示してみて、どういう結果となるか確認してみます。

上記サンプルは、第1引数にパッケージ名を受け取ることを想定して書いています。とりあえず、実行してみましょう。対象は「root」パッケージとします。

まずは、クラスファイルを特定のディレクトリに展開している場合。

$ java -cp ../java/classes:. ClassFinder root
URL = file:/xxxxx/java/classes/root
URLConnection = sun.net.www.protocol.file.FileURLConnection:file:/xxxxx/java/classes/root

続いて、JARファイルにパッケージングしている場合。

$ java -cp ../java/classes.jar:. ClassFinder root
URL = jar:file:/xxxxx/java/classes.jar!/root
URLConnection = sun.net.www.protocol.jar.JarURLConnection:jar:file:/xxxxx/java/classes.jar!/root

見ての通り、結果が異なります。対象のクラスがファイルシステムからロードされた場合はFileURLConnectionが、JARファイルからロードされた場合はJarURLConnectionが返ってきています。この結果、クラスパスの通し方によって、クラスの探索方法が異なることになります。

では、最初のサンプルを、目的の特定のパッケージ配下のクラスを検索するように書き直してみます。

プロトコルがfileなのかjarなのかで動作を変えることになるので、そのように条件分岐します。

        String protocol = url.getProtocol();
        if ("file".equals(protocol)) {
            // fileの場合
        } else if ("jar".equals(protocol)) {
            // JARファイルからロードした場合
        }

FILEプロトコルの場合は、URLからファイルパスが取得できるので、ここからFileクラスのインスタンスを生成します。

new File(url.getFile());

あとは、このディレクトリを基点に配下のクラスを検索していくコードを書きます。

    private List<Class<?>> findClassesWithFile(String packageName, File dir) throws Exception {
        List<Class<?>> classes = new ArrayList<Class<?>>();

        for (String path : dir.list()) {
            File entry = new File(dir, path);
            if (entry.isFile() && isClassFile(entry.getName())) {
                classes.add(classLoader.loadClass(packageName + "." + fileNameToClassName(entry.getName())));
            } else if (entry.isDirectory()) {
                classes.addAll(findClassesWithFile(packageName + "." + entry.getName(), entry));
            }
        }

        return classes;
    }

メソッドisClassFileは、拡張子が「.class」かどうかを判定し、fileNameToClassNameは拡張子「.class」を除去するだけのメソッドです。サブディレクトリがある場合を考えて、「.」を足して再帰呼び出ししています。このロード方法の場合、すでに特定のパッケージの配下を走査していることになるので、単純にクラスを検索して足していくだけですね。

続いてJARファイルの場合。JARファイルの場合は、URL#openConnectionメソッドを呼び出してJarConnectionを取得します。

        JarURLConnection jarUrlConnection = (JarURLConnection)jarFileUrl.openConnection();

取得したJarURLConnectionからは、JarFileが取得できます。

            jarFile = jarUrlConnection.getJarFile();

あとは、JarFile内のエントリをひとつひとつ確認していくことになります。特定のパッケージの配下ということで、String#startsWithで当てることになるのですが、この時JARファイル内のエントリの名前は「.」ではなく「/」となっているので注意が必要です。

            String packageNameAsResourceName = packageNameToResourceName(rootPackageName);

            while (jarEnum.hasMoreElements()) {
                JarEntry jarEntry = jarEnum.nextElement();
                if (jarEntry.getName().startsWith(packageNameAsResourceName) && isClassFile(jarEntry.getName())) {
                    classes.add(classLoader.loadClass(resourceNameToClassName(jarEntry.getName())));
                }
            }

今回は、引数のパッケージ名の「.」をあらかじめ「/」に変換してString#startsWithで当てるようにしています。なお、この場合も拡張子の「.class」はクラス取得時には不要なので取り除きます。

JarFileは、一応閉じておきます。

        } finally {
            if (jarFile != null) {
                jarFile.close();
            }
        }

JARファイルからの検索処理のコード片は、上記を合わせてこんな感じになりました。

    private List<Class<?>> findClassesWithJarFile(String rootPackageName, URL jarFileUrl) throws Exception {
        List<Class<?>> classes = new ArrayList<Class<?>>();

        JarURLConnection jarUrlConnection = (JarURLConnection)jarFileUrl.openConnection();
        JarFile jarFile = null;

        try {
            jarFile = jarUrlConnection.getJarFile();
            Enumeration<JarEntry> jarEnum = jarFile.entries();

            String packageNameAsResourceName = packageNameToResourceName(rootPackageName);

            while (jarEnum.hasMoreElements()) {
                JarEntry jarEntry = jarEnum.nextElement();
                if (jarEntry.getName().startsWith(packageNameAsResourceName) && isClassFile(jarEntry.getName())) {
                    classes.add(classLoader.loadClass(resourceNameToClassName(jarEntry.getName())));
                }
            }
        } finally {
            if (jarFile != null) {
                jarFile.close();
            }
        }

        return classes;
    }

では、実行してみます。
まずは展開されたクラスファイルに対して走査する場合。

$ java -cp ../java/classes:. ClassFinder root
class root.sub.SubPackageClass1$InnerClass1
class root.sub.SubPackageClass1
class root.sub.SubPackageClass2
class root.RootPackageClass1
class root.RootPackageClass2

続いて、JARファイル内のクラスを走査する場合。

$ java -cp ../java/classes.jar:. ClassFinder root
class root.sub.SubPackageClass1$InnerClass1
class root.sub.SubPackageClass1
class root.sub.SubPackageClass2
class root.RootPackageClass1
class root.RootPackageClass2

ちゃんとインナークラスも取得できています。

もうひとつパッケージを落としても、実行可能です。

$ java -cp ../java/classes.jar:. ClassFinder root.sub
class root.sub.SubPackageClass1$InnerClass1
class root.sub.SubPackageClass1
class root.sub.SubPackageClass2

Seasar2のコードとかでは、その他Zipファイルからクラスをロードした場合とかWARファイルからクラスをロードした場合とかを考慮しているようです。とはいえ、基本はこんなんできっと大丈夫…でしょう。

一応、今回作成したソース全体を載せておきます。

import java.io.File;
import java.net.JarURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.List;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class ClassFinder {
    private ClassLoader classLoader;

    public static void main(String[] args) throws Exception {
        ClassFinder classFinder = new ClassFinder();
        // classFinder.printClasses(args[0]);

        for (Class<?> clazz : classFinder.findClasses(args[0])) {
            System.out.println(clazz);
        }
    }

    public ClassFinder() {
        classLoader = Thread.currentThread().getContextClassLoader();
    }

    public ClassFinder(ClassLoader classLoader) {
        this.classLoader = classLoader;
    }

    public void printClasses(String rootPackageName) throws Exception {
        String resourceName = rootPackageName.replace('.', '/');
        URL url = classLoader.getResource(resourceName);
        System.out.println("URL = " + url);
        System.out.println("URLConnection = " + url.openConnection());
    }

    private String fileNameToClassName(String name) {
        return name.substring(0, name.length() - ".class".length());
    }

    private String resourceNameToClassName(String resourceName) {
        return fileNameToClassName(resourceName).replace('/', '.');
    }

    private boolean isClassFile(String fileName) {
        return fileName.endsWith(".class");
    }

    private String packageNameToResourceName(String packageName) {
        return packageName.replace('.', '/');
    }

    public List<Class<?>> findClasses(String rootPackageName) throws Exception {
        String resourceName = packageNameToResourceName(rootPackageName);
        URL url = classLoader.getResource(resourceName);

        if (url == null) {
            return new ArrayList<Class<?>>();
        }

        String protocol = url.getProtocol();
        if ("file".equals(protocol)) {
            return findClassesWithFile(rootPackageName, new File(url.getFile()));
        } else if ("jar".equals(protocol)) {
            return findClassesWithJarFile(rootPackageName, url);
        }

        throw new IllegalArgumentException("Unsupported Class Load Protodol[" + protocol + "]");
    }

    private List<Class<?>> findClassesWithFile(String packageName, File dir) throws Exception {
        List<Class<?>> classes = new ArrayList<Class<?>>();

        for (String path : dir.list()) {
            File entry = new File(dir, path);
            if (entry.isFile() && isClassFile(entry.getName())) {
                classes.add(classLoader.loadClass(packageName + "." + fileNameToClassName(entry.getName())));
            } else if (entry.isDirectory()) {
                classes.addAll(findClassesWithFile(packageName + "." + entry.getName(), entry));
            }
        }

        return classes;
    }

    private List<Class<?>> findClassesWithJarFile(String rootPackageName, URL jarFileUrl) throws Exception {
        List<Class<?>> classes = new ArrayList<Class<?>>();

        JarURLConnection jarUrlConnection = (JarURLConnection)jarFileUrl.openConnection();
        JarFile jarFile = null;

        try {
            jarFile = jarUrlConnection.getJarFile();
            Enumeration<JarEntry> jarEnum = jarFile.entries();

            String packageNameAsResourceName = packageNameToResourceName(rootPackageName);

            while (jarEnum.hasMoreElements()) {
                JarEntry jarEntry = jarEnum.nextElement();
                if (jarEntry.getName().startsWith(packageNameAsResourceName) && isClassFile(jarEntry.getName())) {
                    classes.add(classLoader.loadClass(resourceNameToClassName(jarEntry.getName())));
                }
            }
        } finally {
            if (jarFile != null) {
                jarFile.close();
            }
        }

        return classes;
    }
}