Scala元编程:在日志库中的应用

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语句,防止不必要的字符串操作,进而改善性能。

那么Scala Logging是如何做到改写表达式的呢?

上一篇实现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是伊甸园元编程中最枯燥最耗时的一个环节,而通过何种方式去将常规的代码和宏生成的代码衔接起来,则是伊甸园中一扇隐秘的大门。

文章已创建 5

发表评论

电子邮件地址不会被公开。 必填项已用*标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部