ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Spring IOC与AOP底层原理:从控制反转到三级缓存与动态代理

Spring IOC与AOP底层原理:从控制反转到三级缓存与动态代理 Spring最核心的IOC和AOP可以说是整个框架的基石。你去看任何一份Java后端面试题这两块内容都绕不开而且面试官特别喜欢从浅到深地追问从什么是控制反转一路问到三级缓存为什么能解决循环依赖Spring AOP和AspectJ有什么区别。很多人在简历上写着熟练掌握Spring但真被问到这些底层细节时往往只能说出个大概甚至会把IOC和DI混为一谈、把Spring AOP和AspectJ混为一谈。这篇内容我按面试八股文的思路来整理但不会只给你标准的背诵答案。每一块我都会把背后的原理拆开讲清楚该给源码的地方给源码该给类比的地方给类比还会结合面试官真正想考察的点做延伸。最后附上高频追问和踩坑总结希望能帮你把这两块硬骨头啃下来。1. IOC的核心逻辑容器反转的不只是控制权1.1 控制反转和依赖注入到底是什么关系IOC全称Inversion of Control直译过来是控制反转。但很多同学对这个概念的理解停在把创建对象的权利交给Spring容器这个层面这其实只答对了一半。控制反转是一种设计思想它的本质是对象不再自己主动去new依赖的组件而是把这种创建和查找依赖的控制权反转给外部容器。比如你写一个OrderService里面需要用到OrderDao传统写法是OrderDao dao new OrderDao()而有了IOC容器之后OrderService只需要声明我需要一个OrderDao容器就会在合适的时机把组装好的OrderDao注入进来。那依赖注入DIDependency Injection是什么它是控制反转思想的具体落地方式。换句话说IOC是目标DI是实现手段。Spring框架采用的就是DI这种实现方式通过构造器、Setter方法或字段注入把依赖关系在容器初始化的时候建立起来。所以如果面试官问IOC和DI的区别你可以这么回答控制反转是设计原则依赖注入是它的实现模式。Spring里最常见的DI实现方式有三种我在下面列一下构造器注入依赖不可变且能保证依赖完全初始化Spring官方推荐这种方式Setter注入灵活可以在运行时改变依赖但可能出现依赖未装配的情况字段注入代码最简洁但不利于单元测试也容易形成循环依赖这里有一个需要避免的坑很多人误以为字段注入是Spring最好的方式因为它写起来最方便。但实际上在Spring官方文档的推荐排序里字段注入优先级是最低的因为它隐藏了依赖关系对测试不友好而且容易和循环依赖问题纠缠在一起。1.2 BeanDefinition与BeanFactory容器运作的原理解读IOC容器不是一个大水缸把所有Bean扔进去就完事了它内部有一套完整的管理模型。BeanDefinition是Spring中描述Bean的元数据类它记录了一个Bean的类名、作用域、是否懒加载、初始化方法、销毁方法、属性值、构造器参数等所有必要信息。在Spring Boot的自动配置机制里大量通过ConditionalOnClass、ConditionalOnMissingBean等条件注解最终都是为了组装出对应的BeanDefinition。BeanFactory则是IOC容器的顶层接口负责根据BeanDefinition去创建Bean实例、管理Bean生命周期。我们常用的ApplicationContext就是BeanFactory的子接口在它基础上增加了国际化、事件发布、自动装配等企业级能力。我把这个过程总结为三步解析阶段Spring读取XML、注解或Java配置类把每个Bean的定义信息封装成BeanDefinition注册阶段把BeanDefinition注册到容器内部的BeanDefinitionRegistry实例化阶段容器根据BeanDefinition通过反射或工厂方法创建对象并进行属性填充和初始化这个过程里有个很关键的细节BeanDefinition在容器启动阶段就已经全部注册完成了而不是你第一次调用getBean才去创建。这也是为什么Spring容器启动的时候很多错误就会被提前暴露出来比如bean类型不匹配、循环依赖、找不到对应的Bean等。注意Bean注解标注的方法在配置类解析时只是被注册为BeanDefinition方法体是否执行要看这个Bean是单例还是原型以及是否被实际使用。这也是为什么懒加载的Bean在容器启动时没有日志输出。1.3 我建议你这样理解单例Bean的三级缓存三级缓存是Spring面试中追问率最高的话题之一几乎每个面试官都会顺着循环依赖怎么解决问到这里。先记住三个缓存的定义一级缓存singletonObjects存放完整的单例Bean二级缓存earlySingletonObjects存放提前暴露的早期Bean引用此时属性还没填充完三级缓存singletonFactories存放单例工厂对象用于生成早期Bean引用很多同学只背了这三层缓存的名称却没有真正理解它为什么需要三级而不是两级。这里我用一个具体场景来说明。假设有两个BeanA和BA依赖BB也依赖A。按照默认的单例实例化流程Spring先创建A在A的属性填充阶段发现需要B于是去创建BB的属性填充阶段又需要A如果此时没有缓存机制B就会去容器找A但A还没创建完这就死循环了。三级缓存的解决流程是Spring创建A通过构造器实例化出原始对象把这个原始对象封装到singletonFactories这个三级缓存里同时放入一个ObjectFactory这个工厂的核心逻辑是getEarlyBeanReference后面还会提到A开始填充属性发现依赖B因此去创建BB实例化后填充属性时发现依赖A这时它从三级缓存中拿到A的ObjectFactory调用工厂方法得到一个早期引用放进二级缓存earlySingletonObjects并把这个引用注入给BB创建完成进入一级缓存A拿到B的引用继续完成属性填充和初始化最终也进入一级缓存现在回答那个经典问题为什么不能只用一个二级缓存关键在于AOP代理。如果A最终需要被代理那么B在依赖A时需要拿到的是代理对象而不是普通的原始引用。Spring设计三级缓存的目的是把创建早期引用这件事推迟到真正用到它的时候而且只延迟到第一次访问。如果在二级缓存阶段就直接放入原始引用那后续再想生成代理对象就晚了因为B已经持有原始引用。而三级缓存里保存的是ObjectFactory这个工厂方法会在适当的时候调用getEarlyBeanReference判断当前Bean是否需要AOP代理如果需要就返回代理对象否则返回原始对象。这里补充一个容易混淆的细节getEarlyBeanReference最终走的是AbstractAutoProxyCreator的getEarlyBeanReference方法它和普通的后置处理器调用顺序不同。当Bean有循环依赖时代理对象会在实例化后、属性填充前提前生成没有循环依赖时代理对象会在初始化阶段由postProcessAfterInitialization生成。1.4 构造器注入和原型Bean为什么无法解决循环依赖面试官如果在三级缓存的问题上继续深挖大概率会问那构造器注入的循环依赖怎么解决答案是解决不了。因为构造器注入发生在实例化阶段此时对象还没创建出来根本没法提前暴露早期引用。你连原始对象都没有往三级缓存里放什么所以Spring会直接抛出BeanCurrentlyInCreationException。原型Bean的循环依赖同样解决不了因为原型Bean本身不经过三级缓存机制每次都是new一个新对象缓存对它没有意义Spring在发现原型Bean循环依赖时会直接抛出异常。还有一种常见情况是Async注解标注的Bean出现循环依赖这个也要特别小心。因为Async会让Bean在初始化阶段被代理而如果它又和其他Bean形成循环依赖早期引用和最终代理对象不是同一个就会出现空指针或行为不一致的问题。注意遇到循环依赖第一步不是去想缓存机制而是审视设计是否合理。循环依赖往往是代码耦合度高的信号能拆分就拆分不能拆分时用Lazy延迟注入可以缓解但根治还得靠重构。2. Spring的依赖注入实现与Bean生命周期全景2.1 从Autowired的自动装配机制看起Autowired是Spring中最常用的注解之一但很多人不清楚它的装配流程。Spring在Bean属性填充阶段会调用AutowiredAnnotationBeanPostProcessor这个后置处理器来处理带有Autowired注解的字段或方法。查找依赖Bean的顺序大致是这样先根据声明的类型查找也就是getBean(Class)的方式如果找到多个候选Bean就按字段名或参数名作为默认的Bean名称去匹配还是匹配不到就尝试用Primary标注的Bean或者用Priority顺序仍然没有就尝试用Qualifier指定的名称用户设置了required false找不到就返回null否则抛出异常实际上这里面还有个Resource和Autowired的区别问题也是一个面试高频题。Resource是JDK自带的注解默认按名称装配名称找不到再按类型Autowired是Spring的注解默认按类型装配如果需要按名称需要用Qualifier组合。实际开发中如果某个Bean名称和其他Bean冲突或者你有多个DataSource类型的BeanResource指定name可能比Qualifier更简洁。提示在写接口参数时尽量不要用Autowired注入了接口的多个实现类却只声明一个参数这种模糊写法。新手最常见的问题就是定义了接口、写了一个实现类时一切正常后面加了第二个实现类就报NoUniqueBeanDefinitionException。2.2 Bean生命周期完整时间线Bean生命周期是Spring八股文里的重头戏面试官很喜欢让你把整个过程按照顺序说出来。我一般这样组织回答从实例化前一直说到销毁分七个阶段。先说实例化前Spring通过InstantiationAwareBeanPostProcessor的postProcessBeforeInstantiation理论上可以让用户返回一个代理或自定义的对象作为Bean如果这里返回了对象后续默认创建流程就短路了。接着说实例化通过构造器反射创建对象这一步对应beanClass.getDeclaredConstructor().newInstance()。然后是实例化后、属性填充前MergedBeanDefinitionPostProcessor的postProcessMergedBeanDefinition会解析Autowired、Value等注解把需要注入的元数据提前缓存起来。接着是属性填充依赖注入InstantiationAwareBeanPostProcessor的postProcessProperties被调用AutowiredAnnotationBeanPostProcessor在这里完成依赖注入。然后是初始化前BeanPostProcessor的postProcessBeforeInitialization被调用这里可以做增强比如ApplicationContextAwareProcessor在这里回调注入容器相关内容。随后是初始化如果Bean实现了InitializingBean接口会调用afterPropertiesSet方法如果配置了自定义initMethod也会在这里执行。最后是初始化后BeanPostProcessor的postProcessAfterInitialization被调用AOP代理通常在这里生成。销毁阶段也有对应流程容器关闭时如果Bean实现了DisposableBean接口会调用destroy方法配置了destroyMethod也会执行。我把这些阶段整理成一张速查表阶段关键扩展点说明实例化前postProcessBeforeInstantiation可自定义Bean生成方式实例化构造器默认无参构造器合并BeanDefinitionpostProcessMergedBeanDefinition预解析注入注解属性填充postProcessPropertiesAutowired依赖注入初始化前postProcessBeforeInitializationAware回调等初始化afterPropertiesSet/initMethod自定义初始化逻辑初始化后postProcessAfterInitializationAOP代理生成点销毁destroy/destroyMethod释放资源2.3 Spring Boot 4.0与Spring AI带来的一些变化从热词里能看到现在面试题已经不满足于只问Spring 5时代的机制了Spring Boot 4.0、Spring AI、Spring AI Alibaba这些关键词也频繁出现在面试题里。我举个例子Spring Boot 4.0中对AOP做了一些调整虽然核心机制没有本质变化但现代Spring Boot应用在很多场景下不再需要显式引入AOP依赖也能完成事务管理因为事务的核心是Transactional而事务注解的底层是AOP代理但spring-boot-starter-aop的引入方式和使用方式确实需要注意避免出现找不到Aspect注解这类问题。在Spring Boot 4.0的启动扫描逻辑里如果自己搭建了一个比较精简的项目又没有把spring-boot-starter-aop加进依赖那么你写Aspect类标注的切面是不会生效的——不是代码写错了是根本没有对应的注解处理器。Spring AI也是当前比较热的扩展方向但它的核心实现思路其实还是Spring那一套通过依赖注入管理客户端、通过配置类自动装配模型实例、通过AOP来记录调用链路或做权限控制。有一个比较常见的误区是有人以为Spring AI是完全独立于Spring生态的一套框架实际上它就是把各种大模型能力包装成Spring Boot Starter底层照样依赖IOC和AOP。你在Service里注入一个ChatClient本质上和注入一个JdbcTemplate没有区别。所以面对这类新热词我建议你在复习时不要被新框架唬住回到根源去看它如何复用Spring的核心机制这样变化再多也能举一反三。3. AOP的核心原理与Spring AOP的落地3.1 从动态代理说起AOP为什么能无侵入增强逻辑AOP全称Aspect Oriented Programming面向切面编程。它解决的问题是在业务代码中有些横切逻辑比如日志、权限校验、事务、性能监控如果不做抽离会散落在每个业务方法里造成大量重复代码。Spring AOP的底层机制是动态代理。它有两种实现方式JDK动态代理基于接口代理类和目标类实现相同接口通过Proxy.newProxyInstance生成代理对象CGLIB动态代理基于继承代理类继承目标类通过字节码技术生成子类这里有个经典问题Spring Boot 2.x之后默认的代理方式是CGLIB而不是以前的JDK动态代理。为什么因为很多现代项目根本不写接口了类上直接加Service、Component注解如果Spring还强制要求必须走JDK动态代理那么没有接口的类就没法被代理。Spring从Boot 2.x开始默认spring.aop.proxy-target-classtrue也就是优先使用CGLIB。但这也带来一个副作用目标类不能是final的因为final类没法被继承CGLIB就无法生成子类代理。我实际工作中就遇到过一个问题一个类通过ConfigurationProperties绑定配置同时加了Transactional注解做事务管理结果怎么写事务都不生效。后来一查是那个类是final类CGLIB没法为它生成代理Spring只能退回到JDK代理但这个类又没有实现接口最终代理能力直接失效了。注意如果某个Bean既没有接口又是final类那所有需要代理的注解比如Transactional、Async都会失效。排查这类问题时第一反应应该是去看目标类能不能被CGLIB继承。3.2 连接点、切点、通知与切面的关系AOP中有几个关键术语面试官通常会让你解释清楚Join Point连接点程序执行的某个特定位置Spring AOP中通常指方法调用Pointcut切点匹配连接点的表达式决定通知应该织入到哪些方法上Advice通知在切点匹配到的方法前后执行的具体逻辑Aspect切面切点和通知的组合Weaving织入把切面应用到目标对象并创建新代理对象的过程在Spring AOP里切点表达式最常见的形式是execution。我给一个简单例子Aspect Component public class LogAspect { // 匹配com.example.service包下所有类的所有方法 Before(execution(* com.example.service..*.*(..))) public void beforeLog(JoinPoint joinPoint) { System.out.println(执行方法 joinPoint.getSignature().getName()); } }这里execution(* com.example.service..*.*(..))的含义是*表示任意返回类型com.example.service..表示包括子包*.*表示任意类的任意方法(..)表示任意方法参数。除了execution还有within、annotation、args、target等切点指示符。annotation在实际项目中用得非常频繁比如自定义一个LogAnnotation然后在切点上用annotation(logAnnotation)限定只拦截加了该注解的方法这种方式比直接按包名拦截更精准、更可控。3.3 通知执行顺序环绕通知和其他通知的执行时机Spring AOP中主要有五种通知类型Before目标方法执行前AfterReturning目标方法正常返回后AfterThrowing目标方法抛出异常后After目标方法执行完后无论是否异常都会执行Around手动控制目标方法的执行能完全接管方法的调用Around是其中最强大的一个通知它可以在方法前后做各种操作还能修改返回值或者捕获异常后吞掉。有一点需要特别注意Around如果没调用proceed()方法目标方法永远不会执行。这在做Retryable这类重试逻辑时特别有用你可以多次调用proceed()来模拟重试。当多个通知同时作用在同一个方法时执行顺序由Order注解或者Ordered接口控制。默认情况下Order值越小的通知越先执行。如果是同一个切面内Around包裹着其他通知。执行顺序大致是先执行Around的前置逻辑然后执行Before接着执行目标方法目标方法结束后执行AfterReturning或AfterThrowing然后执行After最后回到Around的后置逻辑我在项目里经常用这个特性做接口耗时统计 异常告警的切面Around负责计时和捕获异常AfterReturning负责记录正常访问日志这样两件事解耦互不干扰。3.4 AOP的应用场景到底有哪些AOP的经典使用场景我总结如下日志记录统一打印请求参数、返回值、耗时事务管理Transactional本身就是AOP的典型应用权限校验方法级别拦截比如RequiresPermission先检查当前用户权限再决定是否放行性能监控在方法调用链上做耗时统计和慢方法告警缓存管理方法调用前查缓存未命中再执行方法返回后写入缓存重试机制通过Retryable实现失败自动重试以事务为例Transactional之所以能让一个普通方法获得数据库事务能力就是因为Spring容器里实际注入的不是你写的UserService而是它的代理对象。代理对象在执行方法前开启事务方法结束后提交或回滚这个逻辑对业务代码完全透明。理解了这一点你就能明白为什么同类内部方法调用Transactional会失效——因为this调用并不会经过代理对象而是直接调用目标对象自身。这是一个面试连环问里特别有价值的点如果saveA()方法没有被Transactional标注它内部调用saveB()而saveB()标注了Transactional那么saveB()的事务生效吗答案是不会。因为saveB()是通过this.saveB()调用的没有经过代理对象所以事务拦截器根本没机会介入。3.5 Spring AOP和AspectJ的关系这个问题也经常出现在面试里。要分清楚两点Spring AOP是Spring框架自己实现的AOP它使用动态代理来织入通知默认支持代理方法级别的切面而AspectJ是一个更完整的AOP框架支持编译期织入、类加载期织入能切入字段访问、构造器调用等更细粒度的连接点。Spring AOP借助了AspectJ的注解规范比如Aspect、Before、Around这些注解其实都来自AspectJ包但Spring并没有使用AspectJ的织入器而是自己用代理来实现。有的面试官会问你那在什么场景下必须用AspectJ而不是Spring AOP答案是你需要对非Spring管理的对象做增强、需要拦截字段访问、需要静态织入时Spring AOP就无能为力了得换成AspectJ。提示Spring AOP只能作用于Spring容器中的Bean切点不能匹配容器外部的对象。AspectJ则没有这个限制。日常业务开发中99%的场景Spring AOP足够了不需要引入AspectJ的编译期织入。4. 面试官高频追问从八股文到源码级理解4.1 三级缓存的问题链怎么回答才不翻车面试官在问三级缓存时通常有两个目的第一看你是不是真的理解了循环依赖的本质第二看你有没有读过源码能说明白为什么是三级不是两级。我建议把回答组织成三个递进层次。第一层说明什么是循环依赖。A依赖BB依赖ASpring默认单例Bean是按需创建不是一次性创建完毕所以创建过程中会互相等待形成循环。第二层说明三级缓存的三个阶段。一级存完整Bean二级存早期Bean引用三级存工厂。创建A时把A的工厂放进三级B创建时需要A就从三级取工厂调用工厂得到早期引用放进二级然后B正常持有这个引用。等A创建完毕二级缓存中的早期引用被替换成完整Bean同时从三级移除。第三层说明为什么需要三级缓存。如果只是解决循环依赖二级缓存就够了早期引用可以先放在二级。但Spring还需要处理AOP代理问题代理对象的生成时机和完整Bean的生成时机存在差异。三级缓存里放的是ObjectFactory可以从源头决定生成的Bean是原始对象还是代理对象避免代理对象重复创建。这是Spring设计上的精妙之处也是回答里的加分项。如果你能再补充一句打开三级缓存后getSingleton方法里先查三级缓存再调用singletonFactory.getObject()然后放进二级缓存最后在addSingleton时放入一级缓存并清掉二级和三级缓存那面试官能感觉你是真正读过DefaultSingletonBeanRegistry源码的。我把相关源码的关键逻辑写出来protected Object getSingleton(String beanName, boolean allowEarlyReference) { Object singletonObject this.singletonObjects.get(beanName); if (singletonObject null isSingletonCurrentlyInCreation(beanName)) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null allowEarlyReference) { synchronized (this.singletonObjects) { singletonObject this.singletonObjects.get(beanName); if (singletonObject null) { singletonObject this.earlySingletonObjects.get(beanName); if (singletonObject null) { ObjectFactory? singletonFactory this.singletonFactories.get(beanName); if (singletonFactory ! null) { singletonObject singletonFactory.getObject(); this.earlySingletonObjects.put(beanName, singletonObject); this.singletonFactories.remove(beanName); } } } } } } return singletonObject; }这段代码对应了三级缓存查找的顺序先看一级再看二级最后从三级缓存取工厂并生成早期引用。4.2 BeanFactory和FactoryBean有什么区别这个问题经常被放在IOC上下文里问而且很容易混淆。BeanFactory是Spring容器的根接口负责管理Bean的创建和获取。而FactoryBean是一个特殊的Bean它本身就是一个工厂用于生产其他Bean实例。当一个类实现FactoryBean接口后你通过getBean(xxx)拿到的并不是这个FactoryBean本身而是它getObject()方法返回的对象。如果想获取FactoryBean本身需要在bean名称前加符号。举个例子Spring整合MyBatis时MapperFactoryBean就是典型的FactoryBean。每个Mapper接口的代理对象都是通过MapperFactoryBean的getObject()方法生成的。所以你从容器里拿UserMapper的时候拿到的是动态代理对象而不是MapperFactoryBean本身这让代码写起来非常自然。4.3 Spring是如何处理Bean后置处理器和Bean工厂后置处理器的后置处理器是Spring扩展体系里最关键的一环面试时至少要能分清两个概念BeanFactoryPostProcessor是容器级别的后置处理器它在所有BeanDefinition加载完成之后、所有Bean实例化之前执行主要用来修改BeanDefinition元数据。经典实现有PropertySourcesPlaceholderConfigurer用来解析${...}占位符。BeanPostProcessor是Bean级别的后置处理器作用于Bean实例化之后、初始化前后前面提到的AutowiredAnnotationBeanPostProcessor、AbstractAutoProxyCreator都是BeanPostProcessor的实现。举一个实际案例假设你在项目中想实现一个所有标注了SensitiveField的字段在返回给前端之前自动脱敏的功能就可以在BeanPostProcessor里判断Bean类中哪些字段有注解然后把脱敏逻辑包装进去。这种扩展点用得好会让代码非常优雅。4.4 手写一个简化版Spring来验证理解面试有时候会考写一个简单的IOC容器这是检验理解深度的好方式。核心思路其实就四步扫描指定包下的所有类找到加了Component等注解的类把类信息封装成自己的BeanDefinition并注册到Map中遍历BeanDefinition通过反射创建实例并用getDeclaredField找到带Autowired注解的字段递归创建依赖对象后设置进去将创建好的单例Bean放入单例池代码骨架大致长这样public class SimpleIocContainer { private MapString, Object singletonObjects new ConcurrentHashMap(); private MapString, Class? beanDefinitions new ConcurrentHashMap(); public void register(String beanName, Class? clazz) { beanDefinitions.put(beanName, clazz); } public Object getBean(String beanName) throws Exception { // 先从单例池拿 Object singleton singletonObjects.get(beanName); if (singleton ! null) { return singleton; } // 反射创建实例 Class? clazz beanDefinitions.get(beanName); Object instance clazz.getDeclaredConstructor().newInstance(); // 注入依赖字段 for (Field field : clazz.getDeclaredFields()) { if (field.isAnnotationPresent(Autowired.class)) { field.setAccessible(true); String fieldName field.getName(); Object dependency getBean(fieldName); field.set(instance, dependency); } } singletonObjects.put(beanName, instance); return instance; } }虽然这个实现很简陋但已经把IOC的核心链路跑通了。能写出这样一段代码你对IOC的理解就远比只会背概念的人扎实。5. 高频场景题与避坑指南5.1 常见问题为什么Spring AOP不生效AOP不生效是我见到过最多的坑而且每次原因都不一样。我总结了几个踩坑点按出现概率排序同类内部方法调用AOP代理只会包裹从外部调用进入的方法。如果同一个类里的方法A调用方法B而且A没有加切面注解B加了注解那么B的切面不会生效。原因就是调用发生在目标对象内部没有经过代理对象。解决办法有三种把B拆到另一个Bean里或者注入自己或者用AopContext.currentProxy()获取当前代理对象再调用。目标类或方法是final的Spring Boot 2.x默认用CGLIBfinal类无法被继承CGLIB无法代理。final方法也无法被覆盖。遇到这类问题要把final修饰去掉或者显式改成JDK代理并新增接口。切点表达式写错特别是execution表达式里包名多写了一个..或者方法参数写成了(*)而不是(..)都会导致切点匹配不上。排查时可以把切点类暂时打印一下匹配结果或者用Pointcut单独抽出来测试。代理对象没有被Spring管理如果你自己用new关键字创建了一个对象然后在这个对象上调用Transactional方法事务肯定不会生效。因为容器中的代理对象和你手动new出来的对象不是同一个。AOP依赖缺失在使用Spring Boot时如果用到Aspect注解需要引入AOP依赖否则解析不了注解。如果项目是自己搭建的Maven工程更不能漏掉这个依赖。5.2 循环依赖的检测与解决实践虽然Spring三级缓存能解决大部分单例Bean的setter循环依赖但我不建议你在设计时主动制造循环依赖。如果项目启动时出现BeanCurrentlyInCreationException我一般按下面这个顺序排查第一步看循环依赖属于哪类。如果是构造器注入引发的基本上是必现问题只能通过Lazy或重构解决。如果是字段或Setter注入Spring默认会尝试解决只有当allowCircularReferences被显式关闭时才报错。第二步看是否有代理Bean参与。例如两个Bean都开启了事务、并且循环引用早期引用和最终代理对象可能不一致导致依赖方拿到的是未代理的原始对象这时事务逻辑会失效。第三步检查设计。循环依赖往往是上帝类设计导致的问题ServiceA和ServiceB互相调用了太多方法就应该考虑把公共逻辑抽取到第三个Service或者独立的领域对象中。排查工具有两个值得推荐一个是IDEA的Spring Assistant插件能画出Bean依赖图非常直观另一个是启动日志里的The dependencies of some of the beans in the application context form a cycle提示直接告诉你是哪几个Bean形成了环。5.3 事务切面、自定义切面与执行顺序的把控当一个方法同时存在Transactional和自定义的事务日志切面时它们的执行顺序会影响最终结果。默认情况下事务切面的Order是Ordered.LOWEST_PRECEDENCE也就是优先级最低通常会最后执行。这意味着自定义切面的Around在事务开启之前执行如果自定义切面里抛出了异常事务切面还没开始就会中断。如果想要严格控制在事务之后执行某些操作比如事务提交后再发消息可以考虑实现TransactionSynchronizationManager.registerSynchronization注册事务同步回调在afterCommit里执行异步入队或消息推送逻辑。这个细节在实际项目中经常用到也容易在面试中成为加分项。5.4 Spring AI和Spring Security中AOP的影子从热词看Spring AI、Spring Security、Spring Cloud这几块也经常和AOP、IOC一起被问到。Spring Security的方法级安全注解比如PreAuthorize、Secured底层用的是Spring AOP或方法拦截器。当你给一个方法加上PreAuthorize(hasRole(ADMIN))后容器中的代理对象会先做权限校验通过后才执行真实方法。Spring AI项目中如果你想给大模型的调用统一加上Token计数、调用日志或降级处理用AOP来做是最合适的。写法上和普通的接口切面没有区别只要切点匹配到ChatClient或ChatModel的调用方法即可。这也验证了一个观点不管Spring生态怎么扩展IOC和AOP始终是它的骨架。6. 最后再聊聊面试答题的节奏与心态我个人在实际面试中的体会是八股文能不能答好关键不在于背了多少概念而在于能不能把知识点串成一条线。比如面试官问你怎么理解Spring一个较好的回答起点是Spring是一个轻量级框架核心是IOC容器和AOPIOC负责Bean的创建和依赖管理AOP负责在不侵入业务代码的前提下做横切逻辑增强。然后展开IOC从BeanFactory到ApplicationContext从Bean生命周期到循环依赖处理再展开AOP从代理机制到切面执行顺序。整条线下来面试官会明显感觉到你不是在背答案而是在讲一个你熟悉的技术体系。在答题节奏上我有一个习惯先说结论再讲原理最后补充细节或示例。如果你上来就把三级缓存三个Map讲得特别细面试官可能会觉得你没有框架感但如果你只说了一句话Spring通过三级缓存解决循环依赖又显得太浅。所以最好的方式是用结论开场观察面试官的反应如果对方追源码就把源码和设计意图都讲出来如果对方只想知道大概就适度收住。再分享一个实用小技巧在回答Spring AOP和AspectJ的区别这类对比题时可以用表格或分类的方式在脑中组织语言先说核心区别再说适用场景最后提一嘴自己的实践。比如你可以说Spring AOP基于动态代理只能作用于容器内方法级别平时用得多AspectJ支持静态织入功能更强大适合更底层的场景但配置成本高我们项目里基本没用到。这种回答方式既完整又真实比干巴巴地念概念好很多。踩过几次坑之后我对Spring最深刻的体会是IOC和AOP不是什么高深莫测的魔法它们就是一套管理对象协作的机制。你把对象创建、依赖装配、横切逻辑这些事都交给了框架业务代码才能干净地专注于自己的职责。面试也一样别太焦虑把这些机制背后的为什么想清楚再配合两个真实的踩坑案例就已经超过大多数候选人了。如果你正在准备面试或者项目里正好遇到AOP不生效、循环依赖这类问题希望这篇内容能帮上忙。下一步我建议你打开IDE启动一个最小Spring项目把文中的代码骨架跑一遍亲眼看一看代理对象长什么样、切面日志是在哪个环节打印的。技术这东西自己动手验证一次比看十篇文章都管用。
返回列表