【问题标题】:Adding annotations at build time to a Java getter, getX(), when the field x is annotated当字段 x 被注释时,在构建时向 Java getter getX() 添加注释
【发布时间】:2015-05-03 19:27:38
【问题描述】:

我想创建以下 Java 注释,并在构建时对其进行处理:

@Target(value = FIELD)
interface @AnnotateGetter {
    Annotation[] value();
}

如果一个字段field@AnnotateGetter注解,那么value数组中的所有Annotations都会被添加到同一个类的getField()方法中,如果这样的方法存在的话。

最简单的方法是什么?

  1. ApectJ,它可以通过声明注释语句向方法添加注释,但是,虽然我知道如何选择一个带有@AnnotateGetter 注释的字段,但我不知道如何选择一个对应的方法带有@AnnotateGetter 注释的字段
  2. 其他一些 AOP 框架
  3. 编写我自己的javax.annotation.processing.Processor,调用一些可以为方法添加注释的库。这样的图书馆的最佳选择是什么?是否必须在 javac 编译源文件后操作字节码,或者我是否可以在生成类文件之前以某种方式挂钩到 javac 并在编译期间添加注释?
  4. 别的...

【问题讨论】:

  • 首先,Java语言不支持Annotation的数组,所以无法编译上面的类声明。
  • 感谢您的信息。由于支持特定注释类型的数组,我只是错误地假设注释数组可以工作。我想我可以编写一个处理器来将直接应用于字段的注释移动到它的 getter,而不是将它们包装在我提议的注释中。这个提议有什么问题吗?如果这可行,我会问一个新问题,或者改写这个问题。推荐哪个?
  • 我认为简单地将注释从字段移动到 getter 应该可以工作(除非它们受到 @Target 注释的限制)
  • 我为我的答案付出了很多努力。一些反馈怎么样?

标签: java annotations aop aspectj bytecode-manipulation


【解决方案1】:

这是通过 AspectJ 使用 APT(注释处理工具)的解决方案。它将指定的注释添加到 getter 方法,但不会从字段中删除它们。所以这是一个“复制”动作,而不是“移动”。

注释处理支持在 1.8.2 版中添加到 AspectJ 中,并在 release notes 中进行了描述。这是一些自洽的示例代码。我从命令行编译了它,因为根据 AspectJ 维护者 Andy Clement's description,我无法从 Eclipse 运行它。

好的,假设我们有一个(Eclipse 或其他)项目目录,目录布局如下:

SO_AJ_APT_MoveAnnotationsFromMemberToGetter
    compile_run.bat
    src
        de/scrum_master/app/Person.java
    src_apt
        de/scrum_master/app/AnnotatedGetterProcessor.java
        de/scrum_master/app/AnnotateGetter.java
        de/scrum_master/app/CollationType.java
        de/scrum_master/app/SortOrder.java
        de/scrum_master/app/Unique.java
        META-INF/services/javax.annotation.processing.Processor

srcsrc_apt 都是源目录,compile_run.bat 是一个 Windows 批处理文件,分两个阶段构建项目(首先是注释处理器,然后是项目的其余部分)并运行最终结果以证明它确实做了它应该做的事情。

用于字段并随后复制到 getter 方法的注释:

package de.scrum_master.app;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface Unique {}
package de.scrum_master.app;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface SortOrder {
    String value() default "ascending";
}
package de.scrum_master.app;

import java.lang.annotation.*;

@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.FIELD, ElementType.METHOD })
public @interface CollationType {
    String value() default "alphabetical";
    String language() default "EN";
}

元注释指定要复制到 getter 方法的字段注释:

请注意,此元注释仅用于注释处理,因此具有SOURCE 保留范围。

package de.scrum_master.app;

import java.lang.annotation.*;

@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)
public @interface AnnotateGetter {
    Class<? extends Annotation>[] value();
}

驱动程序应用:

注意事项:

  • 有四个带注释的字段(idfirstNamelastNamefieldWithoutGetter),但只有前三个有相应的 getter 方法,最后一个没有。所以我们希望 fieldWithoutGetter 稍后会被优雅地处理,稍后通过 APT 生成一个空的或没有 ITD 方面。

  • Person 类上的元注释 @AnnotateGetter({ Unique.class, SortOrder.class, CollationType.class }) 指定考虑将哪些注释复制到 getter 方法。稍后您可以使用它,看看如果您删除其中任何一个,结果会如何变化。

  • 我们还有一些虚拟方法 doSomething()doSomethingElse() 应该不受以后任何注释复制的影响,即它们不应该通过 AspectJ 获得任何新的注释。 (有一个否定的测试用例总是好的。)

  • main(..) 方法使用反射来打印所有字段和方法,包括它们的注释。

package de.scrum_master.app;

import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Method;

@AnnotateGetter({ Unique.class, SortOrder.class, CollationType.class })
public class Person {
    @Unique
    private final int id;

    @SortOrder("descending")
    @CollationType("alphabetical")
    private final String firstName;

    @SortOrder("random")
    @CollationType(value = "alphanumeric", language = "DE")
    private final String lastName;

    @SortOrder("ascending")
    @CollationType(value = "numeric")
    private final int fieldWithoutGetter;

    public Person(int id, String firstName, String lastName, int fieldWithoutGetter) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.fieldWithoutGetter = fieldWithoutGetter;
    }

    public int getId() { return id; }
    public String getFirstName() { return firstName; }
    public String getLastName() { return lastName; }
    public void doSomething() {}
    public void doSomethingElse() {}

    public static void main(String[] args) {
        System.out.println("Field annotations:");
        for (Field field : Person.class.getDeclaredFields()) {
            System.out.println("  " + field.getName());
            for (Annotation annotation : field.getAnnotations())
                System.out.println("    " + annotation);
        }
        System.out.println();
        System.out.println("Method annotations:");
        for (Method method : Person.class.getDeclaredMethods()) {
            System.out.println("  " + method.getName());
            for (Annotation annotation : method.getAnnotations())
                System.out.println("    " + annotation);
        }
    }
}

没有 APT + AspectJ 的控制台输出:

如您所见,打印了字段注释,但没有方法注释,因为我们还没有定义注释处理器(见下文)。

Field annotations:
  id
    @de.scrum_master.app.Unique()
  firstName
    @de.scrum_master.app.SortOrder(value=descending)
    @de.scrum_master.app.CollationType(value=alphabetical, language=EN)
  lastName
    @de.scrum_master.app.SortOrder(value=random)
    @de.scrum_master.app.CollationType(value=alphanumeric, language=DE)
  fieldWithoutGetter
    @de.scrum_master.app.SortOrder(value=ascending)
    @de.scrum_master.app.CollationType(value=numeric, language=EN)

Method annotations:
  main
  getId
  doSomething
  doSomethingElse
  getFirstName
  getLastName

注释处理器:

现在我们需要一个注解处理器为要复制的每个字段和注解组合生成一个切面。这样的方面应该是这样的:

package de.scrum_master.app;

public aspect AnnotateGetterAspect_Person_CollationType_lastName {
    declare @method : * Person.getLastName() : @de.scrum_master.app.CollationType(value = "alphanumeric", language = "DE");
}

很简单,不是吗?注释处理器应该将这些方面生成到目录 .apt_generated 中。 AspectJ 编译器将为我们处理这些,我们将在后面看到。但首先是注释处理器(对不起,代码太长了,但这是你要求的):

package de.scrum_master.app;

import java.io.*;
import java.util.*;

import javax.tools.*;
import javax.annotation.processing.*;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.lang.model.type.*;

@SupportedAnnotationTypes(value = { "*" })
@SupportedSourceVersion(SourceVersion.RELEASE_7)
public class AnnotatedGetterProcessor extends AbstractProcessor {
    private Filer filer;

    @Override
    public void init(ProcessingEnvironment env) {
        filer = env.getFiler();
    }

    @SuppressWarnings("unchecked")
    @Override
    public boolean process(
        Set<? extends TypeElement> elements,
        RoundEnvironment env
    ) {

        // Get classes annotated with something like: @AnnotateGetter({ Foo.class, Bar.class, Zot.class })
        env.getElementsAnnotatedWith(AnnotateGetter.class)
            .stream()
            .filter(annotatedClass -> annotatedClass.getKind() == ElementKind.CLASS)

            // For each filtered class, copy designated field annotations to corresponding getter method, if present
            .forEach(annotatedClass -> {
                String packageName = annotatedClass.getEnclosingElement().toString().substring(8);
                String className = annotatedClass.getSimpleName().toString();

                /*
                 * Unfortunately when we do something like this:
                 *   AnnotateGetter annotateGetter = annotatedClass.getAnnotation(AnnotateGetter.class);
                 *   Class<? extends Annotation> annotationToBeConverted = annotateGetter.value()[0];
                 * We will get this exception:
                 *   Internal compiler error:
                 *     javax.lang.model.type.MirroredTypesException:
                 *       Attempt to access Class objects for TypeMirrors
                 *       [de.scrum_master.app.Unique, de.scrum_master.app.SortOrder, de.scrum_master.app.CollationType]
                 *       at org.aspectj.org.eclipse.jdt.internal.compiler.apt.model.AnnotationMirrorImpl.getReflectionValue
                 *
                 * Thus, we have to use annotation mirrors instead of annotation classes directly,
                 * then tediously extracting annotation values from a nested data structure. :-(
                 */

                // Find @AnnotateGetter annotation and extract its array of values from deep within
                ((List<? extends AnnotationValue>) annotatedClass.getAnnotationMirrors()
                    .stream()
                    .filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(AnnotateGetter.class.getName()))
                    .map(AnnotationMirror::getElementValues)
                    .map(Map::values)
                    .findFirst()
                    .get()
                    .stream()
                    .map(AnnotationValue::getValue)
                    .findFirst()
                    .get()
                )
                    .stream()
                    .map(annotationValueToBeCopied -> (TypeElement) ((DeclaredType) annotationValueToBeCopied.getValue()).asElement())
                    // For each annotation to be copied, get all correspondingly annotated fields
                    .forEach(annotationTypeElementToBeCopied -> {
                        env.getElementsAnnotatedWith(annotationTypeElementToBeCopied)
                            .stream()
                            .filter(annotatedField -> ((Element) annotatedField).getKind() == ElementKind.FIELD)
                            // For each annotated field create an ITD aspect
                            .forEach(annotatedField -> {
                                String fieldName = annotatedField.getSimpleName().toString();
                                String aspectName =
                                    "AnnotateGetterAspect_" + className + "_" +
                                    annotationTypeElementToBeCopied.getSimpleName() + "_" + fieldName;

                                StringBuilder annotationDeclaration = new StringBuilder()
                                    .append("@" + annotationTypeElementToBeCopied.getQualifiedName() + "(");

                                annotatedField.getAnnotationMirrors()
                                    .stream()
                                    .filter(annotationMirror -> annotationMirror.getAnnotationType().toString().equals(annotationTypeElementToBeCopied.getQualifiedName().toString()))
                                    .map(AnnotationMirror::getElementValues)
                                    .forEach(annotationParameters -> {
                                        annotationParameters.entrySet()
                                            .stream()
                                            .forEach(annotationParameter -> {
                                                ExecutableElement annotationParameterType = annotationParameter.getKey();
                                                AnnotationValue annotationParameterValue = annotationParameter.getValue();
                                                annotationDeclaration.append(annotationParameterType.getSimpleName() + " = ");
                                                if (annotationParameterType.getReturnType().toString().equals("java.lang.String"))
                                                    annotationDeclaration.append("\"" + annotationParameterValue + "\"");
                                                else
                                                    annotationDeclaration.append(annotationParameterValue);
                                                annotationDeclaration.append(", ");
                                            });
                                        if (!annotationParameters.entrySet().isEmpty())
                                            annotationDeclaration.setLength(annotationDeclaration.length() - 2);
                                        annotationDeclaration.append(")");
                                    });

                                // For each field with the current annotation, create an ITD aspect
                                // adding the same annotation to the member's getter method
                                String aspectSource = createAspectSource(
                                    annotatedClass, packageName, className,
                                    annotationDeclaration.toString(), fieldName, aspectName
                                );
                                writeAspectSourceToDisk(packageName, aspectName, aspectSource);
                            });
                    });
            });
        return true;
    }

    private String createAspectSource(
        Element parentElement,
        String packageName,
        String className,
        String annotationDeclaration,
        String fieldName,
        String aspectName
    ) {
        String getterMethodName = "get" + fieldName.substring(0, 1).toUpperCase() + fieldName.substring(1);

        StringBuilder aspectSource = new StringBuilder()
            .append("package " + packageName + ";\n\n")
            .append("public aspect " + aspectName + " {\n");

        for (Element childElement : parentElement.getEnclosedElements()) {
            // Search for methods
            if (childElement.getKind() != ElementKind.METHOD)
                continue;
            ExecutableElement method = (ExecutableElement) childElement;

            // Search for correct getter method name
            if (!method.getSimpleName().toString().equals(getterMethodName))
                continue;
            // Parameter list for a getter method must be empty
            if (!method.getParameters().isEmpty())
                continue;
            // Getter method must be public
            if (!method.getModifiers().contains(Modifier.PUBLIC))
                continue;
            // Getter method must be non-static
            if (method.getModifiers().contains(Modifier.STATIC))
                continue;

            // Add call to found method
            aspectSource.append(
                "    declare @method : * " + className + "." + getterMethodName + "() : " +
                annotationDeclaration + ";\n"
            );
        }

        aspectSource.append("}\n");

        return aspectSource.toString();
    }

    private void writeAspectSourceToDisk(
        String packageName,
        String aspectName,
        String aspectSource
    ) {
        try {
            JavaFileObject file = filer.createSourceFile(packageName + "." + aspectName);
            file.openWriter().append(aspectSource).close();
            System.out.println("Generated aspect " + packageName + "." + aspectName);
        } catch (IOException ioe) {
            // Message "already created" can appear if processor runs more than once
            if (!ioe.getMessage().contains("already created"))
                ioe.printStackTrace();
        }
    }
}

注解处理器我就不多说了,请仔细阅读。我还添加了一些源代码cmets,希望它们足够好被理解。

src_apt/META-INF/services/javax.annotation.processing.Processor:

我们需要这个文件,以便注释处理器稍后与 AspectJ 编译器 (ajc) 一起工作。

de.scrum_master.app.AnnotatedGetterProcessor

批处理文件构建和运行项目:

对不起,如果这是特定于平台的,但我想您可以轻松地将其转换为 UNIX/Linux shell 脚本,这非常简单。

@echo off

set SRC_PATH=C:\Users\Alexander\Documents\java-src
set ASPECTJ_HOME=C:\Program Files\Java\AspectJ

echo Building annotation processor
cd "%SRC_PATH%\SO_AJ_APT_MoveAnnotationsFromMemberToGetter"
rmdir /s /q bin
del /q processor.jar
call "%ASPECTJ_HOME%\bin\ajc.bat" -1.8 -sourceroots src_apt -d bin -cp "%ASPECTJ_HOME%\lib\aspectjrt.jar"
jar -cvf processor.jar -C src_apt META-INF -C bin .

echo.
echo Generating aspects and building project
rmdir /s /q bin .apt_generated
call "%ASPECTJ_HOME%\bin\ajc.bat" -1.8 -sourceroots src -d bin -s .apt_generated -inpath processor.jar -cp "%ASPECTJ_HOME%\lib\aspectjrt.jar";processor.jar -showWeaveInfo

echo.
echo Running de.scrum_master.app.Person
java -cp bin;"%ASPECTJ_HOME%\lib\aspectjrt.jar" de.scrum_master.app.Person

构建 + 运行过程的控制台日志:

构建处理器+注解类,然后将它们打包成一个processor.jar

Building annotation processor
Manifest wurde hinzugefügt
Eintrag META-INF/ wird ignoriert
META-INF/services/ wird hinzugefügt(ein = 0) (aus = 0)(0 % gespeichert)
META-INF/services/javax.annotation.processing.Processor wird hinzugefügt(ein = 45) (aus = 46)(-2 % verkleinert)
de/ wird hinzugefügt(ein = 0) (aus = 0)(0 % gespeichert)
de/scrum_master/ wird hinzugefügt(ein = 0) (aus = 0)(0 % gespeichert)
de/scrum_master/app/ wird hinzugefügt(ein = 0) (aus = 0)(0 % gespeichert)
de/scrum_master/app/AnnotatedGetterProcessor.class wird hinzugefügt(ein = 8065) (aus = 3495)(56 % verkleinert)
de/scrum_master/app/AnnotateGetter.class wird hinzugefügt(ein = 508) (aus = 287)(43 % verkleinert)
de/scrum_master/app/CollationType.class wird hinzugefügt(ein = 520) (aus = 316)(39 % verkleinert)
de/scrum_master/app/SortOrder.class wird hinzugefügt(ein = 476) (aus = 296)(37 % verkleinert)
de/scrum_master/app/Unique.class wird hinzugefügt(ein = 398) (aus = 248)(37 % verkleinert)

方面生成 + 项目构建(由于其内置的注释处理支持,只需一个 AspectJ 编译器调用即可完成):

Generating aspects and building project
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_Unique_id
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_fieldWithoutGetter
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_firstName
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_lastName
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_fieldWithoutGetter
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_firstName
Generated aspect de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_lastName
'public int de.scrum_master.app.Person.getId()' (Person.java:31) is annotated with @de.scrum_master.app.Unique method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_Unique_id' (AnnotateGetterAspect_Person_Unique_id.java:4)

'public java.lang.String de.scrum_master.app.Person.getFirstName()' (Person.java:32) is annotated with @de.scrum_master.app.SortOrder method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_firstName' (AnnotateGetterAspect_Person_SortOrder_firstName.java:4)

'public java.lang.String de.scrum_master.app.Person.getFirstName()' (Person.java:32) is annotated with @de.scrum_master.app.CollationType method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_firstName' (AnnotateGetterAspect_Person_CollationType_firstName.java:4)

'public java.lang.String de.scrum_master.app.Person.getLastName()' (Person.java:33) is annotated with @de.scrum_master.app.CollationType method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_CollationType_lastName' (AnnotateGetterAspect_Person_CollationType_lastName.java:4)

'public java.lang.String de.scrum_master.app.Person.getLastName()' (Person.java:33) is annotated with @de.scrum_master.app.SortOrder method annotation from 'de.scrum_master.app.AnnotateGetterAspect_Person_SortOrder_lastName' (AnnotateGetterAspect_Person_SortOrder_lastName.java:4)

最后但同样重要的是,我们再次运行驱动程序应用程序。这次我们应该看到从注解字段复制到对应的getter方法的注解(如果存在这样的方法):

Running de.scrum_master.app.Person
Field annotations:
  id
    @de.scrum_master.app.Unique()
  firstName
    @de.scrum_master.app.SortOrder(value=descending)
    @de.scrum_master.app.CollationType(value=alphabetical, language=EN)
  lastName
    @de.scrum_master.app.SortOrder(value=random)
    @de.scrum_master.app.CollationType(value=alphanumeric, language=DE)
  fieldWithoutGetter
    @de.scrum_master.app.SortOrder(value=ascending)
    @de.scrum_master.app.CollationType(value=numeric, language=EN)

Method annotations:
  main
  getId
    @de.scrum_master.app.Unique()
  doSomethingElse
  getLastName
    @de.scrum_master.app.CollationType(value=alphanumeric, language=DE)
    @de.scrum_master.app.SortOrder(value=random)
  getFirstName
    @de.scrum_master.app.SortOrder(value=descending)
    @de.scrum_master.app.CollationType(value=alphabetical, language=EN)
  doSomething

瞧!享受并随时提出问题。 :-)

更新(2015-05-03):注意,在我的注释处理器代码中,我最初忘记复制注释参数值,因此只为每个注释创建了默认值。我刚刚解决了这个问题,使得注释处理器代码更加冗长。因为我想让它值得重构代码并从中学习一些东西,即使您已经接受了原始答案并以另一种方式解决了您的问题,我还是使用了 Java 8 的东西,例如 lambdas、流、过滤器、映射。如果这个概念对你来说是新的,那么这不是特别可读,特别是对于嵌套的 forEach 循环,但我想试试看我能用它走多远。 ;-)

【讨论】:

  • 我在不移动注释的情况下解决了我的工作问题,从那时起我就被工作淹没了,所以我没有时间看这个。我会在周末阅读它。感谢您的详细回答。
  • 请注意我更新的代码,它现在复制了包括参数在内的完整注释。
【解决方案2】:

你可以试试我的图书馆Byte Buddy。您可以在应用程序启动时在构建过程中运行它,甚至可以从 Java 代理运行它。使用最新版本可以按如下方式创建注释:

DynamicType.Unloaded<?> type = new ByteBuddy()
  .makeAnnotation()
  .name("AnnotateGetter")
  .annotateType(new Target() {
    public ElementType value() { return ElementType.FIELD; }
    public Class<? extends Annotation> annotationType() { return Target.class; }
  }).defineMethod("value", 
                  SomeAnnotation.class, 
                  Collections.emptyList(),
                  Visibility.PUBLIC)
  .withoutCode()
  .make();

然后,您可以通过添加此生成的注释的实例来创建或操作现有类。特定领域的语言保持相似。请参阅教程以获取库的detailed introduction

【讨论】:

  • 有没有办法使用字节好友从字段中删除注释?我想将注释从字段移动到其对应的 getter,这需要 1)找到字段的注释,2)将它们的副本添加到相应的 getter,3)从字段中删除原始注释。为了给出上下文,我想用 Jackson 注释来注释 Groovy 属性,但是由于 GORM 延迟加载,我需要将它们应用于 getter 而不是字段。 Groovy 仅将属性注释应用于字段,因此我正在编写一个处理器来将注释从字段移动到 getter。
  • 我将首先编写将注释从字段移动到方法的处理器(任何方法,但可以轻松选择 getter、setter 等作为目标),然后我将编写一些注释来指定哪个应该移动注释,以及一些其他相关设置(例如如何处理丢失的目标方法:忽略、生成或抛出异常等)。
  • Byte Buddy 并不是为了在重新定义类时删除东西。通过这种方式,Byte Buddy 确保了二进制兼容性。我建议您:将任何自定义注释添加到字段(您可以轻松找到),然后根据观察这些注释,将实际注释添加到相应的 getter。如果这还不够好,Byte Buddy 会公开底层 ASM API,您可以使用它来删除。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 2010-12-10
  • 1970-01-01
  • 1970-01-01
  • 2021-04-02
  • 2015-11-11
  • 1970-01-01
  • 2014-04-02
相关资源
最近更新 更多