Spring AOP & Cross-Cutting Concerns

Advice Types

18 min Lesson 4 of 13

Advice Types

In Spring AOP, an advice is the action your aspect takes at a particular join point. Spring gives you five distinct advice annotations, each with a different relationship to the target method's execution. Picking the right one is not a matter of style — each type has specific guarantees, specific capabilities, and specific trade-offs that affect correctness, performance, and maintainability.

All five are in the package org.aspectj.lang.annotation and work with Spring 6 / Spring Boot 3 on the jakarta.* namespace.

@Before — Run Before the Method

@Before advice executes before the target method is called. It cannot stop execution (unless it throws an exception) and it cannot see the method's return value because the method has not run yet.

Typical uses: access-control checks, input validation, pre-condition assertions, logging that a call is about to happen.

import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; @Aspect @Component public class SecurityAspect { @Before("execution(* com.example.service.OrderService.placeOrder(..))") public void checkAuthentication(JoinPoint jp) { // jp.getArgs() gives you the method arguments Object[] args = jp.getArgs(); System.out.println("About to call: " + jp.getSignature().getName() + " with " + args.length + " argument(s)"); // throw an exception to abort the call if (!SecurityContext.isAuthenticated()) { throw new SecurityException("Unauthenticated call rejected"); } } }
Key guarantee: @Before always runs before the method body. If it throws, the target method is never invoked and Spring propagates the exception to the caller. This makes it the right hook for gate-keeping logic.

@After — Run After the Method (Finally)

@After advice runs after the method completes — whether it returned normally or threw an exception. Think of it as the aspect equivalent of a finally block. It cannot access the return value and it cannot suppress or replace the exception.

Typical uses: releasing resources, clearing thread-local state, audit trail "end of call" records.

import org.aspectj.lang.annotation.After; @Aspect @Component public class CleanupAspect { @After("execution(* com.example.service.*.*(..))") public void releaseContext(JoinPoint jp) { // Runs regardless of success or failure RequestContext.clear(); System.out.println("Context cleared after: " + jp.getSignature()); } }
@After cannot change the outcome. If the target method threw an exception, that exception is still propagated after your advice runs. If you need to react differently to success vs. failure, use @AfterReturning and @AfterThrowing instead — they give you that discrimination.

@AfterReturning — Run After a Normal Return

@AfterReturning fires only when the target method completes without throwing. Its key superpower: you can bind the actual return value and inspect (or log) it. You cannot replace the return value from this advice type.

import org.aspectj.lang.annotation.AfterReturning; @Aspect @Component public class AuditAspect { // 'returning' names the parameter that will receive the return value @AfterReturning( pointcut = "execution(* com.example.repository.UserRepository.save(..))", returning = "savedUser" ) public void auditSave(JoinPoint jp, Object savedUser) { // savedUser is the exact object returned by UserRepository.save() System.out.println("Persisted: " + savedUser); AuditLog.record("USER_SAVED", savedUser); } }

You can narrow the type of the bound parameter. If you write Object savedUser, it matches any return type. If you write User savedUser, Spring only applies the advice when the return value is assignment-compatible with User.

Cache population pattern: @AfterReturning is the cleanest place to populate a cache after a successful database read. The method ran, it succeeded, and you now have the result — no try/catch boilerplate in your service layer.

@AfterThrowing — Run When an Exception Escapes

@AfterThrowing fires only when the target method throws an exception that propagates past it. You can bind the exception, log it, alert monitoring systems, or enrich it — but the exception still propagates unless you throw a different one.

import org.aspectj.lang.annotation.AfterThrowing; @Aspect @Component public class ExceptionMonitorAspect { // 'throwing' names the parameter that will receive the exception @AfterThrowing( pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex" ) public void onServiceException(JoinPoint jp, RuntimeException ex) { // Only fires for RuntimeException and its subtypes System.err.println("Exception in " + jp.getSignature() + ": " + ex.getMessage()); MetricsClient.increment("service.error", "method", jp.getSignature().getName()); } }

As with @AfterReturning, you can constrain the exception type. Declaring RuntimeException ex means checked exceptions pass through without triggering this advice. Declaring Throwable ex catches everything.

@Around — Full Control Over Execution

@Around is the most powerful advice type. It wraps the entire method call: you decide if and when to invoke the target method, you can modify arguments before calling it, and you can modify or replace the return value after it returns. This power comes with responsibility — if you forget to call proceed(), the target method never executes.

import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; @Aspect @Component public class TimingAspect { @Around("execution(* com.example.service.*.*(..))") public Object measureTime(ProceedingJoinPoint pjp) throws Throwable { long start = System.currentTimeMillis(); try { // Invoke the actual method; pjp.proceed() returns its return value Object result = pjp.proceed(); long elapsed = System.currentTimeMillis() - start; System.out.println(pjp.getSignature() + " completed in " + elapsed + " ms"); return result; // MUST return — caller expects it } catch (Throwable t) { long elapsed = System.currentTimeMillis() - start; System.out.println(pjp.getSignature() + " FAILED after " + elapsed + " ms"); throw t; // re-throw so normal exception handling applies } } }
Always return the result of pjp.proceed(). The method's return type might be void (in which case the value is null and discarding it is fine), but for all other types the caller is waiting for that value. Forgetting the return statement compiles fine but produces silent null bugs at runtime.

Execution Order When Multiple Advices Apply

When several advice methods match the same join point, Spring invokes them in a defined order around the call stack:

  1. @Around (before proceed())
  2. @Before
  3. Target method executes
  4. @AfterReturning or @AfterThrowing (whichever applies)
  5. @After
  6. @Around (after proceed() returns)

When two advices of the same type in different aspects apply to the same join point, you control their relative order with @Order(n) on the aspect class — lower number runs outermost (first before, last after).

import org.springframework.core.annotation.Order; @Aspect @Component @Order(1) // outermost — runs first on the way in, last on the way out public class TransactionAspect { ... } @Aspect @Component @Order(2) // inner — runs second on the way in, first on the way out public class LoggingAspect { ... }

Choosing the Right Advice Type

  • Need to gate or validate before execution? Use @Before.
  • Need guaranteed cleanup regardless of outcome? Use @After.
  • Need the return value to log, cache, or react on success? Use @AfterReturning.
  • Need to detect, log, or enrich a specific exception? Use @AfterThrowing.
  • Need to modify arguments, suppress exceptions, or replace the return value? Use @Around.

Prefer the least-powerful type that satisfies your requirement. @Around is capable of everything, but using it for simple logging obscures intent and introduces the risk of accidentally swallowing exceptions or return values. Reserve it for cross-cutting concerns that genuinely need full control: caching, retries, transactions, circuit breakers.

Summary

Spring AOP's five advice types form a spectrum from minimal responsibility (@Before, @After) to complete control (@Around). Each type has a clear contract: what it sees, when it runs, and what it can change. Matching the right type to your requirement keeps aspect code readable, prevents subtle bugs, and signals intent to the next developer. In the following lessons you will put this into practice with @Around and the ProceedingJoinPoint API, then build real-world logging and security aspects.