Magnolia Tech

いつもコードのことばかり考えている人のために。

Scalaのメソッドのパラメータ名をリフレクションで取得する

長い人生、生きていれば、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に存在するのは正確にはパラメータ名ではなく、パラメータで引き渡された値が格納される変数名です。

github.com

Java 8 Reflection

Java8ではパラメター名を取得するためのメソッドが用意されました。また、パラメータ名を格納するようにバイトコードも拡張されました。

irof.hateblo.jp

Scalaにおける注意事項

Scalaではメソッドのパラメータ名に、Javaでは使用できない記号が使える。

www.ne.jp

ただし、バイトコード上では名前が変換される(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"

しかし、最後のパターンが例外を送出している。これは以下のコミットにより、パラメータ名に不正な文字が混入している場合は例外を送出するようにチェックがかかっている。

8020981: Update methods of java.lang.reflect.Parameter to throw corre… · openjdk/jdk@367fa5a · GitHub

チェック対象の文字として、.;[/の4文字…シグニチャで使われる文字が選定されているが…今ひとつ理由は分からない…パラメータ名にシグニチャで使われる文字列が混入するのか??その理由は探せなかった。

また、これ以外にもScalaのTypeTagというリフレクションの仕組みを使ってもパラメータ名は取得できるが、結果はParanamerと同じだったので、おそらくLocalVariableTableから取得しているように見える。

おわりに

Scalaでは有効なパラメータ名なのに、Javaのリフレクション経由だと不正と言われてしまうパターンが存在することが分かった。

Json4sの中でParanamerを使っている箇所をJava8のリフレクションで置き換えられないか、試してみると意味不明な例外が出たので解析したら、上記のことが分かった。とくにいますぐParanamerを止める理由も無いが、いつかJava8のリフレクションに置き換えたい、というソースコードに書かれてたコメントのとおりにやってみようと思ったけど、できなかった、という記録です。

Javaのチェックが意味不明なので、そっちを直すべきのように思えるけど、非常に特殊な状況でのみ顕在化するので、そのままでもいいかな。