使用 Scala Macro Annotation 实现配置项绑定(上)

故事是这么开始的

在用 Scala Macro Annotation 实现之前, 我是根据 Akka 官方文档建议的 扩展 机制来绑定配置:

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 外, 加入扩展的内容:

akka { ... }

kafka_consumer.broker {
  host:10.0.0.1
  port:9092
}

随着配置项个数增加一个量级, 这类 getXxx(...) 写得也是让我 醉了, 更不要谈重构的时候...[不忍直视]

活不能再这么糙下去

我开始寻思着能不能这样:

class KafkaConsumer extends Actor {
  @conf val brokerHost = ""
  @conf val brokerPort = 0
}

然后让编译器 智能 的帮我 挡酒 , 她酒量可比我好太多了.

踏上去往天堂的路

下面就是我以 sbt-example-paradise 为基础实现的步骤:

Say hello to hell

修改 Test.scala 为:

object Test extends App {
  @hello val i = 0
  println(i)
}

执行 sbt clean run, 不出意料, 报错了:

[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.scalamatch case 没有考虑 @helloval 上的情况, 那不如先来看看它是啥:

annottees.map(_.tree).toList match {
  case t :: Nil => println(t.getClass); t
}

其实前面的错误信息已经 暗示了 t 的内容是 val i = 0, 因此println(t) 已经没有意义了, 但弄清它的类型, 有助于替换 =右边的部分 .

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-annotation 与我一起探讨.

更为复杂的案例请见json-annotation.

annotationconfigurationmacroscalaComment(0)