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 元编程:在日志库中的应用