103

Consider the following code:

A.java:

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
@interface A{}

C.java:

import java.util.*;

@A public class C {
        public static void main(String[] args){
                System.out.println(Arrays.toString(C.class.getAnnotations()));
        }
}

Compiling and running works as expected:

$ javac *.java
$ java -cp . C
[@A()]

But then consider this:

$ rm A.class
$ java -cp . C
[]

I would've expected it to throw a ClassNotFoundException, since @A is missing. But instead, it silently drops the annotation.

Is this behaviour documented in the JLS somewhere, or is it a quirk of Sun's JVM? What's the rationale for it?

It seems convenient for things like javax.annotation.Nonnull (which seems like it should've been @Retention(CLASS) anyway), but for many other annotations it seems like it could cause various bad things to happen at runtime.

Matt McHenry
  • 20,009
  • 8
  • 65
  • 64

3 Answers3

99

In the earlier public drafts for JSR-175 (annotations), it was discussed if the compiler and runtime should ignore unknown annotations, to provide a looser coupling between the usage and declaration of annotations. A specific example was the use of applications server specific annotations on an EJB to control the deployment configuration. If the same bean should be deployed on a different application server, it would have been convenient if the runtime simply ignored the unknown annotations instead of raising a NoClassDefFoundError.

Even if the wording is a little bit vague, I assume that the behaviour you are seeing is specified in JLS 13.5.7: "... removing annotations has no effect on the correct linkage of the binary representations of programs in the Java programming language." I interpret this as if annotations are removed (not available at runtime), the program should still link and run and that this implies that the unknown annotations are simply ignored when accessed through reflection.

The first release of Sun's JDK 5 did not implement this correctly, but it was fixed in 1.5.0_06. You can find the relevant bug 6322301 in the bug database, but it does not point to any specifications except claiming that "according to the JSR-175 spec lead, unknown annotations must be ignored by getAnnotations".

Chris Povirk
  • 3,738
  • 3
  • 29
  • 47
jarnbjo
  • 33,923
  • 7
  • 70
  • 94
  • I suspect that JLS 13.5.7 is getting at something else -- for example, that you can compile code against a library in which `foo` is unannotated and rely on it still to work later if `foo` is annotated in the meantime. That said, I'm not 100% sure, and thank you for the links to that and to JDK-6322301 regardless! It does seem that the Java developers additionally aim to provide some resilience during reflective annotation lookup in the face of missing classes, even if some edge cases may remain here and there (e.g., [JDK-8247797](https://bugs.openjdk.java.net/browse/JDK-8247797)). – Chris Povirk Mar 05 '21 at 22:21
  • More history: After Java introduced type annotations, [JDK-8152174](https://bugs.openjdk.java.net/browse/JDK-8152174) could produce a `NullPointerException` (in `TypeAnnotationParser.mapTypeAnnotations`) if a _type_ annotation was missing at runtime. That got fixed in a JDK9 update, but it's an additional bug to watch for under older JDKs. – Chris Povirk Mar 31 '21 at 17:06
  • (Yes, JDK-8152174 says "the specification requires" the annotation to be ignored. I still don't think that's covered by JLS 13.5.7. But I note that APIs like [`Method.getAnnotatedReturnType`](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/reflect/Method.html#getAnnotatedReturnType()) don't document any thrown exceptions, whereas APIs like [`AnnotatedElement`](https://docs.oracle.com/en/java/javase/14/docs/api/java.base/java/lang/reflect/AnnotatedElement.html) do document failures from, e.g., "attempting to read the class by calling the relevant Class-returning method.") – Chris Povirk Mar 31 '21 at 17:09
37

Quoting the JLS:

9.6.1.2 Retention Annotations may be present only in the source code, or they may be present in the binary form of a class or interface. An annotation that is present in the binary may or may not be available at run-time via the reflective libraries of the Java platform.

The annotation type annotation.Retention is used to choose among the above possibilities. If an annotation a corresponds to a type T, and T has a (meta-)annotation m that corresponds to annotation.Retention, then:

  • If m has an element whose value is annotation.RetentionPolicy.SOURCE, then a Java compiler must ensure that a is not present in the binary representation of the class or interface in which a appears.
  • If m has an element whose value is annotation.RetentionPolicy.CLASS, or annotation.RetentionPolicy.RUNTIME a Java compiler must ensure that a is represented in the binary representation of the class or interface in which a appears, unless m annotates a local variable declaration. An annotation on a local variable declaration is never retained in the binary representation.

If T does not have a (meta-)annotation m that corresponds to annotation.Retention, then a Java compiler must treat T as if it does have such a meta-annotation m with an element whose value is annotation.RetentionPolicy.CLASS.

So RetentionPolicy.RUNTIME ensures that the annotation is compiled into the binary but an annotation present in the binary doesn't have to be available at runtime

Guillaume
  • 14,306
  • 3
  • 43
  • 40
11

if you actually have code that reads @A and does something with it, the code has a dependency on class A, and it will throw ClassNotFoundException.

if not, i.e. no code cares specificly about @A, then it's arguable that @A doesn't really matter.

irreputable
  • 44,725
  • 9
  • 65
  • 93