在用 Scala Macro Annotation 实现以前, 我是根据 Akka 官方文档建议的 扩展 机制来绑定配置:html
class SettingsImpl(config: Config) extends Extension { import config._ val BrokerHost = getString("kafka_consumer.broker.host") val BrokerPort = getInt("kafka_consumer.broker.port") } object Settings extends ExtensionId[SettingsImpl] with ExtensionIdProvider { def createExtension(system: ExtendedActorSystem) = new SettingsImpl(system.settings.config) def lookup() = Settings } class KafkaConsumer extends Actor { val settings = Settings(context.system) val brokerHost = settings.BrokerHost val brokerPort = settings.BrokerPort def receive = ??? }
application.conf
除了akka
外, 加入扩展的内容:java
akka { ... } kafka_consumer.broker { host:10.0.0.1 port:9092 }
随着配置项个数增长一个量级, 这类 getXxx(...)
写得也是让我 醉了, 更不要谈重构的时候...[不忍直视]git
我开始寻思着能不能这样:github
class KafkaConsumer extends Actor { @conf val brokerHost = "" @conf val brokerPort = 0 }
而后让编译器 智能 的帮我 挡酒 , 她酒量可比我好太多了.json
下面就是我以 sbt-example-paradise 为基础实现的步骤:app
修改 Test.scala 为:ide
object Test extends App { @hello val i = 0 println(i) }
执行 sbt clean run
, 不出意料, 报错了:scala
[error] scala.MatchError: List(val i = 0) (of class scala.collection.immutable.$colon$colon) [error] at helloMacro$.impl(Macros.scala:10) [error] @hello val i = 0 [error]
显然 Macros.scala 中 match case
没有考虑 @hello
在 val
上的状况, 那不如先来看看它是啥:code
annottees.map(_.tree).toList match { case t :: Nil => println(t.getClass); t }
其实前面的错误信息已经 暗示了 t
的内容是 val i = 0
, 所以println(t)
已经没有意义了, 但弄清它的类型, 有助于替换 =
右边的部分 .htm
sbt clean run
:
class scala.reflect.internal.Trees$ValDef [info] Running Test 0
去查看 ValDef
源码, 你会发现:
case class ValDef(mods: Modifiers, name: TermName, tpt: Tree, rhs: Tree) ...
这一步已经涉及抽象语法树的范畴, 有兴趣的请阅读 reflection 中的
Tree
的部分
啊哈, 这也就意味着能够这样写:
annottees.map(_.tree).toList match { case (t @ ValDef(mods, name, tpt, rhs)) :: Nil => println(rhs); t }
直觉告诉我 rhs
就是 0
, sbt clean run
:
0 [info] Running Test 0
如今, 只要弄清楚怎么构造我想要的 rhs
就能够达到目的了. 怎么作呢, 看看 Macros.scala 的示范, 不难想到:
annottees.map(_.tree).toList match { case ValDef(mods, name, tpt, rhs) :: Nil => ValDef(mods, name, tpt, q"10") }
sbt clean run
:
[info] Running Test 10
q"..." 是一种叫 quasiquotes 的特性, 它使得构造语法树过程的变得异常的简单
请不要天真的觉得将 q"0"
改为 q"""config.getInt("test.i")"""
就大功告成, 后面还有不少问题:
config
对象引用从哪里来?@conf
修饰的值类型怎么判断?case q"..." =>
来替代 case ValDef(...) =>
?2.10
到 2.11
版本之间的差别?这些问题的留个你们一块儿思考, 也能够关注个人开源项目 config-annotation 与我一块儿探讨.
更为复杂的案例请见json-annotation.