If you've worked on a Java project, you would have seen the little @
symbols that seem to hang off your code like special tags. These are annotations, and they are one of the most powerful and elegant features in the Java language. They allow us to add metadata to our code, which can then be used by the compiler, by development tools, or by our own code at runtime. Before annotations came along, we had to create separate XML files to do such a configuration mostly.
In this tutorial, we'll go from the absolute basics to creating and processing our own custom annotations, demystifying how modern frameworks like Spring, JUnit, and Jackson perform their "magic."
Before we can create our own annotations, let's understand some annotations that Java gives us out of the box. These are fundamental, and you likely use them in most projects.
@Override
It tells the compiler that the annotated method is intended to override a method in a superclass. It prevents bugs by ensuring you don't accidentally overload a method when you meant to override it.
class Animal { void makeSound() { System.out.println("Some generic sound"); } } class Dog extends Animal { @Override // Ensures we are correctly overriding the superclass method void makeSound() { System.out.println("Woof!"); } }
@Deprecated
This annotation marks a class, method, or field as "deprecated," meaning it should no longer be used. Compilers and IDEs will generate a warning whenever deprecated code is used. It's a clear way to signal that a piece of code is outdated and will be removed in a future version.
class OldApiClient { /** * @deprecated This method is inefficient. Use newConnect() instead. */ @Deprecated void connect() { // Old connection logic } void newConnect() { // New, improved connection logic } }
You can define your own annotations to add custom metadata to your code. To do this, you use the @interface
keyword.
To control how your custom annotations are used, you use meta-annotations.
1. @Target
: Specifies where your annotation can be applied (e.g., on a class, method, field).
ElementType.TYPE
: Class, interface, or enum.ElementType.METHOD
: Method.ElementType.FIELD
: Field (instance variable).ElementType.PARAMETER
: Method parameter.ElementType.CONSTRUCTOR
: Constructor.2. @Retention
: Specifies how long the annotation should be kept.
RetentionPolicy.SOURCE
: The annotation is discarded by the compiler. Useful for compiler-time checks.RetentionPolicy.CLASS
: The annotation is stored in the .class
file but is not available at runtime. This is the default.RetentionPolicy.RUNTIME
: The annotation is stored in the .class
file and is available at runtime through reflection. This is what you need for most framework-like "magic".Let's create our first custom annotation:
// This annotation can only be applied to methods @Target(ElementType.METHOD) // This annotation will be available at runtime @Retention(RetentionPolicy.RUNTIME) public @interface MyCustomAnnotation { // Annotations can have elements (like methods in an interface) String description() default "No description"; int level() default 1; }
Now we can use it in our code:
public class AnnotatedClass { @MyCustomAnnotation(description = "This is an important method", level = 2) public void myAnnotatedMethod() { System.out.println("Executing important method..."); } @MyCustomAnnotation public void anotherAnnotatedMethod() { System.out.println("Executing another method..."); } }
Annotations are just metadata. They don't do anything by themselves. You need a processor to read the annotations and perform actions based on them. The most common way to do this is with Java Reflection.
Here's an annotation processor that scans a class, finds methods marked with @MyCustomAnnotation
, and executes them if they meet a certain level.
import java.lang.reflect.Method; public class AnnotationProcessor { public static void process(Object object) { Class<?> clazz = object.getClass(); // Iterate over all methods in the class using reflection for (Method method : clazz.getDeclaredMethods()) { // Check if the method has our custom annotation if (method.isAnnotationPresent(MyCustomAnnotation.class)) { // Get the annotation instance MyCustomAnnotation annotation = method.getAnnotation(MyCustomAnnotation.class); System.out.println("Found annotated method: " + method.getName()); System.out.println("Description: " + annotation.description()); System.out.println("Level: " + annotation.level()); // Perform some logic based on the annotation's values if (annotation.level() >= 2) { System.out.println("Level is high enough. Executing method..."); try { // Invoke the method on the object instance method.invoke(object); } catch (Exception e) { e.printStackTrace(); } } else { System.out.println("Level is too low. Skipping execution."); } System.out.println("---"); } } } public static void main(String[] args) { AnnotatedClass annotatedObject = new AnnotatedClass(); process(annotatedObject); } }
When you run AnnotationProcessor
, it will inspect the AnnotatedClass
, find the two annotated methods, print their metadata, and only execute the one with level = 2
. This is the fundamental mechanism behind frameworks like JUnit (which finds @Test
methods) and Spring (which finds @Component
, @Autowired
, etc.).
@JsonProperty
)Let's see how this pattern can be used in a real world example, the scenario is a simple JSON serializer.
Imagine we want to convert a Java object to a JSON string, but we want to control the names of the JSON fields.
The Annotation:
import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface JsonField { String value(); // The desired name for the JSON field }
The Annotated Class:
public class Product { @JsonField("product_name") private String name; @JsonField("product_price") private double price; private String internalId; // This field won't be serialized public Product(String name, double price, String internalId) { this.name = name; this.price = price; this.internalId = internalId; } }
The Processor:
import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; public class JsonSerializer { public static String serialize(Object object) throws IllegalAccessException { Map<String, Object> jsonMap = new HashMap<>(); Class<?> clazz = object.getClass(); for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(JsonField.class)) { field.setAccessible(true); // Allow access to private fields JsonField annotation = field.getAnnotation(JsonField.class); jsonMap.put(annotation.value(), field.get(object)); } } String jsonString = jsonMap.entrySet() .stream() .map(entry -> "\"" + entry.getKey() + "\":\"" + entry.getValue() + "\"") .collect(Collectors.joining(",")); return "{" + jsonString + "}"; } public static void main(String[] args) throws IllegalAccessException { Product product = new Product("Laptop", 1200.00, "SKU12345"); String json = serialize(product); System.out.println(json); // Output: {"product_name":"Laptop","product_price":"1200.0"} } }
Java Annotations are far more than just syntactic sugar. They are a cornerstone of modern Java development, enabling the creation of powerful, declarative, and boilerplate-free frameworks and libraries. By adding a layer of metadata that can be processed at compile-time or runtime, annotations allow you to:
@Test
or @Entity
make the intent of the code immediately clear.For more in-depth tutorials on AI, Java, Spring, and modern software development practices, follow me for more content:
🔗 Blog 🔗 YouTube 🔗 LinkedIn 🔗 Medium 🔗 Github
Stay tuned for more content on the latest in AI and software engineering!
Master Java Streams and Collectors with this practical guide. Learn how to transform, filter, and process data efficiently through real-world examples, from basic operations to complex data manipulations.
Master Java unit testing with this practical guide and cheat sheet. Learn how to write effective tests using JUnit, Mockito, AssertJ and best practices, with real-world examples for modern Java projects.
Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.