Custom Annotations in Java

Naveen AnamaneniUpdated · 6 min read

A custom annotation is an annotation you declare yourself, using the keyword @interface. It lets you attach your own information to classes, methods or fields, and then write code (or use a framework) that reads that information and acts on it. This is how @Test in JUnit, @Entity in JPA and @GetMapping in Spring work: each is an ordinary annotation declared with @interface, plus code that looks for it. In this lesson, you will learn how to declare an annotation, the rules for its elements, and how to read it at run time to build a small validator.

Syntax

Syntax
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@interface AnnotationName {
    Type elementName();
    Type otherElement() default defaultValue;
}

The parts inside look like methods, but they are called elements. Each one is a value the user of the annotation can supply.

Rules for Elements

RuleDetails
Allowed typesPrimitives, String, Class, enums, other annotations, and arrays of these
Not allowedWrapper types (Integer), collections (List), or any other class
No parameters, no throwsAn element is written like a method with no parameters
Default valuesdefault gives a value used when the element is left out
No nullNeither a default nor a supplied value can be null
The value shortcutIf the only element you set is named value, you may leave out value =
ArraysOne item can be written without braces: teachers = "Navika"

Breaking the type rules gives compile errors:

Compiler output
BadEl.java:7: error: element value must be a constant expression
    @interface Nul { String name() default null; }
                                           ^
BadEl.java:4: error: invalid type for annotation interface element
        List<String> names();
            ^
BadEl.java:5: error: invalid type for annotation interface element
        Integer count();
        ^
3 errors

And leaving out an element that has no default is also an error:

Compiler output
Miss.java:3: error: annotation @Course is missing a default value for the element 'credits'
    @Course("Java")
    ^
1 error

Example: Declaring and Reading an Annotation

Let's see an example of a @Course annotation with four elements, used in the short and the full form:

Info.java▶ Try it
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.Arrays;
 
public class Info {
    enum Level { LOW, MEDIUM, HIGH }
 
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface Course {
        String value();
        int credits() default 3;
        Level level() default Level.MEDIUM;
        String[] teachers() default {};
    }
 
    @Course("Java Basics")
    static class Basics { }
 
    @Course(value = "Concurrency", credits = 5, level = Level.HIGH,
            teachers = {"Navika", "Srihitha"})
    static class Advanced { }
 
    static void show(Class<?> c) {
        Course course = c.getAnnotation(Course.class);
        System.out.println(course.value() + ": " + course.credits()
            + " credits, " + course.level() + ", teachers "
            + Arrays.toString(course.teachers()));
    }
 
    public static void main(String[] args) {
        show(Basics.class);
        show(Advanced.class);
        Course course = Basics.class.getAnnotation(Course.class);
        System.out.println(course.annotationType().getSimpleName());
        System.out.println(course.getClass().getName().contains("Proxy"));
    }
}
Output
Java Basics: 3 credits, MEDIUM, teachers []
Concurrency: 5 credits, HIGH, teachers [Navika, Srihitha]
Course
true

Explanation:

  • @Course("Java Basics") sets only value, so the name can be left out, and the other three elements take their defaults.
  • The full form names every element. When you set more than one, value = must be written.
  • getAnnotation(Course.class) returns an object that implements the Course interface. Calling course.credits() returns the value written in the source.
  • The last line shows that this object is a proxy the JVM generates at run time; an annotation is really an interface that extends java.lang.annotation.Annotation. "Dynamic Proxies in Java" explains proxies.

Example: A Field Validator

Here is the idea behind validation libraries such as Jakarta Bean Validation. Two annotations mark the rules, and one method checks any object against them:

Validate.java▶ Try it
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
 
public class Validate {
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @interface NotBlank { }
 
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @interface Range {
        int min() default 0;
        int max();
    }
 
    static class Student {
        @NotBlank
        String name;
 
        @Range(max = 100)
        int marks;
 
        @Range(min = 5, max = 25)
        int age;
 
        Student(String name, int marks, int age) {
            this.name = name;
            this.marks = marks;
            this.age = age;
        }
    }
 
    static List<String> validate(Object o) throws IllegalAccessException {
        List<String> errors = new ArrayList<>();
        for (Field f : o.getClass().getDeclaredFields()) {
            f.setAccessible(true);
            Object value = f.get(o);
            if (f.isAnnotationPresent(NotBlank.class)
                    && (value == null || value.toString().isBlank())) {
                errors.add(f.getName() + " must not be blank");
            }
            Range r = f.getAnnotation(Range.class);
            if (r != null) {
                int n = (int) value;
                if (n < r.min() || n > r.max()) {
                    errors.add(f.getName() + " must be between "
                        + r.min() + " and " + r.max() + ", but was " + n);
                }
            }
        }
        return errors;
    }
 
    public static void main(String[] args) throws Exception {
        System.out.println(validate(new Student("Advitha", 88, 12)));
        System.out.println(validate(new Student(" ", 140, 3)));
    }
}
Output
[]
[name must not be blank, marks must be between 0 and 100, but was 140, age must be between 5 and 25, but was 3]

Explanation: validate knows nothing about Student. It asks the object's class for its fields, reads each field's value, and checks whichever annotations it finds. The rules live next to the data they describe, and the same method can validate any class. @NotBlank is a marker annotation: its presence is the whole message. @Range carries values. The reflection calls used here (getDeclaredFields, setAccessible, get) are explained in "Reflection API in Java" and the lessons after it.

Common Mistakes

Forgetting @Retention(RetentionPolicy.RUNTIME). getAnnotation returns null, and the annotation appears to do nothing.

Using types that aren't allowed. Integer, List and your own classes can't be element types; use int, arrays and enums.

Expecting null as "not set". Use a default such as "" or -1 to mean "no value".

Naming the single element something other than value. Then users must always write name = ....

Interview Corner

How do you create a custom annotation in Java?

Declare it with @interface, add elements written like parameterless methods (optionally with default values), and apply meta-annotations: @Retention(RetentionPolicy.RUNTIME) if it must be read at run time, and @Target to restrict where it can be used. Element types are limited to primitives, String, Class, enums, annotations and arrays of those. To act on it, code reads it with reflection, for example method.getAnnotation(MyAnnotation.class), or an annotation processor handles it at compile time.

What is a marker annotation?

An annotation with no elements, such as @Override or a custom @NotBlank. Its presence alone carries the meaning.

Practice

  1. Easy: Add a @Positive annotation for int fields to the validator and report fields whose value is zero or negative.
  2. Harder: Add a message element with a default of "" to @Range. When it isn't empty, use it as the error text instead of the generated one.

Quick check: Custom annotations

4 questions
1Which keyword declares an annotation?
2Which element type is NOT allowed?
3When can you write @Course("Java") without "value ="?
4A custom annotation has no @Retention. Can reflection read it at run time?

Key Takeaways

Was this tutorial helpful?

Lesson 465 of 482