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)