Using Annotations at Runtime

Author: Ter-Petrosyan Hakob

So far, you have learned how to add annotations to your Java code and how to create your own annotation types. Now, let’s see how we can actually use those annotations while the program is running. This is called runtime annotation processing, and it can make your code more flexible and easier to maintain.

Why Process Annotations at Runtime?

Imagine you often need to create toString methods for your classes. These methods return a text representation of an object, usually showing the values of its fields. Writing them by hand can be repetitive. You could write a generic toString using reflection that lists every field automatically—but sometimes you want more control.

For example, consider a Coordinate class:

Annotations allow you to mark which classes and fields should be included in the custom string format.

Defining a @ToString Annotation

First, we define the @ToString annotation like this:

import java.lang.annotation.*;

@Target({ElementType.FIELD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface ToString {
    boolean includeName() default true;  
}

Using the Annotation on Classes

Here’s an example with two classes, Coordinate and Box:

@ToString(includeName=false)
public class Coordinate {
    @ToString(includeName=false) private int x;
    @ToString(includeName=false) private int y;

    public Coordinate(int x, int y) {
        this.x = x;
        this.y = y;
    }
}

@ToString
public class Box {
    @ToString(includeName=false) Coordinate topLeft;
    @ToString int width;
    @ToString int height;

    public Box(Coordinate topLeft, int width, int height) {
        this.topLeft = topLeft;
        this.width = width;
        this.height = height;
    }
}

If we annotate a class with @ToString, it can be processed by a runtime method that automatically creates a string.

For a Box, the string might look like:

Box[[5, 10], width=20, height=30]

Processing Annotations at Runtime

We cannot change the toString method of a class at runtime. But we can write a utility method that reads the annotations and formats objects accordingly.

Java’s reflection API provides methods to access annotations, such as:

Here’s the main idea:

import java.lang.reflect.*;

public class ToStringUtil {
    public static String toString(Object obj) {
        if (obj == null) {
            return "null";
        }

        Class<?> cls = obj.getClass();
        ToString ts = cls.getAnnotation(ToString.class);
        if (ts == null) {
            return obj.toString();
        }

        StringBuilder sb = new StringBuilder();
        if (ts.includeName()) {
            sb.append(cls.getSimpleName());
        }
        sb.append("[");

        boolean first = true;
        for (Field f : cls.getDeclaredFields()) {
            ts = f.getAnnotation(ToString.class);
            if (ts != null) {
                if (first) {
                    first = false;
                } else {
                    sb.append(",");
                }
                f.setAccessible(true);
                if (ts.includeName()) {
                    sb.append(f.getName()).append("=");
                }
                try {
                    sb.append(toString(f.get(obj)));  // recursive call
                } catch (IllegalAccessException e) {
                    e.printStackTrace();
                }
            }
        }
        sb.append("]");
        return sb.toString();
    }
}

How It Works

  1. The method checks if the object’s class has a @ToString annotation.
  2. If it does, it loops over the fields and looks for @ToString annotations on them.
  3. If a field is annotated, it includes the field in the output string.
  4. The method calls itself recursively if a field is an object that also has annotations.
  5. If a class or field is not annotated, the normal toString is used.

Using the Utility

public class RuntimeAnnotationDemo {
    public static void main(String[] args) {
        var box = new Box(new Coordinate(5, 10), 20, 30);
        System.out.println(ToStringUtil.toString(box));
    }
}

Output:

Box[[5,10],width=20,height=30]

Key Takeaways

Annotations are powerful tools for simplifying repetitive tasks and adding metadata to your code. They work best when combined with reflection for runtime processing.