Dev Learning Lab

The labs on this site need JavaScript to run. The same content is written out below.

Fundamentals › Java

Annotations

What is an annotation, and what happens when you add one?

An annotation is a marker that you put on code and that starts with @, such as @Run or @Override. In this lab you create a marker yourself with @interface Run and put @Run above the work method. But a marker does not run anything by itself. For it to have an effect, something has to read the marker and act on it, and that is done by the compiler, by a framework such as Spring, or by code we write. Here we write a for loop ourselves that "finds the methods that have Run and calls them" to show that relationship. You remove @Run, remove the reader, and remove @Retention, compile and run the same code each time, and you see which methods are called.

Checked against real execution · Twelve combinations were compiled with javax.tools on JDK 21 and run, and the methods called and the markers read at run time were checked

Concepts in this lab

Annotation
This is metadata that you attach to code. It does nothing by itself; the compiler or a framework reads it and acts on it. Some, like @Override, are used by the compiler for checks, and others, like @Transactional and @Autowired, are read by a framework at run time through reflection to apply a feature.
@interface
@interface is a declaration that creates a new annotation (a marker that starts with @). If you put @ before the interface of an ordinary interface declaration and write @interface Run { }, an annotation called Run is created, and after that you can put it above a class or a method as @Run. If the inside of the braces is empty, it is a marker that holds no values (a marker annotation), and if you put in declarations shaped like methods, they become elements that hold values, as in @Name(value). The declaration alone does not create any code that runs, and something else has to read the marker and act on it. If you do not write @Retention on an annotation you created yourself, it is not kept while the program runs.
Reflection
This feature lets a running program inspect information about a class (its fields, methods, constructors, and annotations) and create objects or call methods by name. Spring's dependency injection, JPA's entity handling, and JSON serialization libraries all work on top of it. It is slower than a normal call and does not get compile-time type checking.
@Retention and @Target
These are two annotations that you apply to a new annotation, when you create it, to set its properties. @Retention decides how far the annotation is retained. SOURCE retains it only in the source code, CLASS retains it up to the class file, and RUNTIME retains it until run time so that it can be read with reflection. When @Retention is not written, the policy is CLASS, so you must write RUNTIME on an annotation that will be read at run time. @Target decides where the annotation can be applied (TYPE is a type declaration such as a class, METHOD is a method, FIELD is a field, and so on), and applying it in a place that does not match is a compile-time error.

Try these

Running with the default settings

  1. The compiler reads the source from the top. First it reads the @interface Run declaration and creates a new marker called Run. A marker that starts with @ is called an annotation, and what you declare with @interface becomes its name. The inside of the { } of Run is empty, so it is a marker that holds no values. The @Retention(RetentionPolicy.RUNTIME) above it tells the compiler how long to keep this marker. RUNTIME means to record it in the class file and keep it so it can be read while the program runs, through reflection (a feature that examines classes and methods while the program is running).
  2. The compiler reads the Job class. @Run is put above the work method. The compiler only writes this marker into the class file as markers that can be read while the program runs, and it does not run or call the methods.
  3. Compilation succeeds. All the compiler did was check the source and create the class file. Neither work nor rest has run even once.
  4. You run it with java Main. main starts, and new Job() creates a Job object. work and rest did not run on this line. A method runs only when there is code that calls it. The @Run above a method is not code that calls it, only a marker.
  5. The for loop runs for the first time, and name is "work". Job.class.getMethod("work") gets the work method, and m.isAnnotationPresent(Run.class) asks, while the program runs, whether this method has Run. What asks this is not @Run but this code that we wrote. The answer is true. This is because the @Run put in the source is still kept in a state that can be read while the program runs.
  6. The answer is true, so execution goes inside the if and m.invoke(job) runs. invoke calls the work method of the job object, and the work method prints "work ran". What made it print is not @Run itself but our code that saw @Run and ran m.invoke(job).
  7. The for loop runs again, and name is "rest". Job.class.getMethod("rest") gets the rest method, and m.isAnnotationPresent(Run.class) asks, while the program runs, whether this method has Run. The answer is false. This method does not have @Run.
  8. The answer is false, so execution does not go inside the if and m.invoke(job) does not run. The rest method was not called, and nothing was printed.
  9. The for loop ends and main ends. The number of methods called is 1, and the output is "work ran".

Result of this run

Code

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

// Create the marker. Write that it should be kept so it can be read at run time
@Retention(RetentionPolicy.RUNTIME)
@interface Run { }

class Job {
    // Marker: put Run on the work method
    @Run
    public void work() {
        System.out.println("work ran");
    }
    public void rest() {
        System.out.println("rest ran");
    }
}

public class Main {
    public static void main(String[] args) throws Exception {
        Job job = new Job();
        // Reader: find only the methods that have Run and call them
        for (String name : new String[] { "work", "rest" }) {
            Method m = Job.class.getMethod(name);
            if (m.isAnnotationPresent(Run.class)) {
                m.invoke(job);
            }
        }
    }
}

Assumptions of this model

The compiler and runtime are JDK 21.0.12.1 (javac 21). For all twelve combinations, the source that is the same as the code on the screen was compiled with javax.tools, and for those that compiled, the class file was loaded with a new class loader and Main.main was run, and the results were checked against the standard output that was captured. All twelve combinations compiled. What was checked is whether compilation succeeded, the output when run, the methods called as read from the output, and the "marker read at run time." The last one is a value measured separately from Main, by a checking program that asks isAnnotationPresent(Run.class) directly about the work and rest methods of the Job class from the same class loader, and it is measured even for combinations that have no reader code. I also checked in what form the marker was written in the class file of Job (RuntimeVisibleAnnotations or RuntimeInvisibleAnnotations), and the step sentences on the screen rely on this result. The documentation was opened and checked directly. I followed the Java Language Specification (Java SE 21), Section 9.6 (an annotation interface is declared with @interface, and one with no elements is a marker annotation interface), Section 9.6.4.2 (if @Retention is absent, it is treated as CLASS; SOURCE is not kept in the class file; CLASS and RUNTIME are recorded in the class file; and RUNTIME must be made readable by reflection at run time), Section 9.7 (an annotation is a marker that associates information with a program element and has no effect at run time) and Section 9.7.2 (marker annotations). I also checked the API documentation of java.lang.annotation.Retention and RetentionPolicy (the default is CLASS, and only RUNTIME can be read by reflection) and of java.lang.reflect.AnnotatedElement (isAnnotationPresent). In what form the compiler writes the marker into the class file is not defined by the specification; it was observed on this javac. The code on the screen uses a for loop to look at the two methods in turn, to explain this lab, and it is different from the way a real framework reads markers. The sentence that Spring's @RestController and @GetMapping have an effect because Spring reads them was not run in this lab, and it points to what was checked in the Fundamentals lab "Controllers". Elements that hold annotation values, @Target, markers that the compiler reads such as @Override, and the actual implementation of frameworks that read markers at run time are not covered. Other behavior is covered in the in-depth lab "Annotations".

Previous lab: Streams · Next lab: Threads

Concepts in this lab

What this lab shows

Settings

Code


      
    

Diagram

Run comparison

Try these