All articles
Spring CoreExam Tips

Spring AOP and Pointcut Expressions Explained: Complete Exam Guide

February 14, 202615 min read

What Is AOP and Why Spring Uses It

Every non-trivial application has code that cuts across multiple modules: logging, transaction management, security checks, caching. These are cross-cutting concerns — they don't belong to any single business class, yet they appear everywhere.

Without AOP, you end up duplicating this logic across dozens of methods. AOP (Aspect-Oriented Programming) lets you define cross-cutting behavior once in a dedicated class called an aspect, and Spring applies it automatically to the methods you specify.

Spring uses AOP internally for some of its most important features — @Transactional, @Cacheable, @Secured, and @Async all work through AOP proxies under the hood. AOP proxies are created during the bean lifecycle — specifically in BeanPostProcessor.postProcessAfterInitialization().

On the 2V0-72.22 exam, AOP falls under Section 1 (Spring Core), which carries approximately 28% of all questions. You will encounter questions about advice types, pointcut expressions, proxy behavior, and the limitations of Spring AOP.


Key AOP Terminology

Before diving into code, you need to know the vocabulary. The exam assumes you understand these terms precisely.

TermDefinitionExample
AspectA class that encapsulates cross-cutting logicA LoggingAspect class that logs method calls
AdviceThe action taken by an aspect at a join pointA @Before method that logs the method name
Join PointA point during execution where advice can be appliedAny method execution in your application
PointcutAn expression that selects join pointsexecution(* com.example.service.*.*(..))
Target ObjectThe object being proxied (the original bean)Your OrderService instance
ProxyThe wrapper object that Spring creates around the targetThe CGLIB or JDK proxy that intercepts calls
WeavingThe process of linking aspects with target objectsSpring does this at runtime (not compile-time)

Exam tip: Spring AOP only supports method execution join points. Unlike full AspectJ, you cannot advise field access, constructor calls, or static methods with Spring AOP.


The Spring AOP Proxy Mechanism

Spring AOP works by wrapping your beans in proxy objects at runtime. When another bean calls a method on your service, the call goes through the proxy first — the proxy executes the advice, then delegates to the actual target method.

JDK Dynamic Proxy vs CGLIB Proxy

Spring chooses the proxy type based on your bean:

ScenarioProxy TypeHow It Works
Bean implements an interfaceJDK Dynamic ProxyCreates a proxy that implements the same interface
Bean does NOT implement an interfaceCGLIB ProxyCreates a subclass of the bean class
// JDK Dynamic Proxy — OrderService implements an interface
public interface OrderService {
    void placeOrder(Order order);
}
 
@Service
public class OrderServiceImpl implements OrderService {
    @Override
    public void placeOrder(Order order) {
        // business logic
    }
}
// Spring creates a JDK proxy implementing OrderService
// CGLIB Proxy — no interface
@Service
public class NotificationService {
    public void sendEmail(String to, String body) {
        // business logic
    }
}
// Spring creates a CGLIB subclass of NotificationService

Note: Since Spring Boot 2.0, CGLIB proxying is the default for AOP (controlled by spring.aop.proxy-target-class=true). This means even beans with interfaces get CGLIB proxies unless you explicitly change this setting.

The Self-Invocation Problem

This is the single most important AOP gotcha on the exam:

@Service
public class OrderService {
 
    @Transactional
    public void placeOrder(Order order) {
        // ...
        this.validateOrder(order); // DIRECT call — bypasses the proxy!
    }
 
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void validateOrder(Order order) {
        // This @Transactional is IGNORED because the call
        // came from within the same class (self-invocation)
    }
}

When placeOrder() calls this.validateOrder(), it calls the method directly on the target object, not through the proxy. The AOP advice (in this case @Transactional) is never triggered on validateOrder().

Why? The proxy intercepts calls from other beans. Internal calls within the same object use this, which is the raw target — not the proxy.

How to fix it:

  • Extract the method into a separate bean (recommended)
  • Inject the bean into itself (works but looks unusual)
  • Use AopContext.currentProxy() (rarely recommended)

Enabling AOP: @EnableAspectJAutoProxy

To use custom aspects in a Spring application, you need two things:

1. Enable AspectJ auto-proxying

@Configuration
@EnableAspectJAutoProxy
public class AppConfig {
    // ...
}

2. Define your aspect as a Spring bean

@Aspect
@Component  // Required! @Aspect alone does NOT make it a Spring bean
public class LoggingAspect {
 
    @Before("execution(* com.example.service.*.*(..))")
    public void logBefore(JoinPoint joinPoint) {
        System.out.println("Calling: " + joinPoint.getSignature().getName());
    }
}

Exam tip: @Aspect is an AspectJ annotation — it tells Spring this class contains advice. But it does NOT register the class as a bean. You must also use @Component (or declare it via @Bean).

Spring Boot: No explicit configuration needed

If you use Spring Boot with spring-boot-starter-aop on the classpath, @EnableAspectJAutoProxy is applied automatically via auto-configuration. You only need the @Aspect + @Component annotations on your aspect class.

<!-- Maven — just add this dependency -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

Advice Types

Spring AOP supports 5 types of advice. Each executes at a different point relative to the target method.

@Before

Runs before the target method executes. Cannot prevent execution (unless it throws an exception).

@Aspect
@Component
public class SecurityAspect {
 
    @Before("execution(* com.example.service.AdminService.*(..))")
    public void checkAdminAccess(JoinPoint joinPoint) {
        // Runs before every method in AdminService
        // If this throws an exception, the target method does NOT execute
        System.out.println("Security check for: " + joinPoint.getSignature());
    }
}

@AfterReturning

Runs after the method returns successfully. You can access the return value.

@AfterReturning(
    pointcut = "execution(* com.example.service.OrderService.findOrder(..))",
    returning = "result"
)
public void logOrderResult(JoinPoint joinPoint, Order result) {
    // 'result' contains the return value of findOrder()
    System.out.println("Order found: " + result.getId());
}

@AfterThrowing

Runs after the method throws an exception. You can access the exception object.

@AfterThrowing(
    pointcut = "execution(* com.example.service.*.*(..))",
    throwing = "ex"
)
public void logException(JoinPoint joinPoint, Exception ex) {
    // 'ex' contains the thrown exception
    System.out.println("Exception in " + joinPoint.getSignature() + ": " + ex.getMessage());
}

Important: @AfterThrowing does not catch the exception. The exception still propagates to the caller. To actually handle (swallow) an exception, you must use @Around.

@After

Runs after the method completes, regardless of outcome (success or exception). This is the AOP equivalent of a finally block.

@After("execution(* com.example.service.*.*(..))")
public void logCompletion(JoinPoint joinPoint) {
    // Runs whether the method succeeded or threw an exception
    System.out.println("Method completed: " + joinPoint.getSignature());
}

@Around

The most powerful advice type. It wraps the target method — you control whether and when the method executes.

@Around("execution(* com.example.service.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint pjp) throws Throwable {
    long start = System.currentTimeMillis();
 
    Object result = pjp.proceed(); // Execute the target method
 
    long duration = System.currentTimeMillis() - start;
    System.out.println(pjp.getSignature() + " took " + duration + "ms");
 
    return result; // MUST return the result (or a modified one)
}

Key rules for @Around:

  • The parameter type is ProceedingJoinPoint (not JoinPoint)
  • You must call pjp.proceed() to execute the target method — if you don't, the method is skipped entirely
  • You must return the result (or the caller receives null)
  • You can modify the arguments by passing them to pjp.proceed(modifiedArgs)

Advice Comparison Table

AdviceWhen It RunsCan Access Return Value?Can Access Exception?Can Prevent Execution?
@BeforeBefore the methodNoNoOnly by throwing an exception
@AfterReturningAfter successful returnYes (returning)NoNo
@AfterThrowingAfter an exceptionNoYes (throwing)No (exception still propagates)
@AfterAfter method (finally)NoNoNo
@AroundWraps the methodYesYesYes (skip proceed())

Exam tip: When multiple advice types apply to the same join point, the execution order is: @Around (before proceed) → @Before → method executes → @AfterReturning / @AfterThrowing@After@Around (after proceed).


Pointcut Expression Syntax — Deep Dive

Pointcut expressions tell Spring which methods to advise. The execution() designator is by far the most common — and the most tested on the exam.

execution() — The Most Important Designator

Full syntax:

execution(modifiers? return-type declaring-type?.method-name(parameters) throws?)

Only return-type, method-name, and parameters are required. The rest are optional.

Wildcards

SymbolMeaning
*Matches any single element (one package level, one type, one method name)
..In parameters: matches zero or more arguments of any type. In packages: matches zero or more sub-packages

Examples — From Simple to Complex

// Match ALL public methods in ALL classes
execution(public * *(..))
 
// Match any method starting with "get" in any class
execution(* get*(..))
 
// Match all methods in OrderService
execution(* com.example.service.OrderService.*(..))
 
// Match all methods in the service package
execution(* com.example.service.*.*(..))
 
// Match all methods in the service package AND sub-packages
execution(* com.example.service..*.*(..))
 
// Match methods that return a String
execution(String com.example.service.*.*(..))
 
// Match methods with exactly one String parameter
execution(* com.example.service.*.*(String))
 
// Match methods with a String first parameter and anything after
execution(* com.example.service.*.*(String, ..))
 
// Match methods with no parameters
execution(* com.example.service.*.*())

Exam tip: Pay close attention to * vs .. in package paths. service.*.*(..) matches classes directly in the service package. service..*.*(..) (two dots) matches classes in service AND all sub-packages.

within() — Restrict by Type

within() matches all methods in a given class or package. It is simpler but less precise than execution().

// All methods in OrderService
@Before("within(com.example.service.OrderService)")
 
// All methods in all classes in the service package
@Before("within(com.example.service.*)")
 
// All methods in the service package and sub-packages
@Before("within(com.example.service..*)")

bean() — Spring-Specific Designator

bean() matches by the Spring bean name. This is not part of AspectJ — it is a Spring AOP extension.

// Match all methods on the bean named "orderService"
@Before("bean(orderService)")
 
// Match all beans whose name ends with "Service"
@Before("bean(*Service)")

@annotation() — Match by Method Annotation

Matches methods annotated with a specific annotation.

// Match any method annotated with @Loggable
@Before("@annotation(com.example.annotation.Loggable)")

This is useful for creating custom cross-cutting annotations:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Loggable {}
 
@Service
public class OrderService {
 
    @Loggable  // This method will be advised
    public void placeOrder(Order order) { ... }
}

@within() — Match by Class Annotation

Matches all methods in classes annotated with a specific annotation.

// Match all methods in classes annotated with @Service
@Before("@within(org.springframework.stereotype.Service)")

Combining Pointcuts with Logical Operators

You can combine pointcut expressions with &&, ||, and !:

// Methods in service package AND annotated with @Loggable
@Before("execution(* com.example.service.*.*(..)) && @annotation(com.example.Loggable)")
 
// Methods in service OR repository packages
@Before("within(com.example.service.*) || within(com.example.repository.*)")
 
// All methods EXCEPT those in the util package
@Before("execution(* com.example..*.*(..)) && !within(com.example.util.*)")

Reusable Pointcuts with @Pointcut

Instead of duplicating expressions, define them once with @Pointcut:

@Aspect
@Component
public class LoggingAspect {
 
    // Define a reusable pointcut
    @Pointcut("execution(* com.example.service.*.*(..))")
    public void serviceLayer() {} // Method body must be empty
 
    @Pointcut("execution(* com.example.repository.*.*(..))")
    public void repositoryLayer() {}
 
    // Use the pointcut by referencing the method name
    @Before("serviceLayer()")
    public void logServiceCall(JoinPoint jp) {
        System.out.println("Service: " + jp.getSignature());
    }
 
    // Combine reusable pointcuts
    @Before("serviceLayer() || repositoryLayer()")
    public void logDataAccess(JoinPoint jp) {
        System.out.println("Data access: " + jp.getSignature());
    }
}

Exam tip: @Pointcut methods must have a void return type and an empty body. The method name becomes the pointcut identifier you reference in advice annotations.


Common AOP Exam Pitfalls

These are the traps that appear most frequently on the 2V0-72.22 exam:

1. Self-invocation bypasses the proxy A method calling another method in the same class via this does not go through the AOP proxy. The advice is not applied. This is the most frequently tested AOP gotcha.

2. @Aspect does not make a class a Spring bean You must also annotate with @Component or declare it as a @Bean. Without this, Spring will not detect the aspect.

3. Spring AOP only works on Spring-managed beans If an object is created with new instead of being managed by the Spring container, no proxy exists — AOP does not apply.

4. Stick to public methods in your pointcuts JDK Dynamic Proxies can only intercept public interface methods. CGLIB proxies can technically intercept protected and package-visible methods, but Spring recommends defining pointcuts for public methods only. Private methods are never advised by either proxy type.

5. @Around must call proceed() If you forget to call pjp.proceed() in an @Around advice, the target method never executes. This is a valid use case (e.g., caching), but if unintentional, it silently breaks your application.

6. @Around must return the result If the target method returns a reference type and your @Around advice doesn't return it, the caller receives null. If the method returns a primitive type (e.g., int, boolean), the null will cause a NullPointerException during auto-unboxing.

7. final methods cannot be advised with CGLIB CGLIB proxies work by creating a subclass of your bean. Since final methods cannot be overridden, they cannot be intercepted. The advice is silently skipped.

8. Spring AOP is runtime-only Spring AOP performs weaving at runtime using proxies. It does not modify bytecode at compile time. This means it has limitations compared to full AspectJ (no field interception, no constructor interception, no static method interception).


Exam Tips

The 2V0-72.22 exam tests these AOP scenarios most frequently:

  1. What happens when a method calls another method in the same class? → The second method's AOP advice is not applied (self-invocation bypasses the proxy)

  2. What proxy type does Spring use for a class that implements an interface? → By default in Spring Boot 2.x, CGLIB (due to spring.aop.proxy-target-class=true). In pure Spring Framework without Boot, JDK Dynamic Proxy.

  3. What is the difference between @After and @AfterReturning?@After runs regardless of outcome (like finally). @AfterReturning runs only after a successful return.

  4. Can @AfterThrowing catch an exception? → No. It observes the exception but does not prevent it from propagating. Only @Around can catch and handle exceptions.

  5. What annotation enables AspectJ support in Spring?@EnableAspectJAutoProxy on a @Configuration class. In Spring Boot with spring-boot-starter-aop, this is auto-configured.

  6. What does execution(* com.example.service..*.*(..)) match? → All methods in all classes in the com.example.service package and all sub-packages.

  7. How do you control the order of multiple aspects? → Use @Order(n) on the aspect class (or implement the Ordered interface). Lower values have higher precedence and execute their "before" advice first.


Summary

ConceptKey Point
AOP PurposeSeparate cross-cutting concerns from business logic
Proxy TypesJDK Dynamic Proxy (interfaces) or CGLIB (classes); Boot defaults to CGLIB
Self-InvocationCalls via this bypass the proxy — advice is not applied
@Aspect + @ComponentBoth required — @Aspect alone is not a Spring bean
@AroundMost powerful; must call proceed() and return the result
execution()Most common pointcut; matches method signatures with wildcards
* vs ..* = one level; .. = zero or more levels (packages) or arguments
@PointcutReusable named expressions; void method with empty body
Runtime onlySpring AOP uses proxies — no compile-time weaving

AOP is one of the most conceptually dense topics in the Spring Core section. Once you understand that everything runs through proxies and you know the limitations that follow from that, the exam questions become straightforward pattern recognition.


Frequently Asked Questions

What is the difference between Spring AOP and full AspectJ?

Spring AOP uses runtime proxies to apply advice — it only supports method execution join points on Spring-managed beans. Full AspectJ performs compile-time or load-time weaving, which allows advising field access, constructor calls, static methods, and objects created with new. For most Spring applications, Spring AOP is sufficient. Full AspectJ is needed only when you require these advanced join point types.

Why does self-invocation bypass Spring AOP?

When a method calls another method in the same class using this, the call goes directly to the target object — it does not pass through the AOP proxy. Since Spring AOP relies on the proxy to intercept calls, the advice is not applied. The recommended fix is to extract the called method into a separate Spring bean, so the call goes through that bean's proxy.

What proxy type does Spring Boot use by default?

Since Spring Boot 2.0, the default is CGLIB (spring.aop.proxy-target-class=true). This means Spring creates a subclass proxy even for beans that implement interfaces. In plain Spring Framework (without Boot), the default is JDK Dynamic Proxy for beans with interfaces and CGLIB for beans without.

Can Spring AOP advise private methods?

No. Neither JDK Dynamic Proxies nor CGLIB proxies can intercept private methods. JDK proxies only intercept public interface methods. CGLIB can technically intercept protected and package-visible methods, but Spring recommends targeting public methods only in pointcut expressions. For private method interception, you need full AspectJ with compile-time weaving.

Test your AOP knowledge with practice questions — 10 exam-style questions with detailed explanations

Spring Bean Lifecycle explained — understand where AOP proxies are created in the lifecycle

Spring Bean Scopes explained — how scoped proxies relate to AOP

View 970+ Questions on Udemy — full practice exam bank with video explanations

Practice This Topic

Reinforce what you've learned with free practice questions and detailed explanations.