0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

图解Spring Bean生成流程,非常详尽

jf_ro2CN3Fa 来源:芋道源码 2023-10-31 15:36 次阅读

很多人把spring的相关内容当作背八股文,认为只在面试时能用上,实际开发根本用不到。实际上早期的我也是这么想的,但随着开发年限的增长,解决了越来越多的难题后,不得不承认,这些基础知识的学习有着无法替代的作用。

就拿我实际遇到的一个例子来说:

有一个大型项目因为安全漏洞的原因要进行升级,需要从springboot1.0升级至springboot2.0,但发现springboot2的默认动态代理方式为CGLIB,而项目上很多地方利用的jdk代理对接口做了增强,切换至CGLIB导致了大量问题。根据百度的内容,设置了proxy-target-class=“false”,然而不起作用,最后发现是某一个三方包内设置了proxy-target-class=“true”,而这个属性只要在工程里任何地方设置过一次true,都会导致代理管理器的同名属性为true,最终采用CGLIB代理,那么有什么简单方式可以解决这个问题

先卖个关子,还是让我们一起学学Bean的生成吧

1引言

作为javaboy的必修课,spring一路伴随着开发者;同样的,也一路伴随着开发者面试,重要性不言而喻,我们经常遇见的问题比如:

代理对象是何时生成的?

循环依赖是怎么解决的?

能说说对Springr容器三级缓存的理解吗?

以上问题,都离不开对bean生成流程的熟悉与理解。但是不得不谈,目前网上文章鱼龙混杂,一些偏颇错误的分析四处流传,我们后面会提到一些常见谬传。至于现在,现在先和我们一起,深入的看下springBean的生成逻辑吧

基于 Spring Boot + MyBatis Plus + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/ruoyi-vue-pro

视频教程:https://doc.iocoder.cn/video/

2创建Bean的极简流程

我们开门见山,直接以单例对象为例子,说一个Bean的极简流程以及其目的

获取Bean定义

扫描工程内所有被标记的Bean,获取其类型,名称,属性,构造方法等信息,存在一个Map里

生成实例

这一步也很简单,遍历上述Map,利用Bean定义里的无参构造方法创建对象,和new 对象同理

属性装填

刚创建的对象所有属性都是默认值,需要我们给它装填上需要的内容

初始化

如果这个Bean实现了InitializingBean接口,则会调用你写在afterPropertiesSet方法里的内容。

到此,一个Bean就创建完毕了,是不是很简单?是的,很简单,逻辑也很清晰。

当然,上面四步是核心功能,Spring为了增强对这些Bean的修改能力,在2-生成实例 3-属性装填 4-初始化的前后都预留了处理点,Spring自己或用户都可以通过编写==Bean后置处理器(BeanPostProcessor)==来实现自己的目的,这些处理器会在对应的处理点被执行,从而完成对Bean的修改,下面会详细讲一下

基于 Spring Cloud Alibaba + Gateway + Nacos + RocketMQ + Vue & Element 实现的后台管理系统 + 用户小程序,支持 RBAC 动态权限、多租户、数据权限、工作流、三方登录、支付、短信、商城等功能

项目地址:https://github.com/YunaiV/yudao-cloud

视频教程:https://doc.iocoder.cn/video/

3后置处理器(PostProcessor)

Spring中的后置处理器分为两大类:

一类是针对Bean工厂的BeanFactoryPostProcessor

一类是针对Bean的BeanPostProcessor

以上两者都是接口,Spring已经给定了一些实现类,用户也可以自己写一些实现类来实现全局的Bean相关的操作;顾名思义,BeanFactoryPostProcessor针对Bean工厂(它还有个子接口BeanDefinitionRegistryPostProcessor),调整Bean工厂的属性、影响Bean定义,注意此时还没有Bean进行实例化。BeanPostProcessor则更直接的作用于Bean实例生成过程中的修改。

BeanFactoryPostProcessor

很多人不知道在实际项目中这个处理器有什么用,好像我们不需要对Bean工厂或者Bean做什么改动吧?大部分项目确实不需要,但很多时候,我们需要添加一些自定义的Bean,或者出于项目需要,改动一些Spring原生Bean属性时就用的上了。

比如我们常用的myBatis组件,我们会在mapper层的接口上写@Mapper注解,最后就会在Spring中生成对应的Bean对象,然而这里有一个问题:

@Mapper注解不是Spring规定的Bean注解,怎么被扫描进容器的?

自然是依托于BeanFactory后置处理器。mybatis中写有工厂后置处理器的实现

2d744ed6-779a-11ee-939d-92fbcf53809c.png

看名字也知道,这个处理器起了扫描的作用,找到了被我们标记的接口,并“捏造”一个Bean定义,并把Bean的类型设置为MapperFactoryBean.class,即工厂类,然后把它添加到Bean定义注册器中。

而在我们需要实例化这个Bean的时候,mybatis又会从这个工厂对象中使用getObject()为我们取出一个Bean实例,这个Bean实例是使用我们写的Mapper接口产生的代理,而后再把这个代理放入Spring容器

2d849af2-779a-11ee-939d-92fbcf53809c.png

BeanPostProcessor

而Bean后置处理器则更加常见,种类也更丰富,他们的详细作用和工作时机都可以在下图中看到

2da07bb4-779a-11ee-939d-92fbcf53809c.png

契机问题的解决

让我们回到契机里提到的那个问题,这个问题简化的讲,其实就是有这么一个Spring内部的Bean名字为org.springframework.aop.config.internalAutoProxyCreator,它有一个属性proxy-target-class,这个属性决定了Spring动态代理的生成用的jdk动态代理还是CGlib,然而在很多地方(三方包)已经给他赋值。

我们必须在它被其他三方包赋值后 ,把它的属性值改为false。这个问题最终怎么做到的呢?就是利用了后置处理器,此处使用工厂后置处理器找到该Bean定义,修改其Bean属性

2dabb36c-779a-11ee-939d-92fbcf53809c.png

4引用与缓存

从上面看,似乎创建一个Bean只需要四步(忽略后置处理器的步骤),十分简单。确实,如果我们的项目只需生成一个Bean,那只要按序完成这四步就可以了。

但实际上,Spring本身和我们的项目要生成的Bean数量远不止一个,复杂的项目一般会达到上千个Bean,Bean之间还有复杂的引用关系。我们不仅要存储这些Bean,还必须考虑到这些引用情形,从而引入缓存的机制。

引用已有的Bean

2db9baca-779a-11ee-939d-92fbcf53809c.png

如图,上述是一种最简单的引用,Parent 里面引用了 Child ,。理想的情况下,我们先创建了Child并保存起来,那么在创建Parent的时候,直接引用现成的Child就好(此处用@DependsOn保证这种顺序)。那么这时我们可以说,容器只需要使用一级缓存,就像养鸡场里饲养着许多鸡,这个缓存里存的就是各个现成的Bean,直接取用即可。

引用未创建的Bean

上述的Parent 里面引用了 Child案例,只是一种理想情况,实际上,大部分的Bean之间加载顺序并不会特意指定,创建的先后顺序自然没了保障(spring会执行默认的加载顺序,如字母排序)。

比如这个案例,如果先创建的是Parent,那么当我们做到属性装填这一步的时候,就会发现Parent的属性里,引用了一个未知的Bean —— Child。

2dc3f9ea-779a-11ee-939d-92fbcf53809c.png

这个时候Spring就会去搜寻并创建Child,此时Parent的创建就停滞了。那么这个创建未半而中道崩殂的Parent也需要有一个地方存起来啊。你或许会说,还是存在上面的一级缓存里面不行吗?

当然可以!但本着人以类聚物以群分的观念,对于这些创建了一半就中断的Bean,我们还是专门引入了三级缓存供其栖息。我们知道,此时Parent已经实例化了,但属性装填没完成,像个未孵化的蛋,而三级缓存就是个保温箱,是存放这些“蛋”的地方。实际上三级缓存里存的全是Bean工厂,可以通过Bean工厂的getEarlyBeanReference获取到这个未完成的Bean(蛋)。

循环引用(循环依赖)

如果不仅Parent里面引用了Child,Child里面也引用了Parent,那么显然,这就构成了循环引用。

2dcb3c78-779a-11ee-939d-92fbcf53809c.png

我们假定Spring先加载了Parent,后发现需要注入Child,又去加载Child,过程中又发现需要注入Parent,那么又去加载Parent…… 那Spring会这么无限的加载下去吗?

答案我们都知道,自然是不会的。实际上,每开始加载一个Bean,Spring都会把Bean名称记录在一个叫SingletonCurrentlyInCreation的Set集合里。

顾名思义,这个集合里都是正在创建中的Bean,这个集合在其他的文档中很少提及,但显然他的作用十分巨大。因为第二次加载Parent时,Spring就发现Parent已经在这个集合中了,才意识到进入循环引用了。

2de0df1a-779a-11ee-939d-92fbcf53809c.png

当发现进入循环引用后,自然Spring不会再傻乎乎的走再走一遍Parent的加载逻辑,而是从三级缓存中取出未完成的Bean,做一些处理后,然后将其放入二级缓存。

这一过程相当于从保温箱取出来未孵化的鸡蛋,孵化出小鸡后,放到专门的小鸡培养室中。而此时,只需要返回这只小鸡(Parent)就可以了,你或许会说,我要的是成品鸡,你给我小鸡有什么用,功能什么的能有保障吗?别急,我下面就为你解释这样的可行性。

循环引用中的代理

我们都知道Parent是创建了一半被放入缓存中的,此时它已经完成的步骤是生成实例正在卡着的步骤是属性装填和初始化,被从缓存中取出后,这两个步骤仍然是未完成的,但我们无需担心,因为此刻我们仅需完成引用,即我要引用Parent(成鸡),你现在给我返回半成品(小鸡)也没关系,因为我现在也不是要立刻就用你,只要你保证小鸡 成鸡在内存中的地址一样即可,即小鸡和成鸡是同一个对象。

你或许会问,小鸡长着长着,还能变了人不成?怎么可能小鸡和成鸡就不是同一个对象了呢?这就不得不谈代理模式了

2e076f72-779a-11ee-939d-92fbcf53809c.png

我们这里不去细谈代理流程,你只需要知道代理模式会产生一个新的对象,相当于一个霸道中介,原本你可以直接联系小鸡,现在小鸡的联系被中介切断了,你需要找小鸡就只能联系中介。所以,一旦成鸡后续需要代理,我们需要联系的就是成鸡的代理了,此时你给我小鸡的联系方式不顶用。

为避免这种情况,我们只能给小鸡生成中介。是的,原来中介是只给成鸡用的,但现在不得不提前到小鸡阶段了,生成中介后,返回给我们小鸡的-中介的-联系方式(即半成品Bean的-代理的-引用),事实上如果你看源码,对成品和半成品Bean生成代理用的是同一个方法wrapIfNecessary,因此生成代理的效果是一样的。当然你也许仍然有顾虑,对成品和半成品生成代理真的没差别吗?

的确,这里就不得不提Spring的代理的特殊点了,代理的基础就是大名鼎鼎的AOP 或者说 切面增强,然而Spring的增强仅针对方法。而半成品和成品,最大的差异是属性值,方法却是一样的,因此增强的效果肯定是一样的。如果哪天Spring的代理生成时会用到当前属性值,那不同阶段的代理功能才会有差异。

5三级缓存的解读

关于三级缓存,市面上有太多的解读文章,也是面试时经常问到的点,我们不妨解读一下三级缓存。

2e131322-779a-11ee-939d-92fbcf53809c.png

我们平常说的三级缓存,大多数人会想到CPU的三级缓存,硬件上之所以缓存分级,是对于成本与性能的考量,一级缓存最快,所以CPU优先从一级缓存取东西,但同样一级缓存最贵,存不了太多数据,所以需要二级缓存。

而这里,三级缓存并没有性能上的区别,所以划分三级缓存并非必须。实际上一个Bean,在同一时间只会出现在某一级缓存中,因此我们可以直接产出一个暴论:Spring可以不用所谓三级缓存,甚至说只需要一个集合就能存下全部

但为什么这里要这么做,因为这是逻辑分层而非必要分层,三级缓存存着不同状态的Bean罢了:一级缓存存成品鸡,二级缓存存小鸡,三级缓存存鸡蛋 一级比一级原始,你要非把成品鸡、小鸡、鸡蛋搁一个房子里也不是不行,所以这种分层是基于逻辑清晰而非逻辑必需。

这里还有个误区,很多人说是因为代理的存在,导致需要三级缓存,如果没有代理,两级就够了。实际上三级缓存并不是因为代理导致的,不管有没有代理,都是三级缓存。

就像我说的一级缓存存成品鸡,二级缓存存小鸡,三级缓存存鸡蛋 ,这里面并不区分代理,成品鸡或者成品鸡的代理都在一级缓存;小鸡或者小鸡的代理都在二级缓存。

实际上我们看代码,只要发生了循环引用,都会导致Bean从三级缓存取出,并放入二级缓存。这个过程中执行wrapIfNecessary,不管生不生成代理都是一样的,只不过如果需要代理,放入二级缓存的是小鸡的代理;如果不需要代理,放入二级缓存的就是小鸡本鸡,因此我们可以说 不管有没有代理,三级缓存的模式都没有变化。

6创建Bean的极详细流程

多说无益,我根据Spring4的源码整理了一份详细的生成流程,这图说是全网最细也不为过,欢迎大家补充和指正

2e1afd62-779a-11ee-939d-92fbcf53809c.png

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 处理器
    +关注

    关注

    68

    文章

    18298

    浏览量

    222224
  • spring
    +关注

    关注

    0

    文章

    333

    浏览量

    14161
  • 安全漏洞
    +关注

    关注

    0

    文章

    148

    浏览量

    16544

原文标题:图解 Spring Bean 生成流程,非常详尽

文章出处:【微信号:芋道源码,微信公众号:芋道源码】欢迎添加关注!文章转载请注明出处。

收藏 人收藏

    评论

    相关推荐

    java spring教程

    Spring核心概念介绍控制反转(IOC)依赖注入(DI)集合对象注入等Bean的管理BeanFactoryApplicationContextSpring 在web中的使用
    发表于 09-11 11:09

    spring实例

    ;UTF-8"?><!DOCTYPE beans PUBLIC"-//SPRING//DTD BEAN//EN""http://www.springframework.org
    发表于 09-11 11:22

    三大框架之Spring

    ;出现了Spring,可以自动创建需要被调用的对象以及进行属性注入,也可以维护这些bean(具体的java类)之间的关系;
    发表于 05-27 07:21

    Spring工作原理

    的依赖关系核心:bean工厂;在Spring中,bean工厂创建的各个实例称作bean二.AOP(Aspect-Oriented Programming): 面向方面编程1.代理的两种
    发表于 07-10 07:41

    Spring认证」Spring Hello World 项目示例

    让我们开始使用 Spring Framework 进行实际编程。在开始使用 Spring 框架编写第一个示例之前,您必须确保已按照Spring - 环境设置章节中的说明正确设置了 Spring
    发表于 08-17 13:49

    Spring应用 1 springXML配置说明

    Spring应用 1 springXML配置说明 隐式对Spring容器注册Process   context:annotation-config / 为了在spring开发过程中,为了简化
    发表于 01-13 12:20 317次阅读

    解析加载及实例化Bean的顺序(零配置)

    作者丨低调的JVM 来自丨CSDN https://blog.csdn.net/qq_27529917/article/details/79329809 在使用Spring时,Bean之间会有些依赖
    的头像 发表于 08-04 16:08 1143次阅读

    Spring Event对解耦业务的重要性

    Spring Event(Application Event)其实就是一个观察者设计模式,一个 Bean 处理完成任务后希望通知其它 Bean 或者说一个 Bean 想观察监听另一个
    发表于 07-25 10:12 433次阅读

    bean放入Spring容器中有哪些方式

    bean放入Spring容器中有哪些方式?
    的头像 发表于 09-19 15:25 558次阅读

    SpringBean的生命周期是怎样的?

    销毁 3. 写在最后 Spring Bean 的生命周期,面试时非常容易问,这不,前段时间就有个读者去面试, 因为不会回答这个问题,一面都没有过。 如果只讲基础知识,感觉和网上大多数文章没有区别
    的头像 发表于 10-11 15:08 1053次阅读

    Spring中获取bean的八种方式

    适用于Spring框架的独立应用程序,须要程序通过配置文件初始化Spring
    的头像 发表于 12-08 10:10 1157次阅读

    Spring Dependency Inject与Bean Scops注解

    DependsOn`注解可以配置Spring IoC容器在初始化一个Bean之前,先初始化其他的Bean对象。下面是此注解使用示例代码:
    的头像 发表于 04-07 11:35 477次阅读
    <b class='flag-5'>Spring</b> Dependency Inject与<b class='flag-5'>Bean</b> Scops注解

    Spring依赖注入Bean类型的8种情况

    今天来讲的一个你可能不曾注意的小东西,那就是Spring依赖注入支持注入Bean的类型,这个小东西可能看似没有用但是实际又有点小用。 其实本来这周没打算写文章,但是突然之间就想到了之前有个妹子问过这个问题,并且网上这块东西说的也不多,所以就赶在周末的末尾匆匆写下了这
    的头像 发表于 05-11 10:53 371次阅读
    <b class='flag-5'>Spring</b>依赖注入<b class='flag-5'>Bean</b>类型的8种情况

    27个非常经典的设备工作流程图解

    今天给大家分享27个非常经典的设备工作流程图解
    的头像 发表于 06-02 17:16 1087次阅读
    27个<b class='flag-5'>非常</b>经典的设备工作<b class='flag-5'>流程图解</b>

    Spring容器原始Bean是如何创建的?Spring源码中方法的执行顺序

    这个话题其实非常庞大,我本来想从 getBean 方法讲起,但一想这样讲完估计很多小伙伴就懵了,所以我们还是一步一步来,今天我主要是想和小伙伴们讲讲 Spring 容器创建 Bean 最最核心的 createBeanInstan
    的头像 发表于 08-04 10:12 438次阅读
    <b class='flag-5'>Spring</b>容器原始<b class='flag-5'>Bean</b>是如何创建的?<b class='flag-5'>Spring</b>源码中方法的执行顺序