Inside Spring AOP: The Auto-Proxy Creator

bee2026-10-0848 min read0 views
Why does @Aspect just work? The answer hides in AnnotationAwareAspectJAutoProxyCreator — how it finds aspects, filters advisors, and picks a proxy factory, plus a complete self-check for dead aspects.
1 / 117
Section
0. The 30-second version
2 / 117

The previous article covered how to write an aspect; this one answers a single question: you only added an @Aspect, so why did the container swap your bean for a proxy? Who swapped it, and at which step? The answer is one long-named component, AnnotationAwareAspectJAutoProxyCreator, and the whole article unpacks its four moves: it is a post-processor → it collects candidate advisors → it evaluates each pointcut → on a hit it builds a proxy and replaces the original object.

3 / 117

First, pin down the five terms for someone who has never written Java. A BeanPostProcessor is a hook the container inserts around every bean it creates — just two methods, yet its return value can replace that bean. An Advisor is a paired object of "pointcut + advice", the unit Spring actually works with internally; your @Aspect gets split into several advisors at startup. A pointcut is the expression deciding "does this method's signature belong to me". Target versus proxy: the target is the raw instance you constructed, the proxy is the shell wrapped over it, holding the target inside. Weaving is the act of attaching advice onto the call path — Spring AOP weaves by building a proxy at runtime.

4 / 117
类比|Analogy

the quality desk at the end of a factory line explains BeanPostProcessor. Every product walks through all its normal operations, but before boxing it passes a quality desk whose inspector has the authority to swap your unit for the same model fitted with an anti-theft chip. postProcessAfterInitialization is that "last look before boxing": whatever it returns is what ends up on the shelf (the singleton pool). So everything you inject elsewhere or fetch with getBean is the swapped unit — and the original was not scrapped, it sits inside the new one as its core part (the target).

5 / 117
类比|Analogy

putting a case on a phone explains "the proxy replaces the original". The phone (business object) is fully assembled and has passed its self-test, and only at the moment of boxing does the case (the proxy) slide on. The case changes no chip inside, yet every button now has to be pressed through it (one extra hop). Remember three consequences together: a case only fits phones that came off the line (objects you new yourself get none), some phones cannot take a case (a final class cannot be inherited), and a phone calling itself bypasses the case (self-invocation).

6 / 117
Diagram
Figure · Chapter map: timeline of one proxy creation
Figure · Chapter map: timeline of one proxy creation
7 / 117

After this section you should be able to answer three questions:

8 / 117
  • At which step of the bean lifecycle is the proxy created, and why does that step explain "no advice fires when I call my own methods from @PostConstruct"?
  • How does one @Aspect class get split into advisors, and what does an empty candidate list imply?
  • When you see Cannot proxy final class or Bean is not yet fully initialized, which station on the timeline above should you inspect?
9 / 117
Section
1. The import chain behind one annotation
10 / 117

Last section we took the proxy factory down to DefaultAopProxyFactory, but the last question remains unanswered: who creates the proxy, and when? You have written your @Aspect and checked the pointcut, yet how does the container know to swap a bean for a proxy at all?

11 / 117

The entry point is the very annotation we habitually add.

12 / 117
Code
Codejava
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Import(AspectJAutoProxyRegistrar.class)   // the key: imports a "registrar"public @interface EnableAspectJAutoProxy {    boolean proxyTargetClass() default false;    boolean exposeProxy() default false;}
Notes
  • @Import is Spring's "manual gearbox": it lets an annotation push a class directly into the container
  • AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar, so it can grab the BeanDefinitionRegistry during parsing
  • It does exactly one thing: register AnnotationAwareAspectJAutoProxyCreator as a BeanDefinition
13 / 117
Code
Codejava
// Core of AspectJAutoProxyRegistrar (simplified)public void registerBeanDefinitions(        AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {    // 1. If absent, register an auto-proxy creator    AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);    // 2. Feed the annotation attributes back onto the creator definition    AnnotationAttributes attrs = AnnotationConfigUtils.attributesFor(            importingClassMetadata, EnableAspectJAutoProxy.class);    if (attrs.getBoolean("proxyTargetClass")) {        AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);    }    if (attrs.getBoolean("exposeProxy")) {        AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);    }}
Notes

Tip: under Spring Boot this happens automatically. As soon as AopAutoConfiguration detects AspectJ support on the classpath it adds that @Import for you (property spring.aop.auto=true), so you never write @EnableAspectJAutoProxy in a Boot project.

14 / 117

Now we must state its real identity: AnnotationAwareAspectJAutoProxyCreator is not an active scanner but a BeanPostProcessor — it does not create beans; it "sticks a hand in" while other beans are being created. More precisely, it also implements BeanFactoryAware, because it needs the BeanFactory to reach other beans in the container.

15 / 117
Section
2. The inheritance chain: who it really is
16 / 117

Peel that long name apart and every layer of capability comes from a clear interface:

17 / 117
Table
Layer (interface → implementation)What this layer does
BeanPostProcessorReceives callbacks before/after initialization — the origin of all "quietly processed" beans
InstantiationAwareBeanPostProcessorAdds instantiation and property-population callbacks
SmartInstantiationAwareBeanPostProcessorAdds "early reference exposure" used to solve circular dependencies
AbstractAutoProxyCreatorThe base that actually implements "decide and create a proxy"
AspectJAwareAdvisorAutoProxyCreatorOrders advisors on the same target by @Order
AnnotationAwareAspectJAutoProxyCreatorAdds @AspectJ parsing — the one we use
18 / 117

Read bottom-up and it is a path of growing capability: from "receiving callbacks" to "sensing the bean", to "replacing the bean with a proxy". That is exactly why Spring uses a BeanPostProcessor as the hook — it only needs to attach one callback to the standard lifecycle, without rewriting container creation.

19 / 117
Code
Codejava
public interface BeanPostProcessor {    // Before initialization (before @PostConstruct)    default Object postProcessBeforeInitialization(Object bean, String beanName) {        return bean;    }    // After initialization (after @PostConstruct) — where auto-proxying happens    default Object postProcessAfterInitialization(Object bean, String beanName) {        return bean;    }}
Notes

Key point: the return value of postProcessAfterInitialization replaces the original bean in the container. Once it returns a proxy, everyone injects the proxy and the original object is hidden inside a SingletonTargetSource — that is the answer to "who wraps whom".

20 / 117
Section
3. When the proxy is created: a wrap after initialization
21 / 117

The main entry is postProcessAfterInitialization, which immediately calls wrapIfNecessary:

22 / 117
java
// AbstractAutoProxyCreator (simplified)public Object postProcessAfterInitialization(Object bean, String beanName) {    if (bean != null) {        Object cacheKey = getCacheKey(bean.getClass(), beanName);        // In circular-dependency scenarios a proxy was already exposed; do not wrap twice        if (this.earlyProxyReferences.remove(cacheKey) != bean) {            return wrapIfNecessary(bean, beanName, cacheKey);        }    }    return bean;}
23 / 117
java
// wrapIfNecessary (simplified) — the heart of "should I proxy?"protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {    if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) {        return bean;   // already an internal target, avoid nesting    }    if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) {        return bean;   // judged before as "no proxy needed"; reuse the cache    }    if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) {        this.advisedBeans.put(cacheKey, Boolean.FALSE);        return bean;   // aspects themselves and infrastructure classes are not proxied    }    // Find every advisor that can apply to this bean    Object[] specificInterceptors =            getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);    if (specificInterceptors != DO_NOT_PROXY) {        this.advisedBeans.put(cacheKey, Boolean.TRUE);        Object proxy = createProxy(                bean.getClass(), beanName, specificInterceptors,                new SingletonTargetSource(bean));        this.proxyTypes.put(cacheKey, proxy.getClass());        return proxy;   // return the proxy, replacing the original instance    }    this.advisedBeans.put(cacheKey, Boolean.FALSE);    return bean;}
24 / 117
Diagram
Figure 1 · The auto-proxy creator pipeline
Figure 1 · The auto-proxy creator pipeline
25 / 117
  • The first gate is caching: advisedBeans remembers "does this class need a proxy", so pointcut evaluation is not repeated per bean
  • The second gate is infrastructure classes: the creator itself, Advisor and Pointcut are excluded to avoid self-entanglement
  • The third gate is getAdvicesAndAdvisorsForBean: the real "find advisors" step, which internally calls findEligibleAdvisors to filter candidates down to those matching this class
  • On a hit, createProxy hands off to ProxyFactory, which builds the proxy and returns it to the container
26 / 117

findEligibleAdvisors is no mystery — it simply "takes all candidates, then tries each against the pointcut":

27 / 117
Code
Codejava
// findEligibleAdvisors (simplified)protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) {    // Candidates: all AspectJ advisors + all Advisor-typed beans (e.g. @Transactional)    List<Advisor> candidateAdvisors = findCandidateAdvisors();    // Test each with ClassFilter / MethodMatcher; keep only those matching the class    List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName);    extendAdvisors(eligibleAdvisors);   // exposeInvocation and other extension points    if (!eligibleAdvisors.isEmpty()) {        eligibleAdvisors = sortAdvisors(eligibleAdvisors);   // order by @Order    }    return eligibleAdvisors;}
Notes
  • findCandidateAdvisors() asks BeanFactoryAdvisorRetrievalHelper: every Advisor-typed bean is a candidate
  • findAdvisorsThatCanApply is the actual pointcut match, using AopUtils.canApply item by item
  • sortAdvisors decides the final advice-chain order — note it is already sorted here; runtime just executes the list
28 / 117

At this point the two timelines deserve to be separated: the left column below happens at startup, exactly once per bean; the right column happens on every single call. Sections 9 and 11 are all about the right column, while the dead-aspect checklist in Section 6 lives almost entirely in the left one:

29 / 117
Diagram
Figure · Two timelines: creation time and call time
Figure · Two timelines: creation time and call time
30 / 117
Section
4. How an Advisor is born: splitting @Aspect into advisors
31 / 117

The AspectJ part of the candidate advisors comes from ReflectiveAspectJAdvisorFactory, which the creator delegates to. It walks the aspect's methods and picks out the ones with advice annotations:

32 / 117
Code
Codejava
// ReflectiveAspectJAdvisorFactory (simplified)public List<Advisor> getAdvisors(MetadataSource metadataSource) {    Class<?> aspectClass = metadataSource.getAspectClass();    List<Advisor> advisors = new ArrayList<>();    // 1. Parse @Pointcut methods to build a "name -> Pointcut" map    for (Method method : aspectClass.getMethods()) {        if (isPointcut(method)) {            // registered into the aspectJAdvisors pointcut cache        }    }    // 2. Then handle advice methods: @Before / @Around / @After, etc.    for (Method method : getAdvisorMethods(aspectClass)) {        Advisor advisor = getAdvisor(method, metadataSource, aspectInstanceFactory);        if (advisor != null) {            advisors.add(advisor);        }    }    return advisors;}
Notes
  • Each advice method is packaged into an InstantiationModelAwarePointcutAdvisorImpl
  • That advisor holds two parts: a Pointcut (where to intercept) and an Advice (what to do when intercepted)
  • Parameter bindings (JoinPoint, returning, throwing) are also parsed into the Pointcut right here

Note: this is why a wrong returning = "result" does not fail compilation — it is parsed at runtime while building the advisor, and a bad binding errors only then. An aspect that starts fine but whose logs report a binding failure is almost always this scene.

33 / 117
Section
5. Choosing the proxy factory: a binary decision
34 / 117

On a hit, createProxy delegates to ProxyFactory. Its inheritance chain is short, and the real capability lives in the parent AdvisedSupport:

35 / 117
text
AdvisedSupport        # holds target, advisors, interfaces, proxyTargetClass  └─ ProxyFactory     # the entry point exposing getProxy()        └─ AopProxyFactory (interface)              └─ DefaultAopProxyFactory   # picks between JDK and CGLIB
36 / 117
Table
ElementJDK dynamic proxyCGLIB
Proxy shape$Proxy0 implementing the target interfaceA subclass extending the target class
PrerequisiteThe target must have an interfaceTarget class and methods must not be final
TriggerhasNoUserSuppliedProxyInterfaces is falseForced proxyTargetClass, or no usable interface
ProductJdkDynamicAopProxyCglibAopProxy
37 / 117
Code
Codejava
// DefaultAopProxyFactory.createAopProxy (simplified)public AopProxy createAopProxy(AdvisedSupport config) {    if (config.isOptimize() || config.isProxyTargetClass()            || hasNoUserSuppliedProxyInterfaces(config)) {        return new CglibAopProxy(config);   // forced CGLIB, or no usable interface    }    return new JdkDynamicAopProxy(config);  // interface present and not forced -> JDK}
Notes

Reminder: since Spring Boot 2.x, proxy-target-class defaults to true, so in a Boot project what you see is almost always a CGLIB proxy. The engine did not change; only the default choice did.

38 / 117
Section
6. A self-check list for dead aspects
39 / 117

"My aspect is written but does absolutely nothing" is the number one problem. Walk this table and you will locate it eighty percent of the time:

40 / 117
Table
SymptomRoot causeHow to verify
Works when called by another bean, dies on internal callsSelf-invocation bypasses the proxyExtract to another bean; or enable exposeProxy and use AopContext.currentProxy()
Advice never fires on private / final / static methodsSuch methods cannot be proxiedCheck modifiers; confirm a final class under CGLIB
Starts cleanly but nothing is interceptedWrong pointcut (package level / signature)Turn on logging.level.org.springframework.aop=DEBUG
The target was newed outside the containerNon-container beans are never post-processedHand it to the container (@Component / @Bean)
The @Aspect class was never registered as a beanMissing @Component; the creator cannot find itCheck it is covered by component scanning
Need a self-proxy but cannot obtain itexposeProxy not enabled@EnableAspectJAutoProxy(exposeProxy = true)
41 / 117
java
// One fix for self-invocation: expose the proxy and fetch it via AopContext@EnableAspectJAutoProxy(exposeProxy = true)   // must be enabled firstpublic void outer() {    // Do NOT write this.inner() — that is the raw object, bypassing the proxy    ((OrderService) AopContext.currentProxy()).inner();}
42 / 117

The first tool is always logging. Turn on AOP DEBUG and the creator prints which advisors each bean matched and which proxy type it used; when a pointcut misses, it writes the exact "does not match" reason. That is far faster than staring at an execution expression and guessing.

43 / 117
Section
7. How it differs from AspectJ compile-time weaving
44 / 117

Many people conflate Spring AOP with AspectJ, but they are two mechanisms:

45 / 117
Table
DimensionSpring AOPAspectJ
Weaving timeRuntime, generating proxies via BeanPostProcessorCompile / load time, rewriting bytecode directly
CapabilityOnly bean methods inside the Spring containerField access, constructors, static methods, non-container objects
PerformanceSmall runtime overhead for proxy forwardingAfter weaving it is ordinary code; no proxy overhead
DependencyNo extra build stepRequires the AspectJ compiler or javaagent
Typical useBusiness aspects: logging, transactions, authorizationDeep monitoring, profiling, enhancing non-Spring objects
46 / 117

One-line distinction: Spring AOP is "proxy-level cross-cutting", AspectJ is "bytecode-level cross-cutting". Everyday business work is fine with Spring AOP; you only need AspectJ weaving to intercept private methods, field access, or non-container objects.

47 / 117
Section
8. Trap: tangled order between @Transactional and a custom aspect
48 / 117
Trap

@Transactional is itself an advisor, competing with your custom aspect on the same advice chain. A wrong order produces a truly bizarre bug — the transaction "does not work". Typical scene: the logging aspect at @Order(1) sits outermost and the transaction advisor is inner; the logging aspect's @Around swallows the exception in a try/catch, so the exception never reaches the outer transaction boundary and the transaction does not roll back. Remember the rule: smaller @Order is more outer, and more outer enters first and leaves last; whoever is allowed to "swallow exceptions" must not sit outside the transaction. To fix it, move the transaction advisor to the outermost ring (a smaller @Order) and let every around advice rethrow exceptions unchanged.

49 / 117
Section
9. Try it: proxy type and the advice chain
50 / 117

The demo below visualizes both proxy creation and advice-chain assembly. Switch the proxy type, watch "who wraps whom", and feel the order of the chain:

51 / 117
Kernel lab
TeaVMInside proxy creation and the advice chainidle
Watch the proxy type and the advice order, and feel out who wraps whom
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
52 / 117
Animation
Animation · From @Aspect to a live proxy
Animation · From @Aspect to a live proxy
53 / 117

Once the proxy exists, how does the chain actually run? This mode hangs all five advice types on one call, and you will see they are really five elements of a list, not a sentence about ordering:

54 / 117
Kernel lab
TeaVMThe advice chain is that interceptor listidle
normal walks the list to its end; switch to order to see two aspects interleaved inside it
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
55 / 117

The animation gives the overview first — how the cursor climbs from 0 to the end of the list and then unwinds:

56 / 117
Animation
Animation · The proceed() recursion, step by step
Animation · The proceed() recursion, step by step
57 / 117

Setting a real breakpoint is what makes "first in, last out" click: it is not a rule, it is the inevitable shape of recursion. Below are the relevant internal lines as a stepping bench, with currentInterceptorIndex refreshed on the right — every increment lets one advice layer in; only when it reaches the list length does the target method appear:

58 / 117
Stepper
StepperStepping the proceed() recursion: how the cursor walks to the target1 / 7
Press next and watch currentInterceptorIndex: still 0 at step 3, and the target is only invoked reflectively at step 6
Code under debug
1CglibAopProxy$DynamicAdvisedInterceptor.intercept(proxy, method, args, mp) { // (1)
2 List<Object> chain = this.advised.getInterceptors(method, target); // (2) pick advice per method
3 return new ReflectiveMethodInvocation(proxy, target, method, args, tp, chain).proceed();
4}
5// ---- this is ReflectiveMethodInvocation.proceed() itself ----
6if (this.currentInterceptorIndex == this.interceptorsAndFilters.size() - 1) { // (3) reached the end?
7 Object interceptor = this.interceptorsAndFilters.get(++this.currentInterceptorIndex);
8 return interceptor.invoke(this); // (4) advice decides when to proceed
9}
10return invokeJoinpoint(); // (5) list exhausted, call the target reflectively
11// once the target returns the stack unwinds: each advice runs its second half in reverse // (6)
Variables now
proxy classOrderServiceImpl$$SpringCGLIB$$0
methodcreate(OrderDTO)
advisedAdvisedSupport (3 advisors)
Call stack
1$SpringCGLIB$$0.create
2CglibAopProxy.intercept
1Every call enters through this line. Notice intercept does no enhancement itself — it only asks 'which chain belongs to this call'. The chain is resolved per method at call time, not welded into the class at startup.
59 / 117
Section
10. Decision: what to check first when an aspect is dead
60 / 117
Decision
Decisionyou wrote a new `@Aspect` class adding logging to the Service methods of its package. It starts cleanly, but at runtime **not a single line is logged**. What is the right first move?
61 / 117
Section
11. Hands on: watch a proxy get built
62 / 117

This section turns all five chains above into things you can click. The core is the four branches of autoptr (the auto-proxy creator); take them in order, because they map onto the four stations of the timeline.

63 / 117

Station one: at which step the proxy is born. This branch shows it hangs off "after initialization", i.e. step six of article nine, and explains why a cycle takes a different entry point, getEarlyBeanReference:

64 / 117
Kernel lab
TeaVMInterception in post-processing: the moment a proxy is bornidle
Focus on step three (lifecycle station six) and the two rules derived in step seven
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
65 / 117

Station two: the verdict "is it worth wrapping". findEligibleAdvisors → shouldWrap → the advisedBeans cache all live here, and it also explains the oddity "I changed the getBean order and the proxy disappeared":

66 / 117
Kernel lab
TeaVMCandidate check: one verdict per bean, for lifeidle
Note step four: a single matching method proxies the whole bean
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
67 / 117

Station three: the factory that actually builds it — the three parts inside ProxyFactory, how the class only materialises at getProxy(), and how proxyTargetClass=true forces CGLIB with a chain of knock-on effects:

68 / 117
Kernel lab
TeaVMProxyFactory: how the tooling room stamps a proxyidle
The ClassCastException in step five pastes straight into the table in Section 13
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
69 / 117

Station four is the one people skip: who stands behind the proxy. The default SingletonTargetSource always returns the same bean, but a pool or prototype gives a fresh target per call; meanwhile Object's toString/equals/hashCode are always answered by the proxy itself and never advised — which is exactly why "two business-equal proxies count as two entries in a HashSet":

70 / 117
Kernel lab
TeaVMTargetSource: one desk number, rotating operatorsidle
Steps five and six explain why a proxy makes a poor map key
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
71 / 117

The creator alone is not enough — put it back into the whole assembly line and into the concrete match. The first demo runs a bean from instantiation all the way to after-initialization so you see where the proxy gets inserted; the second proves that matching happens per method, since the same package text means different things in execution versus within; the third shows how the proxy type is chosen and why self-invocation bypasses all of it:

72 / 117
Kernel lab
TeaVMPut the proxy back into a bean's whole lifeidle
Run the singleton round once; station six, after-init, is precisely Section 4 of this article
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
73 / 117
Kernel lab
TeaVMMatching happens at method levelidle
Compare with Section 4: this is what findAdvisorsThatCanApply does method by method
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
74 / 117
Kernel lab
TeaVMJDK or CGLIB, plus the self-invocation trapidle
First compare the two proxy shapes, then switch to self-invocation to see the one limit that is not a creation-time gate
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
75 / 117
Animation
Animation · The swap: from finished bean to shelf
Animation · The swap: from finished bean to shelf
76 / 117

Station five goes back to call time: where several aspects sit inside one interceptor list. "Nested timeline" enumerates it frame by frame (Around1-before → Around2-before → Before → target → After → Around2-after → Around1-after), which is exactly the multiplayer version of the stepping bench above:

77 / 117
Kernel lab
TeaVMWhere several advisors sit in the same listidle
inner for the in-and-out walk, reverse for why a missing @Order makes the order unpredictable, txaspect for why transactions must not sit inside a custom aspect
Scenario
Click “Run demo” to execute the AOT-compiled Java kernel right in your browser, step by step.
78 / 117

The kernel class names are the intimidating part, yet each maps onto exactly one job. Pair these seven regulars of stack traces and DEBUG logs — a wrong match tells you which station of the timeline you mixed up:

79 / 117
Match
MatchKernel class ↔ the one job it doesMatched 0/7 · Missed 0
Left: names you will meet in stacks and DEBUG output. Right: their actual responsibility
Pick a card on the left first
80 / 117

From here, type the commands yourself. This console is attached to the same in-browser container and every response comes from the kernel — walk the timeline station by station:

81 / 117
Console
82 / 117
Section
12. Sandbox: flip three switches and watch the proxy shape change
83 / 117

On the left, set the proxy strategy, the shape of the target class, and whether an interface exists; on the right you immediately get "which proxy, which type may you inject, and which limit will you hit". These three switches cover the entire comparison table in Section 5:

84 / 117
Sandbox
SandboxProxy choice sandbox: three targets x two strategies
Result
DefaultAopProxyFactory: user-supplied interface -> JdkDynamicAopProxy
getClass() -> com.sun.proxy.$Proxy42
Injecting the UserService interface: OK
Plain Spring's default (proxyTargetClass=false) plus an interface — the zero-change combination.
85 / 117
Note

the last cell deserves an extra word. You may believe proxyTargetClass=false guarantees a JDK proxy, but the very first condition in DefaultAopProxyFactory is hasNoUserSuppliedProxyInterfaces(config) — with no usable interface it goes to CGLIB unconditionally. And optimize sits in the same if, so the answer to "I set false, why is it still CGLIB" usually lives in those two lines of source rather than in your properties file.

86 / 117
Section
13. Common errors quick reference
87 / 117

Each fragment below can be pasted verbatim into a search box; the last column points back to the timeline in Section 11 so you know which station to inspect:

88 / 117
Table
Error text (fragment)Real cause30-second rescueRead deeper in
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.example.service.UserService': Cannot proxy final class [com.example.service.UserService]The target is a final class; CGLIB builds a subclass and a final class cannot be extendedRemove final; if it must stay, hand-write the decorator or move to AspectJ compile-time weaving#12 Dynamic proxy · this article, Section 12 cell final-type
NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.aop.Advisor' available (or ...BeanFactoryTransactionAttributeSourceAdvisor)Someone injects an Advisor bean explicitly, but that bean was never registered (no @Bean / @Component, or outside component scanning)Confirm the advisor is exposed through a @Bean method; for transaction-related ones check @EnableTransactionManagementSection 3 findCandidateAdvisors · #8 Container refresh
BeanCurrentlyInCreationException: Bean is not yet fully initialized (often beside is not fully initialized / received null on self reference)A cycle exposes the proxy early: before the post-processors finish, another bean asks for the early reference, so getEarlyBeanReference produces the proxy ahead of timeRefactor to one-way dependencies first; if it must stay, break one edge with @Lazy; confirm both beans are singletons#10 Circular dependency · this article, Section 11 autoptr station one
IllegalStateException: Required to bind 2 arguments, but only bound 1 (JoinPointMatch was NOT bound)The pointcut binds a parameter (e.g. @annotation(x)) but the advice method omits that parameter, or names it differentlyMake the annotation variable name identical to the parameter name, and keep JoinPoint/ProceedingJoinPoint first#13 @AspectJ details, Section 5
ClassCastException: class com.sun.proxy.$Proxy42 cannot be cast to class com.example.service.UserServiceImplThe target has an interface so a JDK proxy was built; a proxy implements interfaces, it does not extend the impl classInject by interface type; or set spring.aop.proxy-target-class=true (already the Boot default)Section 5 · Section 12 sandbox
Startup log contains ...is not eligible for getting processed by all BeanPostProcessors, or advice never fires inside your own aspectThat bean is created too early (e.g. depended on by a BPP), so some post-processors miss it; aspect classes themselves are never proxied eitherDo not let BPPs directly depend on business or aspect beans; add @Lazy or use ObjectProvider to defer#8 Container refresh · #9 Bean lifecycle
AOP configuration problem: no matching method found for advice (or clean startup with not one advice line)The candidate advisor list is empty: the @Aspect class is not a bean, or scanning does not reach itCount with getBeanNamesForType(Advisor.class); add @Component and verify the scan pathSection 6 checklist
Cannot convert class ... to required type ...; nested exception is java.lang.IllegalStateException: Failed to create dynamic proxyProxy creation itself failed — typically target/interface configuration conflict (e.g. generic interfaces) or a CGLIB version clashRead the following Caused by; for jar conflicts run mvn dependency:tree -Dincludes=org.aspectj:aspectjweaver,cglib#2 Maven mediation · Section 5
Advice disappears on internal self-calls (no exception at all)this.method() lands on the raw object, so the call never passes the proxy nor the fruit of wrapIfNecessaryExtract to another bean; or @EnableAspectJAutoProxy(exposeProxy = true) then AopContext.currentProxy()Section 6 · #15 AOP practice
89 / 117
Trap

Bean is not yet fully initialized is the easiest message to misread as "my init code is broken". It really means this bean was handed out while still under construction. In plain Spring AOP, once a bean participates in a cycle and is exposed early, its proxy is generated ahead of schedule inside getEarlyBeanReference; if you additionally rely on "injecting my own proxy", you can end up with two distinct instances of one bean. The tell: DefaultSingletonBeanRegistry.getSingleton and AbstractAutoProxyCreator.getEarlyBeanReference appear together in the stack.

90 / 117

The aggravated version of that trap blocks startup outright, and its message mixes AOP vocabulary with circular-dependency vocabulary, which makes it look far worse than it is. Practise — and note the guilty frame is not the top one:

91 / 117
Triage
Error triageBeanCurrentlyInCreationException: injected in its raw version but eventually wrapped
A dependency cycle meets AOP proxying: only one of the two versions may live

OrderService injects UserService, UserService injects OrderService, and neither carries @Transactional. You add a logging aspect hitting OrderService, restart, and the application refuses to come up.

APPLICATION FAILED TO START
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'orderService': Bean with name 'orderService' has been injected into other beans [userService] in its raw version as part of a circular reference, but has eventually been wrapped. This means that said other beans do not use the final version of the bean. This is often the result of over-eager type matching - consider using getBeanNamesForType with the allowEagerInit flag turned off, for example.
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:643)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:456)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.getEarlyBeanReference(AbstractAutoProxyCreator.java:321)
at org.springframework.aop.framework.autoproxy.AbstractAutoProxyCreator.wrapIfNecessary(AbstractAutoProxyCreator.java:352)
Click the frame you blame — guessing is allowed
No pressure: guess the exception first, then which line actually made the call.
92 / 117
Section
14. Quick quizzes
93 / 117
Quiz
Check yourselfYou declare `public final class OrderService`, the pointcut is verified correct, yet startup fails outright. Which error and which root cause?
Pick one — you get feedback right away
94 / 117
Quiz
Check yourselfA startup log shows `Bean is not yet fully initialized`, and the stack also contains `AbstractAutoProxyCreator.getEarlyBeanReference`. What happened?
Pick one — you get feedback right away
95 / 117
Section
15. Exercises
96 / 117
Section
Tier one · Follow along
97 / 117

Goal: bypass @Aspect entirely and drive the auto-proxy creator with your own Advisor bean, while printing its decision process. Do this once and "an advisor = pointcut + advice" and "empty candidates means no proxy" stop being slogans.

98 / 117
java
package com.example.internals;import org.aopalliance.intercept.MethodInterceptor;import org.springframework.aop.Advisor;import org.springframework.aop.support.DefaultPointcutAdvisor;import org.springframework.aop.support.annotation.AnnotationMatchingPointcut;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import org.springframework.stereotype.Service;import java.lang.annotation.*;@Configurationpublic class ManualAopConfig {    @Target(ElementType.METHOD)    @Retention(RetentionPolicy.RUNTIME)    public @interface Timed {}    @Service    public static class ReportService {        @Timed        public String build(String day) {   // matches: carries @Timed            return "report-" + day;        }        public String ping() {              // does not match: no annotation            return "pong";        }    }    /** Key: the advisor must enter the container via @Bean, otherwise findCandidateAdvisors never sees it */    @Bean    public Advisor timedAdvisor() {        MethodInterceptor interceptor = invocation -> {            long start = System.currentTimeMillis();            try {                return invocation.proceed();            } finally {                System.out.println("[advisor] " + invocation.getMethod().getName()                        + " took " + (System.currentTimeMillis() - start) + "ms");            }        };        // pointcut: methods carrying @Timed; advice: the interceptor above        return new DefaultPointcutAdvisor(                AnnotationMatchingPointcut.forMethodAnnotation(Timed.class), interceptor);    }}
99 / 117
java
package com.example.internals;import org.springframework.boot.SpringApplication;import org.springframework.boot.autoconfigure.SpringBootApplication;import org.springframework.context.ConfigurableApplicationContext;@SpringBootApplicationpublic class InternalsApplication {    public static void main(String[] args) {        try (ConfigurableApplicationContext ctx =                     SpringApplication.run(InternalsApplication.class, args)) {            ReportService svc = ctx.getBean(ReportService.class);            System.out.println("proxy class: " + svc.getClass().getName());            System.out.println(svc.build("2026-01-01"));            System.out.println(svc.ping());            System.out.println("advisors in container: "                    + ctx.getBeanNamesForType(org.springframework.aop.Advisor.class).length);        }    }}
100 / 117

Add this line before starting, otherwise the creator's decisions stay invisible:

101 / 117
properties
logging.level.org.springframework.aop=DEBUG
102 / 117

Expected output (timestamps and prefixes elided):

103 / 117
Code
Codetext
Creating instance of bean 'manualAopConfig.ReportService'Creating shared instance of singleton bean 'timedAdvisor'Adding SLF4J-based advisor: ... InstantiationModelAwarePointcutAdvisorImpl ...   # only if you also have an @AspectCreated JDK/AOP Alliance ProxyFactoryBean-based proxy with characteristics [...] # proxy built after a hitproxy class: com.example.internals.ManualAopConfig$ReportService$$SpringCGLIB$$0[advisor] build took 0msreport-2026-01-01pongadvisors in container: 1
Notes
  • build gets an [advisor] line, ping does not: matching truly happens per method, yet one hit proxies the whole bean (Section 11, station two)
  • The class name carries $$SpringCGLIB$$: getBean hands you the proxy, and the raw ReportService instance survives only as its target
  • Delete @Bean from timedAdvisor() and the advisor count becomes 0, and the DEBUG lines stop mentioning a proxy — that is "empty candidate advisors ⇒ no proxy at all"
104 / 117
Section
Tier two · Variations
105 / 117
  1. Replace the pointcut with new ExecutionPointcut("com.example.internals...(..)"). You will observe: ping() now prints [advisor] too — the same advisor, only a different "where to grab", with the advice body untouched.
  2. Make ReportService a final class. You will observe: startup throws BeanCreationException: ... Cannot proxy final class, matching row one of Section 13.
  3. Keep CGLIB but fetch the bean by its concrete type. You will observe: still fine. Now set spring.aop.proxy-target-class=false and extract an interface for ReportService, then receive it as the implementation class — ClassCastException appears, the live version of the sandbox cell auto|iface|by-impl.
  4. Add a method self() inside ReportService that calls this.build("x"). You will observe: no second [advisor] line, because this is the raw object. This is the one limit in this article that lives at call time rather than creation time.
106 / 117
Section
Tier three · Build one
107 / 117

Build a proxy health report: once the app is ready, automatically state which beans are proxied, with which proxy kind, and which advisors hit them.

108 / 117
  • Implement an ApplicationListener<ApplicationReadyEvent> (or use SmartInitializingSingleton) and walk beanFactory.getBeanDefinitionNames()
  • For each bean call beanFactory.getBean(name) and classify with AopUtils.isAopProxy(bean) / isCglibProxy / isJdkDynamicProxy
  • For proxied ones, print the simple names from ((Advised) bean).getAdvisors(); catch exceptions per bean, record the name and continue so one failure never aborts the report
  • Emit a summary table: total / CGLIB count / JDK count / top 5 advisors by hits, and skip everything when spring.aop.report=false
109 / 117

Acceptance checklist: ① the numbers agree with a manual count from logging.level.org.springframework.aop=DEBUG; ② comment out one @Aspect's @Component and its advisor vanishes from the report while business output is unchanged — proof that empty candidates mean neither a proxy nor an error; ③ you can use the report to explain to a colleague why their service class name grew a $$SpringCGLIB$$ tail; ④ the report itself triggers no extra eager bean creation (no new Creating shared instance lines appear because of it).

110 / 117
Section
16. Self-check
111 / 117
Self-check

from @EnableAspectJAutoProxy to "the proxy enters the singleton pool", name the five key methods in order, and say at which one the pointcut is evaluated.

112 / 117
Self-check

what is special about the return value of postProcessAfterInitialization, and why is it the answer to "who wraps whom"?

113 / 117
Self-check

what do the three gates of wrapIfNecessary each block, and what would happen without the advisedBeans cache?

114 / 117
Self-check

under what condition does DefaultAopProxyFactory ignore proxyTargetClass=false and use CGLIB anyway?

115 / 117
Self-check

why must the proxy be created early under a circular dependency, which entry method does it, and how is exactly-one-proxy guaranteed?

116 / 117

Mantra: **the creator is the quality desk, one look before boxing; return a proxy and the swap is done, the original hides inside the shell.**

117 / 117
Summary

memorize this as one line — @EnableAspectJAutoProxy imports AspectJAutoProxyRegistrar via @Import, which registers AnnotationAwareAspectJAutoProxyCreator; as a BeanPostProcessor it calls wrapIfNecessary from postProcessAfterInitialization after a bean initializes; wrapIfNecessary uses findEligibleAdvisors to find advisors matching the class, and on a hit hands off to ProxyFactory, where DefaultAopProxyFactory chooses between JdkDynamicAopProxy and CglibAopProxy, finally replacing the original instance with the proxy. When an aspect is dead, first turn on AOP DEBUG logging to get the facts, then walk the six-item list — self-invocation, private/final, a wrong pointcut, a non-container object, an unregistered @Aspect, and exposeProxy not enabled. And a failing @Transactional is usually aspect ordering that keeps the exception from reaching the transaction boundary.