🌐 Building a Custom Framework in Java: From Dependency Injection to AOP

saurabhkurve

Saurabh Kurve

Posted on November 10, 2024

🌐 Building a Custom Framework in Java: From Dependency Injection to AOP

In the world of Java development, frameworks like Spring and Hibernate make life easier by managing complex functionalities for developers, such as Dependency Injection (DI) and Aspect-Oriented Programming (AOP). But what if you want to create a custom framework with some of these features? 🛠️ Building your own framework can be a valuable exercise to deepen your understanding of how frameworks operate under the hood. In this blog, we'll cover how to create a simple framework in Java that supports DI and AOP with examples and use cases.


🧠 Why Build a Custom Framework?

Creating a custom framework isn’t about replacing established frameworks in production applications. Instead, it's a hands-on way to learn:

  • How Dependency Injection Works: Understand how DI containers manage object creation and wiring. 🤖
  • Aspect-Oriented Programming Basics: Learn how to apply cross-cutting concerns (like logging and security) dynamically. 🔍
  • Custom Solution for Specific Needs: Sometimes, frameworks like Spring may feel heavy for smaller projects, and a custom solution can be lightweight and more specific to your use case. 🌱

📜 Core Concepts: Dependency Injection and AOP

Before we dive into the code, let’s briefly recap the two concepts we’re focusing on.

🔄 Dependency Injection (DI)

DI is a design pattern that enables objects to receive their dependencies from an external source rather than creating them internally. In our framework, we’ll create a simple DI container to manage object creation and injection.

🌐 Aspect-Oriented Programming (AOP)

AOP allows us to separate cross-cutting concerns—such as logging, security, and transaction management—from the main business logic. By using AOP, we can add these functionalities dynamically without modifying existing code.


📝 Step-by-Step: Building the Framework

🏗️ Step 1: Setting Up the Project

Let's create a Java project and add two basic packages:

  • 📂 com.example.di: For dependency injection functionalities.
  • 📂 com.example.aop: For aspect-oriented programming.

We’ll add some example services and aspect functionalities to demonstrate these concepts.

🧩 Step 2: Implementing Dependency Injection

We’ll create a Container class to act as our DI container. This class will manage instances of beans and inject dependencies based on annotations.

1️⃣ Define Custom Annotations

We’ll define two custom annotations: @Service for services and @Inject for injected dependencies.

// 📁 Service.java
package com.example.di;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface Service {}
Enter fullscreen mode Exit fullscreen mode
// 📁 Inject.java
package com.example.di;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {}
Enter fullscreen mode Exit fullscreen mode

2️⃣ Create the Container Class

The Container class will scan for classes annotated with @Service, create instances of these classes, and inject dependencies marked with @Inject.

// 📁 Container.java
package com.example.di;

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

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

    public Container(Class<?>... classes) throws Exception {
        for (Class<?> clazz : classes) {
            if (clazz.isAnnotationPresent(Service.class)) {
                services.put(clazz, clazz.getDeclaredConstructor().newInstance());
            }
        }
        for (Object service : services.values()) {
            for (Field field : service.getClass().getDeclaredFields()) {
                if (field.isAnnotationPresent(Inject.class)) {
                    field.setAccessible(true);
                    field.set(service, services.get(field.getType()));
                }
            }
        }
    }

    public <T> T getService(Class<T> clazz) {
        return clazz.cast(services.get(clazz));
    }
}
Enter fullscreen mode Exit fullscreen mode

3️⃣ Create Sample Services

We’ll create two services: UserService and NotificationService, where NotificationService is injected into UserService.

// 📁 UserService.java
package com.example.di;

@Service
public class UserService {
    @Inject
    private NotificationService notificationService;

    public void registerUser(String username) {
        System.out.println("User registered: " + username);
        notificationService.sendNotification(username);
    }
}

// 📁 NotificationService.java
package com.example.di;

@Service
public class NotificationService {
    public void sendNotification(String username) {
        System.out.println("Notification sent to " + username);
    }
}
Enter fullscreen mode Exit fullscreen mode

4️⃣ Testing Dependency Injection

To test our DI setup, we can create a Main class to initialize the container and retrieve the UserService.

// 📁 Main.java
package com.example;

import com.example.di.Container;
import com.example.di.UserService;

public class Main {
    public static void main(String[] args) throws Exception {
        Container container = new Container(UserService.class, NotificationService.class);
        UserService userService = container.getService(UserService.class);
        userService.registerUser("Alice");
    }
}
Enter fullscreen mode Exit fullscreen mode

Running this code should produce output indicating that the NotificationService is successfully injected into UserService. ✔️


🛠️ Step 3: Adding Aspect-Oriented Programming (AOP) Support

Now, let's add a simple form of AOP by using dynamic proxies. Our goal is to log method calls and execution times. ⏱️

1️⃣ Define the @LogExecutionTime Annotation

// 📁 LogExecutionTime.java
package com.example.aop;

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

@Retention(RetentionPolicy.RUNTIME)
public @interface LogExecutionTime {}
Enter fullscreen mode Exit fullscreen mode

2️⃣ Create an AOP Proxy

The AOPProxy class will wrap our services and log execution times for methods annotated with @LogExecutionTime.

// 📁 AOPProxy.java
package com.example.aop;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class AOPProxy {
    public static <T> T createProxy(T target, Class<T> interfaceType) {
        return (T) Proxy.newProxyInstance(
                interfaceType.getClassLoader(),
                new Class<?>[]{interfaceType},
                new InvocationHandler() {
                    @Override
                    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                        if (method.isAnnotationPresent(LogExecutionTime.class)) {
                            long start = System.currentTimeMillis();
                            Object result = method.invoke(target, args);
                            long end = System.currentTimeMillis();
                            System.out.println("Execution time: " + (end - start) + "ms");
                            return result;
                        }
                        return method.invoke(target, args);
                    }
                });
    }
}
Enter fullscreen mode Exit fullscreen mode

3️⃣ Update the UserService with AOP

We’ll annotate the registerUser method in UserService to log execution time.

// 📁 UserService.java
package com.example.di;

import com.example.aop.LogExecutionTime;

@Service
public class UserService {
    @Inject
    private NotificationService notificationService;

    @LogExecutionTime
    public void registerUser(String username) {
        System.out.println("User registered: " + username);
        notificationService.sendNotification(username);
    }
}
Enter fullscreen mode Exit fullscreen mode

4️⃣ Integrate AOP in Main

We wrap UserService in an AOP proxy when retrieving it from the container.

// 📁 Main.java
package com.example;

import com.example.aop.AOPProxy;
import com.example.di.Container;
import com.example.di.UserService;

public class Main {
    public static void main(String[] args) throws Exception {
        Container container = new Container(UserService.class, NotificationService.class);
        UserService userService = AOPProxy.createProxy(container.getService(UserService.class), UserService.class);
        userService.registerUser("Alice");
    }
}
Enter fullscreen mode Exit fullscreen mode

With this setup, calling userService.registerUser("Alice") will trigger AOP logging, printing the execution time of the registerUser method.


💡 Use Cases

  1. Small Projects: Custom frameworks are helpful in small projects where only specific functionalities are needed.
  2. Learning Tool: Building a framework is a great way to deepen your understanding of DI and AOP.
  3. Microservices: Lightweight custom frameworks can be ideal for microservices where you don’t need all the features of a full-scale framework like Spring.

Creating a custom framework in Java is an insightful exercise to understand the inner workings of popular frameworks. In this blog, we implemented a basic DI container and added AOP for logging execution times. With these foundational elements, you can expand your framework by adding other features such as configuration management, caching, and advanced AOP functionalities. Whether used as a learning tool or a lightweight solution for small projects, custom frameworks offer both flexibility and deep insight into the mechanics of enterprise-level Java development.

💖 💪 🙅 🚩
saurabhkurve
Saurabh Kurve

Posted on November 10, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related