優化的日志方式
package macros_demo
import scala.language.experimental.macros
import org.slf4j._
import scala.reflect.macros.whitebox.Context
object Macros {
implicit class LoggerEx(val logger: Logger) {
def DEBUG(msg: String): Unit = macro LogMacros.DEBUG1 def DEBUG(msg: String, exception: Exception): Unit = macro LogMacros.DEBUG2 }
object LogMacros {
def DEBUG1(c: Context)(msg: c.Tree): c.Tree = {
import c.universe._
val pre = c.prefix q""" val x = $pre.logger if( x.isDebugEnabled ) x.debug($msg) """ }
def DEBUG2(c:Context)(msg: c.Tree, exception: c.Tree): c.Tree = {
import c.universe._
val pre = c.prefix q""" val x = $pre.logger if(x.isDebugEnabled) x.debug( $msg, $exception ) """ } } }
package macros_test
import org.slf4j._
import macros_demo.Macros._
class LogTest {
val logger = LoggerFactory.getLogger(getClass) logger.DEBUG(s"Hello, today is ${new java.util.Date}")
}
在這個例子中:
-
我們通過隱式轉換的方式,為
org.slf4j.Logger
擴展了 DEBUG 方法,使用上與 原有的debug 一致,我們期望新的 DEBUG 匹配如下的模式:
// logger.DEBUG(message) will expand to at compile timeif(logger.isDebugEnabled) logger.debug(message)
-
可以使用這個選項來看看 scala 編譯生成的代碼:(可以直接在sbt中
set scalacOption := Seq(“-Ymacro-debug-lite”)
開啟選項)
val x = macros_demo.Macros.LoggerEx(LogTest.this.logger).logger;
if (x.isDebugEnabled) x.debug(scala.StringContext.apply("Hello, today is ", "").s(new java.util.Date()))
else ()
//Block(List(ValDef(Modifiers(), TermName("x"), TypeTree(), Select(Apply(Select(Select(Ident(macros_demo), macros_demo.Macros), TermName("LoggerEx")), List(Select(This(TypeName("LogTest")), TermName("logger")))), TermName("logger")))), If(Select(Ident(TermName("x")), TermName("isDebugEnabled")), Apply(Select(Ident(TermName("x")), TermName("debug")), List(Apply(Select(Apply(Select(Select(Ident(scala), scala.StringContext), TermName("apply")), List(Literal(Constant("Hello, today is ")), Literal(Constant("")))), TermName("s")), List(Apply(Select(New(Select(Select(Ident(java), java.util), java.util.Date)), termNames.CONSTRUCTOR), List()))))), Literal(Constant(()))))
上面的第一段代碼,是 scalac 生成的等效代碼,可以看到,已經符合了我們的預期,盡在debug級別生效時,才會對messgae進行求助計算,避免不必要的開銷,使得這段代碼,在debug級別關閉時,基本上沒有任何性能的損失。
而第二段代碼,有如天書,難以閱讀。其實,這就是scalac內部的對這一段代碼的表示格式,一般的,我們稱之為 Abstracted Syntax Tree(AST),有興趣的同學,可以通過這個網站 *AST explorer* 來幫助閱讀AST。 scala 2.10時代,寫macro,就必須自己來構建AST,相當於你要徒手寫出這么復雜的一個表達式,這是一件近乎不可完成的任務,所以,macro書寫的難度時及其至高的,好在后續的版本中提供了 q”” 插值,我們可以直接使用q”val x = $pre.logger; if( x.isDebugEnabled ) x.debug($msg)”
來替代上面這么一個復雜的AST,讓 macro 的編寫門檻極大幅度的降低下來。
不過,即使這樣,要想很好的駕馭macro,你還是要懂一些 AST 的知識,否則,還是很難的。 所以,書寫Macro,其實就是一個和編譯器協同工作的過程,這就是macro的難度之所在。或許,未來,隨着 scalameta 和 dotty的成熟,macro的編寫可以進一步的降低吧。
參考: