1度やってみたかったんですよね、これ。DIコンテナとかで、よく特定のパッケージ配下のクラスを検索するような機能がありますが、これを自分で書いてみようと思います。
検索対象のクラスが配置されているパターンとしては、
- ディレクトリ配下に.classファイルが配置してある
- 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; } }