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.
| Term | Definition | Example |
|---|---|---|
| Aspect | A class that encapsulates cross-cutting logic | A LoggingAspect class that logs method calls |
| Advice | The action taken by an aspect at a join point | A @Before method that logs the method name |
| Join Point | A point during execution where advice can be applied | Any method execution in your application |
| Pointcut | An expression that selects join points | execution(* com.example.service.*.*(..)) |
| Target Object | The object being proxied (the original bean) | Your OrderService instance |
| Proxy | The wrapper object that Spring creates around the target | The CGLIB or JDK proxy that intercepts calls |
| Weaving | The process of linking aspects with target objects | Spring 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:
| Scenario | Proxy Type | How It Works |
|---|---|---|
| Bean implements an interface | JDK Dynamic Proxy | Creates a proxy that implements the same interface |
| Bean does NOT implement an interface | CGLIB Proxy | Creates 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 NotificationServiceNote: 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:
@Aspectis 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:
@AfterThrowingdoes 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(notJoinPoint) - 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
| Advice | When It Runs | Can Access Return Value? | Can Access Exception? | Can Prevent Execution? |
|---|---|---|---|---|
@Before | Before the method | No | No | Only by throwing an exception |
@AfterReturning | After successful return | Yes (returning) | No | No |
@AfterThrowing | After an exception | No | Yes (throwing) | No (exception still propagates) |
@After | After method (finally) | No | No | No |
@Around | Wraps the method | Yes | Yes | Yes (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
| Symbol | Meaning |
|---|---|
* | 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 theservicepackage.service..*.*(..)(two dots) matches classes inserviceAND 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:
@Pointcutmethods must have avoidreturn 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:
-
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)
-
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. -
What is the difference between
@Afterand@AfterReturning? →@Afterruns regardless of outcome (likefinally).@AfterReturningruns only after a successful return. -
Can
@AfterThrowingcatch an exception? → No. It observes the exception but does not prevent it from propagating. Only@Aroundcan catch and handle exceptions. -
What annotation enables AspectJ support in Spring? →
@EnableAspectJAutoProxyon a@Configurationclass. In Spring Boot withspring-boot-starter-aop, this is auto-configured. -
What does
execution(* com.example.service..*.*(..))match? → All methods in all classes in thecom.example.servicepackage and all sub-packages. -
How do you control the order of multiple aspects? → Use
@Order(n)on the aspect class (or implement theOrderedinterface). Lower values have higher precedence and execute their "before" advice first.
Summary
| Concept | Key Point |
|---|---|
| AOP Purpose | Separate cross-cutting concerns from business logic |
| Proxy Types | JDK Dynamic Proxy (interfaces) or CGLIB (classes); Boot defaults to CGLIB |
| Self-Invocation | Calls via this bypass the proxy — advice is not applied |
| @Aspect + @Component | Both required — @Aspect alone is not a Spring bean |
| @Around | Most 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 |
| @Pointcut | Reusable named expressions; void method with empty body |
| Runtime only | Spring 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