如何在 Java 中实现构建特定注释保留
How to implement build specific annotation retention in Java
我有一个注释,目前仅用于内部构建和文档目的。它在 运行 时不提供任何价值,这就是我选择 @Retention(SOURCE)
:
的原因
@Retention(SOURCE)
public @interface X
但是,为了验证它的正确用法,我想实现一个单元测试来导航整个 API 以检查注释是否应用到它应该应用的所有地方。通过使用普通的 Java 反射 APIs 可以很容易地实现该单元测试,但我不能这样做,因为测试无法反映注释,因为它是 @Retention(SOURCE)
.
为了在测试中使用反射,我必须将其更改为 @Retention(RUNTIME)
,由于 运行 时字节代码的开销,我想避免这种情况。
我知道的解决方法:
一如既往的解决方法。我知道这些:
- 我们可以使用构建失败的注解处理器,而不是 运行 单元测试。这是可行的但不太理想,因为测试非常复杂并且使用注释处理器更难实现,而不是使用 junit APIs 和 much 更方便的单元测试反射 API。我只想将此解决方法用作最后的手段。
- 我们可以将源代码中的
@Retention
更改为 RUNTIME
,使用这些额外的测试构建源代码,然后预处理 API 以再次删除保留,然后第二次构建 API 以供生产使用。这是一个烦人的解决方法,因为它会使构建复杂化并减慢构建速度。
问题:
是否有更方便的方法来使用 Maven 在 运行 时间仅用于测试,而不是在实际构建的 jar 文件中保留注释?
我认为您很好地涵盖了解决方案 space。
还有两个你没有讲到:
稍后在 post 处理步骤中使用 proguard 等工具删除注释。
破解你的编译器以根据标志切换注释保留。很确定您可以在内部元数据中切换一些标志。可能由注解 @DynamicRetention("flag")?
触发的另一个注解处理器注入
其他解决方法之一可能包括:
- 保留默认值
retention = CLASS
。
- 使用将读取 字节码 直接.
的库
@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 is an all purpose Java bytecode manipulation and analysis framework
此代码使用 Spring 框架中的一些 类:
ClassPathResource
- 类似于 java.io.File
AnnotationMetadataReadingVisitor
(source code) - is a ClassVisitor
收集注释元数据
但是这种方法存在与您描述的相同的缺点:
overhead in byte code at run time
因为(来自javadoc):
Annotations are to be recorded in the class file by the compiler but need not be retained by the VM at run time.
public static void main(String[] args) throws IOException {
X x = AnnotationUtils.findAnnotation(Main.class, X.class);
System.out.println(x);
}
输出:null
这是一种可能有效的混合方法。
编写一个注解处理器,它不执行您想要执行的完整测试,而是仅在出现注解的边车文件中进行记录。如果您要注释 classes、方法和字段,可以使用 package-qualified class 名称加上方法或字段描述符相当直接地记录位置。 (不过,如果您的注释可以出现在更隐蔽的地方,例如方法参数或类型使用站点,这可能会更困难。)然后,您可以将保留策略保持为 SOURCE
.
接下来,编写你的 junit 测试来做你打算做的任何反射分析。与其尝试反射性地查找注释,不如(因为它们不会在那里)读入 sidecar 文件并查看那里。
如果@Retention(CLASS)
可以接受,那么我会推荐使用ArchUnit。您描述的任务听起来很合适。 ArchUnit 可用于为您的架构定义和验证规则。例如,它可用于限制某些 classes/packages 之间的访问,或者例如验证 class 层次结构、类型名称 - 或注释。
它通常作为单元测试由 JUnit 或任何其他测试框架执行。它通过分析字节码来工作,因此无需切换到运行时保留。
流利的 API 很好,在我看来,比使用反射或注释处理这个用例更具可读性。例如,要确保某些 classes 应该始终具有特定的注释,您可以在单元测试中编写此规则:
classes().that().areAssignableTo(MyService.class).should().beAnnotatedWith(MyAnnotation.class)
也可以创建自定义规则来声明更复杂的约束。
我有一个注释,目前仅用于内部构建和文档目的。它在 运行 时不提供任何价值,这就是我选择 @Retention(SOURCE)
:
@Retention(SOURCE)
public @interface X
但是,为了验证它的正确用法,我想实现一个单元测试来导航整个 API 以检查注释是否应用到它应该应用的所有地方。通过使用普通的 Java 反射 APIs 可以很容易地实现该单元测试,但我不能这样做,因为测试无法反映注释,因为它是 @Retention(SOURCE)
.
为了在测试中使用反射,我必须将其更改为 @Retention(RUNTIME)
,由于 运行 时字节代码的开销,我想避免这种情况。
我知道的解决方法:
一如既往的解决方法。我知道这些:
- 我们可以使用构建失败的注解处理器,而不是 运行 单元测试。这是可行的但不太理想,因为测试非常复杂并且使用注释处理器更难实现,而不是使用 junit APIs 和 much 更方便的单元测试反射 API。我只想将此解决方法用作最后的手段。
- 我们可以将源代码中的
@Retention
更改为RUNTIME
,使用这些额外的测试构建源代码,然后预处理 API 以再次删除保留,然后第二次构建 API 以供生产使用。这是一个烦人的解决方法,因为它会使构建复杂化并减慢构建速度。
问题:
是否有更方便的方法来使用 Maven 在 运行 时间仅用于测试,而不是在实际构建的 jar 文件中保留注释?
我认为您很好地涵盖了解决方案 space。
还有两个你没有讲到:
稍后在 post 处理步骤中使用 proguard 等工具删除注释。
破解你的编译器以根据标志切换注释保留。很确定您可以在内部元数据中切换一些标志。可能由注解 @DynamicRetention("flag")?
触发的另一个注解处理器注入
其他解决方法之一可能包括:
- 保留默认值
retention = CLASS
。 - 使用将读取 字节码 直接. 的库
@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 is an all purpose Java bytecode manipulation and analysis framework
此代码使用 Spring 框架中的一些 类:
ClassPathResource
- 类似于java.io.File
AnnotationMetadataReadingVisitor
(source code) - is aClassVisitor
收集注释元数据
但是这种方法存在与您描述的相同的缺点:
overhead in byte code at run time
因为(来自javadoc):
Annotations are to be recorded in the class file by the compiler but need not be retained by the VM at run time.
public static void main(String[] args) throws IOException {
X x = AnnotationUtils.findAnnotation(Main.class, X.class);
System.out.println(x);
}
输出:null
这是一种可能有效的混合方法。
编写一个注解处理器,它不执行您想要执行的完整测试,而是仅在出现注解的边车文件中进行记录。如果您要注释 classes、方法和字段,可以使用 package-qualified class 名称加上方法或字段描述符相当直接地记录位置。 (不过,如果您的注释可以出现在更隐蔽的地方,例如方法参数或类型使用站点,这可能会更困难。)然后,您可以将保留策略保持为 SOURCE
.
接下来,编写你的 junit 测试来做你打算做的任何反射分析。与其尝试反射性地查找注释,不如(因为它们不会在那里)读入 sidecar 文件并查看那里。
如果@Retention(CLASS)
可以接受,那么我会推荐使用ArchUnit。您描述的任务听起来很合适。 ArchUnit 可用于为您的架构定义和验证规则。例如,它可用于限制某些 classes/packages 之间的访问,或者例如验证 class 层次结构、类型名称 - 或注释。
它通常作为单元测试由 JUnit 或任何其他测试框架执行。它通过分析字节码来工作,因此无需切换到运行时保留。
流利的 API 很好,在我看来,比使用反射或注释处理这个用例更具可读性。例如,要确保某些 classes 应该始终具有特定的注释,您可以在单元测试中编写此规则:
classes().that().areAssignableTo(MyService.class).should().beAnnotatedWith(MyAnnotation.class)
也可以创建自定义规则来声明更复杂的约束。