Spring AOP 内核:自动代理创建器的秘密
上一篇讲的是「切面怎么写」,这一篇只回答一个问题:你明明只加了个 @Aspect,容器凭什么就把你的 Bean 换成了代理?是谁换的、在第几步换的? 答案是一个名字很长的组件 AnnotationAwareAspectJAutoProxyCreator,整篇文章就是在拆它的四条动作:它是个后置处理器 → 它会找候选顾问 → 它逐个求值切点 → 命中就造代理并把原对象掉包。
先把这五个词按「完全没写过 Java」的口径钉死。BeanPostProcessor——容器在造每个 Bean 前后插进来的钩子,只有两个方法,但它的返回值能替换掉原本那个 Bean。Advisor(顾问)——一个「切点 + 通知」的配对对象,是 Spring 内部真正使用的单位;你的 @Aspect 会在启动时被拆成好几个 Advisor。切点(pointcut)——判断「这个方法的签名归不归我管」的那段表达式。目标对象(target)与代理(proxy)——前者是你 new 出来的原始实例,后者是套在它外面的那层壳,壳里存着 target。织入(weaving)——把通知接到调用路径上的动作,Spring AOP 靠「运行期造代理」来完成织入。
工厂流水线末端的质检台讲的是 BeanPostProcessor。流水线上每一台产品都要照常走完全部工序,但在装箱前会经过一个质检台,质检员有权把你的那台换成同型号加装了防盗芯片的一台。postProcessAfterInitialization 就是「装箱前最后一眼」这个权力:它返回什么,货架上(单例池)就摆什么。所以你在别处注入到的、getBean 取到的,全都是被换过的那台——原始那台并没有报废,它被塞进新机器肚子里当核心零件(target)。
给手机套壳讲的是「代理替换原对象」。手机(业务对象)已经组装完工、开机自检也跑完了,才在装盒那一刻被套上壳(代理)。壳不改变手机里的任何一颗芯片,只是所有按键都得从壳上按一遍(多一层转发)。三件事必须一起记:壳只能套在从流水线上下来的手机上(自己 new 的对象没人给它套壳)、壳太厚时套不上(final 类无法继承)、手机自己呼叫自己时绕过了壳(同类自调用)。

学完这一节,你要能回答三个问题:
- 代理是在 Bean 生命周期的哪一步生成的?为什么这一步决定了「
@PostConstruct里调自己的方法没有通知」? - 一个
@Aspect类是怎么被「拆」成 Advisor 的?候选 Advisor 为空意味着什么? - 看到
Bean is not yet fully initialized、Cannot proxy final class这类报错,该沿着上面这条时间轴的哪一站去找?
上一节我们把代理工厂拆到了 DefaultAopProxyFactory,但还有最后一个问题没回答:代理到底是谁、在什么时候创建的? 你已经写好了 @Aspect,也核对过切点,可容器凭什么知道要把某个 Bean 换成代理?
入口就是我们习惯性加上去的那一行注解。
@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Import(AspectJAutoProxyRegistrar.class) // 关键:导入一个「注册器」public @interface EnableAspectJAutoProxy { boolean proxyTargetClass() default false; boolean exposeProxy() default false;}@Import是 Spring 装配的「手动挡」:它让一个注解可以直接往容器里塞进一个类AspectJAutoProxyRegistrar实现的是ImportBeanDefinitionRegistrar,能在解析阶段拿到BeanDefinitionRegistry- 它其实只做一件事:把
AnnotationAwareAspectJAutoProxyCreator注册成一个 BeanDefinition
// AspectJAutoProxyRegistrar 的核心(简化)public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 1. 容器里还没有的话,就注册一个自动代理创建器 AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry); // 2. 把注解上的属性回填到创建器的 definition 上 AnnotationAttributes attrs = AnnotationConfigUtils.attributesFor( importingClassMetadata, EnableAspectJAutoProxy.class); if (attrs.getBoolean("proxyTargetClass")) { AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry); } if (attrs.getBoolean("exposeProxy")) { AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry); }}提示:Spring Boot 下这一步是自动完成的。AopAutoConfiguration 一旦发现类路径上有 AspectJ 支持,就会替你把上面那行 @Import 补上(对应 spring.aop.auto=true),所以 Boot 项目里你从来不用手写 @EnableAspectJAutoProxy。
到这里必须点破它的真实身份:AnnotationAwareAspectJAutoProxyCreator 不是一个主动扫描的组件,而是一个 BeanPostProcessor——它不创造 Bean,只在别的 Bean 的创建流程里「顺手插一杠子」。更准确地说,它还实现了 BeanFactoryAware,因为要拿到 BeanFactory,才能引用容器里其他 Bean。
把那个长名字一层层剥开,你会发现每一层能力都来自一个清晰的接口:
| 层级(接口 → 实现) | 这一层负责什么 |
|---|---|
BeanPostProcessor | 在 Bean 初始化前后拿到回调,所有「偷偷加工 Bean」的起点 |
InstantiationAwareBeanPostProcessor | 多出实例化前后、属性填充前的回调 |
SmartInstantiationAwareBeanPostProcessor | 多出「提前暴露引用」的能力,用于解决循环依赖 |
AbstractAutoProxyCreator | 真正实现「判断并创建代理」的基类 |
AspectJAwareAdvisorAutoProxyCreator | 按 @Order 对同一目标上的 Advisor 排序 |
AnnotationAwareAspectJAutoProxyCreator | 增加「解析 @AspectJ 注解」的能力,就是我们在用的这个 |
从下往上看,正好是一条能力递进的路线:从「能收到回调」到「能感知 Bean 的存在」,再到「能把 Bean 换成代理对象」。这也是 Spring 选择用 BeanPostProcessor 做切入点的原因——它只需要在标准生命周期流程里挂一个回调,无需改写容器的创建逻辑。
public interface BeanPostProcessor { // 初始化之前(@PostConstruct 之前) default Object postProcessBeforeInitialization(Object bean, String beanName) { return bean; } // 初始化之后(@PostConstruct 之后)——自动代理就发生在这里 default Object postProcessAfterInitialization(Object bean, String beanName) { return bean; }}要点:postProcessAfterInitialization 的返回值会替换容器里原本的 Bean。一旦这里返回代理,别人注入到的就是代理,原始对象被藏在 SingletonTargetSource 里——这正是「who wraps whom」的答案。
把所有环节串起来,主入口是 postProcessAfterInitialization,它随即调用 wrapIfNecessary:
// AbstractAutoProxyCreator(简化)public Object postProcessAfterInitialization(Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); // 循环依赖场景下已经提前暴露过代理,这里不要重复包装 if (this.earlyProxyReferences.remove(cacheKey) != bean) { return wrapIfNecessary(bean, beanName, cacheKey); } } return bean;}// wrapIfNecessary(简化)——判断「要不要代理」的核心protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) { if (StringUtils.hasLength(beanName) && this.targetSourcedBeans.contains(beanName)) { return bean; // 已经是内部目标对象,避免套娃 } if (Boolean.FALSE.equals(this.advisedBeans.get(cacheKey))) { return bean; // 之前判定过「不需要代理」,直接用缓存结果 } if (isInfrastructureClass(bean.getClass()) || shouldSkip(bean.getClass(), beanName)) { this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean; // 切面自己、以及基础设施类不代理 } // 找出所有能作用在当前 Bean 上的 Advisor 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; // 返回代理,替换原始实例 } this.advisedBeans.put(cacheKey, Boolean.FALSE); return bean;}
- 第一道闸是缓存:
advisedBeans记住「这个类要不要代理」,避免每个 Bean 都重复求值切点 - 第二道闸是基础设施类:代理创建器自己、
Advisor、Pointcut这些不参与匹配,否则会自我缠绕 - 第三道闸是
getAdvicesAndAdvisorsForBean:它是真正「找顾问」的地方,内部会调用findEligibleAdvisors,把候选 Advisor 过滤成命中当前类的那一份 - 命中后进入
createProxy,由ProxyFactory接手,产出代理并返回给容器
findEligibleAdvisors 的过滤逻辑并不神秘,无非「先拿全部候选,再逐个用切点去试」:
// findEligibleAdvisors(简化)protected List<Advisor> findEligibleAdvisors(Class<?> beanClass, String beanName) { // 候选:所有 AspectJ Advisor + 所有 Advisor 类型的 Bean(比如 @Transactional) List<Advisor> candidateAdvisors = findCandidateAdvisors(); // 逐个用 ClassFilter / MethodMatcher 判定,只留下能命中当前类的 List<Advisor> eligibleAdvisors = findAdvisorsThatCanApply(candidateAdvisors, beanClass, beanName); extendAdvisors(eligibleAdvisors); // 保证 exposeInvocation / 排序等扩展点 if (!eligibleAdvisors.isEmpty()) { eligibleAdvisors = sortAdvisors(eligibleAdvisors); // 按 @Order 排序 } return eligibleAdvisors;}findCandidateAdvisors()会去问BeanFactoryAdvisorRetrievalHelper:容器里所有Advisor类型的 Bean 都算候选findAdvisorsThatCanApply才是真正的「切点匹配」,用AopUtils.canApply逐个比对sortAdvisors决定最终通知链的顺序——注意这里已经排好了序,运行期只是照单执行
读到这里可以把两条线分开了:左边这一列发生在启动、每个 Bean 只有一次;右边这一列发生在每一次调用。第九节与第十一节的实验都在右边那一列上,而第六节那张「切面不生效自查清单」基本都在左边那一列里:

候选 Advisor 里的 AspectJ 部分,来自 AnnotationAwareAspectJAutoProxyCreator 委托的 ReflectiveAspectJAdvisorFactory。它逐一读取切面类里的方法,把带通知注解的挑出来:
// ReflectiveAspectJAdvisorFactory(简化)public List<Advisor> getAdvisors(MetadataSource metadataSource) { Class<?> aspectClass = metadataSource.getAspectClass(); List<Advisor> advisors = new ArrayList<>(); // 1. 先解析 @Pointcut 方法,建立「切点名 → Pointcut」的映射 for (Method method : aspectClass.getMethods()) { if (isPointcut(method)) { // 注册到 aspectJAdvisors 的切点缓存 } } // 2. 再处理通知方法:@Before / @Around / @After 等 for (Method method : getAdvisorMethods(aspectClass)) { Advisor advisor = getAdvisor(method, metadataSource, aspectInstanceFactory); if (advisor != null) { advisors.add(advisor); } } return advisors;}- 每个通知方法都会被打包成一个
InstantiationModelAwarePointcutAdvisorImpl - 这个 Advisor 内部持有两部分:
Pointcut(在哪儿拦)+Advice(拦下来做什么) - 通知方法的形参绑定(
JoinPoint、returning、throwing)也在这里被解析成Pointcut的一部分
说明:这也是为什么 returning = "result" 写错不会编译失败——它到运行期构建 Advisor 时才被解析,绑定不上就在此时报错。切面能启动、日志却报绑定失败,多半是这一幕。
命中之后,createProxy 交给 ProxyFactory。ProxyFactory 的继承链很短,核心能力都在父类 AdvisedSupport 上:
AdvisedSupport # 保存 target、advisors、interfaces、proxyTargetClass └─ ProxyFactory # 对外入口,暴露 getProxy() └─ AopProxyFactory(接口) └─ DefaultAopProxyFactory # 在 JDK / CGLIB 之间二选一| 要素 | JDK 动态代理 | CGLIB |
|---|---|---|
| 代理类形态 | 实现目标接口的 $Proxy0 | 继承目标类的子类 |
| 前置条件 | 目标必须有接口 | 目标类与方法不能是 final |
| 触发条件 | hasNoUserSuppliedProxyInterfaces 为假 | 强制 proxyTargetClass 或没有可用接口 |
| 产物 | JdkDynamicAopProxy | CglibAopProxy |
// DefaultAopProxyFactory.createAopProxy(简化)public AopProxy createAopProxy(AdvisedSupport config) { if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) { return new CglibAopProxy(config); // 强制 CGLIB,或目标没有可用接口 } return new JdkDynamicAopProxy(config); // 有接口且未强制 -> JDK}提醒:Spring Boot 2.x 起 proxy-target-class 默认为 true,所以 Boot 项目里你看到的基本都是 CGLIB 代理。这不是换了引擎,只是选型默认值变了。
「明明写了切面却毫无反应」的头号难题,按这张表逐一排查,八成能定位:
| 症状 | 根因 | 验证方法 |
|---|---|---|
| 被别的 Bean 调用生效,自己内部调用失效 | 内部自调用绕过代理 | 抽出到另一个 Bean;或开 exposeProxy 后 AopContext.currentProxy() |
| private / final / static 方法通知完全不触发 | 这些方法天生不可代理 | 看方法修饰符;或确认是否 CGLIB 的 final 类 |
| 启动无报错,一个方法都没被拦 | 切点表达式写错(包层级 / 签名) | 开 logging.level.org.springframework.aop=DEBUG 看匹配日志 |
目标对象是容器外 new 出来的 | 非容器 Bean 不会被后置处理器加工 | 让容器管理(@Component / @Bean) |
@Aspect 类从没被注册成 Bean | 少了 @Component,创建器找不到它 | 检查是否被组件扫描覆盖 |
| 需要自调用但拿不到自身代理 | 未开启 exposeProxy | @EnableAspectJAutoProxy(exposeProxy = true) |
// 自调用场景的正解之一:暴露代理后用 AopContext 取自身代理@EnableAspectJAutoProxy(exposeProxy = true) // 必须先开启public void outer() { // 别写 this.inner()——那是原始对象,不经过代理 ((OrderService) AopContext.currentProxy()).inner();}排查的第一手段永远是日志。打开 AOP 的 DEBUG,创建器会打印每个 Bean 命中了哪些 Advisor、用了哪种代理类型;切点没匹配上时,还会写出「does not match」的具体原因。这比盯着 execution 猜要快得多。
很多人把 Spring AOP 和 AspectJ 混为一谈,其实它们是两套机制:
| 维度 | Spring AOP | AspectJ |
|---|---|---|
| 织入时机 | 运行期,由 BeanPostProcessor 动态生成代理 | 编译期 / 类加载期,直接改写字节码 |
| 能力范围 | 只能拦 Spring 容器里的 Bean 方法 | 能拦字段访问、构造器、静态方法、容器外对象 |
| 性能 | 代理转发有少量运行开销 | 织入后相当于普通代码,无代理开销 |
| 依赖 | 无需额外编译步骤 | 需要 AspectJ 编译器或 javaagent |
| 典型用途 | 业务切面:日志、事务、权限 | 深度监控、性能剖析、非 Spring 对象增强 |
一句话区分:Spring AOP 是「代理级别的横切」,AspectJ 是「字节码级别的横切」。日常业务用 Spring AOP 足够;当你要拦私有方法、字段访问或容器外的对象时,才需要 AspectJ 织入。
@Transactional 本质也是一个 Advisor,和你的自定义切面在同一条通知链上竞争。如果顺序配错,会得到一种极其诡异的 bug——事务「不生效」。典型现场:日志切面 @Order(1) 排在最外层,事务切面在内层;日志切面的 @Around 在 try/catch 里吞掉了异常,于是异常根本没传到外层事务切面,事务自然不回滚。记住一条规律:@Order 数字越小越外层,越外层越先进入、最后离开;谁有资格「吞异常」,谁就不能排在事务外面。需要修复时,把事务切面的顺序调到最外层(更小的 @Order),并让所有环绕通知原样向上抛异常。
下面这个演示把代理的创建与通知链的组装可视化出来。切换代理类型,观察「谁包着谁」,再体会一下通知链的顺序:

代理造好之后,通知链是怎么「跑」的?advices 这一支把五种通知挂在一次调用上,你会看到它们其实是链表里的五个元素,而不是一句「先后顺序」:
动画先给全景——游标怎么从 0 一路加到链表末尾,再逐层退回:

真正把断点打进去看,才明白「先进后出」不是口诀而是递归的必然。下面是 Spring 内部那几行的单步台,右边同步刷新 currentInterceptorIndex——它每加一次,就有一层通知进入;它等于链表长度时,才换目标方法出场:
CglibAopProxy$DynamicAdvisedInterceptor.intercept(proxy, method, args, mp) { // ① List<Object> chain = this.advised.getInterceptors(method, target); // ② 按切点取通知 return new ReflectiveMethodInvocation(proxy, target, method, args, tp, chain).proceed();}// 下面就是 ReflectiveMethodInvocation.proceed() 本体if (this.currentInterceptorIndex == this.interceptorsAndFilters.size() - 1) { // ③ 走到尽头了吗 Object interceptor = this.interceptorsAndFilters.get(++this.currentInterceptorIndex); return interceptor.invoke(this); // ④ 通知自己决定何时 proceed}return invokeJoinpoint(); // ⑤ 链表走完,反射调用目标方法// 目标返回后逐层出栈:每个通知的「后半段」按相反顺序执行| 代理类 | OrderServiceImpl$$SpringCGLIB$$0 |
| method | create(OrderDTO) |
| advised | AdvisedSupport(3 个 Advisor) |
$SpringCGLIB$$0.createCglibAopProxy.intercept这一节把前面五条链路全部做成可以点的实验。核心是 autoptr(自动代理创建器)的四个分支,建议按顺序点,因为它们正好对应时间轴上的四站。
第一站:代理到底在第几步诞生。这一支会告诉你它挂在 Bean 生命周期的「初始化后置」,也就是第九篇讲的第⑥步;还会解释循环依赖时为什么改走 getEarlyBeanReference 这个入口:
第二站:判定「值不值得包」。findEligibleAdvisors → shouldWrap → advisedBeans 缓存这三件事都在这一支,它顺带解释了那个怪现象——「换了个 getBean 顺序,代理就没了」:
第三站:真正动手造代理的工厂。ProxyFactory 里的三件套、getProxy() 那一刻才生成类、以及 proxyTargetClass=true 会把选择强行掰向 CGLIB 并引发一连串连锁反应:
第四站最容易被忽略:代理背后站的到底是谁。默认是 SingletonTargetSource(永远是同一个 Bean),但换成池或原型就能做到「每次调用换一个目标」;同时 Object 的 toString/equals/hashCode 永远由代理自己回答,不进通知链——这解释了「两个业务相等的代理放进 HashSet 却算两个」:
光看创建器还不够,还要把它放回整条流水线和具体判定里看。第一个实验把 Bean 从实例化一路跑到「初始化后置」,你能看清代理是在哪一步插进来的;第二个实验验证「判定发生在方法级」这件事——同一段包路径写进 execution 与 within 含义不同;第三个实验则演示代理类型怎么选、以及自调用为什么会绕过这一切:

第五站回到调用期:多个切面在同一条链表里怎么排。inner 会逐帧列出「Around1 前 → Around2 前 → Before → 目标 → After → Around2 后 → Around1 后」,正好是上面那个单步台的多人版:
内核里的类名长得劝退,其实它们和职责是一一对应的死映射。这一局把这七个「栈帧常客」配一遍——配错了会告诉你它在时间轴的哪一站:
实验做到这里,可以换成命令行自己敲。这台控制台连着浏览器里的同一个容器,回显全部由内核算出来——按本篇时间轴的顺序敲,四站一站一站过:
左边拨三个开关——代理策略、目标类的形态、有没有接口——右边立刻给出「用哪种代理、注入该写什么类型、会撞哪条限制」。这三个开关覆盖了本篇第五节整张对比表:
DefaultAopProxyFactory: hasUserSuppliedInterface -> JdkDynamicAopProxygetClass() -> com.sun.proxy.$Proxy42注入 UserService 接口:OK
沙盘最后一格值得多说一句——你以为写了 proxyTargetClass=false 就一定是 JDK 代理,其实 DefaultAopProxyFactory 的第一个条件就是 hasNoUserSuppliedProxyInterfaces(config):没有可用接口时无条件走 CGLIB。另外 optimize 也在同一个 if 里,所以「我明明设了 false 怎么还是 CGLIB」的答案通常在这两行代码里,不在配置文件里。
以下片段都能整段粘进搜索框;「沿时间轴定位」那一列指回第十一节的时间轴图,告诉你该在哪一站找:
| 报错原文(片段) | 真实原因 | 30 秒自救 | 深挖看第几篇 |
|---|---|---|---|
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'com.example.service.UserService': Cannot proxy final class [com.example.service.UserService] | 目标是 final 类,CGLIB 靠继承生成子类,而 final 类不能被继承 | 去掉 final;确实要保留就把该能力改成装饰器手写,或换 AspectJ 编译期织入 | #12 动态代理 · 本篇第十二节沙盘 final-type |
NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.aop.Advisor' available(或 ...BeanFactoryTransactionAttributeSourceAdvisor) | 有人显式注入了一个 Advisor Bean,但那个 Bean 从没被注册(少了 @Bean / @Component,或不在扫描范围) | 确认 Advisor 是通过 @Bean 方法暴露的;事务相关则检查是否启用了 @EnableTransactionManagement | 本篇第三节 findCandidateAdvisors · #8 容器刷新 |
BeanCurrentlyInCreationException: Bean is not yet fully initialized(常伴 received null on self reference / is not fully initialized) | 循环依赖让代理提前曝光:BPP 还没跑完,别的 Bean 就来取早期引用,此时 getEarlyBeanReference 已经产出了代理 | 优先重构单向依赖;必须留就用 @Lazy 打断一条边;确认这两个 Bean 都是 singleton | #10 循环依赖 · 本篇第十一节 autoptr 第一站 |
IllegalStateException: Required to bind 2 arguments, but only bound 1 (JoinPointMatch was NOT bound) | 切点里有形参绑定(如 @annotation(x))但通知方法少写了对应形参,或形参名不一致 | 让注解里的变量名与形参名完全一致,并把 JoinPoint/ProceedingJoinPoint 放在第一位 | #13 @AspectJ 细节第五节 |
ClassCastException: class com.sun.proxy.$Proxy42 cannot be cast to class com.example.service.UserServiceImpl | 目标有接口且走了 JDK 代理,代理只是接口的实现,不是实现类的子类 | 注入改成接口类型;或 spring.aop.proxy-target-class=true(Boot 默认已 true) | 本篇第五节 · 第十二节沙盘 |
启动日志出现 ...is not eligible for getting processed by all BeanPostProcessors / 通知在自己的切面里不生效 | 该 Bean 太早创建(例如被某个 BPP 依赖),部分后置处理器来不及挂上;切面类本身也从不被代理 | 别把切面/基础设施类写成被 BPP 直接依赖的对象;加 @Lazy 或用 ObjectProvider 推迟 | #8 容器刷新 · #9 Bean 生命周期 |
AOP configuration problem: no matching method found for advice(或启动正常但一行通知都没打) | 候选 Advisor 为空:@Aspect 类没注册成 Bean,或组件扫描没覆盖到它 | getBeanNamesForType(Advisor.class) 数一数;补 @Component 并确认扫描路径 | 本篇第六节自查清单 |
Cannot convert class ... to required type ...; nested exception is java.lang.IllegalStateException: Failed to create dynamic proxy | 代理创建阶段就失败,通常是 target 与 interfaces 配置冲突(如目标类实现了泛型接口)或 CGLIB 版本冲突 | 看紧随其后的 Caused by;jar 冲突时用 mvn dependency:tree -Dincludes=org.aspectj:aspectjweaver,cglib | #2 Maven 依赖仲裁 · 本篇第五节 |
| 自调用时通知消失(无异常) | this.method() 落在原始对象上,压根没经过代理,因此也就没经过 wrapIfNecessary 的成果 | 抽到另一个 Bean;或 @EnableAspectJAutoProxy(exposeProxy = true) 后用 AopContext.currentProxy() | 本篇第六节 · #15 AOP 实战 |
Bean is not yet fully initialized 这一句最容易让人误以为是「初始化写错了」。它真正的意思是:这个 Bean 还在创建过程中就被别人提前拿走了。在纯 Spring AOP 里,一旦某个 Bean 参与循环依赖又被提前曝光,代理就会在 getEarlyBeanReference 里提前生成一次;如果这时你又给它加了「依赖自身代理」的逻辑,就会出现「同一个 Bean 两个不同实例」的诡异行为。判断依据:栈里同时出现 DefaultSingletonBeanRegistry.getSingleton 与 AbstractAutoProxyCreator.getEarlyBeanReference,就是这条路。
上面那段坑的「加重版」会直接把应用拦在启动阶段,而且报错里同时出现 AOP 和循环依赖两套词汇,读起来特别吓人。练一次——注意它的凶手行不在最上面:
OrderService 注入 UserService,UserService 又注入 OrderService,两边都没有 @Transactional;你给 OrderService 新加了一个日志切面,重启后应用起不来。
目标:绕开 @Aspect,亲手用一个 Advisor Bean 让代理创建器干活,并把它的判定过程打印出来。这一步做完,你会真正理解「Advisor = 切点 + 通知」和「候选为空就不代理」这两句话。
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) { // 命中:带 @Timed return "report-" + day; } public String ping() { // 不命中:没有注解 return "pong"; } } /** 关键:Advisor 必须以 @Bean 的形式进容器,才会被 findCandidateAdvisors 收进候选 */ @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"); } }; // 切点:方法上带 @Timed;通知:上面那个拦截器 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("代理类名:" + svc.getClass().getName()); System.out.println(svc.build("2026-01-01")); System.out.println(svc.ping()); System.out.println("容器里的 Advisor 数量:" + ctx.getBeanNamesForType(org.springframework.aop.Advisor.class).length); } }}启动前先加一行 DEBUG,才能看到创建器的判定过程:
logging.level.org.springframework.aop=DEBUG预期输出(省略时间戳与前缀):
Creating instance of bean 'manualAopConfig.ReportService'Creating shared instance of singleton bean 'timedAdvisor'Adding SLF4J-based advisor: ... InstantiationModelAwarePointcutAdvisorImpl ... # 若你另有 @Aspect 切面Created JDK/AOP Alliance ProxyFactoryBean-based proxy with characteristics [...] # 命中后创建代理代理类名:com.example.internals.ManualAopConfig$ReportService$$SpringCGLIB$$0[advisor] build took 0msreport-2026-01-01pong容器里的 Advisor 数量:1build有[advisor]前缀日志,ping没有:判定确实发生在方法级,但只要有一个方法命中,整个 Bean 就被代理(第十一节第二站)- 类名里出现
$$SpringCGLIB$$:你getBean拿到的是代理,原始ReportService实例只在它肚子里当 target - 把
@Bean从timedAdvisor()上删掉,Advisor 数量变成 0,两条业务输出前的 DEBUG 也不再提代理——这就是「候选 Advisor 为空 ⇒ 完全不建代理」
- 把切点换成
new ExecutionPointcut("com.example.internals...(..)")。你会观察到:ping()也开始打印[advisor],说明同一份 Advisor 只是换了「在哪拦」,通知代码一个字不用动。 - 把
ReportService改成final class。你会观察到:启动直接抛BeanCreationException: ... Cannot proxy final class,对照第十三节第一行。 - 把注入声明改成
ManualAopConfig.ReportService svc = ctx.getBean(...)并保持 CGLIB。你会观察到:仍然正常;再把spring.aop.proxy-target-class=false且给ReportService抽出一个接口,然后尝试按实现类接收——ClassCastException出现,沙盘auto|iface|by-impl那一格的现场版。 - 在
ReportService里加一个方法self(),内部调this.build("x")。你会观察到:[advisor]不打第二条,因为this是原始对象。这是本篇唯一一条不在「创建期」而在「调用期」的限制。
做一个代理体检工具:启动完成后自动报告「哪些 Bean 被代理了、用了哪种代理、被哪些 Advisor 命中」。
- 实现一个
ApplicationListener<ApplicationReadyEvent>(或用SmartInitializingSingleton),遍历beanFactory.getBeanDefinitionNames() - 对每个 Bean 调
beanFactory.getBean(name),用AopUtils.isAopProxy(bean)/isCglibProxy/isJdkDynamicProxy分类计数 - 命中代理的,再用
((Advised) bean).getAdvisors()打印 Advisor 简名列表;捕获异常时记录 Bean 名继续跑,不要因单个 Bean 失败中断报告 - 输出一张汇总表:总数 / CGLIB 数 / JDK 数 / 命中最多的前 5 个 Advisor;并在
spring.aop.report=false时整体跳过
验收清单:① 表格数字与 logging.level.org.springframework.aop=DEBUG 的人工计数一致;② 故意把一个 @Aspect 的 @Component 注释掉,报告里对应 Advisor 消失且业务输出不变(证明候选为空 ⇒ 不代理,也不报错);③ 你能用这张报告向同事解释「为什么我的 Service 类名后面多了一串 $$SpringCGLIB$$」;④ 报告本身不触发任何额外 Bean 的提前创建(观察 DEBUG 里没有出现新的 Creating shared instance)。
从 @EnableAspectJAutoProxy 到「代理进单例池」,按顺序说出五个关键方法名,并指出哪一步做了切点求值。
postProcessAfterInitialization 的返回值有什么特殊语义?为什么说它是「who wraps whom」的答案?
wrapIfNecessary 的三道闸分别挡住什么?如果没有 advisedBeans 缓存会有什么后果?
DefaultAopProxyFactory 在什么条件下会无视 proxyTargetClass=false 直接用 CGLIB?
循环依赖下代理为什么要提前生成?哪条入口方法负责这件事,它如何保证代理只有一个?
创建器是质检台,装箱前一眼看;返回代理即掉包,原对象藏在壳里面。
把这一节的链路记成一条线——@EnableAspectJAutoProxy 通过 @Import 注册了 AnnotationAwareAspectJAutoProxyCreator;它作为 BeanPostProcessor,在 Bean 初始化之后的 postProcessAfterInitialization 里调用 wrapIfNecessary;wrapIfNecessary 用 findEligibleAdvisors 找到能命中当前类的 Advisor,命中后交给 ProxyFactory,由 DefaultAopProxyFactory 在 JdkDynamicAopProxy 与 CglibAopProxy 之间二选一,最终用代理替换原始实例。切面不生效时,先开 AOP 的 DEBUG 日志拿事实,再对照「自调用、private/final、切点写错、非容器对象、未注册成 Bean、未开 exposeProxy」六条清单排查;而 @Transactional 的失效,多半是切面顺序让异常没能传到事务边界。