使用 Scala Macro Annotation 實現配置項綁定

故事是這麼開始的

在用 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

Say hello to hell

修改 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.scalamatch case 沒有考慮 @helloval 上的狀況, 那不如先來看看它是啥: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.102.11 版本之間的差別?

這些問題的留個你們一塊兒思考, 也能夠關注個人開源項目 config-annotation 與我一塊兒探討.

更爲複雜的案例請見json-annotation.

相關文章
相關標籤/搜索