Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is adding a method to a Java annotation safe for backward compatibility

I have an annotation that is available in a library. Is it safe for me to add a new value to this annotation in a subsequent release without breaking those that compiled against the previous version of the library ?

For example:

// version 1
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation{
    String firstValue();
    String secondValue();
}

If I add a method called "String thirdValue()", I assume a default value will be required since the legacy annotation users will not define that property.

// version 2
@Retention(RetentionPolicy.RUNTIME)
public @interface MyAnnotation{
    String firstValue();
    String secondValue();
    String thirdValue() default "third";
}

At runtime, I have some code that will attempt to read all values:

Class myClass = MyObject.class;
MyAnnotation annotation = myClass.getAnnotation(MyAnnotation.class);
String firstValue  = annotation.firstValue();
String secondValue = annotation.secondValue();
String thirdValue  = annotation.thirdValue();

The java specification isn't clear about whether or not this is safe. http://docs.oracle.com/javase/specs/jls/se7/html/jls-13.html section "13.5.7. Evolution of Annotation Types" just mentions that annotations behave as interfaces.

like image 967
Steve McDuff Avatar asked Oct 01 '22 20:10

Steve McDuff


2 Answers

Quoted from here:

13.5.7. Evolution of Annotation Types

Annotation types behave exactly like any other interface. Adding or removing an element from an annotation type is analogous to adding or removing a method. There are important considerations governing other changes to annotation types, but these have no effect on the linkage of binaries by the Java Virtual Machine. Rather, such changes affect the behavior of reflective APIs that manipulate annotations. The documentation of these APIs specifies their behavior when various changes are made to the underlying annotation types.

Adding or removing annotations has no effect on the correct linkage of the binary representations of programs in the Java programming language.

Then again on that same page, you'll find:

13.5.3. Interface Members

Adding a method to an interface does not break compatibility with pre-existing binaries.

So I would expect that adding a method, with or without default value, has no effect on code that was compiled against the previous version of your annotation.

Well, I tried it. I created an annotation

@Retention(RetentionPolicy.RUNTIME)
@Target (ElementType.TYPE)
@interface MyAnnotation {
    String foo ();
}

I use this annotation on some class:

@FooAnnotation(foo = "Foo")
public class MyAnnotatedClass {
    public static void main (String[] args) {
        FooAnnotation annot = MyAnnotatedClass.class.getAnnotation(FooAnnotation.class);
        Method[] methods = FooAnnotation.class.getDeclaredMethods();
        System.out.println("Methods:");
        for (Method method : methods) {
            System.out.println(method.getName() + "() returns:\n");
            try {
                String value = (String) method.invoke(annot);
                System.out.println("\t" + value);
            } catch (Exception e) {
                System.out.println("\tERROR! " + e.getMessage());
            }
        }
    }
}

Then I compiled everything and the program prints the following:

Methods:
foo() returns:

        Foo

Then, I added a new method to my annotation:

@Retention(RetentionPolicy.RUNTIME)
@Target (ElementType.TYPE)
@interface MyAnnotation {

    String foo ();
    String bar ();
}

I compiled this annotation again, thereby NOT recompiling MyAnnotatedClass. I cannot even compile it due to a compiler error: the newly added method in MyAnnotation has no default value so the compiler requires that MyAnnotatedClass explicitly sets it. This is what it prints now:

Methods:
bar() returns:

        ERROR! null
foo() returns:

        Foo

Conclusion? It is still working! With reflection we proved that the new method bar() is indeed in the freshly compiled annotation. So, you can safely add new methods to the annotation without breaking existing classes that were compiled and linked to your old annotation.

I left away the actual stack trace generated by the exception in the example above. With the newest version of the annotation, this is the stack trace you'll get:

java.lang.reflect.InvocationTargetException
        at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
        at sun.reflect.NativeMethodAccessorImpl.invoke(Unknown Source)
        at sun.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
        at java.lang.reflect.Method.invoke(Unknown Source)
        at example.MyAnnotatedClass.main(MyAnnotatedClass.java:16)
Caused by: java.lang.annotation.IncompleteAnnotationException: example.FooAnnotation missing element bar
        at sun.reflect.annotation.AnnotationInvocationHandler.invoke(Unknown Source)
        at com.sun.proxy.$Proxy1.bar(Unknown Source)
        ... 5 more

So trying to invoke the bar() method will raise an IncompleteAnnotationException. It is very interesting to read the Javadoc for this class:

Thrown to indicate that a program has attempted to access an element of an annotation type that was added to the annotation type definition after the annotation was compiled (or serialized). This exception will not be thrown if the new element has a default value. This exception can be thrown by the API used to read annotations reflectively.

like image 129
Timmos Avatar answered Oct 05 '22 12:10

Timmos


Based on the rules in JLS 9.7, I believe that adding an element with a default value cannot cause another class that uses the annotation to become illegal.

like image 20
ajb Avatar answered Oct 05 '22 11:10

ajb