Reputation: 908
I am struggling to get a value from a scala annotation in a Play controller method.
I defined a class for the annotation:
case class Auth(perm: String) extends scala.annotation.StaticAnnotation
Then I am reading it in one of the Play's filters:
import scala.reflect.runtime.{universe => u}
val res = u.runtimeMirror(handlerDef.classLoader)
.classSymbol(Class.forName(handlerDef.controller))
.info
.decls
.find(_.name.toString == handlerDef.method)
.flatMap(_.asMethod.annotations.find(_.tree.tpe =:= u.typeOf[Auth]))
Now, I am getting Option[Annotation]
and when I println
it, it's: Some(Auth("test"))
- the value that I put in the annotation, so it's all good.
I can't wrap my head around how to actually convert Annotation
to my Auth
.
Help is appreciated, thanks!
Upvotes: 0
Views: 336
Reputation: 51683
Scaladoc of scala.reflect.api.Annotations
says
Unlike Java reflection, Scala reflection does not support evaluation of constructor invocations stored in annotations into underlying objects. For instance it's impossible to go from
@ann(1, 2) class C
toann(1, 2)
, so one has to analyze trees representing annotation arguments to manually extract corresponding values. Towards that end, arguments of an annotation can be obtained viaannotation.tree.children.tail
.
https://github.com/scala/scala/blob/2.13.x/src/reflect/scala/reflect/api/Annotations.scala#L39-L42
You can use Toolbox
to evaluate annotation tree
// libraryDependencies += scalaOrganization.value % "scala-compiler" % scalaVersion.value
import scala.tools.reflect.ToolBox
val rm = u.runtimeMirror(handlerDef.classLoader)
val tb = rm.mkToolBox()
res.map(a => tb.eval(tb.untypecheck(a.tree)).asInstanceOf[Auth]) // Some(Auth(some permission))
Calling a method from Annotation using reflection
Or (e.g. if you want to depend on scala-reflect
only and not on scala-compiler
) you can evaluate the tree manually:
res.map(a => {
val arg = a.tree.children.tail match { case List(q"${s: String}") => s }
Auth(arg)
}) // Some(Auth(some permission))
or
res.map(_.tree match {
case q"new ${t@TypeTree()}(${s: String})" /*if t.tpe == typeOf[Auth]*/ => Auth(s)
}) // Some(Auth(some permission))
Notice that the tree will not match pattern q"new com.example.Auth(${s: String})"
because annotation trees have different shape.
By the way, with rm.staticClass(...)
instead of rm.classSymbol(Class.forName(...))
you can use Scala class name (e.g. org.example.App.MyClass
) instead of Java class name (e.g. org.example.App$MyClass
). Also you can try scala.reflect.runtime.currentMirror
instead of u.runtimeMirror(classLoader)
. Also .decls.find(_.name.toString == methodName)
can be replaced with .decl(u.TermName(methodName))
.
Just in case, if you know types of class and annotation and method name at compile time then you can do the same using compile-time reflection
import scala.language.experimental.macros
import scala.reflect.macros.blackbox
def getMethodAnnotation[Cls, Ann](methodName: String): Ann = macro getMethodAnnotationImpl[Cls, Ann]
def getMethodAnnotationImpl[Cls: c.WeakTypeTag, Ann: c.WeakTypeTag](c: blackbox.Context)(methodName: c.Tree): c.Tree = {
import c.universe._
val q"${methodNameStr: String}" = methodName
weakTypeOf[Cls]
.decl(TermName(methodNameStr))
.annotations.find(_.tree.tpe =:= weakTypeOf[Ann]).get.tree match {
case q"new ${t: TypeTree}[..$targs](...$argss)" => q"new ${t.tpe}[..$targs](...$argss)"
}
}
class MyClass {
@Auth("some permission")
def myMethod(): Unit = ()
}
getMethodAnnotation[MyClass, Auth]("myMethod") //scalac: new Auth("some permission")
Upvotes: 2