A Deep Dive into Java Annotations

    A Deep Dive into Java Annotations

    13/08/2025

    Introduction

    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."

    Built-in Annotations

    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 } }

    Creating Your Own Annotations

    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.

    Meta-Annotations: The Annotations for Your 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..."); } }

    Processing Annotations at Runtime

    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.

    Runtime Annotation Processing Flow 1. Annotated Code public class MyClass { @MyAnnotation void myMethod() {} } 2. Processor Uses Reflection method.isAnnotation Present(...) 3. Action Perform "Magic" (e.g., run test, inject bean)

    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.).

    Real-World Scenario Example: JSON Serialization (like Jackson's @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.

    Real-World Example: JSON Serialization 1. Java Object public class Product { @JsonField("product_name") private String name; @JsonField("product_price") private double price; private String internalId; } JsonSerializer serialize(object) 3. JSON String { "product_name": "Laptop", "product_price": "1200.0" }

    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"} } }

    Conclusion

    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:

    • Reduce Boilerplate Code: Frameworks can handle setup and configuration based on simple annotations.
    • Improve Code Readability: Declarative annotations like @Test or @Entity make the intent of the code immediately clear.
    • Enable "Magic": They are the key to how Dependency Injection, Object-Relational Mapping (ORM), and testing frameworks work.

    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!

    Summarise

    Transform Your Learning

    Get instant AI-powered summaries of YouTube videos and websites. Save time while enhancing your learning experience.

    Instant video summaries
    Smart insights extraction
    Channel tracking