Spring AOP Under the Hood: How proxies work

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:
- JDK Dynamic proxies (the default for interface — based proxies).
- 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
Have you encountered any tricky issues with Spring AOP in your projects? Feel free to share them in comments!