CLOVER🍀

That was when it all began.

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

以前、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
}