How to describe Java code with annotations

0
116

You’ve probably encountered situations where you need to associate metadata (data that describes other data) with classes, methods, and/or other application elements. For example, your programming team might need to identify unfinished classes in a large application. For each unfinished class, the metadata would likely include the name of the developer responsible for finishing the class and the class’s expected completion date.

Before Java 5, comments were the only flexible mechanism that Java had to offer for associating metadata with application elements. However, comments are a poor choice. Because the compiler ignores them, comments are not available at runtime. And even if they were available, the text would have to be parsed to obtain crucial data items. Without standardizing how the data items are specified, these data items might prove impossible to parse.

download

Download the source code for examples in this Java 101 tutorial. Created by Jeff Friesen for InfoWorld.

Java 5 changed everything by introducing annotations, a standard mechanism for associating metadata with various application elements. This mechanism consists of four components:

  • An @interface mechanism for declaring annotation types.
  • Meta-annotation types, which you can use to identify the application elements to which an annotation type applies; to identify the lifetime of an annotation (an instance of an annotation type); and more.
  • Support for annotation processing via an extension to the Java Reflection API (to be discussed in a future article), which you can use to discover a program’s runtime annotations, and a generalized tool for processing annotations.
  • Standard annotation types.

I’ll explain how to use these components as we work our way through this article.

Declaring annotation types with @interface

You can declare an annotation type by specifying the @ symbol immediately followed by the interface reserved word and an identifier. For example, Listing 1 declares a simple annotation type that you might use to annotate thread-safe code.

Listing 1: ThreadSafe.java

public @interface ThreadSafe
{
}

After declaring this annotation type, prefix the methods that you consider thread-safe with instances of this type by prepending @ immediately followed by the type name to the method headers. Listing 2 offers a simple example where the main() method is annotated @ThreadSafe.

Listing 2: AnnDemo.java (version 1)

public class AnnDemo
{ @ThreadSafe public static void main(String[] args) { }
}

ThreadSafe instances supply no metadata other than the annotation type name. However, you can supply metadata by adding elements to this type, where an element is a method header placed in the annotation type’s body.

As well as not having code bodies, elements are subject to the following restrictions:

  • The method header cannot declare parameters.
  • The method header cannot provide a throws clause.
  • The method header’s return type must be a primitive type (e.g., int), java.lang.String, java.lang.Class, an enum, an annotation type, or an array of one of these types. No other type can be specified for the return type.

As another example, Listing 3 presents a ToDo annotation type with three elements identifying a particular coding job, specifying the date when the job is to be finished, and naming the coder responsible for completing the job.

Listing 3: ToDo.java (version 1)

public @interface ToDo
{ int id(); String finishDate(); String coder() default "n/a";
}

Note that each element declares no parameter(s) or throws clause, has a legal return type (int or String), and terminates with a semicolon. Also, the final element reveals that a default return value can be specified; this value is returned when an annotation doesn’t assign a value to the element.

Listing 4 uses ToDo to annotate an unfinished class method.

Listing 4: AnnDemo.java (version 2)

public class AnnDemo
{ public static void main(String[] args) { String[] cities = { "New York", "Melbourne", "Beijing", "Moscow", "Paris", "London" }; sort(cities); } @ToDo(id = 1000, finishDate = "10/10/2019", coder = "John Doe") static void sort(Object[] objects) { }
}

Listing 4 assigns a metadata item to each element; for example, 1000 is assigned to id. Unlike coder, the id and finishDate elements must be specified; otherwise, the compiler will report an error. When coder isn’t assigned a value, it assumes its default "n/a" value.

Java provides a special String value() element that can be used to return a comma-separated list of metadata items. Listing 5 demonstrates this element in a refactored version of ToDo.

Listing 5: ToDo.java (version 2)

public @interface ToDo
{ String value();
}

When value() is an annotation type’s only element, you don’t have to specify value and the = assignment operator when assigning a string to this element. Listing 6 demonstrates both approaches.

Listing 6: AnnDemo.java (version 3)

public class AnnDemo
{ public static void main(String[] args) { String[] cities = { "New York", "Melbourne", "Beijing", "Moscow", "Paris", "London" }; sort(cities); } @ToDo(value = "1000,10/10/2019,John Doe") static void sort(Object[] objects) { } @ToDo("1000,10/10/2019,John Doe") static boolean search(Object[] objects, Object key) { return false; }
}

Using meta-annotation types — the problem of flexibility

You can annotate types (e.g., classes), methods, local variables, and more. However, this flexibility can be problematic. For example, you might want to restrict ToDo to methods only, but nothing prevents it from being used to annotate other application elements, as demonstrated in Listing 7.

Listing 7: AnnDemo.java (version 4)

@ToDo("1000,10/10/2019,John Doe")
public class AnnDemo
{ public static void main(String[] args) { @ToDo(value = "1000,10/10/2019,John Doe") String[] cities = { "New York", "Melbourne", "Beijing", "Moscow", "Paris", "London" }; sort(cities); } @ToDo(value = "1000,10/10/2019,John Doe") static void sort(Object[] objects) { } @ToDo("1000,10/10/2019,John Doe") static boolean search(Object[] objects, Object key) { return false; }
}

In Listing 7, ToDo is also used to annotate the AnnDemo class and cities local variable. The presence of these erroneous annotations might confuse someone reviewing your code, or even your own annotation processing tools. For the times when you need to narrow an annotation type’s flexibility, Java offers the Target annotation type in its java.lang.annotation package.

Target is a meta-annotation type — an annotation type whose annotations annotate annotation types, as opposed to a non-meta-annotation type whose annotations annotate application elements, such as classes and methods. It identifies the kinds of application elements to which an annotation type is applicable. These elements are identified by Target’s ElementValue[] value() element.

java.lang.annotation.ElementType is an enum whose constants describe application elements. For example, CONSTRUCTOR applies to constructors and PARAMETER applies to parameters. Listing 8 refactors Listing 5’s ToDo annotation type to restrict it to methods only.

Listing 8: ToDo.java (version 3)

import java.lang.annotation.ElementType;
import java.lang.annotation.Target; @Target({ElementType.METHOD})
public @interface ToDo
{ String value();
}

Given the refactored ToDo annotation type, an attempt to compile Listing 7 now results in the following error message:

AnnDemo.java:1: error: annotation type not applicable to this kind of declaration
@ToDo("1000,10/10/2019,John Doe")
^
AnnDemo.java:6: error: annotation type not applicable to this kind of declaration @ToDo(value="1000,10/10/2019,John Doe") ^
2 errors

Additional meta-annotation types

Java 5 introduced three additional meta-annotation types, which are found in the java.lang.annotation package:

  • Retention indicates how long annotations with the annotated type are to be retained. This type’s associated java.lang.annotation.RetentionPolicy enum declares constants CLASS (compiler records annotations in class file; virtual machine doesn’t retain them to save memory — default policy), RUNTIME (compiler records annotations in class file; virtual machine retains them), and SOURCE (compiler discards annotations).
  • Documented indicates that instances of Documented-annotated annotations are to be documented by javadoc and similar tools.
  • Inherited indicates that an annotation type is automatically inherited.

Java 8 introduced the java.lang.annotation.Repeatable meta-annotation type. Repeatable is used to indicate that the annotation type whose declaration it (meta-)annotates is repeatable. In other words, you can apply multiple annotations from the same repeatable annotation type to an application element, as demonstrated here:

@ToDo(value = "1000,10/10/2019,John Doe")
@ToDo(value = "1001,10/10/2019,Kate Doe")
static void sort(Object[] objects)
{
}

This example assumes that ToDo has been annotated with the Repeatable annotation type.

Processing annotations

Annotations are meant to be processed; otherwise, there’s no point in having them. Java 5 extended the Reflection API to help you create your own annotation processing tools. For example, Class declares an Annotation[] getAnnotations() method that returns an array of java.lang.Annotation instances describing annotations present on the element described by the Class object.

Listing 9 presents a simple application that loads a class file, interrogates its methods for ToDo annotations, and outputs the components of each found annotation.

Listing 9: AnnProcDemo.java

import java.lang.reflect.Method; public class AnnProcDemo
{ public static void main(String[] args) throws Exception { if (args.length != 1) { System.err.println("usage: java AnnProcDemo classfile"); return; } Method[] methods = Class.forName(args[0]).getMethods(); for (int i = 0; i < methods.length; i++) { if (methods[i].isAnnotationPresent(ToDo.class)) { ToDo todo = methods[i].getAnnotation(ToDo.class); String[] components = todo.value().split(","); System.out.printf("ID = %s%n", components[0]); System.out.printf("Finish date = %s%n", components[1]); System.out.printf("Coder = %s%n%n", components[2]); } } }
}

After verifying that exactly one command-line argument (identifying a class file) has been specified, main() loads the class file via Class.forName(), invokes getMethods() to return an array of java.lang.reflect.Method objects identifying all public methods in the class file, and processes these methods.

Method processing begins by invoking Method’s boolean isAnnotationPresent(Class<? extends Annotation> annotationClass) method to determine if the annotation described by ToDo.class is present on the method. If so, Method’s <T extends Annotation> T getAnnotation(Class<T> annotationClass) method is called to obtain the annotation.

The ToDo annotations that are processed are those whose types declare a single String value() element (see Listing 5). Because this element’s string-based metadata is comma-separated, it needs to be split into an array of component values. Each of the three component values is then accessed and output.

Compile this source code (javac AnnProcDemo.java). Before you can run the application, you’ll need a suitable class file with @ToDo annotations on its public methods. For example, you could modify Listing 6’s AnnDemo source code to include public in its sort() and search() method headers. You’ll also need Listing 10’s ToDo annotation type, which requires the RUNTIME retention policy.

Listing 10: ToDo.java (version 4)

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target; @Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface ToDo
{ String value();
}

Compile the modified AnnDemo.java and Listing 10, and execute the following command to process AnnDemo’s ToDo annotations:

java AnnProcDemo AnnDemo

If all goes well, you should observe the following output:

ID = 1000
Finish date = 10/10/2019
Coder = John Doe ID = 1000
Finish date = 10/10/2019
Coder = John Doe

Standard annotation types

Along with Target, Retention, Documented, and Inherited, Java 5 introduced java.lang.Deprecated, java.lang.Override, and java.lang.SuppressWarnings. These three annotation types are designed to be used in a compiler context only, which is why their retention policies are set to SOURCE.

Deprecated

Source