Inside Spring AOP: The Auto-Proxy Creator
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.
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.
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).
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).

After this section you should be able to answer three questions:
- 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
@Aspectclass get split into advisors, and what does an empty candidate list imply? - When you see
Cannot proxy final classorBean is not yet fully initialized, which station on the timeline above should you inspect?
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?
The entry point is the very annotation we habitually add.
@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;}@Importis Spring's "manual gearbox": it lets an annotation push a class directly into the containerAspectJAutoProxyRegistrarimplementsImportBeanDefinitionRegistrar, so it can grab theBeanDefinitionRegistryduring parsing- It does exactly one thing: register
AnnotationAwareAspectJAutoProxyCreatoras a BeanDefinition
// 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); }}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.
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.
Peel that long name apart and every layer of capability comes from a clear interface:
| Layer (interface → implementation) | What this layer does |
|---|---|
BeanPostProcessor | Receives callbacks before/after initialization — the origin of all "quietly processed" beans |
InstantiationAwareBeanPostProcessor | Adds instantiation and property-population callbacks |
SmartInstantiationAwareBeanPostProcessor | Adds "early reference exposure" used to solve circular dependencies |
AbstractAutoProxyCreator | The base that actually implements "decide and create a proxy" |
AspectJAwareAdvisorAutoProxyCreator | Orders advisors on the same target by @Order |
AnnotationAwareAspectJAutoProxyCreator | Adds @AspectJ parsing — the one we use |
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.
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; }}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".
The main entry is postProcessAfterInitialization, which immediately calls wrapIfNecessary:
// 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;}// 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;}
- The first gate is caching:
advisedBeansremembers "does this class need a proxy", so pointcut evaluation is not repeated per bean - The second gate is infrastructure classes: the creator itself,
AdvisorandPointcutare excluded to avoid self-entanglement - The third gate is
getAdvicesAndAdvisorsForBean: the real "find advisors" step, which internally callsfindEligibleAdvisorsto filter candidates down to those matching this class - On a hit,
createProxyhands off toProxyFactory, which builds the proxy and returns it to the container
findEligibleAdvisors is no mystery — it simply "takes all candidates, then tries each against the pointcut":
// 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;}findCandidateAdvisors()asksBeanFactoryAdvisorRetrievalHelper: everyAdvisor-typed bean is a candidatefindAdvisorsThatCanApplyis the actual pointcut match, usingAopUtils.canApplyitem by itemsortAdvisorsdecides the final advice-chain order — note it is already sorted here; runtime just executes the list
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:

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:
// 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;}- Each advice method is packaged into an
InstantiationModelAwarePointcutAdvisorImpl - That advisor holds two parts: a
Pointcut(where to intercept) and anAdvice(what to do when intercepted) - Parameter bindings (
JoinPoint,returning,throwing) are also parsed into thePointcutright 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.
On a hit, createProxy delegates to ProxyFactory. Its inheritance chain is short, and the real capability lives in the parent AdvisedSupport:
AdvisedSupport # holds target, advisors, interfaces, proxyTargetClass └─ ProxyFactory # the entry point exposing getProxy() └─ AopProxyFactory (interface) └─ DefaultAopProxyFactory # picks between JDK and CGLIB| Element | JDK dynamic proxy | CGLIB |
|---|---|---|
| Proxy shape | $Proxy0 implementing the target interface | A subclass extending the target class |
| Prerequisite | The target must have an interface | Target class and methods must not be final |
| Trigger | hasNoUserSuppliedProxyInterfaces is false | Forced proxyTargetClass, or no usable interface |
| Product | JdkDynamicAopProxy | CglibAopProxy |
// 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}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.
"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:
| Symptom | Root cause | How to verify |
|---|---|---|
| Works when called by another bean, dies on internal calls | Self-invocation bypasses the proxy | Extract to another bean; or enable exposeProxy and use AopContext.currentProxy() |
| Advice never fires on private / final / static methods | Such methods cannot be proxied | Check modifiers; confirm a final class under CGLIB |
| Starts cleanly but nothing is intercepted | Wrong pointcut (package level / signature) | Turn on logging.level.org.springframework.aop=DEBUG |
The target was newed outside the container | Non-container beans are never post-processed | Hand it to the container (@Component / @Bean) |
The @Aspect class was never registered as a bean | Missing @Component; the creator cannot find it | Check it is covered by component scanning |
| Need a self-proxy but cannot obtain it | exposeProxy not enabled | @EnableAspectJAutoProxy(exposeProxy = true) |
// 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();}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.
Many people conflate Spring AOP with AspectJ, but they are two mechanisms:
| Dimension | Spring AOP | AspectJ |
|---|---|---|
| Weaving time | Runtime, generating proxies via BeanPostProcessor | Compile / load time, rewriting bytecode directly |
| Capability | Only bean methods inside the Spring container | Field access, constructors, static methods, non-container objects |
| Performance | Small runtime overhead for proxy forwarding | After weaving it is ordinary code; no proxy overhead |
| Dependency | No extra build step | Requires the AspectJ compiler or javaagent |
| Typical use | Business aspects: logging, transactions, authorization | Deep monitoring, profiling, enhancing non-Spring objects |
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.
@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.
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:

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:
The animation gives the overview first — how the cursor climbs from 0 to the end of the list and then unwinds:

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:
CglibAopProxy$DynamicAdvisedInterceptor.intercept(proxy, method, args, mp) { // (1) List<Object> chain = this.advised.getInterceptors(method, target); // (2) pick advice per method return new ReflectiveMethodInvocation(proxy, target, method, args, tp, chain).proceed();}// ---- this is ReflectiveMethodInvocation.proceed() itself ----if (this.currentInterceptorIndex == this.interceptorsAndFilters.size() - 1) { // (3) reached the end? Object interceptor = this.interceptorsAndFilters.get(++this.currentInterceptorIndex); return interceptor.invoke(this); // (4) advice decides when to proceed}return invokeJoinpoint(); // (5) list exhausted, call the target reflectively// once the target returns the stack unwinds: each advice runs its second half in reverse // (6)| proxy class | OrderServiceImpl$$SpringCGLIB$$0 |
| method | create(OrderDTO) |
| advised | AdvisedSupport (3 advisors) |
$SpringCGLIB$$0.createCglibAopProxy.interceptThis 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.
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:
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":
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:
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":
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:

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:
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:
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:
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:
DefaultAopProxyFactory: user-supplied interface -> JdkDynamicAopProxygetClass() -> com.sun.proxy.$Proxy42Injecting the UserService interface: OK
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.
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:
| Error text (fragment) | Real cause | 30-second rescue | Read 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 extended | Remove 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 @EnableTransactionManagement | Section 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 time | Refactor 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 differently | Make 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.UserServiceImpl | The target has an interface so a JDK proxy was built; a proxy implements interfaces, it does not extend the impl class | Inject 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 aspect | That bean is created too early (e.g. depended on by a BPP), so some post-processors miss it; aspect classes themselves are never proxied either | Do 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 it | Count with getBeanNamesForType(Advisor.class); add @Component and verify the scan path | Section 6 checklist |
Cannot convert class ... to required type ...; nested exception is java.lang.IllegalStateException: Failed to create dynamic proxy | Proxy creation itself failed — typically target/interface configuration conflict (e.g. generic interfaces) or a CGLIB version clash | Read 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 wrapIfNecessary | Extract to another bean; or @EnableAspectJAutoProxy(exposeProxy = true) then AopContext.currentProxy() | Section 6 · #15 AOP practice |
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.
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:
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.
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.
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); }}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); } }}Add this line before starting, otherwise the creator's decisions stay invisible:
logging.level.org.springframework.aop=DEBUGExpected output (timestamps and prefixes elided):
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: 1buildgets an[advisor]line,pingdoes not: matching truly happens per method, yet one hit proxies the whole bean (Section 11, station two)- The class name carries
$$SpringCGLIB$$:getBeanhands you the proxy, and the rawReportServiceinstance survives only as its target - Delete
@BeanfromtimedAdvisor()and the advisor count becomes 0, and the DEBUG lines stop mentioning a proxy — that is "empty candidate advisors ⇒ no proxy at all"
- 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. - Make
ReportServiceafinal class. You will observe: startup throwsBeanCreationException: ... Cannot proxy final class, matching row one of Section 13. - Keep CGLIB but fetch the bean by its concrete type. You will observe: still fine. Now set
spring.aop.proxy-target-class=falseand extract an interface forReportService, then receive it as the implementation class —ClassCastExceptionappears, the live version of the sandbox cellauto|iface|by-impl. - Add a method
self()insideReportServicethat callsthis.build("x"). You will observe: no second[advisor]line, becausethisis the raw object. This is the one limit in this article that lives at call time rather than creation time.
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.
- Implement an
ApplicationListener<ApplicationReadyEvent>(or useSmartInitializingSingleton) and walkbeanFactory.getBeanDefinitionNames() - For each bean call
beanFactory.getBean(name)and classify withAopUtils.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
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).
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.
what is special about the return value of postProcessAfterInitialization, and why is it the answer to "who wraps whom"?
what do the three gates of wrapIfNecessary each block, and what would happen without the advisedBeans cache?
under what condition does DefaultAopProxyFactory ignore proxyTargetClass=false and use CGLIB anyway?
why must the proxy be created early under a circular dependency, which entry method does it, and how is exactly-one-proxy guaranteed?
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.**
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.