
Spring的IOC和AOP是我这两年面试和被面试时几乎每次都要聊的话题。很多人能说出“控制反转就是把对象的创建交给容器”“AOP是通过动态代理实现的”但继续追问下去——容器到底是怎么把Bean创建好的Bean创建完又经历了哪些步骤动态代理为什么分JDK代理和CGLIB代理Spring又依据什么选型——能讲透的人就没那么多了。这篇文章不打算写官方文档我想用这些年拆源码、写业务、排故障积累下来的视角把IOC和AOP这两块掰开揉碎讲一遍它们各自解决什么问题、底层走的什么机制、业务落地时最容易在哪个环节翻车。文章会覆盖Bean生命周期、三级缓存、JDK与CGLIB动态代理、AOP切面写法以及Spring事务的AOP本质。适合刚学Spring的人建立正确认知也适合准备面试的人拿来查缺补漏如果你正在排查某个代理失效的问题最后那部分可能就是你需要的。1. 为什么IOC和AOP是Spring的魂1.1 控制反转把“找对象”的权力交给容器先说IOCInversion of Control控制反转。这三个字翻译过来其实有点抽象怎么理解以前的代码里对象与对象之间的依赖是你自己new出来的public class OrderService { private final UserService userService new UserService(); }这种做法看起来没什么但一旦UserService的构造器需要DataSourceDataSource又需要连接池配置你每在一个地方用到UserService就要把这些依赖重新组装一遍。更麻烦的是测试的时候你很难替换成一个Mock对象因为UserService是写死在OrderService里面的。IOC做的核心事情就是把这种“亲自new依赖”的权力收回到一个统一容器让容器负责创建对象、管理依赖、决定生命周期。你只需要声明“我需要一个UserService”容器就把已经准备好的实例推给你。生活化类比以前你自己买菜、洗菜、炒菜现在你点外卖菜品由中央厨房统一采购、加工、配送你只负责吃换餐厅换实现也不用重新学做饭。容器就是那个“中央厨房”。这个类比的细节不一定完全贴切但控制权和复用性的感觉是到位的。IOC不是一种具体技术而是一种设计思想的落地Spring用BeanFactory和ApplicationContext把这个思想变成了可运行的基础设施。1.2 BeanFactory与ApplicationContext两个容器接口的差异在Spring里容器不是单一对象而是一套接口体系。最底层的接口是BeanFactory它定义了getBean、getType、containsBean这些最朴素的能力而ApplicationContext继承并扩展了BeanFactory同时补齐了国际化、事件发布、资源加载、自动注册多个内置后置处理器等能力。日常开发里我们几乎碰不到直接用BeanFactory的场景SpringBoot注入的、我们在测试里拿到的、甚至手动new的ClassPathXmlApplicationContext或AnnotationConfigApplicationContext本质都是ApplicationContext的实现类。说个容易踩的认知误区很多人以为ApplicationContext里塞了一堆Bean其实它不是直接存放Bean的Map它内部持有的是BeanFactory最终的操作还是会委托给BeanFactory去完成。你可以把BeanFactory理解成一台发动机ApplicationContext是配好了仪表盘、导航和空调的整车。配置参数、注册中心这些东西都是通过ApplicationContext往发动机里灌的。面试里问“容器里Bean存在哪里”标准答案不是ApplicationContext而是singletonObjects这个Map——它才是单例Bean真正的主人。平时我们获取Bean也有好几种姿势。最基础的是通过ApplicationContext.getBean(String或Class)拿在注入点用Autowired按类型注入Resource按名称注入ObjectProvider 则用于延迟获取或者拿多个候选Bean。后者在框架代码里很常见你可以把ObjectProvider理解成一个懒加载的Bean引用真正需要的时候再getIfAvailable()。接手别人项目时如果发现“这地方到底怎么拿到那个Bean的”看不明白时间允许就断点在getBean方法上观察一下调用栈很快就能理清来龙去脉。1.3 启动流程里藏着的三个关键环节容器的启动并不是直接把class变成对象那么简单。以AnnotationConfigApplicationContext为例核心的refresh()方法里藏着一条完整的流水线扫描配置类或Component注解把每一个Bean定义收集成BeanDefinition并注册到容器调用BeanFactoryPostProcessor对已有的BeanDefinition做二次修改比如PropertySourcesPlaceholderConfigurer就是把Value里的占位符解析掉注册各种BeanPostProcessor为后面的AOP和扩展留好钩子实例化所有非懒加载的单例Bean把创建好的实例放进单例缓存。第四个环节平时最容易被忽略其实它就是“容器启动慢”的根源。Spring Boot启动时那串日志里很多时间都花在把单例Bean预创建出来。如果你在项目中遇到启动很慢除了检查数据库连接池还可以看看是不是有大量单例Bean在启动阶段就初始化了重型资源。拉长refresh()方法打断点观察beanFactory.getBean()被调用的顺序你会看到真正的初始化顺序并不是写代码时的定义顺序而是由依赖关系推导出来的拓扑排序。这一点对理解“为什么这个Bean先创建、那个Bean后创建”非常有帮助。2. Bean的整个生命周期与循环依赖2.1 Bean的一生从BeanDefinition到销毁要理解三级缓存先要完整过一遍Bean的生命周期。很多面试答案只背“实例化、初始化、销毁”三个词但真实流程比这细得多。我用一条常规链路概括一下解析BeanDefinition记录类的元信息、构造器参数、属性值、初始化方法名实例化调用构造器或工厂方法创建一个原始对象属性填充执行依赖注入把Autowired、Value的内容填进去Aware回调如果Bean实现了BeanNameAware、BeanFactoryAware这些接口Spring会把对应的容器信息告诉它BeanPostProcessor的postProcessBeforeInitialization初始化前拦截执行初始化包括PostConstruct、InitializingBean的afterPropertiesSet、init-methodBeanPostProcessor的postProcessAfterInitialization这一步是AOP代理生成的绝佳时机对象在这里会从原始对象变成代理对象单例Bean进入一级缓存开始提供给外部使用容器关闭时触发DisposableBean的destroy方法和destroy-method。步骤5和7中间的钩子是整个Spring扩展机制的命脉。你自己写一个BeanPostProcessor在postProcessAfterInitialization里return一个代理对象就能实现“假装还是原来那个Bean实际已经换了身份”。读源码时你会发现Spring内置的AbstractAutoProxyCreator就是这么做的它不是通过某种神秘魔法生成代理而是老老实实等Bean初始化完成之后把代理覆盖到原来的实例上。Spring里大量能力都是靠后置处理器实现的比如AutowiredAnnotationBeanPostProcessor处理Autowired和ValueAbstractAutoProxyCreator负责AOP代理。想读懂Spring优先读懂BeanPostProcessor这条线会比读具体功能类更高效。2.2 三级缓存循环依赖是怎么被“偷偷”解决的循环依赖指的是Bean A依赖Bean BBean B又依赖Bean A。如果没有特殊处理创建A时发现缺B创建B时又发现缺A两边都没法收工——这是对象创建层面的僵局。Spring的解法是提前暴露半成品在A尚未完成属性填充时先把A的ObjectFactory放进一个缓存里等到解析B的依赖时发现需要A就从缓存里把这个半成品A拿出来给B用B创建完了A再继续完成自己的初始化。对应到代码就是三级缓存这三个Map// 一级缓存完整的成品Bean private final MapString, Object singletonObjects new ConcurrentHashMap(); // 二级缓存提前暴露的半成品或早期代理对象 private final MapString, Object earlySingletonObjects new HashMap(); // 三级缓存存放ObjectFactory延迟决策是否生成代理 private final MapString, ObjectFactory? singletonFactories new HashMap();在Bean创建过程中三级缓存会短暂存放一个ObjectFactory如果没有循环依赖这个工厂永远不会被调用最终Bean以成品身份进入一级缓存二级和三级缓存被清空。只有当循环依赖发生Spring在getSingleton阶段发现一级缓存没有、二级缓存也没有、三级缓存有工厂时才会调用工厂的getObject()把得到的对象放入earlySingletonObjects再返回给依赖方。之后这个半成品会继续完成属性填充和初始化最终替换成成品放进一级缓存。2.3 为什么必须三级缓存二级行不行这是面试追问率最高的问题。很多人不明白既然二级缓存已经能存放半成品为什么还要加一层ObjectFactory核心原因在于代理对象的创建时机。当A被B提前引用时A可能还没执行AOP的增强逻辑如果A最终需要被代理那么B拿到的引用就不能是原始对象必须是代理对象。可问题是代理对象要等A初始化完、经过postProcessAfterInitialization才能确定要不要创建。如果只有二级缓存方案就是在A被提前引用时“提前调用一次AOP后处理器”先把代理确定下来。“提前确定”会让一个原本不需要提前代理的Bean也可能白白生成代理。更麻烦的是如果A会被多个Bean循环引用二级缓存里到底放原始对象还是代理对象就很难决策。三级缓存把“是否生成代理”推迟到真正有人引用它的时候用ObjectFactory这个回调来延迟判断没有循环依赖就走正常流程有循环依赖需要代理就给代理不需要就给原始对象。这就是“三级缓存比二级多出的一层价值”。补充一个关键前提能通过三级缓存解决的循环依赖是有条件的必须是单例Bean、且通过Setter或属性注入形成循环。构造器注入的循环依赖在实例化阶段就卡死了三级缓存根本救不了多例或原型Bean Spring也懒得救直接抛异常。2.4 极简手写容器理解IOC的底层骨架说再多不如写一遍。我当年为了搞清楚IOC的原理自己写过一个简化版容器核心其实只有三件事收集Bean定义、按定义创建实例、把实例缓存起来复用。缩减到最核心的骨架大概是这样public class MiniIocContainer { private final MapString, Object singletonObjects new ConcurrentHashMap(); private final MapString, Class? beanDefinitions new ConcurrentHashMap(); public void register(Class? clazz) { String simpleName clazz.getSimpleName(); String beanName Character.toLowerCase(simpleName.charAt(0)) simpleName.substring(1); beanDefinitions.put(beanName, clazz); } public Object getBean(String beanName) throws Exception { if (singletonObjects.containsKey(beanName)) { return singletonObjects.get(beanName); } Object instance beanDefinitions.get(beanName) .getDeclaredConstructor().newInstance(); injectFields(instance); singletonObjects.put(beanName, instance); return instance; } private void injectFields(Object instance) throws Exception { for (Field field : instance.getClass().getDeclaredFields()) { if (field.isAnnotationPresent(MiniAutowired.class)) { field.setAccessible(true); field.set(instance, getBean(field.getName())); } } } }这段代码当然远不能和Spring比但“定义收集、实例创建、依赖递归注入、单例缓存”这四段逻辑恰好就是Spring容器的骨架。基于它再去理解BeanPostProcessor你会发现它的位置无非就是在injectFields前后各插一个回调再理解三级缓存也只需要在getBean里加上提前暴露分支。读源码时不要把Spring想象成天书它只是把这个极简流程每个环节都做了工程化扩展而已。3. AOP代理机制完全拆解3.1 AOP核心概念一个切面到底包含什么AOP面向切面编程。适用场景大家都熟日志、权限、事务、监控、分布式锁、参数校验。但落到实现上先要把一系列名词对齐连接点程序执行的某个具体位置比如方法执行、异常抛出切点用表达式描述“哪些连接点需要被增强”比如execution(* com.example.service..(..))通知增强逻辑本身分为Before、AfterReturning、AfterThrowing、After、Around切面切点加通知的组合体织入把通知应用到目标对象生成代理对象的过程。很多人混淆切点和连接点切点是从所有连接点里筛选出来的一个子集。比如类里写了50个方法切点表达式可能只匹配其中3个那这3个方法才是真正被代理增强的连接点。AOP概念理解到位后再看动态代理就不会晕因为动态代理就是“织入”的运行时实现方式它不是AOP的全部只是Spring AOP采用的具体手段。3.2 JDK动态代理基于接口的运行时代理JDK动态代理是Java标准库自带的能力核心类就是java.lang.reflect.Proxy和InvocationHandler。它要求目标对象必须实现至少一个接口代理类会在运行时实现同样的接口然后把所有方法调用转发给InvocationHandler的invoke方法。写个最简单的例子public class JdkLogProxy { public static Object create(Object target) { return Proxy.newProxyInstance( target.getClass().getClassLoader(), target.getClass().getInterfaces(), (proxy, method, args) - { System.out.println(before: method.getName()); Object result method.invoke(target, args); System.out.println(after: method.getName()); return result; } ); } }这里最关键的一点是代理对象和原始对象是“平级”的它们共同实现接口但代理对象内部持有原始对象的引用。调用方拿到的是代理对象调代理的方法代理再反射调用原始对象。正因为代理基于接口所以一个没有实现任何接口的类用JDK动态代理根本无法生成代理对象。这是Spring默认代理策略里“有接口用JDK、无接口用CGLIB”的根源。JDK动态代理的优点是纯JDK实现不依赖第三方字节码库显式的接口约束也是优点也常被当成限制。业务模块如果从设计上就面向接口编程JDK代理非常自然。但有一类坑很常见目标类实现了接口但注入时如果用具体类型而不是接口类型代理会因为类型不匹配导致转换异常或者注入失败。3.3 CGLIB动态代理通过字节码生成子类CGLIB的思路和JDK代理完全不同它是通过字节码生成目标类的一个子类在子类里重写目标方法把增强逻辑插入到方法执行前后。因此它不要求目标类实现接口但也带来一个限制final方法无法被重写final类也无法被代理。CGLIB内部会用ASM之类的字节码框架产出新类再通过Enhancer设置回调写法大致如下Enhancer enhancer new Enhancer(); enhancer.setSuperclass(UserService.class); enhancer.setCallback(new MethodInterceptor() { Override public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { System.out.println(before: method.getName()); Object result proxy.invokeSuper(obj, args); System.out.println(after: method.getName()); return result; } }); UserService proxy (UserService) enhancer.create();注意示例里用的是methodProxy.invokeSuper(obj, args)不是method.invoke(obj, args)。如果在这里误用了反射调用原始方法可能会躲过CGLIB的FastClass机制导致性能下降甚至栈溢出。CGLIB通过MethodProxy直接定位父类方法比反射更轻量这也是它适合高频调用的一个原因。但CGLIB生成的子类在对象比较、getClass()结果这类场景会和原始类不同像ClassUtils.isCglibProxy()这类判断在框架里很常见业务代码如果对类型过于敏感也会在这里吃到暗亏。3.4 Spring与Spring Boot对两种代理的选择Spring在创建AOP代理时实际决策入口是AbstractAutoProxyCreator最终会调用DefaultAopProxyFactory的createAopProxy。如果你手动用ProxyFactory创建代理同样会走这层决策逻辑。选择逻辑可以概括为目标对象实现了接口且没有把proxyTargetClass配置成true使用JDK动态代理目标对象没有接口或者proxyTargetClasstrue使用CGLIB代理从Spring Boot 2.x开始spring.aop.proxy-target-class默认值为true所以在Spring Boot项目里基本都是CGLIB代理即使你写了接口也一样。这个默认行为差异是很多老Spring程序员从XML切换到Spring Boot后踩坑的地方。以前Spring XML默认按JDK代理走注入时用接口类型天经地义到了Spring Boot默认CGLIB注入具体类型也能过代码反而松弛了一些。但从代理机制本身看CGLIB生成子类是继承式代理对象构造链路会多一层父类而JDK动态代理生成的是兄弟类。两种方式各有适用边界没有好坏之分面试时能把“接口、继承、final限制、proxy-target-class”这四个关键词说全就已经甩开大部分背概念的人了。4. AOP实战从切面到事务4.1 一个业务切面的完整落地过程纸上谈兵结束上点能直接抄的代码。假设要对所有Service方法做耗时监控和日志打印最快的方式是定义一个切面Aspect Component public class ServiceLogAspect { Pointcut(execution(* com.example.service..*.*(..))) public void serviceLayer() {} Around(serviceLayer()) public Object recordTime(ProceedingJoinPoint pjp) throws Throwable { String method pjp.getSignature().toShortString(); long start System.currentTimeMillis(); try { return pjp.proceed(); } finally { long cost System.currentTimeMillis() - start; if (cost 100) { System.out.println(slow method method cost cost ms); } } } }这段代码里最值得说的是pjp.proceed()。它调用的是拦截器链上的下一个方法可能指向另一个通知也可能最终落到目标方法本身。如果在Around里不调用proceed()目标方法根本不会执行——这是最容易被忽略的一点。写Around通知时一定要确认在哪些分支需要放行目标方法否则会出现“接口不报错但方法内部逻辑完全没跑”的灵异现象。通知的执行顺序也可以当成小抄记住Around的前半段然后Before然后目标方法然后AfterReturning或AfterThrowing然后After最后回到Around的后半段。整体顺序和try-catch-finally一致Around就是最外层的try块Before是try里的第一行After是finally的那段。多个切面共存时执行顺序由Order注解或者实现Ordered接口的顺序决定数值越小越早进入切面也越晚退出切面可以类比为洋葱的多层皮。4.2 execution表达式怎么设计才算稳切点表达式看起来简单写起来很容易翻车。execution(* com.example.service...(..))这个表达式的结构是返回值类型用表示任意包名com.example.service后的..表示当前包及子包.*表示任意类的任意方法(..)表示任意参数。如果想只匹配某个特定注解标注的方法也可以写成annotation(com.example.annotation.NeedLog)。实际经验里我会建议把表达式拆成常量统一放到一个切面类里避免散落各处表达式的粒度宁细勿粗用..跨过整个service包虽然省事但会把很多内部辅助类也拦截进去日志量瞬间翻倍排查时到处都是噪音。还有一点表达式的匹配发生在目标对象还是代理对象上偶尔也会引发理解偏差。Spring AOP的切点匹配是基于代理的也就是说某个方法final了、私有了或者类本身不满足被代理条件切点写对了也不会生效。不要靠直觉猜“这个包应该会被拦截到”验证的最快方式是在Around里打印pjp.getSignature()临时观察哪些方法被命中。生产环境想排查就更简单直接看方法调用栈里有没有出现代理类名比如$$EnhancerBySpringCGLIB$$这样的类名。4.3 事务的本质也是AOP很多人学Transactional只背传播行为却忽略了它背后的实现机制。Spring事务管理本质就是一个AOP切面当方法标注Transactional后容器会为这个Bean生成代理在方法执行前开启事务方法正常返回后提交事务方法抛出异常时回滚事务。这个切面是TransactionInterceptor它借助PlatformTransactionManager完成真正的数据库事务动作。Transactional(rollbackFor Exception.class) public void createOrder(OrderDTO dto) { orderMapper.insert(dto); inventoryService.deduct(dto.getSkuId(), dto.getCount()); }事务切面在工作时可以理解成一个标准的Around通知只不过真正的业务方法是在事务管理器的doBegin和doCommit之间执行的。这个“代理增加事务控制”的视角可以解释后面第五部分里大量的失效场景——既然事务是代理做的那代理没生效事务自然也不会生效。理解这一层比背十种传播行为都更有用。传播行为本身也很重要但那是“嵌套调用时如何组织事务”的策略问题和AOP机制是两个维度别混在一起谈。5. 高频翻车点与排查实战5.1 同类内部调用AOP失效这是所有AOP失效问题里出现频率最高的一个。假设OrderService有一个事务方法createOrder内部又调用了同一个类的另一个方法updateStockTransactional public void createOrder(...) { ... updateStock(...); } Transactional public void updateStock(...) { ... }createOrder走的是代理对象但进入方法体后里面的updateStock其实是this.updateStock这个this是原始对象不是代理对象。事务切面的拦截逻辑在没有经过代理的情况下根本不会被触发所以updateStock上的Transactional不会产生任何效果。这个自己调自己的问题本质上和“代理对象自调用”是同一件事。解决办法通常是把updateStock拆到别的Bean里面或者通过AopContext.currentProxy()获取当前代理再调用。拆Bean的方式最干净也最好测试AopContext.currentProxy()有时可用但需要配置exposeProxy true使用起来多少有点“击穿抽象”的味道。我个人的排查习惯是只要发现某个标注了Transactional的方法没有事务效果先想两件事这个方法是不是被同类调用的这个Bean的代理是不是确实创建了前者看调用链后者直接看Bean的class是不是代理类。这两个方向覆盖了80%的事务失效场景。5.2 事务失效的六个典型场景我整理了一个速查表按实际发生率排序失效场景原因解决方式同类内部调用方法内部this调用未被代理拆Bean或AopContext.currentProxy()异常被catch吞掉事务切面没有机会感知到异常异常上抛或在catch中手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()方法非publicSpring默认代理方式对非public方法不执行事务逻辑改成public并走代理入口受检异常默认只回滚RuntimeException和Error配置rollbackFor Exception.classAOP配置失效EnableTransactionManagement缺失或切面未生效检查启动类配置确认容器里Bean是代理类数据库表不支持事务例如MyISAM引擎改用InnoDB最后一条容易被忽略。很多人把事务问题全部归结到Spring配置查了半天才发现表引擎是MyISAM事务提交和回滚根本无效。排障时先确认“底层数据库确实具备事务能力”再用AOP的视角看Spring这一侧顺序不要反。关于异常被catch吞掉的场景补一句AOP的Around通知会在业务方法抛出异常后进入回滚逻辑如果业务代码把异常吞了AOP层面根本不知道发生错误自然不会回滚。这也是“事务失效”最常见且最隐蔽的原因。代码评审时建议重点Review所有catch块看到“catch (Exception e) { log.error(...) }”这种写法就要联想到事务回滚是否被破坏。5.3 构造器注入的循环依赖为什么无解前面提到三级缓存能解决的循环依赖有限。具体来说Setter注入或字段注入时Spring会先把Bean实例化再填充依赖所以可以把半成品提前暴露给别人但构造器注入要求把依赖作为构造参数传进去Spring在实例化阶段就得拿到完整依赖A还没建完、B也没建完两边都拿不到可用的对象于是直接抛BeanCurrentlyInCreationException。这就是为什么现在Spring团队越来越推荐构造器注入的同时又得告诉你循环依赖得靠重构来解。如果你的项目是Spring Boot 2.6以上还要知道一个默认行为变化Spring Boot 2.6开始默认禁止循环依赖spring.main.allow-circular-references默认改为false项目里有循环依赖会直接启动失败。这是官方在推动大家消除设计隐患而不是让开发者继续依赖三级缓存。看到这个配置时不用慌把循环依赖解开启动和运行都更稳。5.4 Configuration的full模式和lite模式最后聊一个容易忽略但非常典型的代理细节。标注Configuration的类Spring会通过CGLIB生成代理子类来处理Bean方法间的依赖调用保证容器里始终是单例但如果配置类没有加Configuration只是普通Component里写了Bean方法就不会被CGLIB代理同一个Bean方法被多次调用时会创建多个实例这就是full模式和lite模式的差别。lite模式的出现场景一般是类既没标Configuration又通过Bean注册了Bean或者配置类被final修饰导致代理失败。这个现象在整理框架配置类时经常出现明明只想定义一个Bean却反复生成多个实例排查时发现配置类没有加Configuration或者加了但方法上有static修饰。终极解法是理解代理模式的约束Configuration类不要被final修饰不要尝试对private方法做增强这类由继承代理带来的限制和CGLIB代理本身的边界完全一致。老实说把IOC和AOP这两个点吃透之后Spring其他模块都没那么神秘了。我在实际排查里最大的体会是遇到问题先别急着怀疑框架——先确认“代理是否真的存在”再看“调用路径是否经过代理”绝大多数AOP和事务问题立刻就有了方向。后续再看Spring Boot自动配置、Spring Cloud网关、分布式事务这些上层框架你会明显感觉没那么玄了因为它们本质上都是在同一个容器和代理体系之上搭积木。所以如果只能给一个学习建议我会说先把IOC容器和AOP代理这两条主干的原理和翻车点打磨清楚再往外延展事半功倍。