如何在Java中实现特定于构建的注释保留

时间:2019-06-11 20:00:37

标签: java unit-testing annotations annotation-processing

我有一个注释,当前仅用于内部构建和文档目的。它在运行时没有任何价值,这就是为什么我选择@Retention(SOURCE)

@Retention(SOURCE)
public @interface X

但是,为了验证其正确用法,我想实现一个单元测试,该单元导航整个API,以检查是否在应将注释应用于任何地方的注释。使用普通的Java反射API可以很容易地实现该单元测试,但是我不能这样做,因为给定了@Retention(SOURCE),测试无法反映注解。

为了在测试中使用反射,我必须将其更改为@Retention(RUNTIME),由于运行时字节码的开销,我想避免这种情况。

我知道的解决方法:

总有一些变通办法。我知道这些:

  • 我们可以使用注释处理器使构建失败,而不是运行单元测试。这是可行的,但不是最佳方法,因为测试相当复杂,使用注释处理器比使用junit API和很多更方便的反射API进行单元测试要困难得多。我只想将此解决方法用作最后的手段。
  • 我们可以将源代码中的@Retention更改为RUNTIME,使用其他测试来构建源代码,然后对API进行预处理以再次删除保留,然后再次构建API用于生产用途。这是一个烦人的解决方法,因为它会使构建复杂化并减慢构建速度。

问题:

是否有更方便的方法在运行时仅使用测试保留批注,而不使用Maven保留在实际构建的jar文件中?

4 个答案:

答案 0 :(得分:6)

这是一种可行的混合方法。

编写一个注释处理器,该处理器不执行您要执行的完整测试,而只是记录在发生注释的sidecar文件中。如果要注释类,方法和字段,则可以使用包限定的类名加上方法或字段描述符来相当直接地记录位置。 (但是,如果注释可以出现在方法参数或类型使用站点等更晦涩的地方,则可能会更加困难。)然后,可以将保留策略保留为SOURCE

接下来,编写您的junit测试以执行您打算进行的任何反射分析。尽管(因为它们将不存在)会读入sidecar文件并在其中查找,而不是尝试反射性地找到这些注释。

答案 1 :(得分:2)

我认为您已经很好地涵盖了解决方案领域。

您没有涵盖的另外两个:

  • 稍后在后期处理步骤中使用诸如proguard之类的工具对注释进行剥离。

  • 使编译器无法根据标志来切换注释保留。相当确定您可以在内部元数据中切换一些标志。也许是由@DynamicRetention(“ flag”)注解触发的另一个注解处理器注入的?

答案 2 :(得分:1)

其他解决方法之一可能包括:

  1. 保留默认的retention = CLASS
  2. 使用的库将直接读取字节码
@interface X {
}

@X
public class Main {
  public static void main(String[] args) throws IOException {
    ClassPathResource classResource = new ClassPathResource("com/caco3/annotations/Main.class");
    try (InputStream is = classResource.getInputStream()) {
      ClassReader classReader = new ClassReader(is);
      AnnotationMetadataReadingVisitor visitor = new AnnotationMetadataReadingVisitor(Main.class.getClassLoader());
      classReader.accept(visitor, 0);
      System.out.println(visitor.getAnnotationTypes());
    }
  }
}

收益:

[com.caco3.annotations.X]

使用的库为ASM

  

ASM是一种通用的Java字节码操作和分析框架

此代码使用Spring Framework中的一些类:

但是这种方法也存在与您描述的相同的缺点:

  

运行时字节码的开销

因为(来自javadoc):

  

注释应由编译器记录在类文件中,但 VM不必在运行时保留它们。

public static void main(String[] args) throws IOException {
    X x = AnnotationUtils.findAnnotation(Main.class, X.class);
    System.out.println(x);
}

输出:null

答案 3 :(得分:1)

如果@Retention(CLASS)是可以接受的,那么我建议使用ArchUnit。您描述的任务听起来很合适。 ArchUnit可用于为您的体系结构定义和验证规则。例如,它可用于限制某些类/包之间的访问,例如验证类层次结构,键入名称或注释。

它通常由JUnit或任何其他测试框架作为单元测试执行。它通过分析字节码来工作,因此无需切换到运行时保留。

流利的API很好,我认为对于此用例而言,与使用反射或注释处理相比,它更易读。例如,为了确保某些类始终具有特定的注释,您可以在单元测试中编写以下规则:

classes().that().areAssignableTo(MyService.class).should().beAnnotatedWith(MyAnnotation.class)

还可以创建自定义规则来声明更复杂的约束。