聊到Java后端Spring的IOC控制反转和AOP面向切面永远绕不开。你去看任何一份Spring面试题这三板斧一定有名字你去翻任何一套有点年头的企业级系统代码里不是挂着Autowired就是铺着Transactional。很多同事用Spring用了两三年天天写注解但对这两个核心机制的理解只停留在“IOC就是不用new对象”“AOP就是切面日志”这个水平。一旦面试官追问Bean的生命周期、三级缓存、JDK动态代理和CGLIB的区别马上就露馅了。这篇文章我打算不只讲原理还把Spring底层那些关键代码的走向、实际开发里的坑、面试最喜欢挖的细节一次性讲清楚。不管是准备面试还是想把手里的Spring Boot项目写得更稳健这篇都值得你静下心来读一遍。1. IOC容器把“找依赖”这件事彻底交出去1.1 控制反转到底反转了什么很多人一说IOC就背“控制反转依赖注入”但你要往深了问一句到底什么被反转了答案是对象获取的控制权。传统的写法是这样的Service里要用Dao直接在Service里new Dao()对象自己掌握创建依赖的主动权死活得自己搞定。这种写法的问题很典型Dao换实现类Service代码要改Dao构造复杂Service也要跟着改单元测试想换个Mock实现还是要改动。Spring的做法是把所有对象都交到一个容器里统一管理。你要用什么不用自己造而是向容器要。容器管着每个对象的创建、初始化、装配、销毁。对象的生命周期管理权从“各个类自己手里”反转到了“容器手里”这就是控制反转这个名词的来历。依赖注入DI是实现IOC的一种手段容器把Service需要的Dao通过构造器、setter或者字段引用的方式“注入”给Service一次注入完成后Service对Dao是完全无感知的它只知道“给我一个Dao我就能用”。这个机制带来的收益非常大。你的类变成纯粹的POJO不依赖Spring的任何API也可以独立运行和测试类之间的耦合降到了最低。这也是Spring能统治Java企业级开发十多年的根本原因之一。1.2 三种依赖注入方式该怎么选Spring支持三种依赖注入方式构造器注入、setter注入、字段注入。我给个表对比一下面试也经常考这三者的差异。注入方式写法特点循环依赖支持不可变性推荐度构造器注入在构造方法上注入依赖不支持支持final字段官方推荐setter注入通过属性set方法注入支持不支持可选字段注入直接Autowired打在字段上支持不支持不推荐为什么字段注入口碑这么差首先字段注入的类很难在外面完成单元测试你不能不启动Spring就给它灌一个Mock进去其次它破坏了封装性字段被反射暴力赋值再一个字段注入容易掩盖依赖过重的坏味道一个类里塞十几个Autowired压根看不出来这个类有多胖。我个人和Spring官方推荐的做法一致能用构造器注入就用构造器注入。依赖多了构造器自然变得很长这是好事它会逼着你停下来思考这个类是否做得太杂了。1.3 Bean生命周期完整梳理大多数人的IOC知识在这里戛然而止知道Bean由容器管理却说不出Bean从定义到销毁经历了哪些阶段。这部分我们展开讲透面试官特别爱问排查很多Bean初始化问题时也真的要用到。一个Bean从生到死大致要经历这些阶段解析BeanDefinition。容器读取配置XML、注解、配置类把它们转成BeanDefinition元数据相当于摸清了每个Bean的长相类名、作用域、延迟加载、属性值、初始化方法等。实例化Instantiation。通过反射调用构造函数或工厂方法把Bean“new”出来。此时Bean还只是一张白纸属性全是默认值。属性填充Populate。容器把BeanDefinition里记录的属性值、依赖通过解析后填充到Bean上包括Autowired、Value的处理都是在这个阶段干的。Aware回调。如果Bean实现了BeanNameAware、BeanFactoryAware、ApplicationContextAware等接口容器会依次回调让Bean知道自己叫什么名字、在哪个容器里。BeanPostProcessor的前置处理。执行postProcessBeforeInitialization方法Spring AOP创建代理的时机就在这一步ConfigurationClassPostProcessor等内部工具也依托这个扩展点干很多活。初始化Initialization。依次执行PostConstruct标注的方法 →InitializingBean接口的afterPropertiesSet→ 配置的init-method。三个方法实际有确定的执行顺序面试常考。BeanPostProcessor的后置处理。执行postProcessAfterInitialization这一步完成后Bean才真正可以使用。AOP代理如果前置阶段没生成也能在这里兜底生成比如AbstractAutoProxyCreator。使用阶段。容器持有Bean的引用供各处注入调用。销毁Destruction。容器关闭时执行销毁逻辑先PreDestroy再DisposableBean.destroy()最后是destroy-method配置的方法。关于第5步和第7步很多人容易混淆。它们统称BeanPostProcessor但前后两个回调有本质区别前者在Bean初始化之前可用于检查、替换Bean对象后者在初始化之后非常适合对最终对象做包装proxy的生成多数发生在这里。这里的每一次回调都是Spring留给开发者的钩子。我们项目里做多数据源切换就要靠BeanPostProcessor在Bean初始化完成后动态替换底层DataSource做敏感字段脱敏也得靠它给Controller返回值做包装。你说不理解生命周期怎么行。2. 三级缓存与循环依赖Spring最烧脑也最实用的机制2.1 什么是循环依赖先明确概念。所谓循环依赖就是两个或多个Bean互相持有对方的引用。最典型的场景Component public class A { Autowired private B b; public A(B b) { } // 构造器注入 } Component public class B { Autowired private A a; }A要创建必须拿到BB要创建必须拿到A。要是没人出来调停这就是死锁。Spring给出的答案是三级缓存同时有一个前提你要记住Spring只能解决默认单例模式下、非构造器注入的循环依赖。构造器注入的循环依赖容器直接抛BeanCurrentlyInCreationException没得商量。2.2 三级缓存到底是哪三级在DefaultSingletonBeanRegistry内部有三个Map记住它们的名字和存放的内容缓存变量名作用一级缓存singletonObjects存放初始化完成的成品Bean二级缓存earlySingletonObjects存放初始化未完成的早期Bean半成品三级缓存singletonFactories存放ObjectFactorylambda表达式用于提前生成Bean或代理关键在第三级缓存。它存的是ObjectFactory而不是Bean本身。这个工厂的作用是在某个Bean还没完成初始化的时候如果其他Bean急需要引用它可以通过工厂拿到它的早期引用甚至可以提前生成代理对象。2.3 一次完整循环依赖怎么被解开的我们走一遍A和B互相依赖的完整流程getSingleton(a)一级缓存没有继续创建A把A放入singletonsCurrentlyInCreation集合标记为“创建中”。A开始实例化属性填充时发现需要B。容器去创建BB实例化属性填充时发现需要A。此时B去getSingleton(a)一级缓存没有看A是不是正在创建中是。于是从三级缓存里找到A的ObjectFactory调用工厂得到A的早期对象此时A的属性还没填充完成把这个早期引用放进二级缓存然后返回给B。B拿到A的早期引用开心地完成了自己的属性填充、初始化然后把自己放进一级缓存。回到A这边B已经拿到了A继续完成初始化最终也被放入一级缓存。这个流程里三级缓存存在的意义就是要解决一个问题在Bean还没成熟时既要让别人拿到引用又要保证如果这个Bean最终需要代理别人拿到的那个引用就是最终的代理对象。为什么不能省掉三级缓存只用两级关键在于代理。假设一个Bean被AOP切了标准的Spring代理创建时机在初始化之后。但如果循环依赖里A需要代理而B提前拿到了A的原始对象的引用等A代理生成完B手里还是那个原始对象这就不对了。三级缓存通过ObjectFactory把“是否生成代理”的判断推迟到了真正需要引用的那一刻B拿到的直接就是代理对象。理解了这一点再去读源码你会突然开朗。读源码时重点看两个方法getSingleton(String beanName)和doCreateBean其中三级缓存的操作都在doCreateBean里靠addSingletonFactory完成。2.4 循环依赖失效的三个典型坑知道原理还不够实战里循环依赖最常见的翻车现场是这三类构造器注入的循环依赖一定失败容器直接抛异常。一句话解释构造器执行时对象连个“半成品”都没有三级缓存也无能为力。Async方法导致循环依赖失败这个坑特别隐蔽。Async底层通过AOP生成代理被代理Bean的创建时机提前。初始化时如果发现循环依赖三级缓存会提前生成代理但Async的代理依赖AsyncAnnotationBeanPostProcessor在初始化之后处理两者时序冲突最终报错。Scope(prototype)的循环依赖非单例不缓存Spring无法提前暴露早期引用必挂。碰到循环依赖的报错我的第一反应不是去看怎么调配置绕过而是去审视代码结构。大多数循环依赖其实是设计问题是类的职责边界没切开。把互相纠缠的逻辑抽出来提取中间层比强行依赖Spring的三级缓存体面得多。记住三级缓存是Spring给你的后门不是让你随便浪的。3. AOP实现原理动态代理到底是何方神圣3.1 AOP四个核心概念先对齐AOP面向切面编程的核心思想是在不侵入业务代码的前提下把日志、事务、权限等公共逻辑横向切入业务方法。相关概念需要先对齐切面Aspect把横切逻辑和切点定义封装在一起的模块就是一个类上面标Aspect。连接点JoinPoint程序执行的某个特定位置Spring AOP里专指方法的执行点。切点Pointcut匹配连接点的表达式决定哪些方法需要被切入。通知Advice切面在切点上执行的动作分Before、After、AfterReturning、AfterThrowing、Around五种。一套切面的代码大概长这样Aspect Component public class LogAspect { // 切点表达式com.example.service 包下所有类的所有方法 Pointcut(execution(* com.example.service..*.*(..))) public void serviceLog() { } Around(serviceLog()) public Object logAround(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); System.out.println(pjp.getSignature() 耗时 (System.currentTimeMillis() - start) ms); return result; } }这里pjp.proceed()是核心它负责放行业务方法往下走Around可以在放行前后插入逻辑还能修改返回值、捕获异常。理解了这个AOP就算入了门。3.2 JDK动态代理基于接口的代理方案Spring AOP运行时靠的是动态代理而动态代理有两把刀第一把是JDK动态代理。JDK动态代理要求目标类必须实现接口。它通过java.lang.reflect.Proxy在运行时生成一个代理类这个代理类和目标类实现同一个接口并把所有方法调用转发给InvocationHandler的invoke方法。public interface UserService { void saveUser(); } public class UserServiceImpl implements UserService { public void saveUser() { System.out.println(保存用户); } } // 代理实现 UserService proxy (UserService) Proxy.newProxyInstance( UserServiceImpl.class.getClassLoader(), new Class[]{UserService.class}, (Object obj, Method method, Object[] args) - { System.out.println(前置逻辑); Object result method.invoke(new UserServiceImpl(), args); System.out.println(后置逻辑); return result; });使用限制很明显目标没实现接口就不存在可以嵌入的接口JDK代理无从下手。很多老项目里Service层没有抽接口想让Spring AOP生效就只能换方案。3.3 CGLIB动态代理基于继承的字节码增强CGLIB用的是“生成子类”的思路。它在运行时动态生成目标类的子类子类重写父类的方法在重写逻辑里插入增强代码从而实现对方法调用的拦截。CGLIB代理不受接口限制但它有两个硬伤无法代理final类因为没法给final类生成子类。无法代理final方法因为final方法不能重写。Spring的源码里封装了这两套方案。ProxyFactory会判断目标类的情况有接口就默认用JDK动态代理没有接口就用CGLIB也可以强制指定用哪种。Spring Boot 2.x之后官方把默认策略改成了CGLIB因为现在的开发习惯不流行每个Service都抽接口了统一用CGLIB减少割裂感。3.4 JDK动态代理和CGLIB怎么选这里我直接给你做一张对比表面试背这一张表基本就值回票价了对比项JDK动态代理CGLIB动态代理实现基础基于接口代理类实现同一接口基于继承生成目标类的子类性能创建阶段较快较慢字节码生成性能调用阶段反射调用JDK8优化后表现不错方法调用直接通常更快限制必须要有接口final类、final方法无法代理Spring Boot 2.x默认否是实际开发中你很少需要手动选Spring Boot的默认策略就能覆盖绝大多数场景。但有些特殊情况值得你思考老项目里如果一个Service实现了接口还开着proxyTargetClassfalse切面打在接口方法上没问题一旦新增了一个接口里没有的类内自调用方法切面就会莫名其妙失效原因详见第4节。AOP原理的面试追问常常这样展开先问AOP是什么再问底层怎么实现接着问JDK代理和CGLIB区别最后问一处事务失效的场景让你解释。环环相扣每一层都卡在原理上。能把上面这几张小节吃透这串问题就可以流畅答完。4. AOP的真实应用场景不止于日志4.1 日志与链路追踪切面最经典的AOP场景就是日志。在Controller层加一个Around切面打印请求参数、响应结果、耗时比业务代码里手写几百行日志清爽太多。贴个可以直接用的示例Aspect Component public class WebLogAspect { Around(annotation(com.example.annotation.WebLog)) public Object logWeb(ProceedingJoinPoint pjp) throws Throwable { // 拿到目标方法、参数 MethodSignature signature (MethodSignature) pjp.getSignature(); Object[] args pjp.getArgs(); // 生成traceId放进MDC日志链路全串起来 String traceId UUID.randomUUID().toString().replace(-, ); MDC.put(traceId, traceId); long start System.currentTimeMillis(); try { return pjp.proceed(); } finally { System.out.println(String.format(方法[%s] 参数[%s] 耗时[%dms], signature.toShortString(), Arrays.toString(args), System.currentTimeMillis() - start)); MDC.remove(traceId); } } }这里用annotation切点可以精准控制只有加了WebLog注解的方法才切入粒度适中。配合Slf4j的MDC做traceId排查问题的时候顺着日志一拉到底爽到飞起。4.2 声明式事务Transactional背后的代理逻辑Spring事务很多人天天用但知道它本质是AOP的人少了一大截。Transactional是通过AOP的TransactionInterceptor实现的目标方法执行前开启事务方法正常结束提交事务抛出异常回滚事务。事务AOP有四个高频失效场景全部指向同一个根源——代理失效了拦截器没执行同类中方法自调用this.method()没有经过代理对象拦截器无从介入。方法不是public默认AOP拦截public方法。异常被catch后吞掉事务拦截器只看异常有没有抛出去吞了就默认提交。抛出检查异常默认只对RuntimeException和Error回滚IOException等检查异常需要rollbackFor指定。排查这些问题时最有用的一个技巧看看调用Transactional方法的时候拿到的实例到底是不是代理对象。在调试器里看类名代理对象会出现$$EnhancerBySpringCGLIB$$或$$JdkDynamicProxy字样一眼就能认出来。4.3 AOP在安全、缓存、微服务体系里的延伸AOP的用武之地远不止日志和事务。权限认证Spring Security的PreAuthorize、Secured注解本身就是AOP实现的在方法调用前拦截校验当前用户是否有角色权限没有就抛AccessDeniedException。缓存切面Cacheable、CacheEvict这些缓存注解同样由AOP拦截方法开始前先查缓存命中就直接返回不执行业务逻辑。性能监控做接口耗时统计、慢SQL定位都可以用一个切面统一做业务代码零污染。Saas多租户隔离、审计日志、API幂等性控制……只要是横切关注点AOP基本都能优雅处理。在微服务架构里Spring Cloud Alibaba的很多能力也跟AOP思想挂钩。Sentinel的SentinelResource注解就是通过拦截器实现限流降级的Seata的GlobalTransactional则是在Transactional之上再套一层分布式事务拦截。你掌握了AOP原理理解这些框架就会特别顺它们只是在方法执行链条上挂了一堆拦截器而已。所以别把AOP当成语法糖它其实是Spring生态各种高级能力的地基。理解了地基上面盖什么楼都看得懂了。5. 面试高压区IOC与AOP高频问题与实战排查技巧5.1 高频面试问题速查表我把Spring MVC和Spring Boot面试里和IOC/AOP强相关的高频问题整理成一个表对照着背、对照着查问题回答要点什么是IOC和DI什么关系控制反转是思想DI是实现手段对象生命周期管理权归容器讲一下Bean的生命周期解析Definition → 实例化 → 属性填充 → Aware回调 → 前置处理 → 初始化 → 后置处理 → 使用 → 销毁三级缓存解决什么问题单例Bean的循环依赖核心是因代理可能提前生成必须用ObjectFactory做延迟判断为什么JDK动态代理必须有接口代理类实现接口才能替换目标类本质上靠接口绑定调用链CGLIB可以代理final类吗不行因为CGLIB通过生成子类实现代理Autowired和Resource区别前者按类型后者按名称名称找不到再按类型Spring默认的Bean作用域singleton线程安全注意成员变量状态Transactional失效场景有哪些同类调用、非public、异常被吞、检查异常未指定rollbackForSpring Boot默认动态代理方案CGLIB5.2 实战排查技巧三件套排查IOC/AOP相关疑难杂症我最常靠三个招第一招识别代理对象。在调试模式下看Bean对象的getClass().getName()。如果是com.example.UserServiceImpl$$EnhancerBySpringCGLIB$$cafebabe说明它是CGLIB代理如果名字里有JdkDynamicProxy说明是JDK代理。拿到这个信息很多自调用失效问题就能定位。第二招确认切点有没有命中。一个切面死活不生效先别怀疑人生用日志把Advice的执行情况打出来。Spring Boot里可以配置logging.level.org.springframework.aopDEBUG直接能看到容器为哪些Bean创建了代理。如果日志里根本没出现你目标类的代理创建记录那就是切点表达式没匹配上去调表达式。第三招读源码断点定位。想真正看明白三级缓存的工作流程直接给DefaultSingletonBeanRegistry.getSingleton和AbstractAutowireCapableBeanFactory.doCreateBean加断点然后跑一个简单的循环依赖Demo。一步一步看一级缓存错过、二级缓存进入、三级缓存生成早期引用的全过程比看十篇博客都扎实。5.3 网上看不到的避坑经验最后分享几条我在真实项目里踩过的坑这些基本不会出现在教科书和大部分教程里坑一成员变量在单例Bean里被污染。有人说Spring的Bean线程不安全准确说是“有状态的成员变量”线程不安全。一个Controller里如果有一个private Map成员变量在请求中被并发改写所有线程都在共享同一份数据。解决方案是把状态放进方法局部变量或者用ThreadLocal。这本质上还是理解IOC里Bean作用域的产物。坑二切面里的事务失效叠加。一个事务方法被Around从外部包裹Around里先开了数据库连接干了一堆事再调pjp.proceed()让事务方法跑一遍两层业务混在一个线程里隔离级别和传播行为全都乱了。遇到这种问题不要急于在切面里开事务事务的活全交给Transactional管切面只做无状态的操作日志、监控、参数校验。坑三内部类里的Autowired永不生效。把依赖注入到一个Component的内部类上是不少新人会犯的错。Spring扫描的是顶层类内部类除非自己也被注册成了Bean否则Autowired注解就是个摆设。判断一个类的Autowired会不会生效最简单的方法是问自己这个类在Spring容器里有没有一个名字没有名字注什么注。这些坑单独看都是细节但组合在一起就直接决定了你的代码在开发环境跑得好好的、一到生产环境就出幺蛾子。结尾在我带过的团队里凡是花时间把IOC和AOP源码啃过一遍的人后面对Spring Boot的自动装配、Spring Cloud的各种组件都上手特别快。这两个机制就是Spring的任督二脉通则全通堵则处处碰壁。我的建议是别急着背结论自己动手写一个小Demo自定义几个Bean互相引用再写一个切面然后开着DEBUG模式一步步看容器怎么处理它们的。看到三级缓存的引用从无到有、看到代理对象偷偷替换掉你写的Bean那一刻的感觉比背一百道面试题都管用。以后再遇到项目奇怪的问题先问一句“这Bean是不是代理对象”很多玄学难题当场破案。