网站 做 专家问答,站内推广方式,重庆做网站公司哪家好,cpu优化软件很多人把spring的相关内容当作背八股文#xff0c;认为只在面试时能用上#xff0c;实际开发根本用不到。实际上早期的我也是这么想的#xff0c;但随着开发年限的增长#xff0c;解决了越来越多的难题后#xff0c;不得不承认#xff0c;这些基础知识的学习有着无法替代…很多人把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的生成逻辑吧
2创建Bean的极简流程 我们开门见山直接以单例对象为例子说一个Bean的极简流程以及其目的
获取Bean定义 扫描工程内所有被标记的Bean获取其类型名称属性构造方法等信息存在一个Map里
生成实例 这一步也很简单遍历上述Map利用Bean定义里的无参构造方法创建对象和new 对象同理
属性装填 刚创建的对象所有属性都是默认值需要我们给它装填上需要的内容
初始化 如果这个Bean实现了InitializingBean接口则会调用你写在afterPropertiesSet方法里的内容。
到此一个Bean就创建完毕了是不是很简单是的很简单逻辑也很清晰。
当然上面四步是核心功能Spring为了增强对这些Bean的修改能力在2-生成实例 3-属性装填 4-初始化的前后都预留了处理点Spring自己或用户都可以通过编写Bean后置处理器BeanPostProcessor来实现自己的目的这些处理器会在对应的处理点被执行从而完成对Bean的修改下面会详细讲一下
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中写有工厂后置处理器的实现
图片 看名字也知道这个处理器起了扫描的作用找到了被我们标记的接口并“捏造”一个Bean定义并把Bean的类型设置为MapperFactoryBean.class即工厂类然后把它添加到Bean定义注册器中。
而在我们需要实例化这个Bean的时候mybatis又会从这个工厂对象中使用getObject()为我们取出一个Bean实例这个Bean实例是使用我们写的Mapper接口产生的代理而后再把这个代理放入Spring容器
图片 BeanPostProcessor 而Bean后置处理器则更加常见种类也更丰富他们的详细作用和工作时机都可以在下图中看到
图片 契机问题的解决 让我们回到契机里提到的那个问题这个问题简化的讲其实就是有这么一个Spring内部的Bean名字为org.springframework.aop.config.internalAutoProxyCreator它有一个属性proxy-target-class这个属性决定了Spring动态代理的生成用的jdk动态代理还是CGlib然而在很多地方三方包已经给他赋值。
我们必须在它被其他三方包赋值后 把它的属性值改为false。这个问题最终怎么做到的呢就是利用了后置处理器此处使用工厂后置处理器找到该Bean定义修改其Bean属性
图片 4引用与缓存 从上面看似乎创建一个Bean只需要四步(忽略后置处理器的步骤)十分简单。确实如果我们的项目只需生成一个Bean那只要按序完成这四步就可以了。
但实际上Spring本身和我们的项目要生成的Bean数量远不止一个复杂的项目一般会达到上千个BeanBean之间还有复杂的引用关系。我们不仅要存储这些Bean还必须考虑到这些引用情形从而引入缓存的机制。
引用已有的Bean 图片 如图上述是一种最简单的引用Parent 里面引用了 Child ,。理想的情况下我们先创建了Child并保存起来那么在创建Parent的时候直接引用现成的Child就好此处用DependsOn保证这种顺序。那么这时我们可以说容器只需要使用一级缓存就像养鸡场里饲养着许多鸡这个缓存里存的就是各个现成的Bean直接取用即可。
引用未创建的Bean 上述的Parent 里面引用了 Child案例只是一种理想情况实际上大部分的Bean之间加载顺序并不会特意指定创建的先后顺序自然没了保障spring会执行默认的加载顺序如字母排序。
比如这个案例如果先创建的是Parent那么当我们做到属性装填这一步的时候就会发现Parent的属性里引用了一个未知的Bean —— Child。
图片 这个时候Spring就会去搜寻并创建Child此时Parent的创建就停滞了。那么这个创建未半而中道崩殂的Parent也需要有一个地方存起来啊。你或许会说还是存在上面的一级缓存里面不行吗
当然可以但本着人以类聚物以群分的观念对于这些创建了一半就中断的Bean我们还是专门引入了三级缓存供其栖息。我们知道此时Parent已经实例化了但属性装填没完成像个未孵化的蛋而三级缓存就是个保温箱是存放这些“蛋”的地方。实际上三级缓存里存的全是Bean工厂可以通过Bean工厂的getEarlyBeanReference获取到这个未完成的Bean蛋。
循环引用循环依赖 如果不仅Parent里面引用了ChildChild里面也引用了Parent那么显然这就构成了循环引用。
图片 我们假定Spring先加载了Parent后发现需要注入Child又去加载Child过程中又发现需要注入Parent那么又去加载Parent…… 那Spring会这么无限的加载下去吗
答案我们都知道自然是不会的。实际上每开始加载一个BeanSpring都会把Bean名称记录在一个叫SingletonCurrentlyInCreation的Set集合里。
顾名思义这个集合里都是正在创建中的Bean这个集合在其他的文档中很少提及但显然他的作用十分巨大。因为第二次加载Parent时Spring就发现Parent已经在这个集合中了才意识到进入循环引用了。
图片 当发现进入循环引用后自然Spring不会再傻乎乎的走再走一遍Parent的加载逻辑而是从三级缓存中取出未完成的Bean做一些处理后然后将其放入二级缓存。
这一过程相当于从保温箱取出来未孵化的鸡蛋孵化出小鸡后放到专门的小鸡培养室中。而此时只需要返回这只小鸡Parent就可以了你或许会说我要的是成品鸡你给我小鸡有什么用功能什么的能有保障吗别急我下面就为你解释这样的可行性。
循环引用中的代理 我们都知道Parent是创建了一半被放入缓存中的此时它已经完成的步骤是生成实例正在卡着的步骤是属性装填和初始化被从缓存中取出后这两个步骤仍然是未完成的但我们无需担心因为此刻我们仅需完成引用即我要引用Parent成鸡你现在给我返回半成品小鸡也没关系因为我现在也不是要立刻就用你只要你保证小鸡 成鸡在内存中的地址一样即可即小鸡和成鸡是同一个对象。
你或许会问小鸡长着长着还能变了人不成怎么可能小鸡和成鸡就不是同一个对象了呢这就不得不谈代理模式了
图片 我们这里不去细谈代理流程你只需要知道代理模式会产生一个新的对象相当于一个霸道中介原本你可以直接联系小鸡现在小鸡的联系被中介切断了你需要找小鸡就只能联系中介。所以一旦成鸡后续需要代理我们需要联系的就是成鸡的代理了此时你给我小鸡的联系方式不顶用。
为避免这种情况我们只能给小鸡生成中介。是的原来中介是只给成鸡用的但现在不得不提前到小鸡阶段了生成中介后返回给我们小鸡的-中介的-联系方式即半成品Bean的-代理的-引用事实上如果你看源码对成品和半成品Bean生成代理用的是同一个方法wrapIfNecessary因此生成代理的效果是一样的。当然你也许仍然有顾虑对成品和半成品生成代理真的没差别吗
的确这里就不得不提Spring的代理的特殊点了代理的基础就是大名鼎鼎的AOP 或者说 切面增强然而Spring的增强仅针对方法。而半成品和成品最大的差异是属性值方法却是一样的因此增强的效果肯定是一样的。如果哪天Spring的代理生成时会用到当前属性值那不同阶段的代理功能才会有差异。
5三级缓存的解读 关于三级缓存市面上有太多的解读文章也是面试时经常问到的点我们不妨解读一下三级缓存。
图片 我们平常说的三级缓存大多数人会想到CPU的三级缓存硬件上之所以缓存分级是对于成本与性能的考量一级缓存最快所以CPU优先从一级缓存取东西但同样一级缓存最贵存不了太多数据所以需要二级缓存。
而这里三级缓存并没有性能上的区别所以划分三级缓存并非必须。实际上一个Bean在同一时间只会出现在某一级缓存中因此我们可以直接产出一个暴论Spring可以不用所谓三级缓存甚至说只需要一个集合就能存下全部
但为什么这里要这么做因为这是逻辑分层而非必要分层三级缓存存着不同状态的Bean罢了一级缓存存成品鸡二级缓存存小鸡三级缓存存鸡蛋 一级比一级原始你要非把成品鸡、小鸡、鸡蛋搁一个房子里也不是不行所以这种分层是基于逻辑清晰而非逻辑必需。
这里还有个误区很多人说是因为代理的存在导致需要三级缓存如果没有代理两级就够了。实际上三级缓存并不是因为代理导致的不管有没有代理都是三级缓存。
就像我说的一级缓存存成品鸡二级缓存存小鸡三级缓存存鸡蛋 这里面并不区分代理成品鸡或者成品鸡的代理都在一级缓存小鸡或者小鸡的代理都在二级缓存。
实际上我们看代码只要发生了循环引用都会导致Bean从三级缓存取出并放入二级缓存。这个过程中执行wrapIfNecessary不管生不生成代理都是一样的只不过如果需要代理放入二级缓存的是小鸡的代理如果不需要代理放入二级缓存的就是小鸡本鸡因此我们可以说 不管有没有代理三级缓存的模式都没有变化。
6创建Bean的极详细流程 多说无益我根据Spring4的源码整理了一份详细的生成流程这图说是全网最细也不为过欢迎大家补充和指正