Building a Simple Java Framework with Annotations and Reflection

Illustration for Building a Simple Java Framework with Annotations and Reflection
By Last updated:

A common mistake beginners make when learning Java Annotations is treating them as mere metadata without realizing their true potential. Many developers use @Override or @Deprecated but don’t know that annotations, combined with reflection, form the backbone of modern frameworks like Spring, Hibernate, and JUnit.

In real-world applications, frameworks scan annotations at runtime and apply behaviors like dependency injection, ORM mapping, or test execution. For example, Spring detects @Autowired fields and injects beans automatically.

Think of annotations as labels on items in a warehouse and reflection as the scanner that reads those labels. Together, they allow frameworks to dynamically identify, process, and manage behaviors without hardcoding.

In this tutorial, we’ll build a mini-framework using annotations and reflection to understand how popular frameworks work under the hood.


Step 1: Define Custom Annotations

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.annotation.ElementType;

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface Component {
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Inject {
}
  • @Component marks classes to be managed by our framework.
  • @Inject marks fields that should be dependency-injected.

Step 2: Create Example Classes

@Component
class Service {
    public void serve() {
        System.out.println("Service is working!");
    }
}

@Component
class Controller {
    @Inject
    private Service service;

    public void handleRequest() {
        service.serve();
    }
}
  • Service is a dependency.
  • Controller depends on Service.

Step 3: Build a Simple Dependency Injector

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

public class SimpleFramework {
    private Map<Class<?>, Object> container = new HashMap<>();

    public void scan(Class<?>... classes) throws Exception {
        // Register @Component classes
        for (Class<?> clazz : classes) {
            if (clazz.isAnnotationPresent(Component.class)) {
                container.put(clazz, clazz.getDeclaredConstructor().newInstance());
            }
        }

        // Inject dependencies into fields marked with @Inject
        for (Object obj : container.values()) {
            for (Field field : obj.getClass().getDeclaredFields()) {
                if (field.isAnnotationPresent(Inject.class)) {
                    field.setAccessible(true);
                    field.set(obj, container.get(field.getType()));
                }
            }
        }
    }

    public <T> T getBean(Class<T> clazz) {
        return (T) container.get(clazz);
    }
}
  • The framework stores components in a container.
  • Dependencies are injected using reflection on fields annotated with @Inject.

Step 4: Use the Framework

public class Main {
    public static void main(String[] args) throws Exception {
        SimpleFramework framework = new SimpleFramework();
        framework.scan(Service.class, Controller.class);

        Controller controller = framework.getBean(Controller.class);
        controller.handleRequest();
    }
}

Output:

Service is working!

Congratulations—you’ve just built a mini dependency injection framework using annotations and reflection!


Real-World Applications

  • Spring Framework – Uses annotations like @Component, @Autowired, and @Service.
  • Hibernate – Uses @Entity and @Table to map Java classes to database tables.
  • JUnit – Uses @Test to mark test methods for execution.

📌 What's New in Java Versions?

  • Java 5 – Introduced annotations and reflection improvements.
  • Java 8 – Added repeatable and type annotations.
  • Java 9 – Module system introduced restrictions on reflection.
  • Java 11 – No major updates for annotation-reflection frameworks.
  • Java 17 – Stronger encapsulation rules in modules.
  • Java 21 – No significant updates specific to annotations + reflection frameworks.

Pitfalls and Best Practices

Pitfalls

  • Forgetting @Retention(RUNTIME) makes annotations unavailable for reflection.
  • Overusing reflection can cause performance issues.
  • Lack of proper error handling may lead to cryptic runtime errors.

Best Practices

  • Always define retention and target clearly.
  • Use caching to reduce reflection overhead.
  • Keep frameworks modular and avoid “annotation hell.”
  • Document framework usage for maintainability.

Summary + Key Takeaways

  • Annotations + Reflection form the foundation of modern Java frameworks.
  • You can build a mini-framework by defining annotations, scanning classes, and injecting dependencies.
  • Reflection allows frameworks to dynamically manage objects at runtime.
  • While powerful, annotations and reflection should be used wisely with caching and clear documentation.

FAQ

  1. Can annotations execute code by themselves?
    No, they’re metadata. Code execution happens when reflection or processors act on them.

  2. Why use annotations over XML configuration?
    Annotations are more concise, type-safe, and easier to maintain.

  3. How does Spring differ from our simple framework?
    Spring provides advanced features like AOP, bean scopes, lifecycle management, and proxies.

  4. Can reflection-based frameworks scale to enterprise apps?
    Yes, with proper caching and optimization. Frameworks like Spring and Hibernate prove this.

  5. What’s the performance cost of reflection in frameworks?
    Slightly slower than direct calls, but negligible when combined with caching.

  6. Can annotations be inherited?
    Only if marked with @Inherited and applied at the class level.

  7. What if a dependency is missing in the container?
    A robust framework should throw a clear exception (e.g., BeanNotFoundException).

  8. Is it possible to build an ORM with annotations?
    Yes, by scanning @Entity classes and mapping fields to database columns.

  9. What’s the difference between runtime and compile-time annotation processing?
    Runtime uses reflection; compile-time uses annotation processors (JSR 269).

  10. Should I use reflection in all projects?
    Not always—use it when flexibility outweighs performance costs.