【问题标题】:How to implement build specific annotation retention in Java如何在 Java 中实现构建特定的注解保留
【发布时间】:2019-10-26 07:18:34
【问题描述】:

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

@Retention(SOURCE)
public @interface X

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

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

我知道的解决方法:

一如既往地有解决方法。我知道这些:

  • 我们可以使用使构建失败的注释处理器,而不是运行单元测试。这是可行的,但不是最优的,因为使用注释处理器而不是使用 junit API 和更方便的反射 API 的单元测试,测试非常复杂并且更难实现。我只想将此解决方法用作最后的手段。
  • 我们可以将源代码中的 @Retention 更改为 RUNTIME,使用这些附加测试构建源代码,然后预处理 API 以再次删除保留,然后再次构建 API 以供生产使用。这是一个烦人的解决方法,因为它会使构建复杂化并减慢构建速度。

问题:

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

【问题讨论】:

    标签: java unit-testing annotations annotation-processing


    【解决方案1】:

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

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

    接下来,编写您的 junit 测试来执行您打算执行的任何反射分析。虽然(因为它们不会在那里)读取边车文件并查看那里,而不是尝试反射性地查找注释。

    【讨论】:

    • ...sidecar文件的格式可以是源代码生成AnnotatedElement对象中的Set,可以通过单元测试编译和调用,无需发明新文件格式或为它实现一个特殊的解析器。
    • @Holger:太好了!实际上,side car 文件可能是与原始代码完全相同的 API,但仅出于测试目的使用合成注释进行注释,它看起来像原始代码,但保留了 RUNTIME,然后将几乎完全实现我的切换保留的原始想法“仅用于测试”
    【解决方案2】:

    我认为您很好地涵盖了解决方案空间。

    另外两个你没有涵盖:

    • 稍后在后处理步骤中使用 proguard 等工具剥离注释。

    • 破解您的编译器以根据标志切换注释保留。很确定你可以在内部元数据中切换一些标志。可能是由注解@DynamicRetention("flag") 触发的另一个注解处理器注入的?

    【讨论】:

      【解决方案3】:

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

      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

      【讨论】:

        【解决方案4】:

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

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

        Fluent API 很好,在我看来,它比在这个用例中使用反射或注释处理更具可读性。例如,为了确保某些类应该始终具有特定的注释,您可以在单元测试中编写此规则:

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

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

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2016-02-08
          • 1970-01-01
          • 1970-01-01
          • 2018-05-10
          • 2014-05-23
          • 2021-03-13
          相关资源
          最近更新 更多