Spring AOP Deep Dive: Interview Quiz & Answers – What's New in Spring Boot 3.x Default Proxy?

This article presents a 17‑question interview self‑test on Spring AOP, providing detailed answers that cover JDK dynamic proxy vs CGLIB, the internal flow of Proxy.newProxyInstance, InvocationHandler dispatch, MethodProxy differences, the interceptor chain, @Around requirements, AopContext ThreadLocal, self‑invocation pitfalls, final/static/private method limitations, @Order priority, Spring AOP vs AspectJ capabilities, performance of @Around vs @Before, multi‑factor aspect ordering, notification order changes between Spring 5 and 6, default proxy strategy differences in Spring Boot 2.x and 3.x, and migration considerations.

CodeSmart Hoops
CodeSmart Hoops
CodeSmart Hoops
Spring AOP Deep Dive: Interview Quiz & Answers – What's New in Spring Boot 3.x Default Proxy?

Interview Self‑Test: Spring AOP Deep Dive

Small A prepared 17 high‑frequency interview questions about Spring AOP and provides reference answers. Readers are encouraged to read the original blog first, then attempt the quiz and compare with the solutions.

1. JDK Dynamic Proxy vs CGLIB

Reference: Chapter 6.1‑6.3 (Two proxy modes)

Proxy mode : JDK creates a proxy that implements the target interface; CGLIB creates a subclass of the target class.

Interface requirement : JDK requires an interface; CGLIB does not.

Final class handling : JDK can proxy a final class (by delegating to the target); CGLIB cannot subclass a final class.

Final method handling : JDK can proxy final methods (no interception); CGLIB cannot override final methods.

Private method handling : Neither can intercept private methods.

Performance : Both are fast; JDK uses LambdaMetafactory on JDK 8+, CGLIB uses FastClass.

Initialization overhead : JDK low; CGLIB medium (bytecode generation).

Analogy : JDK proxy is like a “stand‑in actor” that must follow a contract (interface); CGLIB is like a “clone” that directly copies the class body.

Spring Boot 2.x defaults to CGLIB for convenience, not performance.

2. Internal Flow of Proxy.newProxyInstance

Reference: Chapter 6.2 (JDK dynamic proxy source analysis)

The execution can be divided into four steps:

Validate interface array : Iterate the interfaces array and ensure each element is an interface via intf.isInterface(). Throw IllegalArgumentException if not.

Find or generate proxy class (core) :

Check if the number of interfaces exceeds the JVM limit (65535).

Look up the proxy class in proxyClassCache to avoid duplicate generation.

If absent, invoke ProxyGenerator.generateProxyClass to generate bytecode.

Obtain proxy class constructor : Reflectively get the constructor that takes an InvocationHandler argument.

Create proxy instance : Call cons.newInstance(new Object[]{h}), storing the handler in the proxy’s Proxy.h field.

Generated proxy class example:

public final class $Proxy0 extends Proxy implements UserService {
    private Method m3; // saveUser
    private Method m4; // getUser
    public $Proxy0(InvocationHandler h) { super(h); }
    @Override
    public final String saveUser(String name) {
        return (String) super.h.invoke(this, m3, new Object[]{name});
    }
}

3. How InvocationHandler.invoke Dispatches to the Target Method

Reference: Chapter 6.2 (JDK dynamic proxy source analysis)

Handle special methods : equals() and hashCode() are processed directly without passing through the interceptor chain.

Build interceptor chain : Call

advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass)

to collect applicable advices, wrap them as MethodInterceptor objects, and sort by priority.

Create MethodInvocation : Wrap proxy, target, method, arguments, and interceptor chain into a ReflectiveMethodInvocation instance.

Execute chain : Invoke invocation.proceed() which iterates the chain; each interceptor calls proceed() until the target method is finally invoked.

Full call chain illustration:

caller → proxy.saveUser()
   → InvocationHandler.invoke(proxy, method, args)
       → build interceptor chain [Interceptor1, Interceptor2, ...]
           → ReflectiveMethodInvocation.proceed()
               → Interceptor1.invoke()
                   → proceed() → Interceptor2.invoke()
                       → proceed() → invokeJoinpoint() → target method
                           → result returns up the chain

4. Difference Between MethodProxy.invoke and invoke

Reference: Chapter 6.3 (CGLIB proxy source analysis)
invoke(target, args)

: Calls the method on the target object via reflection; may trigger interceptors again (possible recursion). invokeSuper(obj, args): Uses FastClass to directly call the original superclass method, bypassing interceptors; faster.

Analogy: invoke is like entering through the main gate where the guard checks you; invokeSuper is a private back‑door that goes straight to the destination.

Spring internally uses invokeSuper in CglibMethodInvocation.invokeJoinpoint() to ensure the target method runs without a second interception.

5. Interceptor Chain (Chain of Responsibility) Execution Flow

Reference: Chapter 6.4 (Interceptor chain execution principle)

The interceptor chain is an implementation of the Chain of Responsibility pattern.

public class ReflectiveMethodInvocation implements MethodInvocation {
    // current interceptor index, initially -1
    private int currentInterceptorIndex = -1;
    // list of interceptors and dynamic method matchers
    protected List<Object> interceptorsAndDynamicMethodMatchers;
}

The proceed() method increments the index, invokes the current MethodInterceptor, and expects the interceptor to call invocation.proceed() to continue. If an @Around advice does not call pjp.proceed(), the chain stops and later advices and the target method are never executed.

Analogy: The chain is a production line; proceed() moves the product to the next station. If a station holds the product, downstream stations receive nothing.

6. Why @Around Must Call pjp.proceed()

References: Chapter 6.4 and Chapter 4.6
@Around

advice is wrapped as a MethodInterceptor. Its invoke() must explicitly call proceed() to hand control to the next interceptor or the target method. Omitting the call aborts the chain.

@Around("execution(* com.example.service.*.*(..))")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
    System.out.println("before");
    // If omitted, target method and later advices never run
    return pjp.proceed(); // must be called
}

Consequences of not calling proceed():

Target method is not executed.

Subsequent advices are skipped.

Custom return value can be provided (e.g., cache hit).

Typical use‑case where omission is intentional: cacheable advice that returns a cached value without invoking the target.

7. Workflow of AnnotationAwareAspectJAutoProxyCreator

Reference: Chapter 6.5 (Auto‑proxy creator implementation)

This class is the “behind‑the‑scenes” component that creates all Spring AOP proxies.

Registration phase : @EnableAspectJAutoProxy imports AspectJAutoProxyRegistrar, which registers AnnotationAwareAspectJAutoProxyCreator as a BeanPostProcessor.

Proxy creation phase (in postProcessAfterInitialization()):

Skip infrastructure beans or beans marked to be skipped.

Retrieve applicable Advisor objects for the bean via AnnotationAwareAspectJAdvisorFactory.

If advisors exist, call createProxy(...) with a SingletonTargetSource to generate the proxy.

If no advisors, return the original bean.

Advisor parsing and ordering : The factory scans @Aspect classes, skips @Pointcut methods, creates an Advisor for each advice method, and sorts them by @Order.

Timing diagram (image):

AOP timing diagram
AOP timing diagram

8. AopContext ThreadLocal Mechanism

Reference: Chapter 6.6 (exposeProxy implementation)
AopContext

stores the current proxy in a ThreadLocal so that self‑invocation can obtain the proxy.

public abstract class AopContext {
    private static final ThreadLocal<Object> currentProxy = new ThreadLocal<>();
    static void setCurrentProxy(Object proxy) { currentProxy.set(proxy); }
    public static Object currentProxy() throws IllegalStateException {
        Object proxy = currentProxy.get();
        if (proxy == null) {
            throw new IllegalStateException("Cannot find current proxy: Set 'exposeProxy' property on Advised to 'true'");
        }
        return proxy;
    }
}

In DynamicAdvisedInterceptor.intercept(), the proxy is set at method entry and cleared after the chain finishes. The feature is disabled by default because of the overhead of ThreadLocal set/get and because most scenarios do not need it.

Analogy: exposing the proxy is like hanging a name‑plate at the office door; by default the plate is not displayed to save cost.

9. Self‑Invocation Failure and Solutions

Reference: Chapter 8 (common trap)

Self‑invocation (e.g., this.saveUser()) bypasses the proxy, so annotations such as @Transactional are ineffective.

@Service
public class UserService {
    public void createUser() {
        // this.saveUser() does not go through proxy → @Transactional ineffective!
        saveUser();
    }
    @Transactional
    public void saveUser() { userRepository.save(); }
}

Three solutions:

Inject self‑proxy (recommended) :

@Service
public class UserService {
    @Autowired @Lazy private UserService self; // inject proxy
    public void createUser() { self.saveUser(); }
    @Transactional
    public void saveUser() { userRepository.save(); }
}

Use AopContext.currentProxy() (requires exposeProxy=true):

public void createUser() {
    ((UserService) AopContext.currentProxy()).saveUser();
}

Split into separate beans (cleanest):

@Service
public class UserService {
    @Autowired private UserSaveService saveService;
    public void createUser() { saveService.saveUser(); }
}
@Service
public class UserSaveService {
    @Transactional
    public void saveUser() { userRepository.save(); }
}

10. Why final , static , and private Methods Cannot Be Proxied

References: Chapter 6.3, Chapter 7 (AOP use cases)

final methods : CGLIB creates a subclass and must override methods to insert interception logic; final methods cannot be overridden.

static methods : Belong to the class, not an instance; proxies intercept instance method calls only.

private methods : Not visible to the proxy; JDK dynamic proxy cannot implement them, and CGLIB does not intercept calls that stay inside the target class.

Summary table (converted to list):

Final method – JDK proxy: proxyable; CGLIB: not proxyable (cannot override).

Static method – both proxies: not proxyable (no instance).

Private method – both proxies: not proxyable (invisible).

11. How @Order Controls Aspect Priority

Reference: Chapter 9

Aspect classes annotated with @Order (lower value = higher priority) are sorted before the interceptor chain is built.

@Aspect @Order(1) @Component public class AuthAspect { ... }
@Aspect @Order(2) @Component public class LogAspect { ... }

Underlying process: AnnotationAwareAspectJAdvisorFactory reads @Order values while parsing @Aspect classes. AnnotationAwareOrderComparator.sort(advisors) sorts all advisors.

Sorted advisors are added to the interceptor chain.

Execution follows the “onion” model: higher‑priority advices run before the target and unwind after it.

Visualization:

Order(1)-before → Order(2)-before → target → Order(2)-after → Order(1)-after

12. Capability Boundary Between Spring AOP and AspectJ

Reference: Chapter 6.7

Spring AOP : Runtime proxy (JDK/CGLIB); method‑level only; cannot intercept constructors, field access, static/final methods; self‑invocation bypasses proxy.

AspectJ : Compile‑time, post‑compile, or load‑time weaving; can intercept methods, constructors, field get/set, static and final methods; works even with self‑invocation.

Analogy: Spring AOP is a “gatekeeper” that only checks people entering the building; AspectJ is an “invisible remodel” that embeds checks throughout the structure.

13. Why @Around Is Usually Faster Than @Before

Reference: Chapter 10 (misconception)

Performance reason: @Around creates a single advice instance (one MethodInterceptor). @Before + @AfterReturning + @AfterThrowing each create separate advice instances, lengthening the interceptor chain.

Longer chain means more proceed() calls, adding overhead.

Empirical data shows a 10‑20% speed advantage for a single @Around in high‑frequency scenarios.

14. Multi‑Factor Aspect Ordering Rules

Reference: Chapter 10 (ordering misconception)

Ordering sources (priority from high to low): @Order annotation value.

Implementation of Ordered interface (method getOrder()). @jakarta.annotation.Priority annotation.

Default: Ordered.LOWEST_PRECEDENCE (Integer.MAX_VALUE).

Example:

@Aspect @Order(1) @Component public class AuthAspect { ... }
@Aspect @Component public class LogAspect implements Ordered { public int getOrder() { return 2; } }
@Aspect @Priority(3) @Component public class CacheAspect { ... }
// Execution order: AuthAspect → LogAspect → CacheAspect

Note: @Order always overrides @Priority if both are present.

15. Notification Execution Order Difference Between Spring 5 and Spring 6

Reference: Chapters 4.1 and 14.1

Spring 5 (Boot 2.x) order:

@Around‑before → @Before → target → @Around‑after → @AfterReturning → @After

Spring 6 (Boot 3.x) order:

@Around‑before → @Before → target → @AfterReturning → @After → @Around‑after

Key change: @Around‑after is moved to the very end, aligning with AspectJ semantics and giving @Around a true “wrap‑around” behavior.

Migration tip: Do not rely on @Around‑after executing before @AfterReturning; adjust logic accordingly.

16. Default Proxy Strategy Difference Between Spring Boot 2.x and 3.x

Reference: Chapters 6.1 and 14.2

Spring Boot 2.x (Spring 5) : Smart choice – uses JDK dynamic proxy when an interface is present, otherwise falls back to CGLIB.

Spring Boot 3.x (Spring 6) : Forces CGLIB regardless of interface presence ( proxy-target-class=true by default).

Configuration differences:

Boot 2.x default: spring.aop.proxy-target-class=false.

Boot 3.x default: spring.aop.proxy-target-class=true.

Reasons for the change:

Performance: Modern JVMs make CGLIB as fast or faster than JDK proxies.

Simplify behavior: One consistent proxy strategy reduces debugging complexity.

Feature completeness: CGLIB can proxy any class, eliminating the need to define interfaces solely for AOP.

Spring 6 itself encourages CGLIB.

Impact example:

// Service without interface – both Boot 2.x and 3.x use CGLIB
@Service public class UserService { public void saveUser() { ... } }
// Service with interface – Boot 2.x uses JDK proxy, Boot 3.x forces CGLIB
@Service public class OrderService implements OrderInterface { public void createOrder() { ... } }

To enable JDK proxy in Boot 3.x, set:

# application.yml
spring:
  aop:
    proxy-target-class: false

or use @EnableAspectJAutoProxy(proxyTargetClass = false) in a configuration class.

17. AOP‑Related Migration Checklist from Spring Boot 2.x to 3.x

Reference: Chapter 14.3 (migration guide)

Proxy class name checks : CGLIB class name changed from $$EnhancerBySpringCGLIB$$ to $$SpringCGLIB$$. Update test assertions accordingly.

Notification order assertions : Adjust expectations – @Around‑after now appears after @After and @AfterReturning.

Explicit JDK proxy configuration : If your code relies on JDK proxies, set spring.aop.proxy-target-class=false or use @EnableAspectJAutoProxy(proxyTargetClass = false).

Do not depend on @Around‑after execution timing : Refactor logic to avoid ordering assumptions; use explicit @Order if needed.

Areas that remain compatible (no changes needed): basic AOP usage, interceptor chain mechanics, self‑invocation limitations, @Order control, AopContext, and the five standard advice types.

Version‑selection advice:

New projects: adopt Spring Boot 3.x + Spring 6.

Existing projects: evaluate impact of notification order and proxy‑strategy changes before upgrading.

When JDK proxy is required: configure explicitly in Boot 3.x.

For maximum stability: stay on Spring Boot 2.7.x (the last 2.x LTS).

Reference Materials

Spring AOP deep‑analysis articles (WeChat links) covering source code and unit tests.

Spring official documentation: AspectJ support.

Spring source classes: ProxyFactory, AbstractAutoProxyCreator, ReflectiveMethodInvocation, CglibAopProxy, AnnotationAwareAspectJAutoProxyCreator.

JDK classes: java.lang.reflect.Proxy, InvocationHandler.

CGLIB classes: net.sf.cglib.proxy.Enhancer, MethodProxy.

Books: "Spring in Action" Chapter 5, "Spring Source Deep Dive" Chapter 8.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

JavaProxyinterviewSpring AOPCGLIBSpring Boot 3
CodeSmart Hoops
Written by

CodeSmart Hoops

A working programmer who loves coding and basketball. By day I debug code; by night I dissect tactics. I write articles to document my journey, focusing on Java, AI, Python and other programming topics, with occasional posts about basketball, English, and books. Hope it's helpful—thanks for following and support.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.