DEV Community

Prabhat Kumar
Prabhat Kumar

Posted on

Unveiling the Inner Workings of Spring AOP

In this post, we will demystify the internal mechanics of Aspect-Oriented Programming (AOP) in Spring. The focus will be on understanding how AOP achieves functionality like logging, often considered a form of “magic.” By walking through a core Java implementation, we’ll see how it’s all about Java’s reflection, proxy patterns, and annotations rather than anything truly magical.

Prerequisites

  • Java Core Proxy API
  • Reflection API
  • Annotation API

These are all part of java.lang.reflect,java.lang.annotation, and javassist.util.proxy packages.

The Core Mechanism

At the heart of Spring AOP lies the concept of proxy objects, method interceptors, and reflection. The key player in this pattern is the MethodHandler (or invocation handler). This handler controls the proxy object’s behavior by intercepting method calls. When a method is invoked on the proxy, it gets passed through the handler, where annotations can be introspected via reflection. Based on the annotations applied, the necessary logic (e.g., logging) can be executed before, after, or on exception.

Breaking it Down

  1. Proxy Objects: These are dynamically created objects that stand in for your actual business objects, routing method calls through the method handler.
  2. Invocation Handlers: This is where the magic of interception happens. Using reflection, the handler can examine annotations present on the target method and alter the behavior accordingly.
  3. Custom Annotations: You can define custom annotations, which serve as markers to trigger additional functionality like logging, security checks, or transaction management.

Example: Suppose we want to add logging before and after certain method executions. Instead of hard-coding logging everywhere, we can annotate methods with @BeforeMethod and @AfterMethod. Our handler inspects the method for this annotation and adds the appropriate logging logic dynamically.

Below are the classes how the Controller and Service look like for our example.

WorkerController.java

package edu.pk.poc.aop.controller;

import edu.pk.poc.aop.annotation.AfterMethod;
import edu.pk.poc.aop.annotation.All;
import edu.pk.poc.aop.annotation.BeforeMethod;
import edu.pk.poc.aop.helper.ProxyFactory;
import edu.pk.poc.aop.service.Worker;
import edu.pk.poc.aop.service.WorkerService;
import edu.pk.poc.aop.service.WorkerServiceImpl;

public class WorkerController {
    WorkerService workerService = ProxyFactory.createProxy(WorkerServiceImpl.class);
    /**
     * This Method 1s annotated with @BeforeMethod and @AfterMethod, So the log statements
     * will be generated before and after method call.
     */
    @BeforeMethod
    @AfterMethod
    public void engageFullTimeWorker() throws Exception {
        Worker fullTimeWorker = new Worker();
        fullTimeWorker.setName("FullTime-Worker");
        fullTimeWorker.setPartTime(false);
        fullTimeWorker.setDuration(9);
        workerService.doWork(fullTimeWorker);
    }
    /**
     * This Method is annotated with @All, So the log statements will be generated before and after method call
     * along with exception if raised.
     */
    @All
    public void engagePartTimeWorker() throws Exception {
        Worker partTimeWorker = new Worker();
        partTimeWorker.setName("PartTime-Worker");
        partTimeWorker.setPartTime(true);
        partTimeWorker.setDuration(4);
        workerService.doWork(partTimeWorker);
    }
}
Enter fullscreen mode Exit fullscreen mode

WorkerServiceImpl.java

package edu.pk.poc.aop.service;

import edu.pk.poc.aop.annotation.AfterMethod;

public class WorkerServiceImpl implements WorkerService {
    /**
     * Here this method is annotated with only @AfterMethod, So only log statement
     * will be generated after method call
     */
    @AfterMethod
    @Override
    public void doWork(Worker worker) throws Exception {
        if (worker.isPartTime()) {
            throw new Exception("Part time workers are not permitted to work.");
        }
        System.out.print("A full time worker is working for " + worker.getDuration() + " hours :: ");
        for (int i = 1; i < worker.getDuration(); i++) {
            System.out.print("* ");
        }
        System.out.println();
    }
}
Enter fullscreen mode Exit fullscreen mode

Main.java test class

package edu.pk.poc.aop.test;

import edu.pk.poc.aop.controller.WorkerController;
import edu.pk.poc.aop.helper.ProxyFactory;
import edu.pk.util.Logger;

public class Main {
    public static void main(String[] args) {
        WorkerController controller = ProxyFactory.createProxy(WorkerController.class);
        Logger logger = new Logger();
        try {
            System.out.println("Testing @BeforeMethod and @AfterMethod");
            System.out.println("-----------------------------------------");
            controller.engageFullTimeWorker();
            System.out.println("Testing @All");
            System.out.println("-----------------------------------------");
            controller.engagePartTimeWorker();
        } catch (Exception e) {
            logger.error("Exception caught in Main class");
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Output

Testing @BeforeMethod and @AfterMethod
-----------------------------------------
>>> Entering into edu.pk.poc.aop.controller.WorkerController.engageFullTimeWorker()
A full time worker is working for 9 hours :: * * * * * * * * 
>>> Exiting from edu.pk.poc.aop.service.WorkerServiceImpl.doWork()
>>> Exiting from edu.pk.poc.aop.controller.WorkerController.engageFullTimeWorker()
Testing @All
-----------------------------------------
>>> Entering into edu.pk.poc.aop.controller.WorkerController.engagePartTimeWorker()
>>> Exception in edu.pk.poc.aop.controller.WorkerController.engagePartTimeWorker()
Exception caught in Main class
Enter fullscreen mode Exit fullscreen mode

How It Works

When a method is invoked on a proxy object, the call is intercepted by the handler, which uses reflection to inspect all the annotations on the target method. Based on those annotations, the handler decides whether to log method entry/exit, log exceptions, or skip logging altogether.

Here’s how you can visualize it:

  • Before Execution: Log method entry.
  • After Execution: Log method exit or success.
  • All: Log method entry, method entry and on exception if raised. This dynamic behavior shows that Spring AOP leverages core Java APIs rather than employing some magical trick.

Define Annotations

package edu.pk.poc.aop.annotation;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface AfterMethod {

}
Enter fullscreen mode Exit fullscreen mode
package edu.pk.poc.aop.annotation;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface BeforeMethod {

}
Enter fullscreen mode Exit fullscreen mode
package edu.pk.poc.aop.annotation;

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

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface All {

}
Enter fullscreen mode Exit fullscreen mode

Define Proxy Factory

package edu.pk.poc.aop.helper;

/**
 * The {@code ProxyFactory} class is responsible for creating proxy objects using the Javassist library.
 * It allows for dynamic generation of proxies for classes or interfaces, with support for method interception.
 */
public class ProxyFactory {

    /**
     * A Javassist ProxyFactory instance used to generate proxy classes.
     */
    private static final javassist.util.proxy.ProxyFactory factory = new javassist.util.proxy.ProxyFactory();

    /**
     * Creates a proxy object for the given class or interface.
     * If the class is an interface, the proxy implements the interface.
     * If it's a concrete class, the proxy extends the class.
     *
     * @param <T>   the type of the class or interface for which the proxy is to be created
     * @param klass the {@code Class} object representing the class or interface to proxy
     * @return a proxy instance of the specified class or interface, or {@code null} if proxy creation fails
     */
    public static <T> T createProxy(Class<T> klass) {
        if (klass.isInterface())
            factory.setInterfaces(new Class[]{klass});
        else
            factory.setSuperclass(klass);
        try {
            return (T) factory.create(new Class<?>[0], new Object[0], new AOPLoggingMethodHandler());
        } catch (Exception e) {
            System.err.println(e.getMessage());
        }
        return null;
    }
}
Enter fullscreen mode Exit fullscreen mode

Define MethodHandler

package edu.pk.poc.aop.helper;

import edu.pk.poc.aop.annotation.AfterMethod;
import edu.pk.poc.aop.annotation.All;
import edu.pk.poc.aop.annotation.BeforeMethod;

import java.lang.annotation.Annotation;
import java.lang.reflect.Method;

import edu.pk.util.Logger;
import javassist.util.proxy.MethodHandler;

/**
 * This class is a MethodHandler implementation for Javassist proxies.
 * It intercepts method calls and provides logging functionality based on custom annotations.
 */
public class AOPLoggingMethodHandler implements MethodHandler {

    private static final Logger logger = new Logger();

    /**
     * This method intercepts method calls on the proxied object.
     * It checks for the presence of custom annotations (@BeforeMethod, @AfterMethod, @All)
     * and logs messages accordingly.
     *
     * @param self       The proxy object.
     * @param thisMethod The method being invoked on the proxy object.
     * @param proceed    The original method being proxied.
     * @param args       The arguments passed to the method.
     * @return The result of the method invocation.
     * @throws Throwable If any exception occurs during method invocation.
     */
    public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
        if (proceed != null) { // Concrete Method
            Object result = null;
            String className = resolveClassName(self);
            try {
                if (isAnnotationPresent(thisMethod, BeforeMethod.class) || isAnnotationPresent(thisMethod, All.class)) {
                    logger.info(">>> Entering into " + className + "." + thisMethod.getName() + "()");
                }
                result = proceed.invoke(self, args);
                if (isAnnotationPresent(thisMethod, AfterMethod.class) || isAnnotationPresent(thisMethod, All.class)) {
                    logger.info(">>> Exiting from " + className + "." + thisMethod.getName() + "()");
                }
            } catch (Throwable t) {
                if (isAnnotationPresent(thisMethod, All.class)) {
                    logger.error(">>> Exception in " + className + "." + thisMethod.getName() + "()");
                }
                throw t;
            }
            return result;
        }
        throw new RuntimeException("Method is Abstract");
    }

    /**
     * Checks if the given method is annotated with the specified annotation class.
     *
     * @param method The method to check for annotation.
     * @param klass  The annotation class to check for.
     * @return True if the annotation is present, false otherwise.
     */
    private boolean isAnnotationPresent(Method method, Class klass) {
        Annotation[] declaredAnnotationsByType = method.getAnnotationsByType(klass);
        return declaredAnnotationsByType != null && declaredAnnotationsByType.length > 0;
    }

    /**
     * Resolves the class name of the proxied object, removing any Javassist proxy suffixes.
     *
     * @param self The proxy object.
     * @return The resolved class name.
     */
    private String resolveClassName(Object self) {
        String className = self.getClass().getName();
        if (className.contains("_$$")) {
            className = className.substring(0, className.indexOf("_$$"));
        }
        return className;
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Spring AOP is a powerful tool for cross-cutting concerns, but it’s not doing anything revolutionary. It’s built on core Java concepts like reflection and proxies, which are available in the language itself. By understanding this, you can better appreciate how Spring simplifies these lower-level mechanics for developer convenience.

Top comments (0)