長い人生、生きていれば、Scalaで実行時にメソッドのパラメータ名を取得したくなることが一度や二度有りますね。
そんな時のためのやり方のメモです。
package example import scala.reflect._ import com.thoughtworks.paranamer.{BytecodeReadingParanamer, CachingParanamer} case class Person(name: String, age: Int) case class Company(会社名: String) case class Country(`country-code`: String) case class Planet(`star.cluster`: String) object GettingParameter { val paranamer = new CachingParanamer(new BytecodeReadingParanamer) def main(argv: Array[String]): Unit = { println("Inspect Person's parameter by Paranamer") inspectByParanamer[Person].foreach(println) println("Inspect Person's parameter by Java 8 reflection") inspectByJava8Reflection[Person].foreach(println) println("Inspect Company's parameter by Paranamer") inspectByParanamer[Company].foreach(println) println("Inspect Company's parameter by Java 8 reflection") inspectByJava8Reflection[Company].foreach(println) println("Inspect Country's parameter by Paranamer") inspectByParanamer[Country].foreach(println) println("Inspect Country's parameter by Java 8 reflection") inspectByJava8Reflection[Country].foreach(println) println("Inspect Planet's parameter by Paranamer ") inspectByParanamer[Planet].foreach(println) try { println("Inspect Planet's parameter by Java 8 reflection") inspectByJava8Reflection[Planet].foreach(println) } catch { case e => println(e) } } def inspectByParanamer[T](implicit m: ClassManifest[T]): Seq[String] = { paranamer.lookupParameterNames(m.erasure.getConstructors.head).toSeq } def inspectByJava8Reflection[T](implicit m: ClassManifest[T]): Seq[String] = { m.erasure.getConstructors.head.getParameters().map(_.getName).toSeq } }
Paranamer
Paranamerは、Java8より前の時代に使われていたメソッドのパラメータ名を取得するためのライブラリです。当時のバイトコードにはパラメータ名は出力されていなかったので、LocalVariableTable
から取得していました。ただし、LocalVariableTable
に存在するのは正確にはパラメータ名ではなく、パラメータで引き渡された値が格納される変数名です。
Java 8 Reflection
Java8ではパラメター名を取得するためのメソッドが用意されました。また、パラメータ名を格納するようにバイトコードも拡張されました。
Scalaにおける注意事項
Scalaではメソッドのパラメータ名に、Javaでは使用できない記号が使える。
ただし、バイトコード上では名前が変換される(scala.reflect.NameTransformer
が使われている)。
しかし、パラメータ名には変換前の、Javaでは使えない識別子が格納される。先程のソースを実行すると、以下の結果となり、Java 8ではソースコードの記載の通りのパラメータを取得しているのが分かる。
Inspect Person's parameter by Paranamer name age Inspect Person's parameter by Java 8 reflection name age Inspect Company's parameter by Paranamer 会社名 Inspect Company's parameter by Java 8 reflection 会社名 Inspect Country's parameter by Paranamer country$minuscode Inspect Country's parameter by Java 8 reflection country-code Inspect Planet's parameter by Paranamer star$u002Ecluster Inspect Planet's parameter by Java 8 reflection java.lang.reflect.MalformedParametersException: Invalid parameter name "star.cluster"
しかし、最後のパターンが例外を送出している。これは以下のコミットにより、パラメータ名に不正な文字が混入している場合は例外を送出するようにチェックがかかっている。
チェック対象の文字として、.;[/
の4文字…シグニチャで使われる文字が選定されているが…今ひとつ理由は分からない…パラメータ名にシグニチャで使われる文字列が混入するのか??その理由は探せなかった。
また、これ以外にもScalaのTypeTagというリフレクションの仕組みを使ってもパラメータ名は取得できるが、結果はParanamerと同じだったので、おそらくLocalVariableTable
から取得しているように見える。
おわりに
Scalaでは有効なパラメータ名なのに、Javaのリフレクション経由だと不正と言われてしまうパターンが存在することが分かった。
Json4sの中でParanamerを使っている箇所をJava8のリフレクションで置き換えられないか、試してみると意味不明な例外が出たので解析したら、上記のことが分かった。とくにいますぐParanamerを止める理由も無いが、いつかJava8のリフレクションに置き換えたい、というソースコードに書かれてたコメントのとおりにやってみようと思ったけど、できなかった、という記録です。
Javaのチェックが意味不明なので、そっちを直すべきのように思えるけど、非常に特殊な状況でのみ顕在化するので、そのままでもいいかな。