Annotations & Reflection — Metadata That Powers Modern Java

Annotation ও Reflection — আধুনিক Java framework-এর ভিত্তি

Read: ~35 min Advanced 5 practice problems Live code runner

1. What Are Annotations?

An annotation is structured metadata that you attach to a class, method, field, parameter, or even another annotation. Annotations do not change what your code does on their own — they add information that tools, compilers, or runtime frameworks can read and act upon. Every @Override you have written, every @Test, every @Autowired in Spring — all annotations.

Annotation হলো আপনার কোডের উপর বসানো একটি structured metadata — class, method, field, parameter বা অন্য annotation-এর উপর। Annotation নিজে কোনো logic চালায় না; এটি শুধু তথ্য দেয়, যা compiler, টুল বা runtime framework পড়ে কাজ করে। আপনি যতবার @Override, @Test, বা Spring-এ @Autowired লিখেছেন — সবই annotation।

Reflection is the partner technology: Java code that inspects Java code at runtime. Together, annotations + reflection let frameworks like Spring, Hibernate, and Jackson read your metadata and wire up dependency injection, ORM mapping, or JSON serialization without you writing glue code.

2. Built-in Annotations You Already Know

The Java platform ships with a handful of core annotations. These are the ones every Java developer sees on day one.

Java standard library-তে কিছু প্রাথমিক annotation আছে — এগুলোই সবচেয়ে বেশি চোখে পড়ে।
AnnotationMeaningবাংলায়
@OverrideAsserts you are overriding a superclass method.আপনি parent class-এর method override করছেন — ভুল হলে compiler জানিয়ে দেবে।
@DeprecatedMarks an API as obsolete; generates warning on use.এই API আর ব্যবহার করবেন না — warning দেখাবে।
@SuppressWarningsTells compiler to silence specific warnings.নির্দিষ্ট warning চেপে রাখতে বলে।
@FunctionalInterfaceGuarantees an interface has exactly one abstract method.Interface-এ ঠিক একটিই abstract method থাকবে — lambda-compatible।
@SafeVarargsSuppresses unchecked-cast warning on generic varargs.Generic varargs-এ unchecked warning বন্ধ করে।
Main.java
class Animal {
    public String speak() { return "some sound"; }
}

class Dog extends Animal {
    // @Override catches typos at compile time
    @Override
    public String speak() { return "Bhau!"; }
}

class Main {
    public static void main(String[] args) {
        Animal a = new Dog();
        System.out.println(a.speak());
    }
}

3. Meta-Annotations — Annotations on Annotations

When you write your own annotation you must tell Java two things — where it can be applied (@Target) and how long the metadata should live (@Retention). These annotations-on-annotations are called meta-annotations.

নিজে annotation তৈরি করলে দুটি জিনিস Java-কে বলে দিতে হয় — কোথায় বসানো যাবে (@Target) এবং কতদিন metadata টিকে থাকবে (@Retention)। এগুলোকে meta-annotation বলা হয়।
RetentionPolicy — কোথা পর্যন্ত বাঁচে SOURCE Compiler discards e.g. @Override CLASS Kept in .class file but NOT readable at runtime RUNTIME Readable via reflection e.g. @Test, @Autowired For frameworks like Spring or JUnit: always pick RUNTIME. Figure 42.1 — Annotation তিন ধরনের retention-এ থাকতে পারে। Framework চাইলে RUNTIME লাগবে।

4. Writing a Custom Annotation & Reading It with Reflection

Let us build a mini "@Audit" annotation that a bKash-scale fintech might use to tag sensitive methods. We define the annotation, attach it to a method, then walk the class with reflection to discover every audited method at runtime.

ধরুন আপনি বাংলাদেশের একটি fintech-এ কাজ করছেন। sensitive method-গুলো চিহ্নিত করতে একটি @Audit annotation তৈরি করবেন। তারপর reflection দিয়ে runtime-এ class-এর সব audited method খুঁজে বার করবেন।
Main.java
import java.lang.annotation.*;
import java.lang.reflect.Method;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@interface Audit {
    String value() default "general";
}

class PaymentService {
    @Audit("money-transfer")
    public void sendMoney(String to, int taka) {
        System.out.println("Sent " + taka + " BDT to " + to);
    }
    public void ping() { System.out.println("pong"); }
}

class Main {
    public static void main(String[] args) throws Exception {
        for (Method m : PaymentService.class.getDeclaredMethods()) {
            Audit a = m.getAnnotation(Audit.class);
            if (a != null) {
                System.out.println("[AUDITED: " + a.value() + "] " + m.getName());
            }
        }
    }
}
What just happened? The compiler stored the @Audit metadata in the .class file (because of RetentionPolicy.RUNTIME). At runtime we used Class.getDeclaredMethods() to walk every method and getAnnotation() to read the tag. This is a toy version of exactly how Spring discovers @Transactional, or how JUnit finds @Test.

5. The Reflection API — A Quick Tour

Everything reflective hangs off a Class<?> object. From there you can list fields, invoke methods by name, read generic type parameters, and construct objects without calling new directly.

Reflection-এর সব কাজ শুরু হয় একটি Class<?> object থেকে। সেখান থেকে field list করা, নাম দিয়ে method call করা, type parameter পড়া এবং new ছাড়াই object তৈরি — সব সম্ভব।
Main.java
import java.lang.reflect.*;

class Account {
    private String owner = "Fatima";
    public int balance() { return 1000; }
}

class Main {
    public static void main(String[] args) throws Exception {
        Class<?> c = Account.class;

        System.out.println("Class: " + c.getName());

        for (Field f : c.getDeclaredFields()) {
            System.out.println("  field  -> " + f.getType().getSimpleName() + " " + f.getName());
        }
        for (Method m : c.getDeclaredMethods()) {
            System.out.println("  method -> " + m.getName());
        }

        // Construct an Account without calling `new` directly
        Account a = (Account) c.getDeclaredConstructor().newInstance();
        Method bal = c.getDeclaredMethod("balance");
        System.out.println("balance = " + bal.invoke(a));
    }
}

6. When Not to Reflect

✅ Good Use of Reflection

  • Writing frameworks (Spring, JUnit, Jackson)
  • Plugin systems — load classes by name
  • Serialization libraries
  • Build-time tools that scan annotations

⚠️ Bad Use of Reflection

  • Accessing private fields "just because"
  • Replacing a simple method call — 10–50× slower
  • Any hot-path code
  • Anything the type system could express directly
Reflection breaks compile-time safety and incurs runtime cost. Production teams increasingly prefer annotation processors and code generation (Lombok, MapStruct, Dagger) that read annotations at build time and emit plain Java — no runtime reflection required.

Reflection compile-time safety নষ্ট করে এবং ধীর। তাই আধুনিক production code-এ annotation processor ও code generation (Lombok, MapStruct, Dagger) বেশি চলে — build time-এ annotation পড়ে সাধারণ Java কোড তৈরি হয়, runtime-এ reflection লাগে না।

7. Vocabulary (শব্দভাণ্ডার)

TermMeaningবাংলায়
AnnotationStructured metadata attached to code.কোডের উপর বসানো metadata।
Meta-annotationAn annotation applied to another annotation.অন্য annotation-এর উপর বসানো annotation।
@RetentionHow long metadata survives: SOURCE/CLASS/RUNTIME.Metadata কতক্ষণ থাকবে।
@TargetWhere an annotation may be applied.কোন element-এ বসানো যাবে।
ReflectionReading / invoking code structure at runtime.Runtime-এ কোডের গঠন পড়া ও চালানো।
Annotation processorBuild-time plugin that reads annotations and generates code.Build-time plugin — annotation পড়ে নতুন কোড তৈরি করে।

8. Practice Problems

Try each problem yourself before opening the answer.

প্রতিটি প্রশ্ন আগে নিজে চেষ্টা করুন, তারপর উত্তর দেখুন।
  1. Create a custom annotation @Version that takes an integer value and attach it to a class.
    একটি custom annotation @Version তৈরি করুন যা integer value নেবে, এবং একটি class-এ প্রয়োগ করুন। Reflection দিয়ে সেটি প্রিন্ট করুন।
    ✨ Show Answer (উত্তর দেখুন)
    Main.java
    import java.lang.annotation.*;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @interface Version { int value(); }
    
    @Version(3)
    class App {}
    
    class Main {
        public static void main(String[] args) {
            Version v = App.class.getAnnotation(Version.class);
            System.out.println("App version = " + v.value());
        }
    }
  2. Explain the difference between RetentionPolicy.CLASS and RetentionPolicy.RUNTIME.
    RetentionPolicy.CLASS এবং RetentionPolicy.RUNTIME-এর পার্থক্য ব্যাখ্যা করুন।
    ✨ Show Answer

    Answer: Both retain the annotation in the compiled .class file, but only RUNTIME makes it visible to the reflection API after the class is loaded. CLASS is for tools that scan bytecode offline (e.g. some static analyzers). If a framework such as Spring needs to discover your annotation while the JVM is running, you MUST use RUNTIME.

    দুটিই .class ফাইলে annotation রাখে, তবে শুধু RUNTIME class load হওয়ার পর reflection API-তে দৃশ্যমান। CLASS bytecode scanner-দের জন্য। Spring-এর মতো framework যদি runtime-এ annotation খুঁজে পেতে চায়, RUNTIME-ই দিতে হবে।

  3. Use reflection to list every method of java.util.ArrayList that starts with "add".
    Reflection ব্যবহার করে java.util.ArrayList-এর সেই method-গুলো list করুন যেগুলো "add" দিয়ে শুরু হয়।
    ✨ Show Answer
    Main.java
    import java.util.ArrayList;
    import java.lang.reflect.Method;
    
    class Main {
        public static void main(String[] args) {
            for (Method m : ArrayList.class.getDeclaredMethods()) {
                if (m.getName().startsWith("add")) {
                    System.out.println(m.getName() + " / params=" + m.getParameterCount());
                }
            }
        }
    }
  4. Why is Method.invoke() slower than a direct call? Give one way to mitigate it.
    Method.invoke() কেন সাধারণ method call-এর চেয়ে ধীর? এটি কমানোর একটি উপায় বলুন।
    ✨ Show Answer

    Answer: Reflective invocation goes through access checks, autoboxing of primitive arguments into Object[], and dynamic dispatch — none of which the JIT can optimize as well as a direct virtual call. Mitigations: (1) cache the Method object once, do not look it up per call, (2) call setAccessible(true) once to skip access checks, (3) for hot paths, use MethodHandle via java.lang.invoke — its JIT-visible and nearly as fast as a direct call.

  5. Build a tiny DI container: given a class that has a @Inject-annotated field, create an instance and set the field to a hard-coded "PaymentAPI" string.
    একটি ছোট DI container বানান: class-এ @Inject-annotated field থাকলে সেটিতে একটি hard-coded "PaymentAPI" string inject করুন।
    ✨ Show Answer
    Main.java
    import java.lang.annotation.*;
    import java.lang.reflect.Field;
    
    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.FIELD)
    @interface Inject {}
    
    class OrderService {
        @Inject String paymentApi;
        public String toString() { return "OrderService using " + paymentApi; }
    }
    
    class Main {
        public static void main(String[] args) throws Exception {
            OrderService o = OrderService.class.getDeclaredConstructor().newInstance();
            for (Field f : OrderService.class.getDeclaredFields()) {
                if (f.isAnnotationPresent(Inject.class)) {
                    f.setAccessible(true);
                    f.set(o, "PaymentAPI");
                }
            }
            System.out.println(o);
        }
    }

    এটিই Spring-এর @Autowired-এর সরলতম সংস্করণ।

Summary — Module 42

Annotations are structured metadata on your code; reflection is the runtime ability to read that metadata. Together they are the invisible machinery under Spring, JUnit, Hibernate, and Jackson. Use them to build frameworks — but reach for annotation processors and plain code for performance-critical paths.

Annotation হলো কোডের উপর structured metadata; reflection হলো runtime-এ সেই metadata পড়ার ক্ষমতা। এই দুইয়ে মিলেই Spring, JUnit, Hibernate, Jackson চলে। নিজে framework বানাতে চাইলে এগুলো জানা জরুরি — কিন্তু performance-critical code-এ annotation processor ও সাধারণ কোড বেছে নিন।

Next Module → Testing: JUnit 5 + Mockito — test ছাড়া কোড production-এ পাঠানো মানে user-দের debugger হিসেবে ব্যবহার করা।