AOP and Proxies
Definition
Aspect-oriented programming adds behaviour around method calls, such as transactions, security or logging, without editing those methods. Spring does this with a proxy: a stand-in object that runs the extra code and then calls the real method.
Why it exists
Concerns like transactions repeat in every service method and clutter the business code. A proxy lets you declare them once and apply them wherever needed.
How it works
Vocabulary
| Term | Meaning |
|---|---|
| Join point | A point in execution where advice can apply. In Spring AOP: a method execution on a Spring bean |
| Pointcut | A predicate selecting join points (execution(* com.shop..*Service.*(..))) |
| Advice | Code to run at a join point: @Before, @AfterReturning, @AfterThrowing, @After (finally), @Around |
| Aspect | A class bundling pointcuts and advice (@Aspect) |
| Weaving | Linking aspects with targets. Spring does it at runtime via proxies |
| Target / Proxy | The real object / the wrapper that callers actually hold |
How the proxy works
When a bean matches an advisor's pointcut, a BeanPostProcessor replaces it with a proxy after initialisation. Callers invoke the proxy, which runs the advice chain and then delegates to the target.
JDK dynamic proxy vs CGLIB
| JDK dynamic proxy | CGLIB | |
|---|---|---|
| Mechanism | Implements the target's interfaces (java.lang.reflect.Proxy) | Generates a subclass of the target |
| Requires | At least one interface; inject by interface type | Non-final class and non-final, non-private methods; a constructor (Spring uses Objenesis, so no constructor call) |
| Injection by concrete class | Fails (the proxy is not an instance of the class) | Works |
Spring Boot defaults to CGLIB (spring.aop.proxy-target-class=true), which avoids "bean is not of type X" surprises. Plain Spring Framework (@EnableAspectJAutoProxy without proxyTargetClass = true) uses JDK proxies when an interface exists. I verified both: a Boot-less context created a $$SpringCGLIB$$ subclass only when CGLIB was requested, and with JDK proxies, asking the container for the concrete class failed.
What is built on this
| Annotation | Proxy advice |
|---|---|
@Transactional | Begin/commit/rollback around the call |
@Cacheable, @CacheEvict | Check/update the cache |
@Async | Submit the call to an executor |
@Retryable (Spring Retry), Resilience4j annotations | Retry, circuit breaker, bulkhead |
@PreAuthorize (method security) | Authorisation check |
@Validated | Method parameter validation |
Micrometer @Timed/@Observed | Metrics and tracing |
They all share the same limitations, below.
Example code
Writing an aspect
@Aspect
@Component
class TimingAspect {
private static final Logger log = LoggerFactory.getLogger(TimingAspect.class);
@Around("execution(* com.shop.orders..*Service.*(..))")
Object time(ProceedingJoinPoint pjp) throws Throwable {
long start = System.nanoTime();
try {
return pjp.proceed(); // call the target (or the next advice)
} finally {
long ms = (System.nanoTime() - start) / 1_000_000;
log.info("{} took {} ms", pjp.getSignature().toShortString(), ms);
}
}
}
Rules for @Around: you must call proceed() (or deliberately skip the target), return its result, and rethrow exceptions (or handle them on purpose). It is the most powerful advice and the easiest to get wrong; use @Before/@AfterReturning when sufficient.
Pointcut expressions
execution(* com.shop..*Service.*(..)) // any method of classes named *Service in com.shop and sub-packages
execution(public * *(..)) // any public method
within(com.shop.orders.OrderService) // all methods of a type
@annotation(com.shop.Audited) // methods annotated with @Audited
@within(org.springframework.stereotype.Service) // methods of classes annotated @Service
bean(*Repository) // Spring-specific: by bean name
args(String, ..) // by argument types
Custom annotations make aspects explicit and discoverable:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)
@interface Audited { String action(); }
@Around("@annotation(audited)")
Object audit(ProceedingJoinPoint pjp, Audited audited) throws Throwable {
// audited.action() available here
return pjp.proceed();
}
Ordering
Several advisors on the same method form a chain; control it with @Order(n) on aspects (lower runs outermost). This matters: retry must wrap the transaction (so each attempt gets a fresh one), and security checks usually run before caching. @Transactional advice has Ordered.LOWEST_PRECEDENCE by default (innermost); @EnableTransactionManagement(order = ...) changes it.
System design considerations
- Know the order of aspects when stacking transaction, security and retry
- Keep aspects fast: they run on every matching call
- Avoid self-invocation of advised methods by splitting beans
When to use it, and when not
Use it when
- A concern applies to many methods (transactions, security, metrics, caching)
- You write a custom annotation with behaviour
- You need behaviour around calls but not inside them
Think twice when
- You need to intercept internal calls or private methods (use AspectJ weaving)
- Logic is business-specific and belongs in the method itself
- Aspects hide control flow that teammates will not expect
Common mistakes
Limitations of proxy-based AOP
- Self-invocation bypasses the proxy. A call from one method to another on
thisis a plain Java call, so no advice runs.
@Component
class GreeterImpl {
public String greet(String n) { return "hi " + n; } // advised when called from outside
public String outer(String n) { return greet(n); } // calls greet on 'this': greet's advice does NOT run
}
A test with an @Around aspect logged before outer, after outer but nothing for the inner greet. Fixes: move the method to another bean, inject the bean into itself (@Lazy self reference), use AopContext.currentProxy() with exposeProxy = true (works, but couples code to AOP), or apply the behaviour programmatically (TransactionTemplate).
- Only public methods (for JDK proxies, interface methods) are intercepted by the standard proxy mechanisms;
private/package-private calls are not advised. - Final classes and methods cannot be overridden by CGLIB subclassing: advice silently does not apply to
finalmethods. - Only Spring-managed beans are proxied; objects created with
neware not. - Only method executions are join points: not field access, constructors or calls within the same object.
- Proxies add small overhead per call, rarely relevant.
- Beans that are advised appear to other code as proxies;
getClass()shows the generated class, so useAopUtils.getTargetClass(bean)andAopProxyUtils.ultimateTargetClassin infrastructure code.
If you need to advise constructors, field access, internal calls or non-Spring objects, use AspectJ with compile-time or load-time weaving (@EnableLoadTimeWeaving, the aspectj-maven-plugin): much more powerful, more complex to build and run.
Debugging "my aspect/annotation does nothing"
- Is the class a Spring bean (component-scanned or
@Bean)? Is the call coming through the bean reference (notthis)? - Is the method public and non-final, class non-final?
- Is the starter present (
spring-boot-starter-aop/AspectJ weaver on classpath for@Aspect)? Is@EnableTransactionManagement/@EnableCaching/@EnableAsyncactive? - Does the pointcut match? Turn on debug logging for
org.springframework.aopandorg.springframework.transaction.interceptor. - Print the bean's class:
bean.getClass()should contain$$SpringCGLIB$$or$Proxy. - Check ordering with other advisors (retry vs transaction vs cache).
- Self-invocation, final methods and private methods silently skipping advice.
- Aspects that forget to call
proceed()or swallow exceptions. - Heavy work in aspects on hot paths.
- Wrong advisor order (retry inside a transaction; cache before security).
- Pointcuts too broad (
execution(* *(..))) advising framework beans. - Using
getClass()on beans and comparing with the target class.
Interview questions
1. How does Spring AOP work?
A BPP wraps matching beans in a JDK or CGLIB proxy. Calls through the proxy run an interceptor chain (advice) and then the target method.
2. JDK dynamic proxy vs CGLIB?
JDK proxies implement interfaces and require interface-based injection; CGLIB subclasses the target class (needs non-final classes/methods). Boot uses CGLIB by default.
3. Why does @Transactional not work when I call another method in the same class?
Self-invocation bypasses the proxy, so the transactional advice never runs. Call through another bean or the proxy, or use TransactionTemplate.
4. Spring AOP vs AspectJ?
Spring AOP: runtime proxies, method execution on beans only, simple. AspectJ: bytecode weaving at compile/load time, any join point (fields, constructors, internal calls), no proxy limits, more complex tooling.
5. How do you control the order of multiple aspects?
@Order on aspect classes (or implement Ordered); lower value is outermost.
6. What are the advice types and when do you use @Around?
@Before, @AfterReturning, @AfterThrowing, @After, @Around. Use @Around when you must control execution (timing, retries, caching, short-circuiting); prefer simpler advice otherwise.
7. Can you advise a private or final method?
Not with Spring AOP proxies. Make it public/non-final, move it to a bean, or use AspectJ weaving.
8. How would you implement an audit log for sensitive service methods?
A custom @Audited annotation plus an @Around aspect matching @annotation(audited), recording actor, action and outcome, ordered outside the transaction if audit must persist even on rollback (and using REQUIRES_NEW persistence).
Check yourself
1A @Transactional method is called from another method in the same class. Is a transaction started?
2Which proxy types does Spring use?
3Why does @Transactional on a private method have no effect?
Key takeaways
- Spring AOP = proxies around beans; advice runs only for calls that go through the proxy.
- CGLIB is the Boot default; self-invocation and final/private methods escape advice.
- Order advisors deliberately: retry → transaction → cache/security as needed.
- Reach for AspectJ weaving only when proxy limits truly block you.
Next: Configuration, profiles and conditions.
Next topicConfiguration, profiles, conditionsFinished this page? Track your progress (saved only in your browser).