Spring AOP Interview Questions – Core Concepts and Advice Types
-
Last Updated: May 6, 2026
-
By: javahandson
-
Series
Learn Java in a easy way
Master Spring AOP interview questions: aspects, advice types, JDK vs CGLIB proxies, pointcuts, @Around, self-invocation, and @Order with code.
Aspect-Oriented Programming (AOP) is one of the most powerful — and most misunderstood — features of the Spring Framework. Whether you are preparing for a spring AOP interview question or debugging a production issue with @Transactional, understanding what happens under the hood matters: how a proxy is created, why private methods cannot be intercepted, or what the self-invocation problem is and how to fix it. These are precisely the questions that separate average Spring developers from senior engineers in technical interviews.
This article covers 15 carefully crafted Spring AOP interview questions — from beginner to advanced — covering AOP fundamentals, all five advice types, JDK Dynamic Proxy vs CGLIB, proxy creation in the bean lifecycle, weaving strategies, @Order on aspects, and the Introduction mechanism. Every answer includes the technical depth and Interview Insight notes that product companies look for in senior Spring developers.
Answer – Aspect-Oriented Programming is a programming paradigm that allows you to separate cross-cutting concerns from your core business logic. A cross-cutting concern is functionality that affects multiple layers of an application — logging, security, transaction management, caching, auditing, and performance monitoring are classic examples. Without AOP, this kind of logic is scattered across dozens of unrelated classes, making it hard to maintain, test, and reason about.
Consider logging method execution times. Without AOP, you would add a start-time measurement at the beginning of every service method and a duration calculation at the end. That duplicated boilerplate spreads across your entire codebase. If the logging format changes, you must update every single method. AOP solves this by allowing you to write the logging logic exactly once — in an Aspect — and declaratively apply it to any number of methods using a Pointcut expression, without touching those methods at all.
Spring AOP implements this using proxies. When Spring creates a bean that is the target of an aspect, it wraps the bean in a proxy object. All method calls go through the proxy, which can execute advice (the cross-cutting logic) before, after, or around the actual method invocation. The bean itself remains unaware that any proxying is happening — its code is clean and focused solely on business logic.
The three core problems AOP solves are: code duplication of cross-cutting logic, tight coupling between business code and infrastructure concerns, and the difficulty of modifying system-wide behavior without touching every affected class.
| 🎯 Interview Insight: A strong answer always connects AOP to a concrete example. Saying ‘AOP separates cross-cutting concerns’ without naming examples like @Transactional, @Cacheable, or Spring Security’s method security — all of which are implemented using AOP internally — sounds theoretical. Show that you understand AOP as the mechanism behind features you already use daily. |
Answer – Understanding AOP requires learning a precise vocabulary. These six terms are the foundation of every AOP discussion, and interviewers expect you to define them accurately and relate them to each other.
A JoinPoint is a specific point in the execution of a program where advice can be applied. In Spring AOP, every JoinPoint is a method execution — Spring AOP does not support field access or constructor interception, unlike full AspectJ. When your advice code runs, it receives a JoinPoint object that gives it access to the method signature, arguments, and target object.
A Pointcut is an expression that matches a set of JoinPoints. It is the selector that tells Spring which method executions your advice should apply to. Spring AOP uses AspectJ’s pointcut expression language. A pointcut like execution(* com.example.service.*.*(..)) matches all methods on all classes in the service package.
Advice is the action taken at a matched JoinPoint. It is the actual code that runs. Spring supports five advice types: @Before (runs before the method), @After (runs after, regardless of outcome), @AfterReturning (runs only if method returns normally), @AfterThrowing (runs only if method throws an exception), and @Around (wraps the entire method execution).
An Aspect is the module that combines a Pointcut with one or more pieces of Advice. In Spring, you annotate a class with @Aspect to declare it as an aspect. The aspect class contains both the pointcut definitions and the advice methods.
The Target is the object whose methods are being advised — the bean wrapped by the proxy. The target is unaware of the proxying.
Weaving is the process of linking aspects with application objects to create an advised object. Weaving can happen at compile time (AspectJ compiler), at class-load time (load-time weaving with a Java agent), or at runtime (Spring AOP’s default — proxy creation during application context startup).
| Term | One-line definition |
| JoinPoint | A specific method execution where advice can be inserted |
| Pointcut | An expression that selects a set of JoinPoints |
| Advice | The code that runs at a matched JoinPoint |
| Aspect | A class that groups related Pointcuts and Advice |
| Target | The real bean object being advised by the proxy |
| Weaving | The process of applying aspects to target objects to create proxies |
| 🎯 Interview Insight: The most common confusion is between Pointcut and JoinPoint. A JoinPoint is a single execution instance — one specific call to one specific method. A Pointcut is a predicate over all possible JoinPoints — it describes the pattern. Think of JoinPoint as ‘this particular method call right now’ and Pointcut as ‘all method calls matching this rule.’ |
Answer – Spring AOP provides five advice types, each running at a different point in the advised method’s execution. Choosing the right type of advice is important for both correctness and interview answers.
@Aspect
@Component
public class LoggingAspect {
// @Before: runs BEFORE the method. Cannot stop execution (unless it throws).
@Before("execution(* com.example.service.*.*(..))")
public void logBefore(JoinPoint jp) {
System.out.println("Calling: " + jp.getSignature().getName());
}
// @AfterReturning: runs AFTER normal return. Can access the return value.
@AfterReturning(
pointcut = "execution(* com.example.service.*.*(..))",
returning = "result")
public void logAfterReturning(JoinPoint jp, Object result) {
System.out.println("Returned: " + result);
}
// @AfterThrowing: runs ONLY if the method throws. Can access the exception.
@AfterThrowing(
pointcut = "execution(* com.example.service.*.*(..))",
throwing = "ex")
public void logAfterThrowing(JoinPoint jp, Exception ex) {
System.out.println("Exception in " + jp.getSignature().getName() + ": " + ex.getMessage());
}
// @After: runs ALWAYS after the method — like a finally block.
// Cannot access return value or exception.
@After("execution(* com.example.service.*.*(..))")
public void logAfter(JoinPoint jp) {
System.out.println("Method completed: " + jp.getSignature().getName());
}
// @Around: wraps the entire execution. Must call proceed() to invoke the real method.
@Around("execution(* com.example.service.*.*(..))")
public Object logAround(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed(); // invoke the real method
long duration = System.currentTimeMillis() - start;
System.out.println(pjp.getSignature().getName() + " took " + duration + "ms");
return result;
}
}
@Before cannot prevent the method from executing unless it throws an exception. If you need to conditionally skip execution or modify arguments, use @Around. @AfterReturning gives you access to the return value via the returning binding, but you cannot replace the return value there — for that, use @Around. @AfterThrowing lets you react to exceptions but does not suppress them. @After is the equivalent of a finally block — it runs regardless of whether the method returned or threw.
| Advice Type | Runs / Can Do |
| @Before | Before method. Cannot stop execution or modify return value. |
| @AfterReturning | Always — like finally. Cannot access the return value or the exception. |
| @AfterThrowing | After exception only. Can read exception. Cannot suppress it. |
| @After | Wraps execution. Must call proceed(). Can modify args, return value, and suppress exceptions. |
| @Around | Wraps execution. Must call proceed(). Can modify args, return value, suppress exceptions. |
| 🎯 Interview Insight: Interviewers often ask: ‘Which advice type is most powerful?’ @Around is the most powerful — it can modify arguments before proceed(), change the return value after proceed(), retry on failure, measure execution time, and even suppress the method call entirely. However, with that power comes responsibility: forgetting to call proceed() silently swallows the method invocation with no warning. |
Answer – @Around is the most flexible advice type in Spring AOP. Unlike the other four advice types, @Around requires a ProceedingJoinPoint parameter — not just a JoinPoint. ProceedingJoinPoint extends JoinPoint and adds the proceed() method, which is what actually invokes the target method. Without calling proceed(), the real method never executes.
@Aspect
@Component
public class AroundExamples {
// 1. Basic timing — proceed and return result unchanged
@Around("execution(* com.example.service.*.*(..))")
public Object measureTime(ProceedingJoinPoint pjp) throws Throwable {
long start = System.currentTimeMillis();
Object result = pjp.proceed();
System.out.println("Duration: " + (System.currentTimeMillis() - start) + "ms");
return result; // must return — caller expects it
}
// 2. Modifying arguments before invocation
@Around("execution(* com.example.service.UserService.findUser(String))")
public Object normalizeInput(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs();
args[0] = ((String) args[0]).trim().toLowerCase(); // sanitize
return pjp.proceed(args); // pass modified args
}
// 3. Modifying the return value
@Around("execution(* com.example.service.PriceService.getPrice(..))")
public Object applyDiscount(ProceedingJoinPoint pjp) throws Throwable {
Double originalPrice = (Double) pjp.proceed();
return originalPrice * 0.9; // return 10% discounted price
}
// 4. Suppressing the method call (caching example)
@Around("execution(* com.example.service.ReportService.generateReport(Long))")
public Object cacheReport(ProceedingJoinPoint pjp) throws Throwable {
Long reportId = (Long) pjp.getArgs()[0];
if (cache.containsKey(reportId)) {
return cache.get(reportId); // return cached — proceed() never called
}
Object report = pjp.proceed();
cache.put(reportId, report);
return report;
}
// 5. Retry logic on exception
@Around("execution(* com.example.service.ExternalApiService.*(..))")
public Object retryOnFailure(ProceedingJoinPoint pjp) throws Throwable {
int maxRetries = 3;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
return pjp.proceed();
} catch (ExternalApiException ex) {
if (attempt == maxRetries) throw ex;
Thread.sleep(1000L * attempt);
}
}
return null;
}
}
One critical rule: the @Around method must declare throws Throwable because proceed() propagates any checked or unchecked exception thrown by the target. If you declare throws Exception instead, you cannot propagate Errors (like OutOfMemoryError), which is incorrect. The return type must be Object so that the caller receives the method’s result. If your @Around advice is on a void method and you return null, the caller sees void normally.
| 🎯 Interview Insight: The most common @Around mistake in production: applying it to methods that return primitives and returning null from the advice without calling proceed(). Spring automatically unboxes Object to the primitive return type, and null causes a NullPointerException during unboxing. Always either call proceed() or return a valid non-null value compatible with the method’s return type. |
Answer – @Aspect marks a class as an aspect — a module that contains pointcut definitions and advice methods. It is not a Spring stereotype annotation, so @Aspect alone does not make the class a Spring-managed bean. You must also annotate the class with @Component (or register it as a bean via @Bean in a @Configuration class) so that Spring picks it up during component scanning.
@EnableAspectJAutoProxy activates Spring’s annotation-driven AOP infrastructure. Without it, Spring ignores @Aspect annotations entirely — your aspects are just plain classes with no effect. @EnableAspectJAutoProxy is typically placed on a @Configuration class or on the main @SpringBootApplication class. In Spring Boot applications, it is auto-configured automatically when spring-boot-starter-aop is on the classpath, so you often do not need to add it manually.
// Enable AOP — needed in plain Spring. Auto-configured in Spring Boot.
@Configuration
@EnableAspectJAutoProxy
public class AopConfig { }
// The aspect itself — must be a Spring bean AND annotated with @Aspect
@Aspect
@Component
public class SecurityAuditAspect {
// Reusable pointcut definition
@Pointcut("execution(* com.example.service.*Service.*(..))")
public void serviceLayer() { }
@Before("serviceLayer()")
public void auditServiceCall(JoinPoint jp) {
String user = SecurityContextHolder.getContext()
.getAuthentication().getName();
System.out.println("User [" + user + "] called " + jp.getSignature());
}
}
// proxyTargetClass=true forces CGLIB proxying for all beans
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class CglibForcedAopConfig { }
The proxyTargetClass attribute on @EnableAspectJAutoProxy controls whether Spring uses JDK Dynamic Proxies (the default; requires the target to implement an interface) or CGLIB subclass proxies (which work on any concrete class). Setting proxyTargetClass = true globally forces CGLIB for all beans. Spring Boot sets this to true by default when AOP is enabled, which is why Spring Boot applications can apply aspects to classes that do not implement interfaces.
| 🎯 Interview Insight: A common interview question: ‘I added @Aspect and @Component to my class, but it is not working.’ The most likely cause is that @EnableAspectJAutoProxy is missing from the configuration. In a plain Spring application, forgetting this annotation means all your @Aspect beans are loaded as normal beans, but their advice is never woven. In Spring Boot with spring-boot-starter-aop, this is auto-configured, but the dependency must still be added. |
Answer – Spring AOP and AspectJ are both implementations of Aspect-Oriented Programming, but they differ fundamentally in scope, capability, and how weaving is performed. Understanding this difference is essential for senior Spring interviews.
Spring AOP is a pure runtime AOP framework implemented using proxies. It only supports method execution JoinPoints on Spring-managed beans. You cannot use Spring AOP to intercept calls to new() constructors, field access, or static method calls. It works entirely within the Spring container — aspects are themselves Spring beans, and the proxying happens during bean creation in the application context. Spring AOP uses AspectJ’s pointcut expression syntax, but it does not use AspectJ’s weaving infrastructure.
AspectJ is a full AOP framework with its own compiler (ajc) and weaving agent. It supports all JoinPoint types: method execution, method call, constructor execution, field get and set, static initializer execution, and more. AspectJ weaves aspect code directly into bytecode — either at compile time, at post-compile time, or at load time using a Java agent. There is no proxy involved. The advice code is literally inserted into the target class bytecode.
| Feature | Spring AOP vs AspectJ |
| Weaving mechanism | Runtime proxy (JDK or CGLIB) vs Bytecode weaving (compile/load time) |
| JoinPoint types | Method execution only vs Method, constructor, field, static init |
| Target scope | Small proxy overhead per call vs. near-zero overhead after weaving |
| Performance | Small proxy overhead per call vs Near-zero overhead after weaving |
| Setup complexity | Simple — just @Aspect + @EnableAspectJAutoProxy vs Requires ajc compiler or Java agent |
| Self-invocation | Does NOT intercept (proxy bypass) vs Always intercepts (bytecode level) |
Spring integrates with AspectJ through the @AspectJ annotation style — you write aspects using @Aspect, @Before, @After, and @Around with the same syntax. But in Spring AOP, this syntax is handled by Spring’s own proxy infrastructure rather than by the AspectJ weaver. If you want full AspectJ capabilities — intercepting non-Spring objects, constructor interception, no self-invocation limitation — you need to enable load-time weaving with the AspectJ agent.
| 🎯 Interview Insight: The critical practical difference is self-invocation. Spring AOP uses proxies, which means if a method inside a bean calls another method in the same bean, the call bypasses the proxy entirely. No advice fires. AspectJ does not have this limitation because advice is woven directly into the bytecode — every invocation is intercepted, including internal ones. This is why @Transactional on a method that calls another @Transactional method in the same class does not work with Spring AOP without a workaround. |
Answer – Spring AOP creates proxies — wrapper objects that intercept method calls and execute advice before delegating to the real target bean. There are two proxying mechanisms Spring can use: JDK Dynamic Proxies and CGLIB subclass proxies.
JDK Dynamic Proxy is Java’s built-in proxy mechanism from java.lang.reflect.Proxy. It requires the target class to implement at least one interface. The proxy implements the same interfaces as the target bean. When a method call arrives at the proxy, it is dispatched through an InvocationHandler — Spring’s implementation is JdkDynamicAopProxy — which runs the advice chain and then calls the real method on the target object.
CGLIB (Code Generation Library) works differently. It creates a subclass of the target class at runtime. The subclass overrides all non-final, non-private methods. When an overridden method is called on the proxy, CGLIB intercepts the call, runs the advice, and optionally delegates to the parent class’s method. CGLIB does not require the target to implement interfaces — it works on any concrete class. Spring Boot defaults to CGLIB proxying.
// JDK Dynamic Proxy — works because OrderService implements an interface
public interface OrderService {
Order placeOrder(OrderRequest request);
}
@Service
public class OrderServiceImpl implements OrderService {
@Override
public Order placeOrder(OrderRequest request) { /* ... */ return order; }
}
// Spring creates a proxy that also implements OrderService
// The proxy's placeOrder() runs advice, then calls OrderServiceImpl.placeOrder()
// CGLIB proxy — no interface needed
@Service
public class InventoryService { // no interface
public void reserveStock(Long productId, int qty) { /* ... */ }
}
// Spring creates a CGLIB subclass:
// class InventoryService$$EnhancerBySpringCGLIB$$abc123 extends InventoryService {
// @Override
// public void reserveStock(Long productId, int qty) {
// // run advice chain, then: super.reserveStock(productId, qty);
// }
// }
| 🎯 Interview Insight: A subtle implication of CGLIB proxying: the target class must not be final, and methods you want to advise must not be final. CGLIB creates a subclass and overrides methods — final classes cannot be subclassed, and final methods cannot be overridden. Spring will throw an exception at startup if it tries to CGLIB-proxy a final class. This is a common issue when using Kotlin with Spring, since Kotlin classes are final by default. The solution is the Kotlin Spring plugin, which automatically opens Spring-annotated classes. |
Answer – Spring’s decision between JDK Dynamic Proxy and CGLIB depends on whether the target bean implements interfaces and on the proxyTargetClass setting.
In plain Spring (without Spring Boot), by default, if the target bean implements one or more interfaces, Spring uses JDK Dynamic Proxies. If the target bean has no interfaces, Spring falls back to CGLIB. In Spring Boot (version 2.0+), the default was changed to always use CGLIB, regardless of whether interfaces are present. This was done because pure CGLIB proxying avoids subtle bugs in which Spring injects a proxy of the interface type, but the injected field is declared as the concrete class type, causing a ClassCastException.
// Force CGLIB globally — on @Configuration class
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class AopConfig { }
// OR in application.properties (Spring Boot)
// spring.aop.proxy-target-class=true (default in Spring Boot)
// spring.aop.proxy-target-class=false (force JDK proxy if interface exists)
// The proxy class name reveals which mechanism was used:
// JDK: com.sun.proxy.$Proxy123
// CGLIB: OrderServiceImpl$$EnhancerBySpringCGLIB$$abc456
System.out.println(orderService.getClass().getName());
| 🎯 Interview Insight: In Spring Boot applications, setting spring.aop.proxy-target-class=false can cause unexpected ClassCastExceptions if your @Autowired field is the concrete class type rather than the interface type. When JDK proxy is used and you try to cast the proxy to the concrete class, it fails because the proxy only implements the interface, not the class. Always inject by interface type when JDK proxying is active — or simply leave Spring Boot’s default CGLIB proxying in place. |
Answer – No. Spring AOP cannot intercept private methods because of how proxying works. Spring AOP creates proxies — either JDK Dynamic Proxies or CGLIB subclasses. In both cases, the proxy intercepts method calls by overriding or implementing the method at the proxy level. Private methods in Java cannot be overridden by subclasses (with CGLIB) and are not part of any interface (with JDK proxies). The proxy, therefore, has no way to intercept them.
Even if a CGLIB subclass is created, private methods are not overridden in the subclass — Java’s access rules prohibit it. When code within the target bean calls a private method, the call goes directly to the method on the target object, bypassing the proxy. No advice fires.
@Service
public class PaymentService {
// PUBLIC method — goes through proxy, advice fires
public void processPayment(Payment payment) {
validate(payment); // INTERNAL CALL — bypasses proxy (self-invocation)
}
// PRIVATE method — Spring AOP cannot intercept this
private void validate(Payment payment) {
// No @Before, @After, or @Around advice will ever fire on this method
if (payment.getAmount() <= 0) throw new IllegalArgumentException("Invalid amount");
}
}
The only way to intercept private methods or self-invocations in Java is to use full AspectJ with bytecode weaving (at compile time or load time). AspectJ modifies the class bytecode directly, so every call to the method — regardless of where it originates or its access modifier — goes through the woven advice.
| 🎯 Interview Insight: Interviewers sometimes ask: ‘I applied @Transactional to a private method — will it work?’ No. @Transactional is implemented using Spring AOP, and Spring AOP cannot intercept private methods. The annotation is silently ignored. Spring does not throw an error — it simply cannot create the transactional proxy for that method. The @Transactional annotation must be on a public method to be effective. |
Answer – The self-invocation problem occurs when a method inside a Spring bean calls another method in the same bean. Because Spring AOP uses proxies, all advice is applied at the proxy level. When a bean calls its own method, the call bypasses the proxy and goes directly to the raw target object. Any advice that should fire on the called method is silently skipped.
This is one of the most common production bugs in Spring applications. The most frequent manifestation is @Transactional: a public @Transactional method A calls a public @Transactional method B in the same class. Method A’s transaction starts correctly. But method B’s @Transactional annotation is ignored because the call never went through the proxy — B executes in A’s transaction, whether you want it to or not.
@Service
public class OrderService {
// Solution 1: Inject self — the injected reference is the PROXY
@Autowired
private OrderService self;
@Transactional
public void placeOrder(Order order) {
saveOrder(order);
self.sendConfirmation(order); // goes through proxy — @Transactional fires
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void sendConfirmation(Order order) { /* separate transaction */ }
// Solution 2: ApplicationContext.getBean()
@Autowired
private ApplicationContext ctx;
public void placeOrderViaContext(Order order) {
saveOrder(order);
ctx.getBean(OrderService.class).sendConfirmation(order);
}
// Solution 3: Restructure — move the called method to a separate Spring bean
// This is the cleanest architectural solution
}
The best engineering solution is usually to refactor: extract the method that needs its own transactional or AOP boundary into a separate Spring-managed bean. This makes the dependency explicit, cleans up the design, and eliminates the self-invocation problem entirely, without any Spring-specific workarounds.
| 🎯 Interview Insight: The self-injection pattern (injecting OrderService into itself) works but requires careful handling of circular dependency detection in Spring. Since Spring 4.3, Spring can handle self-injection via @Autowired because it detects it as a special case. An alternative is @Lazy on the self-injected field, which defers proxy resolution and avoids circular dependency issues at startup time: @Autowired @Lazy private OrderService self. |
Answer – When multiple aspects apply to the same method, Spring must determine the order in which their advice executes. @Order controls this sequence. Aspects with lower @Order values run their before advice first and their after advice last — they wrap outer layers around the execution. Aspects with higher @Order values are closer to the actual target method invocation.
// Outermost — runs first on entry, last on exit
@Aspect
@Component
@Order(1)
public class MetricsAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long t = System.nanoTime();
try {
return pjp.proceed(); // calls SecurityAspect next
} finally {
System.out.println("Metrics: " + (System.nanoTime() - t) + "ns");
}
}
}
@Aspect
@Component
@Order(2)
public class SecurityAspect {
@Around("execution(* com.example.service.*.*(..))")
public Object secure(ProceedingJoinPoint pjp) throws Throwable {
checkPermission(pjp);
return pjp.proceed(); // calls real method
}
}
// Execution order:
// MetricsAspect starts timing
// SecurityAspect checks permission
// real method executes
// SecurityAspect returns
// MetricsAspect records timing
If @Order is not specified on an aspect, its ordering relative to other unordered aspects is undefined — Spring makes no guarantee about the sequence. This can cause subtle non-deterministic behavior if advice from two aspects depends on each other. Always use @Order explicitly when the execution order of aspects matters.
| 🎯 Interview Insight: A common trap: @Order on the aspect class controls ordering between different aspects. @Order on an @EventListener method controls ordering between multiple listeners for the same event. These are two separate uses of @Order that happen to use the same annotation. In the AOP context, @Order is placed at the class level on the @Aspect class — putting it on individual advice methods within the same aspect has no effect on inter-aspect ordering. |
Answer – Weaving is the process of applying aspects to the target code. The three strategies differ in when and how the aspect code is merged with the target class’s bytecode, and each has distinct trade-offs in performance, complexity, and capability.
Compile-time weaving (CTW) uses the AspectJ compiler (ajc) instead of the standard javac. The compiler directly weaves aspect code into the target class bytecodes during compilation. The .class files already contain all the woven advice — no proxy or agent is needed at runtime. This gives the best performance (zero runtime overhead per join point), supports all join point types, including field access and constructors, and eliminates the self-invocation problem. The tradeoff: you must use the AspectJ compiler in your build pipeline, which adds build complexity.
Load-time weaving (LTW) applies aspect weaving as classes are loaded by the Java classloader, using a Java instrumentation agent (-javaagent:aspectjweaver.jar). The agent intercepts class loading, weaves the aspect’s bytecode at that point, and the class loader sees already-woven classes. LTW supports all AspectJ join point types and works with third-party libraries. The tradeoff: requires a JVM agent and adds class-loading overhead.
Runtime weaving is what Spring AOP uses. Proxies are created at application context startup. No bytecode modification happens — instead, wrapper objects intercept method calls at runtime. This is the simplest approach, requiring no build changes or JVM agents. The tradeoffs: method execution join points only, self-invocation bypasses proxies, and small performance overhead per method call.
| Aspect | Compile-time / Load-time | Spring AOP (runtime) |
| JoinPoint types | All (field, constructor, method) | Method execution only |
| Self-invocation | Intercepted | Not intercepted (proxy bypass) |
| Build complexity | Requires an AJC compiler or a JVM agent | None — just @Aspect |
| Performance | Best — zero runtime overhead | Small overhead per call |
| Use case | Advise non-Spring or all join points | Standard Spring apps |
| 🎯 Interview Insight: The question ‘when would you use AspectJ LTW over Spring AOP?’ has a clear answer: when you need to advise code you don’t own (third-party libraries), when you need constructor or field join points, or when the self-invocation problem is causing bugs that refactoring cannot solve cleanly. For the vast majority of Spring applications — applying @Transactional, @Cacheable, @Async — runtime proxying is entirely sufficient and simpler to operate. |
Answer – Spring AOP proxies are created during the bean lifecycle through a specific BeanPostProcessor called AbstractAutoProxyCreator (and its most common concrete subclass, AnnotationAwareAspectJAutoProxyCreator). This post-processor is registered automatically when @EnableAspectJAutoProxy is active.
The proxy creation happens in the postProcessAfterInitialization() phase of the bean lifecycle — after the bean has been fully constructed, all @Autowired dependencies have been injected, and @PostConstruct has run. This is a critical ordering detail: when the proxy is created, the target bean is already fully initialized. The proxy wraps the fully initialized target, and from that point forward, any other beans that receive this bean via @Autowired receive the proxy, not the raw target.
// What AnnotationAwareAspectJAutoProxyCreator does (simplified):
public Object postProcessAfterInitialization(Object bean, String beanName) {
// Check: are there any aspects whose pointcuts match this bean's methods?
List<Advisor> advisors = findEligibleAdvisors(bean.getClass());
if (!advisors.isEmpty()) {
// Wrap the bean in a proxy — JDK or CGLIB
return createProxy(bean, beanName, advisors);
// The proxy is stored in the singleton registry
// and injected into other beans
}
return bean; // no matching aspects — return original bean unchanged
}
// Verify in code:
ApplicationContext ctx = ...;
OrderService os = ctx.getBean(OrderService.class);
System.out.println(os.getClass());
// Prints: OrderService$$EnhancerBySpringCGLIB$$...
A critical implication: BeanPostProcessors themselves are not proxied by AOP. BeanPostProcessors are created very early in the ApplicationContext lifecycle, before most regular beans. If an @Aspect depends on a BeanPostProcessor bean, Spring logs a warning, and the aspect may not be applied to certain early-initialized beans.
| 🎯 Interview Insight: The lifecycle order matters for understanding why @PostConstruct cannot call @Transactional methods and have the transaction respected. The @Transactional proxy is created in postProcessAfterInitialization(), which runs after @PostConstruct (which runs in postProcessBeforeInitialization()). At @PostConstruct time, the transactional proxy does not exist yet. For transactional initialization, use ApplicationRunner or CommandLineRunner, which run after the full context and all proxies are ready. |
Answer – Introduction (called @DeclareParents in Spring’s @AspectJ syntax) is an AOP feature that allows you to add new interfaces — and their implementations — to existing Spring beans without modifying those beans’ source code. Instead of just intercepting method calls, an Introduction actually adds new methods to the target type’s apparent API.
// Step 1: Define the interface to introduce
public interface Auditable {
String getLastModifiedBy();
void setLastModifiedBy(String user);
}
// Step 2: Provide a default implementation (the mixin)
public class AuditableMixin implements Auditable {
private String lastModifiedBy;
@Override public String getLastModifiedBy() { return lastModifiedBy; }
@Override public void setLastModifiedBy(String user) { this.lastModifiedBy = user; }
}
// Step 3: Declare the Introduction in an Aspect
@Aspect
@Component
public class AuditIntroductionAspect {
@DeclareParents(
value = "com.example.service.*+", // all types in service package
defaultImpl = AuditableMixin.class
)
public static Auditable mixin;
}
// Step 4: Use it — cast the bean to the introduced interface
@Service
public class ConsumerService {
@Autowired
private OrderService orderService;
public void auditOrder() {
// The proxy now implements both OrderService AND Auditable
Auditable auditable = (Auditable) orderService;
auditable.setLastModifiedBy("admin@example.com");
System.out.println("Last modified by: " + auditable.getLastModifiedBy());
}
}
Introduction only works with CGLIB proxies when introducing an interface to a class that did not originally implement it. Each proxy instance gets its own instance of the defaultImpl class to hold the introduced state, which is why AuditableMixin has its own lastModifiedBy field — that state is per-bean-instance.
| 🎯 Interview Insight: Introduction is rarely used in typical enterprise applications but appears frequently in interviews as a test of AOP depth. The key distinction to articulate: regular advice modifies behavior at join points (intercepting existing methods). Introduction modifies the type — it adds new methods to an existing class’s interface. It is the AOP equivalent of the Mixin pattern in object-oriented design. |
Answer – Yes, multiple aspects can absolutely apply to the same method. In fact, most Spring applications do this routinely: @Transactional, @Cacheable, @Secured, and custom aspects may all target the same service method simultaneously. Each aspect contributes one or more pieces of advice, and they are applied in a layered, nested fashion around the target method.
When multiple aspects target the same method, Spring builds an advice chain. The aspects are ordered by their @Order value — the lowest order value is the outermost layer. Each @Around advice in the chain calls proceed(), which passes control to the next advice in the chain. The result is deeply nested: each aspect’s before-logic runs in order, the innermost advice calls the target method, and each aspect’s after-logic runs in reverse order.
@Aspect @Component @Order(1) // outermost
public class MetricsAspect {
@Around("@annotation(Monitored)")
public Object measure(ProceedingJoinPoint pjp) throws Throwable {
long t = System.nanoTime();
try { return pjp.proceed(); }
finally { System.out.println("Metrics: " + (System.nanoTime() - t) + "ns"); }
}
}
@Aspect @Component @Order(2)
public class SecurityAspect {
@Around("@annotation(Monitored)")
public Object secure(ProceedingJoinPoint pjp) throws Throwable {
checkPermission(pjp);
return pjp.proceed();
}
}
@Aspect @Component @Order(3) // innermost (closest to real method)
public class TransactionAspect {
@Around("@annotation(Monitored)")
public Object transact(ProceedingJoinPoint pjp) throws Throwable {
beginTransaction();
try {
Object result = pjp.proceed(); // calls real method
commitTransaction();
return result;
} catch (Exception e) { rollbackTransaction(); throw e; }
}
}
// Call stack:
// MetricsAspect starts timing
// SecurityAspect checks permission
// TransactionAspect begins transaction
// real method executes
// TransactionAspect commits
// SecurityAspect returns
// MetricsAspect records timing
If two aspects have the same @Order value, their relative execution order is undefined. For production code where aspect ordering matters for correctness — security must run before audit logging, which must run before transaction management — always assign explicit @Order values.
| 🎯 Interview Insight: The most revealing follow-up question is: ‘What happens if the outermost aspect’s @Around advice does not call proceed()?’ The entire advice chain is short-circuited. The real method never executes, and none of the inner aspects run at all. This is both a power and a risk of @Around — intentional short-circuiting is how caching works, but accidental failure to call proceed() in error-handling code silently breaks the entire method invocation chain. |
Spring AOP is not just a theoretical concept — it is the mechanism that powers @Transactional, @Cacheable, @Async, @Secured, and dozens of other Spring features you use every day. Understanding how it works under the hood distinguishes engineers who can simply use these annotations from those who can debug, extend, and reason about Spring behavior in production.
Key takeaways from this article:
AOP addresses cross-cutting concerns by separating logging, security, transactions, and caching from business logic. The six core AOP terms form the vocabulary used in every Spring interview.
Spring AOP supports five advice types. @Around is the most powerful — it can modify arguments, change return values, suppress exceptions, and implement retry logic — but it requires explicitly calling proceed() to invoke the real method.
Spring AOP creates proxies at runtime — either JDK Dynamic Proxies or CGLIB subclass proxies. Spring Boot defaults to CGLIB. Both share the same fundamental limitation: private methods and self-invocations bypass the proxy and are not advised.
The self-invocation problem is the most common Spring AOP bug in production. Solutions include self-injection via @Autowired, ApplicationContext.getBean(), or refactoring the called method into a separate Spring bean.
@Order on aspect classes controls execution sequence when multiple aspects target the same method. The lowest order value is the outermost wrapper.