I delve into the details of Java’s RetentionPolicy for Annotations and take an in depth look into the uses of annotations with the SOURCE
Retention Policy.
What is an Annotation?
Simply put, an annotation is what the name states. It’s an annotation that is added to Java code, similar to a comment. Unlike comments however, annotations can be read by the compiler and included in the compiled class files. In more details, annotations include some “metadata” which, in most cases, can be applied to variables and methods.
Why use annotations?
The default annotations that are supplied by the standard Java library are the most commonly used annotations, for example @Override
, or @Deprecated
which indicate methods that are overridden by subclasses or deprecated respectively. These are all used to inform the compiler of the nature of the respective class/method/variable that is being annotated, and allows the compiler to take action based on their nature.
For example, if a method has the annotation @Deprecated
, that means that the method is discouraged for use in programs. If a program were to use a method with this annotation, the compiler may throw a warning stating that the method is deprecated.
Custom annotations can be created easily, and are often used within code along with reflection. This allows Java programs to search for methods or variables with specific annotations and perform actions on them. For example, invoking methods with a specific annotation, or performing checks on a variable with a specific annotation.
Retention policies
Retention policies are basically flags for annotations which indicate when they can be discarded by the compiler. In Java, there are three retention policies:
SOURCE
The compiler discards annotations with the SOURCE
retention policy after the compiling a Java program. The annotation is not included in the resulting .class files.
SOURCE
retention policy annotations tend to be much more powerful as they are immune to reflection and can go unnoticed as they do not exist in compiled .class files.
CLASS
The compiler keeps the annotations in the .class files, however they are not loaded by the ClassLoader when running a program.
This can be seen by using the Java Class File Disassembler and running the javap -v <FILE>
command, where it produces an output containing the following:
1
2
3
flags:
RuntimeInvisibleAnnotations:
0: #8()
The class retention policy is the default policy which is used by the Java compiler if no retention policy has been declared.
Class retention policies seem to be the most complex and “redundant” policy available. There are very little uses for it, considering that SOURCE
exists and that annotations which use this retention policy cannot be used during runtime. However, one of the main uses of this retention policy is bytecode manipulation and analysis. As the annotation exists in bytecode, tools such as ASM, a bytecode manipulation and analysis framework, are able to utilize the metadata declared in the annotation.
RUNTIME
The compiler does not discard the annotation. Similarly, by using the javap
command stated above, this output is produced instead:
1
2
3
flags:
RuntimeVisibleAnnotations:
0: #8()
This is the most common retention policy used in custom annotations, as these can be accessed using Java’s reflection. This blog post will not cover accessing annotations with reflection.
Annotation processing
Due to the “rules” of annotation usage, annotations can be processed at compile time. Baeldung, a site dedicated to Java tutorials and how-to’s, has a great tutorial on how to use Java’s annotation processing API to generate new files during the processing of annotations at compile time. This blog post will not delve into the technicalities of how annotation processing works using the annotation processing API, but I just want to point out what can be done with the annotation processing API:
- Generating new classes, based on annotations
- Generating other file types (for example, log files, or csv files)
- Generating documentation based on the values supplied to the annotations
‘Advanced’ annotation processing
Despite the power of the annotation processing API, there is one major limitation: You can only use it to create new files - you cannot modify existing files. To truly take advantage of the power of annotations, we must explore the Java compiler (javac
), and Abstract Syntax Trees (ASTs). Due to the complexity of this topic, this blog post will not go into how this works in detail (I’ll spare that for another blog post), however I’ll discuss the potential of using the Java compiler directly to process annotations and examples for why you’d create an annotation which requires this level of processing power.
Javac plugins
The Java compiler allows you to create “plugins” - addon content - to tweak the functionality of the compiler. This can range from simple things such as method or variable analysis - throwing an error if a variable is null, to entire code generation and modification.
I must say, creating Javac plugins is not the easiest of tasks - the documentation that’s available to you is very poor and even the example by Baeldung, although informative, is not comprehensive for such a vast topic.
Examples for Javac plugins
Since it’s such a tough concept, examples are by far the best way to explain the potential uses for Javac plugins in the context of annotations.
Positive integers
Say you want to ensure that all numbers for a method are non-negative (for example, a factorial function or accessing indices of an array).
1
2
3
public void myFunction(int i) {
//code here
}
The generic solution to this problem is to hard code a check for such numbers:
1
2
3
4
5
6
public void myFunction(int i) {
if(i < 0) {
throw new IllegalArgumentException("Integer cannot be negative!");
}
//code here
}
But if you have say, 10 methods and each method has multiple parameters, this can be tedious! Instead, say we have an annotation @Positive
:
1
2
3
public void myFunction(@Positive int i) {
//code here
}
If we have a Javac plugin which looks for the @Positive
annotation, and then generate checks to ensure that the variable is not negative, that would save having to repeat the same code again and again.
Extending the @Positive annotation
Of course, annotations can have parameters! Say we add further constraint to number parameters where they have to be greater than a specific value:
1
2
3
public void myFunction(@GreaterThan(20) int i) {
//code here
}
Or say we have our integer bound between a range of numbers:
1
2
3
public void myFunction(@Between(min = 10, max = 100) int i) {
//code here
}
Database sanitization
A good use of chaining annotations together would be in a situation such as storing data into a database. For example, say you want to store a client’s details such that:
- Their name is less than 20 characters
- Their age is between 18 and 100
- Their phone number is valid (by matching some regular expression)
This can be complete with annotations, given a valid Javac plugin which handles the above checks:
1
2
3
4
5
6
public void addToDatabase(
@MaxNameLength(20) String name,
@Age(min = 18, max = 100) int age,
@PhoneNo("^\\+(?:[0-9] ?){6,14}[0-9]$") String phoneNumber) {
//code here
}
Conclusion
In short, annotations are pretty boring on their own. The SOURCE
retention policy, although uncommon in regular everyday practice, has amazing potential when combined with compile-time annotation processing. In addition, it has protection against reflection as they are not included in compiled classes, which may be a desired design choice when creating annotations.
In addition, the CLASS
retention policy has all of the benefits of the SOURCE
retention policy as it is handled by the compiler as well, with the added bonus of being able to be used by bytecode manipulation tools.
The SOURCE
retention policy is often overlooked in its powerful functionality, due to the fact that RUNTIME
exists and reflection is normally the first thing that comes to mind when others think of annotations. From an API designer’s point of view, SOURCE
retention policies make code streamlined and easy to produce, without the risk of users abusing its abilities.