MindIQ Academy

03 - Spring AOP Notes

A beginner-to-advanced guide to Spring AOP, aspects, proxies, advice types, pointcut expressions, join points, and ordering for Spring Professional Certification candidates. Covers Spring Framework 6 and Spring Boot 3 concepts.


Table of Contents

  1. AOP fundamentals
  2. Proxy mechanisms
  3. Aspect lifecycle
  4. Advice types
  5. Pointcut expressions
  6. Common mistakes
  7. Interview questions
  8. Cheat sheet

1. AOP fundamentals

Aspect-Oriented Programming (AOP) separates cross-cutting concerns from business logic. Cross-cutting concerns are behaviors needed across many parts of an application, such as logging, transactions, security, caching, metrics, retries, and auditing.

In Spring, AOP is commonly used to apply these concerns declaratively without duplicating code in every service method.

Core AOP terminology

TermMeaning
AspectA module that contains cross-cutting logic, usually a class annotated with @Aspect.
AdviceThe action taken by an aspect at a specific point, such as before or after a method call.
Join pointA point during program execution where advice can be applied. In Spring AOP, this is usually a method execution.
PointcutAn expression that selects join points where advice should run.
Target objectThe actual bean being advised.
ProxyThe Spring-created object that wraps the target and applies advice.
WeavingThe process of applying aspects to target objects. Spring AOP performs weaving at runtime through proxies.
IntroductionAdding new behavior or interfaces to an existing type.

Spring AOP scope

Spring AOP is proxy-based and primarily supports method execution join points on Spring-managed beans.

It does not advise:

  • Direct field access
  • Constructor execution
  • Private methods
  • Static methods
  • Calls made through this inside the same class
  • Objects not managed by the Spring container

For full bytecode-level weaving, use AspectJ instead of Spring AOP.

2. Proxy mechanisms

Spring AOP works by creating a proxy around a target bean. Client code calls the proxy, and the proxy decides whether to run advice before delegating to the target method.

JDK dynamic proxies

JDK dynamic proxies are used when the target bean implements at least one interface.

Characteristics:

  • Proxy implements the same interface as the target
  • Only interface methods are proxied
  • Based on java.lang.reflect.Proxy
  • Preferred when programming to interfaces

Example:

public interface PaymentService {
    void pay();
}

@Service
public class PaymentServiceImpl implements PaymentService {
    public void pay() {
        // business logic
    }
}

Spring can create a JDK proxy implementing PaymentService.

CGLIB proxies

CGLIB proxies are used when there is no suitable interface or when class-based proxying is forced.

Characteristics:

  • Proxy is a subclass of the target class
  • Can proxy public and protected non-final methods
  • Cannot proxy final classes or final methods
  • Requires method calls to go through the proxy

Class-based proxying can be enabled with:

@EnableAspectJAutoProxy(proxyTargetClass = true)

or in Spring Boot:

spring.aop.proxy-target-class=true

Self-invocation problem

Advice is not applied when a method in the same class calls another advised method through this.

@Service
public class OrderService {

    public void placeOrder() {
        validateOrder(); // bypasses proxy
    }

    @Transactional
    public void validateOrder() {
        // transaction advice will not run during self-invocation
    }
}

Fixes:

  • Move the advised method to another Spring bean
  • Inject the proxy of the same bean carefully if appropriate
  • Use AspectJ weaving for non-proxy-based interception

3. Aspect lifecycle

Spring aspects are Spring beans. Their lifecycle is managed by the Spring container.

Typical lifecycle:

  1. Spring scans and creates bean definitions.
  2. @Aspect beans are detected by AOP infrastructure.
  3. Target beans are created.
  4. Bean post-processors wrap matching target beans with proxies.
  5. Calls to proxied beans are intercepted at runtime.
  6. Advice executes according to pointcut matches and advice type.

Enabling Spring AOP

In a Spring application:

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}

In Spring Boot, AOP auto-configuration is available when spring-boot-starter-aop is on the classpath.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Aspect bean scope

Most Spring aspects are singleton beans by default.

Avoid storing request-specific, thread-specific, or method-specific mutable state in aspect fields unless the state is properly scoped or synchronized.

Bad:

@Aspect
@Component
public class AuditAspect {
    private String currentUser; // unsafe shared mutable state
}

Better:

  • Use method-local variables
  • Use request-scoped collaborators
  • Use ThreadLocal only when lifecycle cleanup is guaranteed
  • Pass data through method parameters or context objects

Advice ordering

When multiple aspects apply to the same join point, order can be controlled with @Order or by implementing Ordered.

@Aspect
@Component
@Order(1)
public class SecurityAspect {
}

@Aspect
@Component
@Order(2)
public class LoggingAspect {
}

Lower order values have higher precedence.

For @Around advice, the highest-precedence advice runs first before proceed() and finishes last after proceed().

4. Advice types

Spring supports several advice types through AspectJ-style annotations.

@Before

Runs before the matched method executes.

Use for:

  • Logging request details
  • Validation
  • Security checks
  • Metrics start markers
@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Calling: " + joinPoint.getSignature());
    }
}

@After

Runs after the method completes, whether it returns normally or throws an exception.

Use for:

  • Cleanup
  • Final logging
  • Releasing resources
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint joinPoint) {
    System.out.println("Completed: " + joinPoint.getSignature());
}

@AfterReturning

Runs only when the method returns successfully.

Use for:

  • Logging return values
  • Post-processing successful results
  • Publishing success events
@AfterReturning(
    pointcut = "execution(* com.example.service.*.*(..))",
    returning = "result"
)
public void logReturn(JoinPoint joinPoint, Object result) {
    System.out.println("Returned: " + result);
}

@AfterThrowing

Runs only when the method throws an exception.

Use for:

  • Error logging
  • Exception metrics
  • Alerting
  • Auditing failures
@AfterThrowing(
    pointcut = "execution(* com.example.service.*.*(..))",
    throwing = "ex"
)
public void logException(JoinPoint joinPoint, Exception ex) {
    System.out.println("Failed: " + joinPoint.getSignature());
}

@Around

Wraps method execution and gives full control over whether, when, and how the target method runs.

Use for:

  • Transactions
  • Caching
  • Retries
  • Performance timing
  • Conditional method execution
@Around("execution(* com.example.service.*.*(..))")
public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long start = System.currentTimeMillis();

    try {
        return joinPoint.proceed();
    } finally {
        long duration = System.currentTimeMillis() - start;
        System.out.println(joinPoint.getSignature() + " took " + duration + " ms");
    }
}

Important rules for @Around:

  • Always call proceed() unless intentionally skipping the target method.
  • Return the result from proceed() for non-void methods.
  • Preserve exceptions unless intentionally translating them.
  • Keep advice logic small and predictable.

5. Pointcut expressions

Common pointcut designators:

DesignatorMeaning
execution(...)Matches method execution by signature.
within(...)Matches join points inside selected types or packages.
this(...)Matches based on proxy type.
target(...)Matches based on target object type.
args(...)Matches based on runtime argument types.
@annotation(...)Matches methods annotated with a specific annotation.
@within(...)Matches types annotated with a specific annotation.
@target(...)Matches runtime target classes annotated with a specific annotation.
@args(...)Matches runtime argument types annotated with specific annotations.
bean(...)Spring-specific designator that matches beans by name.

Common examples

Match all methods in a package:

@Before("execution(* com.example.service.*.*(..))")

Match all methods in a package and subpackages:

@Before("execution(* com.example.service..*(..))")

Match methods annotated with a custom annotation:

@Around("@annotation(com.example.audit.Auditable)")

Match all methods in classes annotated with @Service:

@Before("@within(org.springframework.stereotype.Service)")

Reusable named pointcut:

@Aspect
@Component
public class CommonPointcuts {

    @Pointcut("within(com.example.service..*)")
    public void serviceLayer() {
    }
}

Use named pointcut:

@Before("com.example.aop.CommonPointcuts.serviceLayer()")
public void beforeServiceCall() {
}

6. Common mistakes

Advising non-Spring objects

Spring AOP only applies to Spring-managed beans. Objects created manually with new are not proxied.

OrderService service = new OrderService(); // not managed by Spring

Use dependency injection instead.

Expecting private methods to be advised

Private methods cannot be intercepted by Spring AOP proxies because they are not called through the proxy.

Move the logic to a public method on another bean if advice is required.

Forgetting self-invocation

A method call from one method to another method in the same class bypasses the proxy.

This commonly breaks:

  • @Transactional
  • @Cacheable
  • @Async
  • Custom AOP annotations

Making advised methods final

CGLIB cannot override final methods, so final methods cannot be advised with class-based proxies.

Avoid final methods on Spring beans that need proxy-based behavior.

Using overly broad pointcuts

Broad pointcuts can accidentally advise controllers, repositories, configuration classes, or infrastructure beans.

Risky:

@Around("execution(* com.example..*(..))")

Better:

@Around("within(com.example.service..*)")

Swallowing exceptions in advice

Advice should not hide failures unless that behavior is explicit and documented.

Bad:

@Around("@annotation(Retryable)")
public Object retry(ProceedingJoinPoint joinPoint) {
    try {
        return joinPoint.proceed();
    } catch (Throwable ex) {
        return null; // hides the real failure
    }
}

Better:

@Around("@annotation(Retryable)")
public Object retry(ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}

Wrong return value from @Around

For non-void methods, returning the wrong object changes application behavior.

@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    joinPoint.proceed();
    return null; // wrong for non-void methods
}

Correct:

@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
    return joinPoint.proceed();
}

Misunderstanding proxy type

When JDK proxies are used, injecting the concrete implementation type can fail because the proxy implements only the interface.

Prefer injecting interfaces:

private final PaymentService paymentService;

instead of:

private final PaymentServiceImpl paymentService;

Storing mutable state in singleton aspects

Aspects are usually singleton beans. Instance fields are shared across threads.

Use local variables inside advice methods unless shared state is intentional and thread-safe.

Ignoring advice order

When multiple aspects are applied, order affects behavior.

Examples:

  • Security should usually run before business logging.
  • Transactions should usually wrap repository or service operations.
  • Metrics may need to include or exclude retry time depending on placement.

Use @Order when behavior depends on ordering.

7. Interview questions

Basic

  1. What is AOP?
  2. What problem does AOP solve?
  3. What is a cross-cutting concern?
  4. What is an aspect?
  5. What is advice?
  6. What is a join point?
  7. What is a pointcut?
  8. What is weaving?
  9. How does Spring AOP apply aspects?
  10. What is the difference between Spring AOP and AspectJ?

Proxy and runtime behavior

  1. What proxy mechanisms does Spring AOP use?
  2. When does Spring use JDK dynamic proxies?
  3. When does Spring use CGLIB proxies?
  4. Why does self-invocation bypass advice?
  5. Can Spring AOP advise private methods?
  6. Can Spring AOP advise final methods?
  7. What happens if a bean implements an interface and Spring creates a JDK proxy?
  8. Why should Spring beans often be injected by interface when using AOP?
  9. How can class-based proxying be enabled?
  10. Why does Spring AOP only work with Spring-managed beans?

Advice and pointcuts

  1. What is the difference between @Before and @Around advice?
  2. What is the difference between @After, @AfterReturning, and @AfterThrowing?
  3. Why must @Around advice call proceed()?
  4. How do you access method arguments in advice?
  5. How do you access the returned value in advice?
  6. How do you access the thrown exception in advice?
  7. What is the purpose of JoinPoint?
  8. What is the purpose of ProceedingJoinPoint?
  9. How do you write a pointcut for methods annotated with a custom annotation?
  10. How do you reuse pointcut expressions?

Design and troubleshooting

  1. Why might an aspect not be triggered?
  2. How do you avoid overly broad pointcuts?
  3. How do you control the order of multiple aspects?
  4. Why should aspects avoid mutable instance fields?
  5. How can AOP be used for logging?
  6. How can AOP be used for transactions?
  7. What are the risks of using AOP heavily?
  8. How would you debug a pointcut that is not matching?
  9. When would you choose AspectJ instead of Spring AOP?
  10. What are common production issues caused by incorrect AOP usage?

8. Cheat sheet

Key annotations

AnnotationPurpose
@AspectMarks a class as an aspect.
@ComponentRegisters the aspect as a Spring bean.
@EnableAspectJAutoProxyEnables proxy-based AOP support.
@PointcutDefines a reusable pointcut expression.
@BeforeRuns before a matched method.
@AfterRuns after a matched method completes or fails.
@AfterReturningRuns after successful method return.
@AfterThrowingRuns after method throws an exception.
@AroundWraps method execution.
@OrderControls aspect precedence.

Advice execution summary

AdviceRuns whenCan prevent target method?Can change return value?
@BeforeBefore method executionNoNo
@AfterAfter success or failureNoNo
@AfterReturningAfter successful returnNoNo
@AfterThrowingAfter exceptionNoNo
@AroundAround method executionYesYes

Proxy selection

SituationDefault proxy type
Bean implements an interfaceJDK dynamic proxy
Bean does not implement an interfaceCGLIB proxy
proxyTargetClass = trueCGLIB proxy
spring.aop.proxy-target-class=trueCGLIB proxy

Pointcut patterns

PatternMeaning
execution(* com.example.service.*.*(..))Any method in direct classes under service.
execution(* com.example.service..*(..))Any method in service package or subpackages.
within(com.example.service..*)Any join point inside service package types.
@annotation(com.example.Loggable)Methods annotated with @Loggable.
@within(org.springframework.stereotype.Service)Types annotated with @Service.
bean(*Service)Beans whose names end with Service.

Quick rules

  1. Spring AOP is proxy-based.
  2. Only calls through the proxy are advised.
  3. Self-invocation bypasses advice.
  4. Private methods are not advised.
  5. Final methods cannot be advised by CGLIB.
  6. Spring AOP advises Spring-managed beans only.
  7. Use @Around only when you need control over execution.
  8. Always return proceed() result from @Around unless intentionally changing behavior.
  9. Keep pointcuts narrow and explicit.
  10. Avoid mutable state in singleton aspects.

Minimal aspect template

@Aspect
@Component
public class MethodLoggingAspect {

    @Around("within(com.example.service..*)")
    public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();

        try {
            return joinPoint.proceed();
        } finally {
            long duration = System.currentTimeMillis() - start;
            System.out.println(joinPoint.getSignature() + " took " + duration + " ms");
        }
    }
}

Troubleshooting checklist

SymptomLikely cause
Aspect does not runAOP not enabled or starter missing.
Aspect runs for some calls but not internal callsSelf-invocation bypassing proxy.
Bean injection fails with concrete classJDK proxy created for interface.
Advice not applied to methodMethod is private, final, static, or not called through proxy.
Advice not applied to objectObject was created with new, not by Spring.
Unexpected methods advisedPointcut is too broad.
Wrong method result@Around advice did not return proceed() result.
Race conditions in aspectMutable state stored in singleton aspect fields.