AOP & Proxies
@Aspect, a complete logging/timing aspect, how Spring AOP proxies actually work, and the self-invocation limitation.
The cross-cutting problem AOP solves
Some concerns cut across many unrelated classes — logging every method call, timing execution, checking a transaction boundary, enforcing security — and don't belong to any single class's core responsibility. Sprinkling logging/timing code by hand into every method scatters the same boilerplate everywhere it applies, mixed in with real business logic. Aspect-Oriented Programming (AOP) lets you declare that cross-cutting logic once, in one place, and apply it to many methods without touching their code at all.
Spring's own core features are themselves built on AOP: @Transactional, @Cacheable, and @Async all work by wrapping your method calls in Spring-managed logic that runs before/after/around them — the same mechanism this page shows you how to use yourself.
Core AOP vocabulary
| Term | Meaning |
|---|---|
| Aspect | A module containing cross-cutting logic — a class annotated @Aspect |
| Advice | The actual code that runs at a matched point (@Before, @After, @Around, etc.) |
| Join point | A point during execution where advice could run — in Spring AOP, always a method call |
| Pointcut | An expression that selects which join points a piece of advice applies to |
| Weaving | The process of applying aspects to target objects — Spring does this at runtime, via proxies |
A complete logging/timing aspect
@Aspect
@Component
public class LoggingAspect {
private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
@Around("execution(* com.example.myapp.service.*.*(..))")
public Object logAndTime(ProceedingJoinPoint joinPoint) throws Throwable {
String method = joinPoint.getSignature().toShortString();
long start = System.currentTimeMillis();
log.info("Entering {} with args {}", method, joinPoint.getArgs());
try {
Object result = joinPoint.proceed(); // actually invokes the real method
log.info("Exiting {} in {} ms", method, System.currentTimeMillis() - start);
return result;
} catch (Throwable ex) {
log.warn("{} threw {}", method, ex.getMessage());
throw ex;
}
}
}
This one aspect now logs and times every public method on every class in com.example.myapp.service — with zero changes to any of those service classes. @Component is still required alongside @Aspect — Spring needs the class registered as a bean before it can weave it in as an aspect.
Pointcut expressions
execution(* com.example.myapp.service.*.*(..))
│ │ │ │ │
│ │ │ │ └─ any arguments
│ │ │ └─ any method name
│ └────────────────────────┘ any class directly in this package
└─ any return type
| Pattern | Matches |
|---|---|
execution(* com.example.service.*.*(..)) |
Any method, any class directly in com.example.service |
execution(* com.example.service..*.*(..)) |
Same, including sub-packages (note the extra .) |
execution(public * *(..)) |
Any public method, anywhere |
@annotation(com.example.Loggable) |
Any method annotated @Loggable, regardless of package |
within(com.example.service.OrderService) |
Any method on this one specific class |
Advice types
| Annotation | Runs | Can it change/skip the return value or throw instead? |
|---|---|---|
@Before |
Before the target method | No — can only inspect args or throw before the call |
@After |
After the method returns or throws (like finally) |
No |
@AfterReturning |
Only after a successful return | Can inspect the return value, not replace it |
@AfterThrowing |
Only if the method threw | Can inspect the exception |
@Around |
Wraps the whole call | Yes — the only advice type that can skip proceed() entirely, change arguments, or swallow/replace the return value |
@Around is the most powerful and the only one used for timing, since it's the only type that can measure both before and after the same invocation.
How Spring AOP actually works: proxies, not bytecode weaving
Spring AOP is proxy-based — at startup, for every bean matched by at least one aspect's pointcut, Spring creates a proxy object and registers that in the container instead of your original object. Every call into the bean actually goes through the proxy first:
Caller --> Proxy (runs advice) --> real OrderService instance --> back through the proxy --> Caller
Spring picks one of two proxy strategies automatically:
| Strategy | Used when | How it works |
|---|---|---|
| JDK dynamic proxy | The bean implements at least one interface | Generates a proxy implementing the same interface(s) at runtime |
| CGLIB proxy | The bean has no interface (a concrete class) | Generates a runtime subclass that overrides the target's methods |
This is exactly what distinguishes Spring AOP from AspectJ's full compile-time/load-time bytecode weaving — Spring AOP only ever intercepts calls that go through the proxy, which is the source of its most important limitation.
The self-invocation limitation
Because advice only runs when a call passes through the proxy, a method calling another method on the same object directly (this.otherMethod()) bypasses the proxy entirely — the advice on otherMethod simply never fires:
@Service
public class OrderService {
@Around("@annotation(Loggable)")
public Object aroundAdvice(ProceedingJoinPoint pjp) throws Throwable { /* ... */ }
public void placeOrder(Order order) {
this.validate(order); // called directly on `this` -- bypasses the proxy, advice does NOT run
}
@Loggable
public void validate(Order order) { /* ... */ }
}
validate is only advised when some other bean calls orderService.validate(...) through the injected, proxied reference. A call to this.validate(...) from inside the same class never goes through the container's proxy at all.
The common fixes: restructure so the advised method lives on a different bean and is called through it, or (as a last resort) inject the bean into itself via @Lazy to obtain a reference to its own proxy. Full AspectJ (compile-time or load-time weaving) doesn't have this limitation, since it rewrites the actual bytecode rather than wrapping calls in a proxy — but it requires a build-time weaving step Spring AOP doesn't need, which is exactly why Spring AOP's simpler proxy-based model is the default for ordinary applications.
Common mistakes
- Expecting an aspect to fire on a
privatemethod — pointcuts can only match calls that go through the proxy, and Spring proxies only ever intercept calls reached from outside the object. - Being surprised that an aspect doesn't run on a self-invoked call (
this.method()) — this is the self-invocation limitation, not a bug, and is one of the most commonly asked "gotcha" questions about Spring AOP. - Writing an overly broad pointcut (
execution(* com.example..*.*(..))) that accidentally matches far more than intended, adding logging/timing overhead to unrelated code paths. - Assuming
@Aroundadvice runs the method automatically — forgetting to calljoinPoint.proceed()means the target method never executes at all, silently swallowing all real behavior.