How to add and use metadata in our code
Photo by Jasmin Ne on Unsplash
?an?no?ta?tion | a-n?-?t?-sh?n1: a note added by way of comment or explanation? ? Merriam-Webster
With JSR-175, Java 5 gained a metadata facility, allowing us to annotate our code with decorative syntactic metadata.
This metadata can be provided for types, fields, methods, parameters, constructors, local variables, type parameters, usage of types, and even other annotation types. It can be used at different steps of our code?s lifecycle by a wide arrangement of tools.
Anatomy of Annotations
The basic definition of an annotation type is simple:
Let?s go through it line-by-line, everything will be explained in detail further down:
- 1: @Retention ? In which lifecycle of our code the annotation will be available.
- 2: @Target ? Where we can we use the annotation.
- 3: @Inherited ? If present, an annotated type will pass it on to any subtypes.
- 4: @Documented ? If present, documentation tools like javadoc can access it.
- 5: @interface ? Marks an annotation type.
- 6?7: Values of the annotation, optionally with a default value.
Basic usage
The simplest annotation use would be @MyAnnotation at a compatible target site.
But annotations can have multiple values that might be required to be set, if no default value is provided. The value name value() is a special one. It can be used without a name if no other values are present.
@Retention
The typical lifecycle of our code is as follows:
Source Code ? ? ? Compiler ?Class file ? ? ? JVM ?Runtime
The retention policy of annotations reflects these lifecycles and provides us with a way to specify the exact availability of metadata:
- RetentionPolicy.SOURCEAnnotations are only available in the source. The compiler will discard the metadata, so neither compiler nor runtime has access to it. This retention policy is useful for pre-compile tools, like annotation processors.
- RetentionPolicy.CLASSThe default retention policy. Annotations are visible to the compiler, and will be available in the class files, but not at runtime. Any post-compile byte-code tools might use the metadata.
- RetentionPolicy.RUNTIMEAll metadata will be available at runtime.
Which retention policy we need for our custom annotations depends on our requirements.
The provided metadata might contain sensitive information on the inner workings of the annotated code. We should always choose the lowest retention possible for our code to still work.
@Target
Not every annotation makes sense on every available target. That?s why we can explicitly set the acceptable targets. The eight available targets are defined in java.lang.annotation.ElementType:
- ElementType.PACKAGE ? Package declarations.
- ElementType.TYPE ? Classes, interfaces, enum.
- ElementType.TYPE_PARAMETER ? Generic type parameters. Available since Java 8.
- ElementType.TYPE_USE ? Any usage of a type, like declarations, generic parameters, or casts. Available since Java 8.
- ElementType.ANNOTATION_TYPE ? Annotation types.
- ElementType.CONSTRUCTOR ? Constructor declaration.
- ElementType.FIELD ? Fields and enum constants.
- ElementType.METHOD ? Method declarations.
- ElementType.LOCAL_VARIABLE ? Local variable declarations (not retained in class files or at runtime).
The @Target annotation accepts an array of targets:
If @Target is not specified, the annotation defaults to every available ElementType, except ElementType.TYPE_PARAMETER.
@Inherited
Annotations are not inherited by default. By adding @Inherited to an annotation type, we allow it to be inherited. This only applies to annotated type declarations, which will pass it down to their subtypes.
@Documented
Java default behavior for documentation is to ignore any annotation. With @Documented we can change this, making the metadata and its values accessible through documentation.
@Repeatable
Until Java 8, we could apply a specific annotation type only once on a target. With the help of the annotation @Repeatable, we can now declare an annotation repeatable by providing an intermediate annotation:
Now we can use our annotation more than once:
Annotation Values
Being able to annotate our code and check if the annotation is present at different lifecycle events is great. But providing additional values besides the annotation type itself is even better. And even default values are supported.
Values are optional, separating annotations into two groups:
- Marker ? No values. The mere presence is the actual metadata. Examples: @Documented, @Inherited, @Override.
- Configuration ? Values present, maybe with default values for less typing when used. Examples: @Target, @Retention.
The Java Language Specification (JLS) splits Configuration into Normal Annotation and Single Element Annotation. But in my opinion, the behavior of those two overlaps enough to be treated as (almost) equal.
Configuration annotations support multiple values. The allowed types are defined in the JLS 9.6.1:
- Primitive types
- String
- The type Class or Class<T>
- Enum types
- Annotation types
- Array of any preceding type (single-dimension only)
Arrays are handled uniquely. If only a single value is provided when used, we can omit the curly braces.
Default values must be constant expressions, although null is not acceptable. Arrays can return an empty array by using {} as their default value.
Built-In Annotations
The JDK includes multiple annotations beside the ones we already encountered for creating annotation types itself:
- @OverrideIndicates that a method overrides/replaces an inherited method. This information is not strictly necessary, but it helps to reduce mistakes. If we want to override a method but have a simple type in the signature, or the wrong argument type, that error might go unnoticed. But if we provide an @Override annotation, the compiler makes sure we actually override a method, and not just accidentally add or overload it.
- @DeprecatedAnother compile-only annotation. We can mark code as deprecated, and the compiler/IDE can access this information to tell us the code isn’t supposed to be used anymore. Since Java 9, this previous marker annotation becomes a configuration annotation. The values String since() default “” and boolean forRemoval() default false were added to provide even more info for compilers and IDE to work with.
- @FunctionalInterfaceSince Java 8, we can mark interfaces to be single abstract method interfaces (SAM), so they can be used as lambdas. This marker annotation allows the compiler to ensure that an interface has precisely one single abstract method. If we add another abstract method, our code will no longer compile. This annotation enables the compiler check but isn?t strictly necessary. Any SAM is automatically a functional interface.
- @SafeVarargsAnother ?trust me, I’m an engineer? marker annotation. Tells the compiler that we won?t do any unsafe operation when using varargs.
- @SuppressWarningsA configuration annotation, accepting an array of warning names that should be disabled during compilation.
How to Access Annotations at Runtime
Adding metadata isn?t enough. We also need to access it somehow. Thanks to reflection, we can access it via the class-object:
Check for annotation
Access metadata
Equivalent to boolean isAnnotationPresent(Class<? extends Annotation> annotationClass), we also have methods for accessing the actual annotation instance, providing us with access to its values.
Here are some of the methods available to different targets:
Classes
- <A extends Annotation> A getAnnotation(Class<A> annotationClass) ? Returns a specific annotation, if present, otherwise null.
- Annotation getAnnotations()? Returns all annotations on a given type.
- <A extends Annotation> A getAnnotationsByType(Class<A> annotationClass)? Returns all annotations of a given annotation type.
Methods
- <T extends Annotation> T getAnnotation(Class<T> annotationClass) ? Returns a specific annotation, if present, otherwise null.
- Annotation getDeclaredAnnotations()? Returns all annotations on the method.
- Annotation getParameterAnnotations()? Returns an two-dimensional array, containing the parameter annotations, in declaration order.
Use Cases
An excellent use-case is serialization. With annotations, a lot of additional metadata can be provided on how our data structures should be processed.
Jackson, a JSON serialization framework, uses the @JsonProperty annotation to provide every information necessary to modify the default serialization process:
Another excellent use-case is how RESTEasy uses annotations to describe REST endpoints, so no additional config is needed elsewhere:
This way, RestEASY can perform routing (@Path), validates allowed HTTP methods (@POST and @HEAD), provides data extracted from the request (@FormParam and @HeaderParam), and uses a defined media type for the response (@Produces).
All without any additional config file or objects. The configuration is right there in the corresponding code.
Conclusion
Annotations are a great way to provide additional data, either for ourselves or third-party tools and libraries. But be aware of the additional costs of parsing, compiling, and lookup of annotations, especially at runtime.
Resources
- The Java Tutorials ? Annotations (Oracle)
- java.lang.annotation package summary (JavaSE 8)
- Creating a Custom Annotation (Baeldung)