以前、JavaとScalaで同じようなタイトルのエントリを書いたことがありましたが、これには元ネタがあって、そちらは実はGroovyだったりします。
元々、仕事でクラスをディレクツリーを辿って動的に走査して、見つかったクラスの継承関係やらアノテーション付与の確認やらをしたかったのが作成の動機です。
JARファイルのことを考えなければ、めっちゃ簡単でこれくらいの量で済みます。
class_finder.groovy
// 引数にスキャン対象のパッケージ名 def rootPackageName = args[0] def classLoader = Thread.currentThread().contextClassLoader def resourceName = rootPackageName.replace('.', '/') def loadedClasses = [] def url = classLoader.getResource(resourceName) if (!url) { // 見つからなかったら、そこで終了 println("Not Found ${rootPackageName} Package.") System.exit(0) } def traverseDir traverseDir = { packageName, f -> if (f.directory) { // ディレクトリの場合は、パッケージ名に .ディレクトリ名 を足して再帰 f.eachFile(traverseDir.curry("${packageName}.${f.name}")) } else if (f.file && f.name.endsWith(".class")) { loadedClasses << classLoader.loadClass("${packageName}.${f.name.replaceAll(/\.class$/, '')}") } } new File(url.file).eachFile(traverseDir.curry(rootPackageName)) loadedClasses.each { println(it.name) }
この実装では、とりあえず見つけたクラスはprintlnして終了しています。
ディレクトリ内を再帰的に走査する際に、現在位置のパッケージ名が欲しいため2つの引数を取るメソッドを再帰呼び出ししたくなります。が、Groovy JDKの簡易メソッドFile#eachFileが引数に取れるのは引数1つのクロージャのため、これを部分適用してごまかしています。
最初のクロージャ呼び出し時と
new File(url.file).eachFile(traverseDir.curry(rootPackageName))
クロージャ内で渡されたFileクラスのインスタンスが、ディレクトリだった場合の再帰処理です。
// ディレクトリの場合は、パッケージ名に .ディレクトリ名 を足して再帰 f.eachFile(traverseDir.curry("${packageName}.${f.name}"))
ちなみに、Closure#curryは部分適用であって、Notカリー化…。
実行例。こういうクラスファイル階層
$ cd /path/to/classes $ find root root root/sub root/sub/SubPackageClass1$InnerClass1.class root/sub/SubPackageClass1.class root/sub/SubPackageClass2.class root/RootPackageClass1.class root/RootPackageClass2.class
に対して適用してみると、こんな感じで表示されます。
$ groovy -cp classes class_finder.groovy root root.sub.SubPackageClass1$InnerClass1 root.sub.SubPackageClass1 root.sub.SubPackageClass2 root.RootPackageClass1 root.RootPackageClass2
ちょっと手直しして、JARファイル内も検索できるようにして、継承クラスおよびインターフェースを探索してくるユーティリティを付けた版を載せておきます。実際の業務でのクラスファイル検索は、こんな感じのものを使っていました。
// 引数にスキャン対象のパッケージ名 def rootPackageName = args[0] def classLoader = Thread.currentThread().contextClassLoader def resourceName = rootPackageName.replace('.', '/') def loadedClasses = [] def url = classLoader.getResource(resourceName) if (!url) { // 見つからなかったら、そこで終了 println("Not Found ${rootPackageName} Package.") System.exit(0) } def traverseDir traverseDir = { packageName, f -> if (f.directory) { // ディレクトリの場合は、パッケージ名に .ディレクトリ名 を足して再帰 f.eachFile(traverseDir.curry("${packageName}.${f.name}")) } else if (f.file && f.name.endsWith(".class")) { loadedClasses << classLoader.loadClass("${packageName}.${f.name.replaceAll(/\.class$/, '')}") } } def findClassesWithJar = { jarFile -> try { for (jarEntry in jarFile.entries()) { if (jarEntry.name.startsWith(resourceName) && jarEntry.name.endsWith(".class")) { def className = jarEntry.name.replace('/', '.').replaceAll(/\.class$/, '') loadedClasses << classLoader.loadClass(className) } } } finally { if (jarFile) { jarFile.close() } } } switch (url.protocol) { case 'file': // パッケージ名を与えて、traverseDirクロージャに部分適用 new File(url.file).eachFile(traverseDir.curry(rootPackageName)) break case 'jar': findClassesWithJar(url.openConnection().jarFile) break default: break } loadedClasses.each { println("${it.name} extends [${findSuperClasses(it).collect{ it.name }.join(', ')}]") } // Utility Methods... def findSuperClasses(targetClass) { def superclasses = [] def findSuperClassesInner = { clazz -> if (!clazz) { return } def sc = clazz.superclass // java.lang.Objectは対象外 if (sc && sc != Object) { superclasses << sc call(sc.superclass) } clazz.interfaces.each { ifc -> superclasses << ifc ifc.interfaces.each { owner.call(it) } } } findSuperClassesInner(targetClass) superclasses }