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 {}
// 📁 Inject.java
package com.example.di;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {}
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));
}
}
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);
}
}
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");
}
}
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 {}
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);
}
});
}
}
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);
}
}
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");
}
}
With this setup, calling userService.registerUser("Alice")
will trigger AOP logging, printing the execution time of the registerUser
method.
💡 Use Cases
- Small Projects: Custom frameworks are helpful in small projects where only specific functionalities are needed.
- Learning Tool: Building a framework is a great way to deepen your understanding of DI and AOP.
- 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)