代码生成之Scala宏编程 类Lombok工具的实现

Scala 中的宏

  1. Scala 中的宏要复杂的多,但它对生成代码的方式,提供了更大的灵活性。
  2. Scala 宏仍然是用 Scala 写的,一定程序上保证了开发体验的一致。
  3. Scala 宏总是要返回一个AST,这需要你对 Scala AST 有一定的了解。
  4. 但 Quasiquotes 可以帮你轻松生成 AST。

如:

import scala.reflect.macros.blackbox
import scala.language.experimental.macros
object Hello {  
  def hello(msg:String): Unit = macro helloImpl
  def helloImpl(c: blackbox.Context)(msg: c.Expr[String]): c.Expr[Unit] = {
    import c.universe._
    c.Expr(q"""println("hello!")""")
  }
}

Scala 宏现状

  • 2.10.x
    • 需要引入外部编译器插件 macro paradise compiler plugin。
    • 需要引入org.scalamacros quasiquotes。
  • 2.11.x+
    • 引入scala-reflect即可。
    • 开始引入黑盒和白盒概念。
    • 宏实现直到2.13.7仍是实验性、不稳定的API。
  • Def macros 实现工具方法,类型转换,减少冗余代码等
  • Implicit macros 与隐式参数结合,在开源库中用的很多
  • Macro annotations 编译期反射
    • 2.13.x 需要scalac参数 -Ymacro-annotations
    • 2.11.x,2.12.x需要 macro paradise compiler plugin。
  • Scala3(dotty是Scala3编译器的名称)重新设计了宏API,不兼容Scala2。

注解宏

  • 同样是编译期改变代码行为,仅支持使用白盒宏。
  • 具有与Java Annotation Processor相似功能。
  • 与Scala注解也类似,继承StaticAnnotation特质。
  • 注解宏执行时依赖macro paradise compiler plugin。
  • 基本与使用Java注解一样。
    • 类上 @SerialVersionUID
    • 方法上 @tailrec @inline
    • 字段上 @transient @volatile @BeanProperty
    • 泛型上 @specialized
    • 其他 @unchecked

Scala中Lombok的可行性

字节码生成+Intellij支持
在这里插入图片描述
在这里custom-scala-plugin指的是自己编写的Scala-Macro-Tools插件。
在这里插入图片描述

编写自己的宏注解一 builder

步骤

  • 定义注解 限定使用场景:替普通类或样例类的主构造函数生成builder模式
  • 定义宏实现
    • 1.获取主构造函数中的必要内容
      • 类的类型名称
      • 是否样例类
      • 构造函数是否柯里化
      • 参数列表的语法树
        • 每个参数的名称
        • 每个参数的类型
        • 每个参数的默认值
        • 每个参数的修饰符
      • 类上的类型参数(泛型)和泛型边界限定符
    • 2.使用1获取的信息创建一个Builder类的语法树
      • 每个参数对应一个内部私有成员变量
      • 每个参数对应一个公共set方法
    • 3.使用1获取的信息创建一个builder方法的语法树,如有必要顺便创建一个该类对应的单例对象的语法树
    • 4.将builder方法和Builder类放入单例对象的定义中,返回

具体实现

/*
 * Copyright (c) 2021 jxnu-liguobin && contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package io.github.dreamylost.macros

import scala.reflect.macros.whitebox

/**
 *
 * @author 梦境迷离
 * @since 2021/7/7
 * @version 1.0
 */
object builderMacro {

  class BuilderProcessor(override val c: whitebox.Context) extends AbstractMacroProcessor(c) {

    import c.universe._

    private def getBuilderClassName(classTree: TypeName): TypeName = {
      TypeName(classTree.toTermName.decodedName.toString + "Builder")
    }

    private def getFieldDefinition(field: Tree): Tree = {
      val ValDef(_, name, tpt, rhs) = field
      q"private var $name: $tpt = $rhs"
    }

    private def getFieldSetMethod(typeName: TypeName, field: Tree, classTypeParams: List[Tree]): Tree = {
      val builderClassName = getBuilderClassName(typeName)
      val returnTypeParams = extractClassTypeParamsTypeName(classTypeParams)
      lazy val valDefMapTo = (v: ValDef) => {
        q"""
          def ${v.name}(${v.name}: ${v.tpt}): $builderClassName[..$returnTypeParams] = {
              this.${v.name} = ${v.name}
              this
          }
         """
      }
      valDefMapTo(field.asInstanceOf[ValDef])
    }

    private def getBuilderClassAndMethod(typeName: TypeName, fieldss: List[List[Tree]], classTypeParams: List[Tree], isCase: Boolean): List[Tree] = {
      val fields = fieldss.flatten
      val builderClassName = getBuilderClassName(typeName)
      val builderFieldMethods = fields.map(f => getFieldSetMethod(typeName, f, classTypeParams))
      val builderFieldDefinitions = fields.map(f => getFieldDefinition(f))
      val returnTypeParams = extractClassTypeParamsTypeName(classTypeParams)
      val builderMethod = q"def builder[..$classTypeParams](): $builderClassName[..$returnTypeParams] = new $builderClassName()"
      val buulderClass =
        q"""
          class $builderClassName[..$classTypeParams] {

            ..$builderFieldDefinitions

            ..$builderFieldMethods

            def build(): $typeName[..$returnTypeParams] = ${getConstructorWithCurrying(typeName, fieldss, isCase)}
          }
        """
      List(builderMethod, buulderClass)
    }

    override def createCustomExpr(classDecl: ClassDef, compDeclOpt: Option[ModuleDef] = None): Any = {
      val classDefinition = mapToClassDeclInfo(classDecl)
      val builder = getBuilderClassAndMethod(classDefinition.className, classDefinition.classParamss,
        classDefinition.classTypeParams, isCaseClass(classDecl))
      val compDecl = appendModuleBody(compDeclOpt, builder, classDefinition.className)
      // Return both the class and companion object declarations
      c.Expr(
        q"""
        $classDecl
        $compDecl
      """)
    }
  }
}

完整代码 https://github.com/jxnu-liguobin/scala-macro-tools/blob/master/src/main/scala/io/github/dreamylost/macros/builderMacro.scala

编写自己的宏注解二 log

步骤

  • 定义注解 限定使用场景:替类生成log属性
  • 定义宏实现
    • 1.获取注解的输入参数
      • 日志的类型枚举
    • 2.使用1获取的信息创建一个私有内部属性log的语法树
    • 3.将log属性添加到类定义体的最前方,返回

在这里插入图片描述

具体实现

/*
 * Copyright (c) 2021 jxnu-liguobin && contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy of
 * this software and associated documentation files (the "Software"), to deal in
 * the Software without restriction, including without limitation the rights to
 * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
 * the Software, and to permit persons to whom the Software is furnished to do so,
 * subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
 * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
 * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
 * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 */

package io.github.dreamylost.macros

import io.github.dreamylost.logs.LogType._
import io.github.dreamylost.logs.{ LogTransferArgument, LogType }
import io.github.dreamylost.{ PACKAGE, logs }

import scala.reflect.macros.whitebox

/**
 *
 * @author 梦境迷离
 * @since 2021/7/7
 * @version 1.0
 */
object logMacro {

  class LogProcessor(override val c: whitebox.Context) extends AbstractMacroProcessor(c) {

    import c.universe._

    private val extractArgumentsDetail: (Boolean, logs.LogType.Value) = extractArgumentsTuple2 {
      case q"new log(logType=$logType)" =>
        val tpe = getLogType(logType.asInstanceOf[Tree])
        (false, tpe)
      case q"new log(verbose=$verbose)" => (evalTree(verbose.asInstanceOf[Tree]), LogType.JLog)
      case q"new log(verbose=$verbose, logType=$logType)" =>
        val tpe = getLogType(logType.asInstanceOf[Tree])
        (evalTree(verbose.asInstanceOf[Tree]), tpe)
      case q"new log()" => (false, LogType.JLog)
      case _            => c.abort(c.enclosingPosition, ErrorMessage.UNEXPECTED_PATTERN)
    }

    private def getLogType(logType: Tree): LogType = {
      if (logType.children.exists(t => t.toString().contains(PACKAGE))) {
        evalTree(logType)
      } else {
        LogType.getLogType(logType.toString())
      }
    }

    private def logTree(annottees: Seq[c.universe.Expr[Any]]): c.universe.Tree = {
      val buildArg = (name: Name) => LogTransferArgument(name.toTermName.decodedName.toString, isClass = true)
      (annottees.map(_.tree) match {
        case (classDef: ClassDef) :: Nil =>
          LogType.getLogImpl(extractArgumentsDetail._2).getTemplate(c)(buildArg(classDef.name))
        case (moduleDef: ModuleDef) :: Nil =>
          LogType.getLogImpl(extractArgumentsDetail._2).getTemplate(c)(buildArg(moduleDef.name).copy(isClass = false))
        case (classDef: ClassDef) :: (_: ModuleDef) :: Nil =>
          LogType.getLogImpl(extractArgumentsDetail._2).getTemplate(c)(buildArg(classDef.name))
        case _ => c.abort(c.enclosingPosition, ErrorMessage.ONLY_OBJECT_CLASS)
      }).asInstanceOf[Tree]
    }

    override def impl(annottees: c.universe.Expr[Any]*): c.universe.Expr[Any] = {
      val resTree = annottees.map(_.tree) match {
        case (classDef: ClassDef) :: _ =>
          if (classDef.mods.hasFlag(Flag.CASE)) {
            c.abort(c.enclosingPosition, ErrorMessage.ONLY_OBJECT_CLASS)
          }
          val newClass = extractArgumentsDetail._2 match {
            case ScalaLoggingLazy | ScalaLoggingStrict =>
              appendImplDefSuper(checkGetClassDef(annottees), _ => List(logTree(annottees)))
            case _ =>
              prependImplDefBody(checkGetClassDef(annottees), _ => List(logTree(annottees)))
          }
          val moduleDef = getModuleDefOption(annottees)
          q"""
             ${if (moduleDef.isEmpty) EmptyTree else moduleDef.get}
             $newClass
           """
        case (_: ModuleDef) :: _ =>
          extractArgumentsDetail._2 match {
            case ScalaLoggingLazy | ScalaLoggingStrict => appendImplDefSuper(getModuleDefOption(annottees).get, _ => List(logTree(annottees)))
            case _                                     => prependImplDefBody(getModuleDefOption(annottees).get, _ => List(logTree(annottees)))
          }
        // Note: If a class is annotated and it has a companion, then both are passed into the macro.
        // (But not vice versa - if an object is annotated and it has a companion class, only the object itself is expanded).
        // see https://docs.scala-lang.org/overviews/macros/annotations.html
      }

      printTree(force = extractArgumentsDetail._1, resTree)
      c.Expr[Any](resTree)
    }
  }
}

完整代码 https://github.com/jxnu-liguobin/scala-macro-tools/blob/master/src/main/scala/io/github/dreamylost/macros/logMacro.scala

让IDEA识别注解生成的语法树

步骤

  • 创建插件,使用PSI编程拓展Intellij Scala插件
  • 编写自定义注入类继承SyntheticMembersInjector类
    • injectFunctions 为类注入函数
    • injectInners 为类注入函数
    • needsCompanionObject 注入条件:是否需要伴生对象
    • injectSupers 为类注入超类
    • injectMembers 为类注入成员
  • log属性在IDEA中的无中生有
    • 重写injectMembers
    • 当类上有注解@log,且参数为JLog时返回log属性字符串:private final val log: java.util.logging.Logger= ???
    • 打包插件,安装插件,识别源码中不存在的log属性。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值