Custom Annotations in Java
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
@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
| Rule | Details |
|---|---|
| Allowed types | Primitives, String, Class, enums, other annotations, and arrays of these |
| Not allowed | Wrapper types (Integer), collections (List), or any other class |
No parameters, no throws | An element is written like a method with no parameters |
| Default values | default gives a value used when the element is left out |
No null | Neither a default nor a supplied value can be null |
The value shortcut | If the only element you set is named value, you may leave out value = |
| Arrays | One item can be written without braces: teachers = "Navika" |
Breaking the type rules gives compile errors:
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 errorsAnd leaving out an element that has no default is also an error:
Miss.java:3: error: annotation @Course is missing a default value for the element 'credits'
@Course("Java")
^
1 errorExample: 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:
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"));
}
}Java Basics: 3 credits, MEDIUM, teachers []
Concurrency: 5 credits, HIGH, teachers [Navika, Srihitha]
Course
trueExplanation:
@Course("Java Basics")sets onlyvalue, 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 theCourseinterface. Callingcourse.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:
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)));
}
}[]
[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
- Easy: Add a
@Positiveannotation forintfields to the validator and report fields whose value is zero or negative. - Harder: Add a
messageelement with a default of""to@Range. When it isn't empty, use it as the error text instead of the generated one.
Key Takeaways
Was this tutorial helpful?