DEV Community

Ankit Verma
Ankit Verma

Posted on

AOP: aspect / pointcut / advice / weaving

Every codebase grows a set of chores that have nothing to do with the actual job of a method, yet somehow end up inside every method anyway. Logging that a call started. Timing how long it took. Checking the user is allowed in. Opening a database transaction. These are cross-cutting concerns — needs that cut across many unrelated methods, the same few lines copied into each one.

Aspect-Oriented Programming — AOP — is the tool for pulling that repeated code out of your methods and describing it in one place instead. You meet it in Spring the first time you use @Transactional, @Cacheable, or @Async: all of them are AOP underneath. So understanding AOP is really understanding how Spring makes a plain method call quietly do more than the method's body says.

The problem, in code

Here is a service method with a couple of those chores baked in:

public Order placeOrder(Cart cart) {
    long start = System.currentTimeMillis();
    log.info("placeOrder started");

    Order order = build(cart);   // the only line doing real work

    log.info("placeOrder took {}ms", System.currentTimeMillis() - start);
    return order;
}
Enter fullscreen mode Exit fullscreen mode

Only one line does the actual job. The rest is timing and logging. Now picture those same four lines wrapped around every service method in the app. Change the log format once and you are editing it in fifty places.

The instinct is to move the noise into a helper. But a helper still has to be called from inside each method — the boilerplate shrinks, it never disappears. What we really want is for placeOrder to contain only its real work, and to describe the timing separately, somewhere else entirely.

A point where behavior could run

Start by naming the moments where extra behavior could plausibly happen. Just before a method starts. Right after it returns. The instant it throws. Each of these is a join point — a single, identifiable point during a program's execution where you could hook in extra code.

Spring keeps this deliberately narrow. In Spring, the only join points are method executions on Spring-managed objects. Every public method call on a bean is a candidate; nothing else is. That one restriction explains a lot of Spring AOP's behavior later on, so keep it in mind.

The code that runs there: advice

The extra behavior you want to run at a join point is called advice. Advice is just a method you write, tagged with when it should run relative to the join point.

The simplest kind runs before the target method:

@Before("...")   // the "..." — which methods — comes later
public void logStart(JoinPoint jp) {
    log.info("{} started", jp.getSignature().getName());
}
Enter fullscreen mode Exit fullscreen mode

There are five kinds of advice, and the tag on the method picks which: @Before (runs first), @AfterReturning (only on success), @AfterThrowing (only on failure), @After (always, like a finally), and @Around (the most powerful — it wraps the call).

Around advice is worth dwelling on, because it can do what the other four can, and more. It receives the call itself as an object you must explicitly run:

@Around("...")
public Object time(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
    Object result = pjp.proceed();   // <-- this runs the real method
    log.info("{} took {}ms", pjp.getSignature().getName(),
             System.currentTimeMillis() - start);
    return result;
}
Enter fullscreen mode Exit fullscreen mode

That pjp.proceed() call is the target method. Everything above it runs before the method; everything below runs after. And if you never call proceed(), the real method simply never runs — which is exactly how a security aspect can block a call outright.

Choosing where advice applies: the pointcut

We have advice, but the "..." in those annotations is still empty. We need to say which join points a piece of advice attaches to. That selector is a pointcut — an expression that matches a set of join points.

The most common form matches by method signature:

@Around("execution(* com.shop.service.*.*(..))")
public Object time(ProceedingJoinPoint pjp) throws Throwable { ... }
Enter fullscreen mode Exit fullscreen mode

Read the execution(...) expression left to right: the first * is "any return type", com.shop.service.* is "any class in that package", and .*(..) is "any method, with any arguments". Put together, this one line means every method of every class in the service package.

A pointcut can also match by annotation instead of by name:

@Around("@annotation(com.shop.Timed)")
Enter fullscreen mode Exit fullscreen mode

This matches any method carrying a @Timed annotation, wherever it lives. That is precisely how @Transactional finds its targets — a pointcut looking for the annotation.

Bundling it together: the aspect

A pointcut plus the advice that runs at it is a natural pair. That pair, grouped into a class for one concern, is an aspect — a class marked @Aspect that holds related pointcuts and advice together.

@Aspect
@Component
public class TimingAspect {

    @Around("execution(* com.shop.service.*.*(..))")
    public Object time(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        try {
            return pjp.proceed();
        } finally {
            log.info("{} took {}ms", pjp.getSignature().getName(),
                     System.currentTimeMillis() - start);
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

@Aspect marks the class as holding advice. @Component makes it a Spring bean, so the container discovers it. With this in place, placeOrder goes back to containing only its real work — the timing now lives here, in exactly one place, and applies to the whole service package at once.

How it actually gets applied: weaving

Now the question that ties everything together. Your service methods contain no reference at all to TimingAspect. So how does calling placeOrder actually run the timing code? The act of combining aspects with the target code, so the advice really fires, is called weaving.

Weaving can happen at three different moments: when you compile, when classes load into the JVM, or at runtime while the app is running. Spring chooses the last one, and it does it with a proxy.

A proxy is a stand-in object that has the very same methods as your bean, but each of its methods first runs the advice and then forwards to the real bean. The trick is in the wiring: when another bean asks the container for OrderService, Spring hands over the proxy, not the real object. The caller cannot tell — same type, same method signatures.

In spirit, the proxy's version of the method looks like this:

// what the generated proxy does, conceptually
public Order placeOrder(Cart cart) {
    long start = System.currentTimeMillis();
    try {
        return realOrderService.placeOrder(cart);   // delegate to the real bean
    } finally {
        log.info("placeOrder took {}ms", ...);
    }
}
Enter fullscreen mode Exit fullscreen mode

Spring builds that proxy in one of two ways. If your bean implements an interface, it can create a JDK dynamic proxy — a runtime object implementing the same interface. If there is no interface (or you ask for it), it uses CGLIB, which generates a subclass of your bean and overrides its methods. Spring Boot defaults to CGLIB, so it works whether or not you have interfaces.

The trap the proxy creates: self-invocation

Because the woven behavior lives in the proxy and not inside your class, one everyday situation silently skips the advice: a method calling another method on the same object.

@Service
public class OrderService {

    public Order placeOrder(Cart cart) {
        return validateAndSave(cart);   // internal call — bypasses the proxy
    }

    @Timed
    public Order validateAndSave(Cart cart) { ... }
}
Enter fullscreen mode Exit fullscreen mode

The proxy only wraps calls that arrive through it, from outside. But placeOrder calls validateAndSave directly on this — the real object, with no proxy in between — so the @Timed advice never runs. This catches everyone exactly once, and the mechanism explains it precisely: nothing sits between an object and a call to its own methods.

The fixes follow from the same fact: move the annotated method into a separate bean, or inject the proxy into itself, so the call goes through the proxy again. (Spring's transaction handling has its own version of this trap, which gets its own deep dive later.)

Where AspectJ comes in

Spring's proxy weaving is powerful, but it is bounded by that one mechanism: only Spring beans, only method-execution join points, and only calls coming from outside the object. Full AspectJ is a separate, more capable AOP system that Spring can integrate with. It weaves at compile time or class-load time by rewriting bytecode directly, so it can advise constructors, field access, and even internal calls — there is no proxy to bypass.

Most applications never need it; proxy-based Spring AOP covers the everyday concerns cleanly. Reach for AspectJ only when you genuinely need to advise something a proxy cannot see.

Putting the vocabulary together

The five words in this topic name five parts of one machine:

  • Join point — a point where behavior could run. In Spring, a method call on a bean.
  • Advice — the code that runs there, tagged with when (before, around, after…).
  • Pointcut — the expression selecting which join points the advice attaches to.
  • Aspect — the class bundling a pointcut and its advice for one concern.
  • Weaving — wiring the aspect into the target so the advice actually fires; Spring does it at runtime, through a proxy.

Every time you write @Transactional or @Cacheable, this entire machine is what turns a one-word annotation into real behavior wrapped around your method — with not a line of boilerplate left in sight.

Top comments (0)