先の日記は、同様の趣旨のプログラムをJavaで書いたのですが、今度はScalaで書いてみました。
パッケージの取得と配下のクラスファイルの走査方法自体は前回で書いているので、今回は割愛。とりあえず、結果のソースを載せてみます。
なお、動作的にScala版がJava版と異なるところは、サポートしていないプロトコルやファイルフォーマットからクラスがロードされていた場合、例外ではなくNilを返却するところですかね。
import scala.collection.JavaConverters._ import scala.collection.mutable.ListBuffer import java.io.File import java.net.{JarURLConnection, URL} import java.util.jar.{JarEntry, JarFile} object ClassFinder { def main(args: Array[String]): Unit = new ClassFinder().findClasses(args(0)).foreach(println) } class ClassFinder(private val classLoader: ClassLoader) { def this() = this(Thread.currentThread.getContextClassLoader) private def pathToClassName(path: String): String = path.substring(0, path.size - ".class".size) private def isClassFile(entry: JarEntry): Boolean = isClassFile(entry.getName) private def isClassFile(file: File): Boolean = file.isFile && isClassFile(file.getName) private def isClassFile(filePath: String): Boolean = filePath.endsWith(".class") private def resourceNameToClassName(resourceName: String): String = pathToClassName(resourceNameToPackageName(resourceName)) private def resourceNameToPackageName(resourceName: String): String = resourceName.replace('/', '.') private def packageNameToResourceName(packageName: String): String = packageName.replace('.', '/') private val finderFunction: PartialFunction[URL, String => List[Class[_]]] = findClassesWithFile orElse findClassesWithJarFile orElse findClassesWithNone def findClasses(rootPackageName: String): List[Class[_]] = { val resourceName = packageNameToResourceName(rootPackageName) classLoader.getResource(resourceName) match { case null => Nil case url => finderFunction(url)(rootPackageName) } } def findClassesWithFile: PartialFunction[URL, String => List[Class[_]]] = { case url if url.getProtocol == "file" => val classes = new ListBuffer[Class[_]] def findClassesWithFileInner(packageName: String, dir: File): List[Class[_]] = { dir.list.foreach { path => new File(dir, path) match { case file if isClassFile(file) => classes += classLoader.loadClass(packageName + "." + pathToClassName(file.getName)) case directory if directory.isDirectory => findClassesWithFileInner(packageName + "." + directory.getName, directory) case _ => } } classes toList } findClassesWithFileInner(_: String, new File(url.getFile)) } def findClassesWithJarFile: PartialFunction[URL, String => List[Class[_]]] = { case url if url.getProtocol == "jar" => def manageJar[T](jarFile: JarFile)(body: JarFile => T): T = try { body(jarFile) } finally { jarFile.close() } def findClassesWithJarFileInner(packageName: String): List[Class[_]] = url openConnection match { case jarURLConnection: JarURLConnection => manageJar(jarURLConnection.getJarFile) { jarFile => jarFile.entries.asScala.toList.collect { case jarEntry if resourceNameToPackageName(jarEntry.getName).startsWith(packageName) && isClassFile(jarEntry) => classLoader.loadClass(resourceNameToClassName(jarEntry.getName)) } } } findClassesWithJarFileInner } def findClassesWithNone: PartialFunction[URL, String => List[Class[_]]] = { case _ => packageName => Nil } }
今回のテーマは、部分関数の関数合成です。クラスを検索するメソッドとして、3つ用意しました。
ファイルシステムから検索するメソッド
def findClassesWithFile: PartialFunction[URL, String => List[Class[_]]]
JARファイルから検索するメソッド
def findClassesWithJarFile: PartialFunction[URL, String => List[Class[_]]]
何もせず、Nilを返すメソッド
def findClassesWithNone: PartialFunction[URL, String => List[Class[_]]] = { case _ => packageName => Nil }
これらのメソッドを、PartialFunction#orElseで合成します。
private val finderFunction: PartialFunction[URL, String => List[Class[_]]] = findClassesWithFile orElse findClassesWithJarFile orElse findClassesWithNone
まあ、PartialFunctionに与えている型パラメータが
PartialFunction[URL, String => List[Class[_]]]
なので、PartialFunction#applyの結果、さらに関数が返ってくるところがちょっと注意ですね。こういうの、かえって読みにくくなるだけかなぁ…?
あとは、このフィールドに対してapplyメソッドを呼び出し、その結果返ってきた関数に対して検索処理を指示します。
def findClasses(rootPackageName: String): List[Class[_]] = { val resourceName = packageNameToResourceName(rootPackageName) classLoader.getResource(resourceName) match { case null => Nil case url => finderFunction(url)(rootPackageName) } }
この部分ですね。
case url => finderFunction(url)(rootPackageName)
ファイルシステムやJARファイル以外に増えても、メソッドを増やしてfinderFunctionに合成すれば大丈夫!最後にNilを返す関数を合成しているのは、ちょっと面倒くさがりだからですかね…。
最近はNettyの写経ばっかりしていたので、Scalaの関数型オブジェクトを使ったプログラミングが楽しいです♪
もちろん、Java版と同じように動作します。
展開されたクラスファイルに対して走査する場合。
$ scala -cp ../java/classes ClassFinder root.sub class root.sub.SubPackageClass1$InnerClass1 class root.sub.SubPackageClass1 class root.sub.SubPackageClass2
続いて、JARファイル内のクラスを走査する場合。
$ scala -cp ../java/classes.jar ClassFinder root.sub class root.sub.SubPackageClass1$InnerClass1 class root.sub.SubPackageClass1 class root.sub.SubPackageClass2