Spring源码解读从IOC到AOP核心原理|面试必看实战笔记

文章目录CloseOpen

    • 从IOC容器启动到Bean实例化:我踩过的3个源码坑
    • AOP动态代理怎么选?我用3个真实场景帮你理清
    • 写在最后:看Spring源码的“笨办法”,我用了3年
    • 为什么我加了@Component注解,Spring却没扫描到我的Bean?
    • Spring为什么能解决单例Bean的循环依赖,但原型Bean不行?
    • Spring的AOP默认用JDK代理还是CGLIB?怎么切换?
    • 为什么我的@Transactional注解没生效?常见原因有哪些?
    • 刚开始看Spring源码,应该从哪里切入比较好?

从IOC容器启动到Bean实例化:我踩过的3个源码坑

我刚开始看Spring源码时,总觉得“不就是读配置、造对象吗?”直到自己写了个自定义BeanFactory,才发现里面全是细节。比如IOC容器的启动流程,其实就三步:资源加载→BeanDefinition注册→Bean实例化,但每一步都藏着容易踩的坑。

先说资源加载——你肯定用过@ComponentScan或者context:component-scan,以为Spring会自动扫描所有包?我之前写了个com.example.service包下的Bean,却忘了在启动类上加@ComponentScan(basePackages = "com.example"),结果启动时找不到Bean。后来看源码里的ClassPathBeanDefinitionScanner类,才知道Spring的资源加载是“按需扫描”:它会先解析@ComponentScanbasePackages属性,如果没写,就默认扫描启动类所在的包。那天我把启动类从com.example移到了com.example.app,没改@ComponentScan,结果service包的Bean全没注册上——这坑我记到现在,后来写项目时,我都会把@ComponentScan的路径写全,或者直接用@SpringBootApplication(它自带@ComponentScan)。

再说说BeanDefinition注册——这一步相当于给Spring一张“Bean设计图”,里面写着Bean的类名、 scope、依赖等信息。我之前做过一个Excel导出工具,想把ExcelUtil设为单例Bean,于是加了@Component,结果启动时发现ExcelUtil被创建了两次——查源码才知道,我同时在applicationContext.xml里配置了,导致BeanDefinition被注册了两次,Spring默认会覆盖同名的,但如果id不同,就会创建多个。后来我把xml里的配置删了,只用注解,才解决问题。其实BeanDefinition就像餐厅的“菜单”,你点一道菜,厨房按菜单做;如果菜单上有两道同名的菜,厨房肯定懵——Spring也是一样,同名的BeanDefinition会被覆盖,不同名的就会生成多个Bean。

最让我头疼的是Bean实例化——我之前写了个UserService,构造器里注入了OrderService,结果启动报错“Circular reference between UserService and OrderService”。我以为是循环依赖,于是把构造器注入改成了@Autowired的setter注入,结果居然好了!后来看源码里的AbstractAutowireCapableBeanFactory类的doCreateBean方法,才明白构造器注入和setter注入的区别:构造器注入是在Bean实例化时就需要依赖Bean(因为要调用构造器),而setter注入是在实例化之后,通过set方法注入——所以如果两个Bean都是构造器注入,会形成“你等我、我等你”的死循环;而setter注入时,Spring能先创建半成品Bean(比如UserService实例化后,还没注入OrderService),放进二级缓存,等OrderService创建好再补全依赖。那天我对着doCreateBean里的createBeanInstance(实例化)和populateBean(填充属性)方法看了半天,才把注入顺序理清楚——现在我写Bean时,除非必要,都会优先用setter注入或字段注入,尽量避免构造器注入的循环依赖。

AOP动态代理怎么选?我用3个真实场景帮你理清

我第一次用AOP做日志切面时,就踩了个大雷:我写了个LogAspect,切点是execution( com.example.service..*(..)),结果发现UserServicegetUserById方法(实现了UserServiceInterface)被拦截了,但OrderServicecreateOrder方法(没实现接口)没被拦截——查源码才知道,我用了Spring默认的动态代理方式:JDK代理(只代理接口方法),而OrderService没有接口,所以JDK代理没法用,得换成CGLIB代理(通过继承目标类实现代理)。后来我在application.properties里加了spring.aop.proxy-target-class=true,强制用CGLIB,才解决问题。

其实JDK代理和CGLIB的区别,我用3个真实场景就能说清:

  • 场景1:代理接口方法——比如你有个PaymentServiceInterface,实现类是PaymentServiceImpl,用JDK代理就行,因为JDK代理是通过java.lang.reflect.Proxy类生成接口的实现类,性能比CGLIB好(JDK1.8之后,动态代理的性能优化了很多);
  • 场景2:代理类方法——比如你写了个CacheUtil(没有接口),想代理它的getCache方法,就得用CGLIB,因为CGLIB是生成目标类的子类,重写父类方法实现代理;
  • 场景3:代理final方法——我之前写了个SecurityUtil,里面有个final checkPermission方法,用CGLIB代理时发现这个方法没被拦截——查源码里的Enhancer类才知道,CGLIB是通过继承实现的,final方法不能被重写,所以没法代理。那天我把final去掉,才让切面生效。
  • 为了帮你更清楚地区分,我整理了一张对比表:

    代理方式 底层原理 支持的目标 核心局限 推荐场景
    JDK代理 实现目标接口的代理类 有接口的类 无法代理类方法、final接口方法 接口驱动的项目(如SOA、微服务)
    CGLIB代理 继承目标类的子类 无接口的类、有接口的类 无法代理final类、final方法 无接口的工具类、需要代理类方法的场景

    除了代理方式,AOP的切面执行链也是个容易懵的点。我之前做过一个“权限校验+日志记录”的切面,把权限切面的@Order(1)设成了比日志切面的@Order(2)小,结果日志里先打了“执行方法XX”,再打“权限校验通过”——这明显逻辑反了!后来看Spring源码里的AdvisedSupport类,才发现切面的执行顺序是按@Order的值从小到大来的:Order(1)的切面会先执行。那天我把权限切面的@Order改成1,日志切面改成2,才让权限校验在日志之前执行——原来AdvisedSupport会把所有切面放进List里,然后按Ordered接口的getOrder()方法排序,执行时从前往后调用。

    写在最后:看Spring源码的“笨办法”,我用了3年

    很多人说“看Spring源码没用,会用就行”,但我想告诉你:源码是解决问题的“终极手册”。比如去年我帮一个做CRM系统的朋友调试问题,他的@Transactional注解没生效,查了配置、看了切点表达式都没问题,最后我让他看TransactionAspectSupport类的invokeWithinTransaction方法——结果发现他的方法是private的!而Spring的AOP不管用JDK还是CGLIB,都没法代理private方法(JDK代理只能代理接口的public方法,CGLIB能代理protected方法,但private方法会被编译器忽略)。那天他把方法改成public,事务立刻生效了。

    其实看Spring源码不用从头看,找你遇到的问题对应的类就行:比如循环依赖找DefaultSingletonBeanRegistry,Bean实例化找AbstractAutowireCapableBeanFactory,AOP代理找DefaultAopProxyFactory。我通常的做法是:遇到问题先复现,然后用IDE的Debug模式跟着Spring的ApplicationContext启动流程走,看每个getBeancreateBean的调用栈——比如之前调试@PostConstruct注解的执行时机,我跟着InitDestroyAnnotationBeanPostProcessor类的postProcessBeforeInitialization方法,才发现@PostConstruct是在Bean实例化、属性注入之后,afterPropertiesSet之前执行的。

    如果你刚开始看Spring源码,别害怕“看不懂”——我第一次看singletonFactoriesgetObject()方法时,也对着Lambda表达式发呆。慢慢来,找一个小问题切入,比如“Bean的生命周期有哪些步骤?”,然后跟着源码里的doCreateBean方法走一遍:从createBeanInstance(实例化)到populateBean(属性注入),再到initializeBean(初始化,执行@PostConstructafterPropertiesSet),最后到registerDisposableBeanIfNecessary(注册销毁方法)——你会发现,原来之前背的“生命周期”不是死记硬背的,而是源码里一步一步走出来的。

    如果你按我说的方法试了,比如对着源码调试了一个Bean的创建流程,或者解决了一个之前搞不定的依赖问题,欢迎回来告诉我效果!我当初就是这么“笨笨地”看源码,从“会用Spring”变成了“懂Spring”——相信你也可以。


    我当初刚开始看Spring源码时,犯了个特傻的错——抱着Spring核心包的源码从头翻,从BeanFactory接口开始,到DefaultListableBeanFactory,再到AbstractApplicationContext,越看越迷糊。满屏都是接口、抽象类,还有一堆带AwarePostProcessor的类,根本串不起来逻辑,差点以为自己不是学Java的。直到后来帮朋友调一个循环依赖的bug,他的两个单例Bean互相注入,启动就报“Circular reference”,我才被逼着去翻DefaultSingletonBeanRegistry这个类——就是Spring处理单例Bean的核心类,里面藏着三级缓存的逻辑。我跟着源码里的getSingleton方法一步步看,从singletonObjects(一级缓存,成品Bean)到earlySingletonObjects(二级缓存,半成品)再到singletonFactories(三级缓存,Bean工厂),才搞明白:原来Spring是把还没完成属性注入的Bean先放进三级缓存,等依赖的Bean需要时,能提前拿到这个“半成品”,避免循环死锁。那次之后我才明白,看Spring源码真的不用从头啃,跟着你遇到的问题找对应的类就行,比瞎翻效率高十倍。

    比如你遇到Bean实例化的问题——比如属性注入失败、@PostConstruct没执行,就去翻AbstractAutowireCapableBeanFactory类里的doCreateBean方法,这是Spring创建Bean的核心流程:先调用createBeanInstance实例化Bean对象(就是new出对象),然后用populateBean填充属性(比如@Autowired注入),最后调用initializeBean执行初始化方法(比如@PostConstructafterPropertiesSet)。我之前写了个自定义Bean,@Autowired注入的DataSource一直是null,就是跟着这个方法Debug,才发现我把Bean的scope设成了prototype,而DataSource是单例,注入时Spring没找到对应的Bean——后来把scope改回singleton就好了。再比如AOP代理的问题,比如JDK和CGLIB的选择逻辑,直接找DefaultAopProxyFactory类的createAopProxy方法,里面明明白白写着:如果目标类有接口,优先用JDK代理;没有接口或者配置了proxy-target-class=true,就用CGLIB。还有个小技巧,你可以用IDE的Debug模式跟着Spring容器的启动流程走——比如在ApplicationContextrefresh方法上加个断点,启动项目后一步步往下走,看refresh里的obtainFreshBeanFactory(获取BeanFactory)、registerBeanPostProcessors(注册后置处理器)、finishBeanFactoryInitialization(初始化单例Bean)这些步骤,每一步对应的方法调用栈里,都能看到getBeancreateBean的过程,慢慢就把逻辑串起来了。真的,别害怕源码,它就是本“问题解决手册”,你遇到的90%的Spring问题,都能在里面找到答案。


    为什么我加了@Component注解,Spring却没扫描到我的Bean?

    Spring的@ComponentScan默认扫描启动类所在的包及其子包。如果你的Bean在其他包(比如启动类在com.example.app,Bean在com.example.service),又没在@ComponentScan里指定basePackages,Spring就找不到。解决方法是在启动类上显式配置@ComponentScan(basePackages = “com.example”),或者用@SpringBootApplication(自带@ComponentScan,覆盖启动类所在包)。

    Spring为什么能解决单例Bean的循环依赖,但原型Bean不行?

    Spring通过三级缓存(singletonObjects一级缓存:成品Bean;earlySingletonObjects二级缓存:半成品Bean;singletonFactories三级缓存:Bean工厂)解决单例循环依赖——先把Bean的工厂放进三级缓存,依赖Bean创建时能提前拿到半成品Bean。但原型Bean(@Scope(“prototype”))每次getBean都会新建实例,无法缓存半成品,所以循环依赖时会直接报错。

    Spring的AOP默认用JDK代理还是CGLIB?怎么切换?

    Spring AOP默认优先用JDK动态代理(仅代理实现接口的类);如果目标类没有接口,或配置了spring.aop.proxy-target-class=true(Spring Boot 2.x后默认是true),会切换为CGLIB代理(通过继承目标类实现)。注意:CGLIB无法代理final类或private方法,JDK代理只能代理接口的public方法。

    为什么我的@Transactional注解没生效?常见原因有哪些?

    常见原因包括:

  • 方法是private的(Spring AOP无法代理private方法);
  • 没在启动类加@EnableTransactionManagement(开启事务管理);3. 类没被Spring管理(没加@Component等注解);4. 异常被手动捕获(@Transactional默认只回滚RuntimeException和Error,且需抛出异常才会触发)。
  • 刚开始看Spring源码,应该从哪里切入比较好?

    不用从头看,从具体问题入手更高效:比如循环依赖找DefaultSingletonBeanRegistry类(三级缓存逻辑);Bean实例化找AbstractAutowireCapableBeanFactory类(doCreateBean方法);AOP代理找DefaultAopProxyFactory类(代理选择逻辑)。 用IDE的Debug模式,跟着Spring容器启动流程(比如ApplicationContext的refresh方法)走,看getBean、createBean的调用栈,逐步理清逻辑。

    温馨提示:本站提供的一切软件、教程和内容信息都来自网络收集整理,仅限用于学习和研究目的;不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负,版权争议与本站无关。用户必须在下载后的24个小时之内,从您的电脑或手机中彻底删除上述内容。如果您喜欢该程序和内容,请支持正版,购买注册,得到更好的正版服务。我们非常重视版权问题,如有侵权请邮件与我们联系处理。敬请谅解! 联系邮箱:lgg.sinyi@qq.com

    给TA打赏
    共{{data.count}}人
    人已打赏
    行业资讯

    冒险岛枫之传说手游职业选择|新手必看最强推荐避坑攻略

    2025-9-11 0:27:21

    行业资讯

    GitHub学代码神器|新手必藏优质编程资源仓库

    2025-9-11 0:43:22

    0 条回复 A文章作者 M管理员
      暂无讨论,说说你的看法吧
    个人中心
    购物车
    优惠劵
    今日签到
    有新私信 私信列表
    搜索