DEV Community

Cover image for ๐ŸŒ Building a Custom Framework in Java: From Dependency Injection to AOP
Saurabh Kurve
Saurabh Kurve

Posted on • Edited on

๐ŸŒ 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.

Top comments (0)