Spring AOP Under the Hood: How proxies work

Under the hood

When I first encountered Spring AOP (Aspect — Oriented Programming), it seemed like magic. My Java code began executing extra behavior without my explicit command. Log statements showed up, transaction handled themselves, and security check happened automatically. But as with any advanced technology, the magic has a clear explanation.

In this article, we will pull back the curtain on Spring AOP and explore how it uses proxy patterns to weave behavior into your applications. We will look at both the theory and practical implementations so you can make the most of this powerful feature.

What is AOP and why should you care?

Aspect — Oriented programming allows you to keep cross — cutting concerns separate. This includes things like logging, security, and transaction management, which don’t mix with your main business logic. Define behavior once instead of repeating them in your code. Then, declare where to use them.

For example, instead of adding transaction management to every service method:

public void transferMoney(Account from, Account to, BigDecimal amount) {
    // Start transaction
    try {
        // Business logic
        from.withdraw(amount);
        to.deposit(amount);
        // Commit transaction
    } catch (Exception e) {
        // Rollback transaction
        throw e;
    }
}

You can focus on just the business logic:

@Transactional
public void transferMoney(Account from, Account to, BigDecimal amount) {
    from.withdraw(amount);
    to.deposit(amount);
}

Spring handles the transaction management aspect through AOP. Clean, elegant, and DRY!

The proxy pattern — the foundation of Spring AOP

At its core, Spring AOP relies on the proxy pattern. A proxy is an object that stands in for another object, controlling access to it. In Spring, these proxies catch method calls to your beans. They can act before, after, or around the actual method runs.

Spring uses two types of proxies:

  1. JDK Dynamic proxies (the default for interface — based proxies).
  2. CGLIB proxies (used when proxying classes directly).

Let’s examine how each works.

JDK Dynamic Proxies

When your Spring bean implements an interface, Spring uses JDK’s built — in dynamic proxy mechanism. The java.lang.reflect.Proxy class creates a proxy instance that implements the specified interfaces.

Here’s how it works in simplified form:

// The interface
public interface UserService {
    User findById(Long id);
}

// Your implementation
@Service
public class UserServiceImpl implements UserService {
    @Override
    public User findById(Long id) {
        // business logic
        return userRepository.findById(id);
    }
}

// What Spring creates behind the scenes (simplified)
UserService proxy = (UserService) Proxy.newProxyInstance(
    UserService.class.getClassLoader(),
    new Class<?>[] { UserService.class },
    new InvocationHandler() {
        private final UserService target = new UserServiceImpl();
        
        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            // Before advice
            System.out.println("Before method: " + method.getName());
            
            try {
                // Actual method invocation
                Object result = method.invoke(target, args);
                
                // After returning advice
                System.out.println("Method completed successfully");
                return result;
            } catch (Exception e) {
                // After throwing advice
                System.out.println("Method threw an exception: " + e.getMessage());
                throw e;
            } finally {
                // After (finally) advice
                System.out.println("After method: " + method.getName());
            }
        }
    }
);

The key insight here is that Spring creates a dynamic proxy that implements the same interface(s) as your bean. When code calls a method on this proxy, the invoke method steps in. It adds the aspect behavior and then passes the call to your real impelementation.

CGLIB Proxies

What if your bean doesn’t implement an interface? In this case, Spring falls back to CGLIB (Code generation library), which creates a subclass of your bean at runtime.

// No interface, just a class
@Service
public class UserService {
    public User findById(Long id) {
        return userRepository.findById(id);
    }
}

// What Spring creates (conceptually)
public class UserService$$EnhancerBySpringCGLIB extends UserService {
    private MethodInterceptor interceptor;
    
    @Override
    public User findById(Long id) {
        // Create a method invocation representing the superclass method
        MethodInvocation invocation = new MethodInvocation() {
            @Override
            public Object proceed() throws Throwable {
                return super.findById(id);
            }
        };
        
        // Execute with the interceptor
        return (User) interceptor.invoke(invocation);
    }
}

CGLIB creates a subclass of your bean. It changes each method to add aspect behavior. Then, it passes the call to the the superclass, which is your original implementation.

Spring AOP in Action — Practical example

Let’s put in place a simple example. We’ll create a custom annotation for logging method execution time.

First, define the annotation:

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

Next, create an aspect to handle this annotation

@Aspect
@Component
public class ExecutionTimeAspect {
    private static final Logger logger = LoggerFactory.getLogger(ExecutionTimeAspect.class);
    
    @Around("@annotation(LogExecutionTime)")
    public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        
        Object result = joinPoint.proceed();
        
        long endTime = System.currentTimeMillis();
        logger.info("{} executed in {} ms", joinPoint.getSignature(), (endTime - startTime));
        
        return result;
    }
}

Now apply it to a service method.

@Service
public class ProductService {
    @LogExecutionTime
    public List<Product> findAllProducts() {
        // Some potentially slow operation
        return productRepository.findAll();
    }
}

When findAllProducts() is called, Spring intercepts the call through a proxy, measure the execution time, and logs it.

AOP proxies: Limitations and gotchas

Understanding how proxies work helps you navigate the limitations of Spring AOP.

1. Self — invocation

The biggest “gotcha” with Spring AOP is that aspects don’t apply when a method calls another method within the same class.

@Service
public class UserService {
    @Transactional
    public void createUser(User user) {
        // This will be wrapped in a transaction
        userRepository.save(user);
    }
    
    public void createUsers(List<User> users) {
        for (User user : users) {
            createUser(user); // Transaction won't be applied!
        }
    }
}

Why? When you call createUser from createUsers you’re calling the method directly on the object instance, not through the proxy. The solution is to autowire the service into itself and call the method on the proxy:

@Service
public class UserService {
    @Autowired
    private UserService self;
    
    @Transactional
    public void createUser(User user) {
        userRepository.save(user);
    }
    
    public void createUsers(List<User> users) {
        for (User user : users) {
            self.createUser(user); // Now transaction works!
        }
    }
}

2. Final methods and classes

CGLIB can’t override final method or extend final classes. If you mark a class or method is final, Spring can’t create a proxy for it.

3. Proxy access through target class

If you try to cast a proxy to its target class, you might lose aspects

@Autowired
private UserService userService; // This is a proxy

public void someMethod() {
    // This bypasses the proxy and aspects won't apply!
    ((UserServiceImpl) userService).someMethod();
}

4. Performance overhead

Proxies introduce a small performance overhead due to the additional interception. For most, applications, this is negligible, but it’s worth keeping in mind for performance — critical code.

Spring Boot auto — configuration for AOP

In a Spring Boot application, enabling AOP is as simple as adding dependency.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Spring Boot’s auto — configuration automatically sets up AOP support with sensible defaults.

Proxy configuration options

Spring provides several configuration options for AOP proxies:

@EnableAspectJAutoProxy(
    proxyTargetClass = true, // Force CGLIB proxies even when interfaces are present
    exposeProxy = true // Make the current proxy available through AopContext
)
@Configuration
public class AppConfig {
}

When exposeProxy is enabled, you can access the current proxy from anywhere:

public void createUsers(List<User> users) {
    for (User user : users) {
        // Get the proxy explicitly
        ((UserService) AopContext.currentProxy()).createUser(user);
    }
}

Debugging Spring AOP proxies

When debugging Spring AOP issues, it helps to understand the proxy hierarchy. You can enable proxy debugging by configuring logging level:

logging:
  level: 
    org.springframework.aop: DEBUG

This will show detailed logs about proxy creation.

You can also examine proxies at runtime with helper methods.

// Check if an object is a proxy
boolean isProxy = AopUtils.isAopProxy(bean);
boolean isJdkProxy = AopUtils.isJdkDynamicProxy(bean);
boolean isCglibProxy = AopUtils.isCglibProxy(bean);

// Get the target (your original object) from a proxy
Object target = AopProxyUtils.getSingletonTarget(bean);

// Get all advisors applied to a proxy
Advisor[] advisors = ((Advised) bean).getAdvisors();

Conclusion

Spring AOP uses proxies to separate cross — cutting concerns from your business logic. This makes your code cleaner and easier to maintain. 

By understanding how these proxies work under the hood, you can use AOP more effictively and avoid common pitfalls. Remember that Spring AOP is just one part of the broader Spring ecosystem, designed to make your life as developer easier. 

The next time you add an @Transactional or @Cacheable annotations to your code, you’ll know exactly what’s happening behind scene!

References

  1. Spring Framework Documentation — AOP
  2. AspectJ Programming Guide

Have you encountered any tricky issues with Spring AOP in your projects? Feel free to share them in comments!

Subscribe to Egor Voronianskii | Java Development and whatsoever

Sign up now to get access to the library of members-only issues.
Jamie Larson
Subscribe