
Scala 中常用的第三方日志库,我这边了解的有 log4s^1和 Scala Logging^2两个。
在 Scala Logging 中:
logger.debug(s"Some $expensive message!") 会被 Scala 的宏转换成:
if (logger.isDebugEnabled) logger.debug(s"Some $expensive message!") 因为在实际代码运行时,实际上会先做字符串插值,然后在看日志级别为 DEBUG 的日志是否需要输出。所以我们通过 if 语句,防止不必要的字符串操作,进而改善性能。
在上一篇^3实现 lombok.Data 的时候,我们实际上是通过注解告诉编译器,我们需要在该注解所作用的类上面生成 getter 和 setter。说白了,就是注解 @data 让我们定位具体的类,然后我们再插入代码。而这个例子实际上是直接将生成代码的规则和具体的方法衔接起来。
完整的实现如下所示:
final class Logger private (val underlying: org.slf4j.Logger) { def debug(message: String): Unit = macro LoggerMacro.debugMessage } private object LoggerMacro { type LoggerCOntext= blackbox.Context {type PrefixType = Logger} private def deconstructInterpolatedMessage(c: LoggerContext) (message: c.Expr[String]) = { import c.universe._ message.tree match { case q"scala.StringContext.apply(..$parts).s(..$args)" => val format = parts.iterator.map({ case Literal(Constant(str: String)) => str }) // Emulate standard interpolator escaping .map(StringContext.treatEscapes) // Escape literal slf4j format anchors if the resulting call will require a format string .map(str => if (args.nonEmpty) str.replace("{}", "\{}") else str) .mkString("{}") val formatArgs = args.map(t => c.Expr[Any](t)) (c.Expr(q"$format"), formatArgs) case _ => (message, Seq.empty) } } private def formatArgs(c: LoggerContext)(args: c.Expr[Any]*) = { import c.universe._ args.map { arg => c.Expr[AnyRef]( if (arg.tree.tpe <:< weakTypeOf[AnyRef]) arg.tree else q"$arg.asInstanceOf[_root_.scala.AnyRef]" ) } } def debugMessageArgs(c: LoggerContext) (message: c.Expr[String], args: c.Expr[Any]*): c.universe.Tree = { import c.universe._ val underlying = q"${c.prefix}.underlying" val anyRefArgs = formatArgs(c)(args: _*) if (args.length == 2) q"if ($underlying.isDebugEnabled) $underlying.debug($message, _root_.scala.Array(${anyRefArgs.head}, ${anyRefArgs(1)}): _*)" else q"if ($underlying.isDebugEnabled) $underlying.debug($message, ..$anyRefArgs)" } def debugMessage(c: LoggerContext) (message: c.Expr[String]): c.universe.Tree = { val (messageFormat, args) = deconstructInterpolatedMessage(c)(message) debugMessageArgs(c)(messageFormat, args: _*) } } 首先,blackbox.Context 事实上限定了这个宏的作用域即在类 Logger 之中。可以观察到,单例 LoggerMacro 的每一个方法都带有 LoggerContext 这个参数,每一个方法的具体实现,也和 LoggerContext 有一定的关系。
debugMessage 函数首先将字符串插值这个表达式通过 deconstructInterpolateMessage 解构成 messageFormat 和 args。下面这段代码可以非常明确的解释,什么是 messageFormat 以及什么是 args:
logger.info("Info :{}" , user.getName()) 如果是 Scala 的字符串插值的话,就是 s"Info :${user.getName}"。
解构之后,我们只需要通过 Quasiquote 将带有条件语句的代码重新构造起来就可以了。
另外一个需要注意的点是,在使用 @data 的时候,我们实际上需要在工程中开启 Paradise 插件,而我们在使用 Scala Logging 的时候,实际上直接依赖 Scala Logging 就可以了,不需要开启 Paradise 插件。这就涉及到一个问题:我们在上一节中做了详细解释的代码,到底是在哪个环节执行的。
很简单,我们可以通过在 debugMessage 增加日志的方式,确定这个细节。
最终发现,实际上,我们依赖了 Scala Logging,但是项目自身没有使用编译插件,在编译过程中,编译器遇到 Scala Logging 中会生成代码的方法时,实际上还是会去利用编译插件,生成代码。
实际上,这一篇的内容虽然在宏的具体使用接口上和 lombok.Data 那一篇有细节上的差异,但实际上最终生成代码的还是在使用 Quasiquote,所以如何高效地在 REPL 中尝试 Quasiquote 至关重要。Quasiquote 是伊甸园元编程中最枯燥最耗时的一个环节,而通过何种方式去将常规的代码和宏生成的代码衔接起来,则是伊甸园中一扇隐秘的大门。
阅读原文:Scala 元编程:在日志库中的应用