How to implement build specific annotation retention in Java

Viewed 611

I have an annotation that I currently use only for internal build and documentation purposes. It does not offer any value at runtime, which is why I chose @Retention(SOURCE):

@Retention(SOURCE)
public @interface X

However, in order to validate its proper usage, I would like to implement a unit test that navigates the entire API to check whether the annotation is applied everywhere it should be applied to. That unit test would be quite easy to implement by using ordinary Java reflection APIs, but I cannot do that as the tests can't reflect over the annotation, given its @Retention(SOURCE).

In order to use reflection in tests, I would have to change it to @Retention(RUNTIME), which I would like to avoid due to the overhead in byte code at run time.

Workarounds I'm aware of:

There are workarounds as always. I'm aware of these:

  • We could use an annotation processor that fails the build instead of running unit tests. This is feasible but less optimal, as the tests are quite sophisticated and much more difficult to implement using annotation processors rather than unit tests using both junit APIs and the much more convenient reflection API. I would like to use this workaround as a last resort only.
  • We could change the @Retention to RUNTIME in our sources, build the sources with these additional tests, then pre-process the API to remove the retention again, and then build the API a second time for production usage. This is an annoying workaround as it would complicate and slow down the build.

Question:

Is there a more convenient way to retain the annotation at runtime only for tests, but not in the actually built jar file, using Maven?

4 Answers

Here's a hybrid approach that might work.

Write an annotation processor that doesn't implement the full testing that you want to do, but instead merely records in a sidecar file where the annotations occurred. If you're annotating classes, methods, and fields, the location can be recorded fairly straightforwardly using the package-qualified class name plus a method or field descriptor. (This may be more difficult, though, if your annotation can appear in more obscure places such as on method parameters or at type use sites.) Then, you can keep the retention policy as SOURCE.

Next, write your junit tests to do whatever reflective analysis you're intending to do. Instead of trying to find the annotations reflectively, though (since they won't be there) read in the sidecar file and look there.

I think you covered the solution space pretty well.

Two more you didn't cover:

  • Strip the annotation later in a post processing step using a tool like proguard.

  • Hack your compiler to switch the annotation retention depending on a flag. Pretty sure you can switch some flag in the internal meta data. Maybe injected by another annotation processor triggered by the annotation @DynamicRetention("flag")?

One of other workarounds may include:

  1. Leaving default retention = CLASS.
  2. Using a library which will read bytecode directly.
@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());
    }
  }
}

yields:

[com.caco3.annotations.X]

The library used is ASM:

ASM is an all purpose Java bytecode manipulation and analysis framework

This code uses some classes from Spring Framework:

However this approach suffers from the same drawback as you described:

overhead in byte code at run time

because (from 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);
}

outputs: null

If @Retention(CLASS) is acceptable, then I would recommend to use ArchUnit. The task you describe sounds like it is a good fit. ArchUnit can be used to define and validate rules for your architecture. For example it can be used to restrict access between certain classes/packages, or e.g. to validate class hierarchies, type names - or annotations.

It is usually executed as a unit test by JUnit or any other test framework. It works by analyzing byte code, so there is no need to switch to runtime retention.

The fluent API is nice and in my opinion way more readable than using reflection or annotation processing for this use case. For example to ensure that certain classes should always have a particular Annotation you would write this rule in a unit test:

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

It's also possible to create custom rules to assert more complex constraints.

Related