Spring —— 容器内部逻辑

引言

上一篇关于IoC容器的详解《Spring —— IoC 容器详解》真是工程浩大,可以说Spring官网对核心中的核心IOC容器做了非常全面的使用说明,包括在《Spring揭秘》中让我一直没有成功的Method Injection,官网也解决了我的疑惑,并最终实验成功(未来会另起一篇单独对“方法注入”做以总结)。

Spring官网的容器说明虽然全面,但是对于容器内部的处理并未深入解释,因此本篇博客做理论性的补充,总结自王富强老师的《Spring揭秘》第四章——“容器背后的秘密”。而且,本篇文章在工作和面试中更具有理论性的指导意义。

一、概述

Spring容器通过某种方式加载xml(或注解、JavaConfig)中的配置数据,然后根据这些信息绑定整个系统的对象,最终组装成一个可用的基于轻量级容器的应用系统。这个过程分为两个阶段,即容器启动阶段Bean实例化阶段:

1、容器启动阶段(四步:加载、解析、组装BeanDefinition、注册):

容器刚开始启动时,首先加载配置,然后是解析并分析配置信息,将分析后的信息装配到相应的BeanDefinition,最后把将BeanDefinition注册到BeanDefinitionRegistry,启动完成。

总的来说,该阶段所做的工作可以认为是准备性的,重点更加侧重于对象管理信息的收集,一些验证性或辅助性的工作也可以在这个阶段完成。

2、Bean实例化阶段(四步:检查、实例化、装配、生命周期回调):

第一阶段后,所有的bean定义信息都通过BeanDefinition的方式注册到了BeanDefinitionRegistry中。当某个请求方通过getBean()方法明确地请求某个对象,或因依赖关系容器需要隐式地调用getBean方法时,就会触发第二阶段。

容器首先会检查所请求的对象之前是否已经初始化。如果没有,则会根据注册的BeanDefinition所提供的信息实例化被请求对象,并为其注入依赖。如果该对象实现了某些回调接口,也会根据回调接口(Aware)的要求来装配它。当该对象装配完毕之后,容器会立即将其返回请求方使用。

二、容器启动阶段的扩展

Spring提供了一种叫做BeanFactoryPostProcessor的容器扩展机制。

该机制允许我们在容器实例化对象之前,对注册到容器中的BeanDefinition进行修改。相当于在容器实现的第一阶段最后加入一道工序,让我们对最终的BeanDefinition做一些额外的操作,比如修改bean的某些属性,为bean定义增加其他信息等。

如果要自定义BeanFactoryPostProcessor,通常就需要实现该接口,因为一个容器可能拥有多个后处理器,因此可能需要同时实现Ordered接口(如果顺序确实必要)。因为Spring已经提供了几个现成的后处理器实现,因此大多数时候我们很少去实现某个后处理器。其中PropertyPlaceholderConfigurer和PropertyOverrideConfigurer是两个比较常用的BeanFactoryPostProcessor。

1、对于BeanFactory需要手动装配BeanFactoryPostProcessor。

2、对于ApplicationContext,可以自动识别配置中的BeanFactoryPostProcessor。xml配置形式如下:

<bean class=”...PropertyPlaceholderConfigurer”>
// 后处理器的一些属性
</bean>

三、bean的生命周期

容器在启动之后,并不会马上就实例化相应的bean定义。刚刚启动的容器仅仅拥有所有对象的BeanDefinition来保存实例化阶段将会用到的必要信息。

BeanFactory的getBean方法可以被客户端对象显式调用,也可以在容器内隐式调用。

隐式调用有以下两种情况:

1、对于BeanFactory来说,对象实例化默认采用延迟初始化。A依赖B,如果当程序请求A对象时,容器会检测A依赖的B是否已经实例化,如果没有会隐式调用getBean实例化B对象,这对于本次请求者是隐式的。

2、ApplicationContext启动之后会实例化所有的bean定义,在ApplicationContext的实现过程中,依然遵循Spring容器的两个阶段,只不过它会在启动阶段完成后,立刻调用所有bean定义的实例化方法getBean这就是为什么当你得到ApplicationContext类型的容器引用时,容器内所有对象已经被全部实例化完成)。

提示:AbstractBeanFactory类的getBean()方法的完整实现逻辑,和AbstractAutowiredCapableBeanFactory类的createBean()方法的全貌

3.1 Bean的实例化策略与BeanWrapper

容器在内部实现的时候,采用“策略模式”来决定采用何种方式实例化bean。

通常可以通过反射或CGLIB动态字节码生成来实例化相应的bean实例或动态生成其子类。

InstantiationStrategy接口定义了bean的实例化策略

SimpleInstantiationStrategy实现了简单的对象实例化功能,可以通过反射来实例化对象,但不支持方法注入方式的对象实例化。

CglibSubclassingInstantiationStrategy扩展了SimpleInstantiationStrategy,加入了CGLIB的动态字节码生成功能,可以动态的生成某个类的子类,满足了方法注入所需的对象实例化需求。这是容器默认采用的实例化策略。

容器只要根据相应的bean的BeanDefinition,和

CglibSubclassingInstantiationStrategy以及不同的bean定义类型,就可以返回实例化完成的对象实例。但不是直接返回构造完成的对象实例,而是以BeanWrapper对构造完成的bean进行了包裹,返回相应的BeanWrapper实例。到这里,第一步实例化结束。

BeanWrapper接口通常在Spring框架内部使用,它有一个实现类:BeanWrapperImpl,其作用是对某个bean进行“包裹”,然后对这个“包裹”的bean进行操作,比如设置或者获取bean的相应属性值。第一步结束后返回BeanWrapper实例而不是原先的对象实例,其目的就是为了第二步的“设置对象属性”。

BeanWrapper继承了PropertyAccessor接口,可以以统一的方式对对象属性进行访问,同时又继承了PropertyEditorRegistry和TypeConverter接口(间接)。当把各种PropertyEditor注册给容器时,后面就会被BeanWrapper用到。

在第一步构造完成对象之后,Spring会根据对象实例构造一个BeanWrapperImpl实例,然后将之前CustomEditorConfigurer注册的PropertyEditor复制一份给BeanWrapperImpl实例,这就是为什么BeanWrapper同时也是PropertyEditorRegistry,这样,BeanWrapper就可以完成类型转换、设置对象属性值等操作了。

以下是两段分别通过BeanWrapper和Java反射API来设置对象属性值和获取属性值的代码片段,相比于Java反射API,Spring提供的BeanWrapper操作起来更加流程简洁:

BeanWrapper方式:

Object provider = Class.forName(“package.name.FXNewsProvider”).newInstance();
Object listener = Class.forName(“...DowJonesNewsListener”).newInstance();
Object persister = Class.forName(“...DowJonesNewsPersister”).newInstance();
BeanWrapper newsProvider = new BeanWrapperImpl(provide);
newsProvider.setPropertyValue(“newsListener”, listener);
newsProvider.setPropertyValue(“newsPersister”, persister);
assertTrue(newsProvider.getWrapperedInstance() instanceof FXNewsProvider);
assertSame(provider, newsProvider.getWrapperedInstance());
assertSame(listener, newsProvider.getPropertyValue(“newsListener”));

Java反射API方式:

Object provider = Class.forName(“package.name.FXNewsProvider”).newInstance();
Object listener = Class.forName(“...DowJonesNewsListener”).newInstance();
Object persister = Class.forName(“...DowJonesNewsPersister”).newInstance();
Class providerClazz= provider.getClass();
Field listenerField = providerClazz.getField(“newsListener”);
listenerField.set(provider , listener);// 只演示listener属性设置,persister类似
assertSame(listener, listenerField.get(provider));

可以看出,Java反射API的方式在使用上相对混乱,且不便于记忆,而且还有紧随其后的各种异常需要处理(上面并未写出)。

3.2 Aware生命周期回调

当对象实例化完成并且相关属性以及依赖设置完成后,spring 容器会检查当前对象是否实现了一系列以Aware结尾的接口。如果是,则将Aware接口中规定的依赖注入给当前实例。

常见的Aware接口有:

1、BeanNameAware,容器会将bean定义对应的beanName设置到当前对象的实例。

2、BeanClassLoaderAware,容器会将对应加载当前bean的Classloader注入到当前对象实例。默认会使用加载springframework..ClassUtils类的Classloader。

3、BeanFactoryAware,BeanFactory容器会将自身设置到当前对象实例。当前对象就拥有了一个BeanFactory容器的引用,并且可以对这个容器内允许访问的对象按照需要进行访问。

这三个Aware接口只针对BeanFactory类型的容器而言,对于ApplicationContext类型的容器,也存在几个Aware相关接口。

ApplicationContext检测以下这些Aware接口并设置相关依赖的实现方式是通过BeanPostProcessor,这与前面的三个有所不同。不过设置Aware接口与BeanPostProcessor是相邻的,也可以放在一起讨论

1、ResourceLoaderAware,ApplicationContext实现了Spring的ResourceLoader接口,当容器检测到对象实现了ResourceLoaderAware接口后,会将当前ApplicationContext自身设置到对象中,这样当前对象就拥有了其所在ApplicationContext的一个引用。

2、ApplicationEventPublisherAware,ApplicationContext同样实现了ApplicationEventPublisher接口,这样,它就可以作为ApplicationEventPublisher来使用,所以,如果对象实现了ApplicationEventPublisherAware接口,容器就同样会将自身注入当前对象。

3、ApplicationContextAware,和前面一样,容器会将自身注入当前对象。

4、MessageSourceAware,和前面一样,容器会将自身注入当前对象。

3.3 BeanPostProcessor

如何与BeanFactoryPostProcessor区分?

BeanPostProcessor存在于对象实例化阶段,BeanFactoryPostProcessor存在于容器启动阶段。BeanFactoryPostProcessor会处理容器内所有符合条件的BeanDefinition,BeanPostProcessor会处理容器内所有符合条件的实例化后的对象实例。它包含两个接口方法:

postProcessBeforeInitialization(Object bean, String beanName);
postProcessAfterInitialization(Object bean, String beanName);

常见的使用场景是处理标记接口实现类,或者为当前对象提供代理实现。

ApplicationContext对应的那些Aware接口实际上就是通过BeanPostProcessor的方式进行处理的。当ApplicationContext中每个对象的实例化过程走到BeanPostProcessor前置处理这一步时,容器会检测到之前注册到容器的ApplicationContextAwareProcessor这个BeanPostProcessor的实现类,然后调用其postProcessBeforeInitialization方法,检查并设置Aware相关依赖:

if (bean instanceof EnvironmentAware) {((EnvironmentAware) bean).setEnvironment(this.applicationContext.getEnvironment());
}
if (bean instanceof EmbeddedValueResolverAware) {((EmbeddedValueResolverAware) bean).setEmbeddedValueResolver(this.embeddedValueResolver);
}
if (bean instanceof ResourceLoaderAware) {((ResourceLoaderAware) bean).setResourceLoader(this.applicationContext);
}
if (bean instanceof ApplicationEventPublisherAware) {((ApplicationEventPublisherAware) bean).setApplicationEventPublisher(this.applicationContext);
}
if (bean instanceof MessageSourceAware) {((MessageSourceAware) bean).setMessageSource(this.applicationContext);
}
if (bean instanceof ApplicationContextAware) {((ApplicationContextAware) bean).setApplicationContext(this.applicationContext);
}

3.4 自定义BeanPostProcessor

首先需要明确,BeanPostProcessor处理的是bean实例化阶段的一个过程,因此需要处理的对象必须是bean,即必须注册到ApplicationContext容器中,才能够被该接口实现类处理。

第一步:明确需要执行的操作,比如需要提前为目标bean的某个属性做转化,或赋值。然后定义该功能的标记接口,提供可以被BeanPostProcessor访问的接口方法。

第二步:让目标bean实现标记接口。

第三步:实现BeanPostProcessor接口,并注册到容器。通过instanceof关键字判断目标bean是否为标记接口的子类,如果是,则进行相关处理逻辑,最后返回bean。

注意,标记接口和BeanPostProcessor接口本身不存在直接的继承或依赖关系,另外,标记接口实际上并不是必须的,这是为了更好的限定处理逻辑的操作范围而存在的,也就是说,我们可以通过自定义的BeanPostProcessor子类,通过instanceof直接判断是否为目标bean类型的对象,然后直接处理,但这样可能会在BeanPostProcessor中暴露太多无关于处理逻辑的属性细节。标记接口很好的隔绝了BeanPostProcessor与目标bean类型之间的耦合。

3.5 InitializingBean和init-method

InitializingBean是容器内部广泛使用的一个对象生命周期标识接口。

它只有一个接口方法:afterPropertiesSet()。其作用是在对象实例化过程调用过“BeanPostProcessor的前置处理”之后,会接着检测当前对象是否实现了InitializingBean接口,如果是,则会调用afterPropertiesSet()方法进一步调整对象实例的状态。

比如,在某些情况下,某个业务对象实例化完成后,还不能处于可以使用的状态。这个时候就可以让该业务对象实现该接口,并在方法afterPropertiesSet()中完成对该业务对象的后续处理。

实现这个接口会让Spring框架带有侵入性,因此init-method属性可以替代“实现InitializingBean接口”完成初始化方法。另外,可以让bean直接定义名为init()的方法,Spring容器也会在这一阶段执行初始化逻辑。

一般,我们在集成第三方库,或者其他特殊的情况下,才会需要使用该特性。

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/558682.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

2020 年度总结

2020年给我的感觉是短平快的一年。 由于年初的新冠肺炎疫情&#xff0c;我大半年都呆在北京的破旧出租屋里写代码。整个春天和夏天&#xff0c;平平无奇。 2月1日返京&#xff0c;居家办公&#xff0c;夜跑。8月复工&#xff0c;疯狂爆痘、烂脸&#xff0c;月末落户天津。9月…

JDBC——概述与JDBC的使用

引言 一直希望深入学习一下数据库持久化技术&#xff0c;接触过Hibernate、Mybatis&#xff0c;也使用过Spring事务管理来控制回滚操作&#xff0c;但是越发觉得底层知识有一定的知识盲区和空洞。 很多ORM框架都是基于JDBC规范来进行构建的&#xff0c;因此&#xff0c;学习J…

JDBC——编程式事务的实现逻辑

引言 数据库事务的概念和基础&#xff0c;总结在《MySQL 基础 ————事务与隔离级别总结》。 本篇博客通过“JDBC 纯编码”方式实现事务控制&#xff0c;完成一个 A 给 B 转账的小功能&#xff0c;在进一步熟练JDBC的编程流程的同时&#xff0c;重点关注 Java 语言如何操作…

排序算法——插入排序

一、算法思想 给定一个无序数列&#xff0c;模拟一个指针从第0位开始向后&#xff0c;始终保持当前位置左边的数列是有序的。 指针位置上的元素依次与前面的元素比较&#xff0c;当遇到小于自己的数或右边已经没有元素时&#xff0c;都停止比较&#xff0c;开始下一轮。 生活…

Linux 底层原理 —— epoll 与多路复用

引言 epoll 是 Linux 系统下高性能网络服务的必备技术&#xff0c;很多面试中高频出现的 Nginx、Redis 都使用了这一技术&#xff0c;本文总结 linux 多路复用模型的演变过程&#xff0c;看一看epoll 是如何实现高性能的。 一、相关基础知识 1.1 文件描述符 文件描述符&…

异或运算的应用

一、基础知识 异或运算&#xff0c;相异为1。 异或运算是一种常用的位运算&#xff0c;在算法题中&#xff0c;对于避免额外的空间复杂度有独特的用处。 异或运算也被称为“无进位相加”&#xff0c;它具有以下特性&#xff1a; 特性1&#xff1a;0 ^ N N 特性2&#xff1a…

单向队列、双端队列、栈的模型实现

引言 自己实现简单的队列、栈的逻辑结构。 队列都包含头和尾两个指针&#xff0c;简单的单向队列只能在一端&#xff08;如&#xff1a;head端&#xff09;入列&#xff0c;在另一端&#xff08;如&#xff1a;tail 端&#xff09;出列&#xff1b;双端队列可以在 head 进出&…

递归算法及其时间复杂度分析

引言 “递归” 一词是比较专业的计算机术语&#xff0c;在现实生活中&#xff0c;有一个更可爱的词——“套娃”。如果把“递归算法”叫做“套娃算法”&#xff0c;或许可以减少一些恐惧程度。 套娃是有限的&#xff0c;同样&#xff0c;递归也是有限的&#xff0c;这和我们经…

算法设计中的基础常用代码

引言 本篇博客旨在记录一些基础算法知识的常见组合用法&#xff0c;以及何时使用&#xff0c;需要注意的问题等&#xff0c;长期更新。 为什么要这样总结呢&#xff1f;难道掌握了位运算、常用算法工具API的定义还不够吗&#xff1f; 这是因为某些知识比如 &、 |、 ~、 …

Redis —— 常用命令一览

引言 参考《菜鸟教程 Redis 常用命令》&#xff0c;其中红色为极其重要&#xff0c;蓝色为重要。 一、总览 二、key相关命令 三、String 相关命令 四、Hash 相关命令 五、List 相关命令 六、Set 相关命令 七、ZSet 相关命令

Redis 实用技术——消息发布和订阅

引言 发布订阅模型是redis的重要功能&#xff0c;它可以像网站动态一样&#xff0c;将消息发送到多个订阅者的主页里。 一、常用命令 二、消息格式 消息是一个有三个元素的多块响应&#xff1a; 如上图&#xff0c;发布者向 mysub 频道发送了一条消息&#xff0c;redis会返回…

Redis 实用技术——事务

引言 redis的事务不像关系型数据库的事务那样完整。 “快”是redis的特征&#xff0c;在事务管理的过程中&#xff0c;使用muti命令开启事务块&#xff0c;当输入多条命令后&#xff0c;再使用exec命令执行事务块中的全部命令。 Redis事务可以保证两件事&#xff1a; 1、隔…

排序算法——归并排序的相关问题

一、小和问题 问题描述&#xff0c;给定一个数组&#xff0c;如[1, 3, 2, 6, 5]&#xff0c;计算每个数左边小于自己的所有数的和&#xff0c;并累加。例如&#xff1a; 1左边没有数 3左边有一个小于自己的数 1 2左边有一个小于自己的数 1 6左边有三个小于自己的数 1 3 2 6…

经典数据结构——堆的实现

一、完全二叉树 堆是一种完全二叉树&#xff0c;什么是完全二叉树&#xff1f; 简单的说&#xff0c;一棵满二叉树表示的是所有节点全部饱和&#xff0c;最后一层全部占满&#xff1a; 而完全二叉树指的是满二叉树的最后一层&#xff0c;所有叶子节点都从左往顺序排满&#x…

排序算法 —— 堆排序

引言 此文基于《经典数据结构——堆的实现》中堆结构&#xff0c;实现一个以堆处理排序的算法。 一、算法思想 基于堆结构的堆排序的算法思想非常简单&#xff0c;循环获取大根堆中的最大值&#xff08;0位置的根节点&#xff09;放到堆的末尾&#xff0c;直到将堆拿空。 由…

经典数据结构——前缀树

引言 前缀树——trie /ˈtraɪ//树&#xff0c;也叫作“单词查找树”、“字典树”。 它属于多叉树结构&#xff0c;典型应用场景是统计、保存大量的字符串&#xff0c;经常被搜索引擎系统用于文本词频统计。它的优点是利用字符串的公共前缀来减少查找时间&#xff0c;最大限度…

排序算法 —— 计数排序

引言 计数排序是桶排序思想的一种具体实现&#xff0c;针对一些具有特殊限制的样本数据&#xff0c;如公司员工年龄&#xff0c;那么样本数据本身就一定在0~200之间&#xff0c;针对这样的数据&#xff0c;使用从0到200 的桶数组&#xff0c;桶的位置已经是有序的&#xff0c;…

Java多线程 —— 线程状态迁移

引言 线程状态迁移&#xff0c;又常被称作线程的生命周期&#xff0c;指的是线程从创建到终结需要经历哪些状态&#xff0c;什么情况下会出现哪些状态。 线程的状态直接关系着并发编程的各种问题&#xff0c;本文就线程的状态迁移做一初步探讨&#xff0c;并总结在何种情况下…

Java中的Unsafe

Java和C语言的一个重要区别就是Java中我们无法直接操作一块内存区域&#xff0c;不能像C中那样可以自己申请内存和释放内存。Java中的Unsafe类为我们提供了类似C手动管理内存的能力。 Unsafe类&#xff0c;全限定名是sun.misc.Unsafe&#xff0c;从名字中我们可以看出来这个类对…

arm中断保护和恢复_浅谈ARM处理器的七种异常处理

昨天的文章&#xff0c;我们谈了ARM处理器的七种运行模式&#xff0c;分别是&#xff1a;用户模式User(usr)&#xff0c;系统模式System(sys)&#xff0c;快速中断模式(fiq)&#xff0c;管理模式Supervisor(svc)&#xff0c;外部中断模式(irq)&#xff0c;数据访问中止模式Abor…