故事是这么开始的
在用 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.scala 中 match case
没有考虑 @hello
在 val
上的情况, 那不如先来看看它是啥:
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
对象引用从哪里来? -
@conf
修饰的值类型怎么判断? - 为什么不用
case q"..." =>
来替代case ValDef(...) =>
? - 如何兼容
2.10
到2.11
版本之间的差异?
这些问题的留个大家一起思考, 也可以关注我的开源项目 config-annotation 与我一起探讨.
更为复杂的案例请见json-annotation.