有没有办法在 Scala 中创建自定义注释并编写自定义注释处理器来验证注释?

use*_*993 6 java annotations scala annotation-processor

我一直在学习注释以及注释处理器是什么。我正在查看 Java 示例,似乎有一种正确的方法可以做到这一点。但是,在 Scala 中,我没有合适的网站/文档来创建自定义注释和注释处理器。

如果在 Scala 中不可能,有没有办法在 Scala 类中使用 Java 自定义注释处理器?

有人可以指出我正确的方向吗?

Dmy*_*tin 2

Scala中有宏注释

https://docs.scala-lang.org/overviews/macros/annotations.html

我猜这类似于Java中的编译时处理注释。


注释处理器可以用 Scala 编写。但注解必须用Java编写(scala注解不能注解Java代码)。并且注释处理器不会处理 Scala 源。Java编译时注释处理由Java编译器处理,它不能编译Scala源代码。

Scala 编译器不知道任何注释处理器。在 Scala 编译时注释处理是宏注释(类似地,它们可以处理 Scala 源,而不是 Java 源)。Scala 宏注释和 Java 注释处理器是两种完全不同的机制,它们协调地使用 Scala 源和 Java 源执行类似的操作。

因此,如果您想以类似的方式处理 Java 和 Scala 源代码,您将不得不重复工作。您必须创建处理 Java 源的注释处理器和宏注释,以执行与 Scala 源类似的操作。

这是创建构建器的示例。注释处理器在 中创建一个构建器target/scala-2.13/classes,宏注释在伴生对象内创建一个构建器。这是处理器和宏注释之间的区别:处理器可以生成代码但不能重写它(没有 Java 编译器内部1 2),宏注释可以重写代码,但只能在类及其同伴中重写。另一个区别是处理器生成 Java 源代码,而宏注释生成 Scala AST。

注释处理器/src/main/java/org/example/BuilderProperty.java

package org.example;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.SOURCE)
public @interface BuilderProperty {
}
Run Code Online (Sandbox Code Playgroud)

注释处理器/src/main/resources/META-INF/services/javax.annotation.processing.Processor

org.example.BuilderProcessor
Run Code Online (Sandbox Code Playgroud)

注释处理器/src/main/scala/org/example/BuilderProcessor.scala

org.example.BuilderProcessor
Run Code Online (Sandbox Code Playgroud)

(由于某种原因println.asScala, 并在处理过程中Option.when抛出NoSuchMethodError。)

注释处理器/src/main/scala/org/example/scalaBuilderProperty.scala

package org.example

//import com.google.auto.service.AutoService
import javax.annotation.processing._
import javax.lang.model.SourceVersion
import javax.lang.model.element.{Element, TypeElement}
import javax.lang.model.`type`.ExecutableType
import javax.tools.Diagnostic
import java.io.IOException
import java.io.PrintWriter
import java.util
import scala.collection.immutable
import scala.jdk.CollectionConverters._
import scala.util.Using

@SupportedAnnotationTypes(Array("org.example.BuilderProperty"))
@SupportedSourceVersion(SourceVersion.RELEASE_8)
//@AutoService(Array(classOf[Processor])) // can't use AutoService because the processor is written in Scala, so using the file in META-INF
class BuilderProcessor extends AbstractProcessor {
  override def process(annotations: util.Set[_ <: TypeElement], roundEnv: RoundEnvironment): Boolean = {
    System.out.println("process")
//    println("process") // java.lang.RuntimeException: java.lang.NoSuchMethodError: scala.Predef$.println(Ljava/lang/Object;)V //com.sun.tools.javac.main.Main.compile //sbt.internal.inc.javac.LocalJavaCompiler.run
//    annotations.asScala.toSet[TypeElement].foreach { annotation => //java.lang.RuntimeException: java.lang.NoSuchMethodError: scala.jdk.CollectionConverters$.SetHasAsScala(Ljava/util/Set;)Lscala/collection/convert/AsScalaExtensions$SetHasAsScala;
    new SetHasAsScala(annotations).asScala.toSet[TypeElement].foreach { annotation =>
      val annotatedElements = roundEnv.getElementsAnnotatedWith(annotation)
      val (setters: Set[Element @unchecked], otherMethods) = new SetHasAsScala(annotatedElements).asScala.toSet.partition(element =>
        element.asType.asInstanceOf[ExecutableType].getParameterTypes.size == 1 &&
          element.getSimpleName.toString.startsWith("set")
      )
      otherMethods.foreach(element =>
        processingEnv.getMessager.printMessage(Diagnostic.Kind.ERROR,
          "@BuilderProperty must be applied to a setXxx method with a single argument", element)
      )
      setters.headOption.foreach { head =>
        val className = head.getEnclosingElement.asInstanceOf[TypeElement].getQualifiedName.toString
        val setterMap = setters.map(setter =>
          setter.getSimpleName.toString -> setter.asType.asInstanceOf[ExecutableType].getParameterTypes.get(0).toString
        )
        writeBuilderFile(className, setterMap)
      }
    }
    true
  }

  @throws[IOException]
  private def writeBuilderFile(className: String, setterMap: immutable.Set[(String, String)]): Unit = {
    val lastDot = className.lastIndexOf('.')
    val packageName = if (lastDot > 0) Some(className.substring(0, lastDot)) else None
//    val packageName = Option.when(lastDot > 0)(className.substring(0, lastDot)) //java.lang.RuntimeException: java.lang.NoSuchMethodError: scala.Option$.when(ZLscala/Function0;)Lscala/Option; //com.sun.tools.javac.main.Main.compile //sbt.internal.inc.javac.LocalJavaCompiler.run
    val simpleClassName = className.substring(lastDot + 1)
    val builderClassName = className + "Builder"
    val builderSimpleClassName = builderClassName.substring(lastDot + 1)
    val builderFile = processingEnv.getFiler.createSourceFile(builderClassName)
    Using(new PrintWriter(builderFile.openWriter)) { out =>
        val packageStr = packageName.map(name => s"package $name;\n\n").getOrElse("")
        out.print(
          s"""${packageStr}public class $builderSimpleClassName {
             |
             |    private $simpleClassName object = new $simpleClassName();
             |
             |    public $simpleClassName build() {
             |        return object;
             |    }
             |
             |${ setterMap.map { case methodName -> argumentType =>
          s"""    public $builderSimpleClassName $methodName($argumentType value) {
             |        object.$methodName(value);
             |        return this;
             |    }
             |""".stripMargin }.mkString("\n") }
             |}
             |""".stripMargin
        )
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

注释-用户/src/main/java/org/example/Person.java

package org.example

import scala.annotation.{StaticAnnotation, compileTimeOnly}
import scala.language.experimental.macros
import scala.reflect.macros.blackbox

@compileTimeOnly("enable macro annotations")
class scalaBuilderProperty extends StaticAnnotation {
  def macroTransform(annottees: Any*): Any = macro BuilderPropertyMacro.impl
}

object BuilderPropertyMacro {
  def impl(c: blackbox.Context)(annottees: c.Tree*): c.Tree = {
    import c.universe._

    def modifyObject(cls: Tree, obj: Tree): Tree = (cls, obj) match {
      case 
        (
          q"$_ class $tpname[..$_] $_(...$paramss) extends { ..$_ } with ..$_ { $_ => ..$_ }",
          q"$mods object $tname extends { ..$earlydefns } with ..$parents { $self => ..$body }"
        ) =>
        val builder = TypeName(s"${tpname}Builder")

        def isBuilderPropertyAnnotated(mods: Modifiers): Boolean = {
          def removeMetaAnnotations(tpe: Type): Type = tpe match {
            case tp: AnnotatedType => removeMetaAnnotations(tp.underlying)
            case _ => tpe
          }

          def getType(tree: Tree): Type = c.typecheck(tree, mode = c.TYPEmode, silent = true).tpe

          mods.annotations
            .collect {
              case q"new { ..$_ } with ..$parents { $_ => ..$_ }" => parents
            }
            .flatten
            .map(t => removeMetaAnnotations(getType(t)))
            .exists(_ =:= typeOf[BuilderProperty])
        }

        val setters = paramss.flatten.collect {
          case q"$mods var $tname: $tpt = $_" if isBuilderPropertyAnnotated(mods) =>
            val setter = TermName(s"set${tname.toString.capitalize}")
            q"""def $setter(value: $tpt): $builder = {
              this.`object`.$setter(value)
              this
            }"""
        }

        q"""$mods object $tname extends { ..$earlydefns } with ..$parents { $self =>
          ..$body
          class $builder {
            private val `object`: $tpname = new $tpname()
            def build: $tpname = this.`object`
            ..$setters
          }
        }"""
    }

    def modify(cls: Tree, obj: Tree): Tree = q"..${Seq(cls, modifyObject(cls, obj))}"

    annottees match {
      case (cls: ClassDef) :: (obj: ModuleDef) :: Nil =>
        modify(cls, obj)
      case (cls@q"$_ class $tpname[..$_] $_(...$_) extends { ..$_ } with ..$_ { $_ => ..$_ }") :: Nil =>
        modify(cls, q"object ${tpname.toTermName}")
      case _ => c.abort(c.enclosingPosition, "@scalaBuilderProperty must annotate classes")
    }
  }
}
Run Code Online (Sandbox Code Playgroud)

注释-用户/target/scala-2.13/classes/org/example/PersonBuilder.java

package org.example;

public class Person {

    private int age;

    private String name;

    public int getAge() {
        return age;
    }

    @BuilderProperty
    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    @BuilderProperty
    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Person{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}
Run Code Online (Sandbox Code Playgroud)

注释-用户/src/main/scala/org/example/ScalaPerson.scala

  // GENERATED JAVA SOURCE
//package org.example;
//
//public class PersonBuilder {
//
//    private Person object = new Person();
//
//    public Person build() {
//        return object;
//    }
//
//    public PersonBuilder setAge(int value) {
//        object.setAge(value);
//        return this;
//    }
//
//    public PersonBuilder setName(java.lang.String value) {
//        object.setName(value);
//        return this;
//    }
//
//}
Run Code Online (Sandbox Code Playgroud)

注释-用户/src/main/scala/org/example/Main.scala

package org.example

//import scala.annotation.meta.beanSetter
import scala.beans.BeanProperty

@scalaBuilderProperty
case class ScalaPerson(
                        @BeanProperty
                        @(BuilderProperty /*@beanSetter @beanSetter*/)
                        var age: Int = 0,

                        @BeanProperty
                        @(BuilderProperty /*@beanSetter*/)
                        var name: String = ""
                      )

  // GENERATED SCALA AST (-Ymacro-debug-lite)
//object ScalaPerson extends scala.AnyRef {
//    def <init>() = {
//      super.<init>();
//      ()
//    };
//    class ScalaPersonBuilder extends scala.AnyRef {
//      def <init>() = {
//        super.<init>();
//        ()
//      };
//      private val `object`: ScalaPerson = new ScalaPerson();
//      def build: ScalaPerson = this.`object`;
//      def setAge(value: Int): ScalaPersonBuilder = {
//        this.`object`.setAge(value);
//        this
//      };
//      def setName(value: String): ScalaPersonBuilder = {
//        this.`object`.setName(value);
//        this
//      }
//    }
//  };
//  ()
//}
Run Code Online (Sandbox Code Playgroud)

构建.sbt

package org.example

object Main {
  def main(args: Array[String]): Unit = {
    val person = new PersonBuilder()
      .setAge(25)
      .setName("John")
      .build

    println(person)//Person{age=25, name='John'}

    val person1 = new ScalaPerson.ScalaPersonBuilder()
      .setAge(25)
      .setName("John")
      .build

    println(person1)//ScalaPerson(25,John)
  }
}
Run Code Online (Sandbox Code Playgroud)
sbt clean compile annotation-user/run
Run Code Online (Sandbox Code Playgroud)