V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
sadhen
V2EX  ›  Scala

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

  •  
  •   sadhen · 2019-07-17 22:29:50 +08:00 · 4584 次点击
    这是一个创建于 1937 天前的主题,其中的信息可能已经有所发展或是发生改变。

    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 是如何做到改写表达式的呢?

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

    目前尚无回复
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   987 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 22:12 · PVG 06:12 · LAX 14:12 · JFK 17:12
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.