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月…

Spring 全局异常捕获

引言 前后端分离开发&#xff0c;后台有时候会出现不可预知的异常&#xff08;运行时异常&#xff09;&#xff0c;在实际生产中通常需要统一返回符合一定响应结构的异常信息给前端&#xff0c;这一方面可以避免用户看到后台的报错信息&#xff0c;一方面也是保护后端程序免受…

JDBC——概述与JDBC的使用

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

JDBC——实现通用的查询

引言 上一篇jdbc的文章《JDBC——概述与JDBC的使用》介绍了JDBC的概念和背景知识&#xff0c;同时也讨论了获取数据库连接的方式&#xff0c;以及简单的实现了入库操作&#xff08;更新、删除同理&#xff09;。 本篇博客将会聚焦 PreparedStatement 的查询操作、以及 Result…

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;我们使用Math.random() 方法可以生成一个随机乱序数组&#xff0c;并通过Arrays.sort 来比较我们的算法是否正确。 代码实现 对数器主要是使用对照来校验是否正确&#xff0c;除了一些比较方法、拷贝方法&#xff0c;…

查找算法——二分法

引言 二分法&#xff0c;顾名思义&#xff0c;即一分为二的方法&#xff0c;通常用于判断在某个有序数列中是否存在某个数&#xff0c;由于其优秀的算法思想&#xff0c;时间复杂度一般都是 O(logN) &#xff0c;通常要 O(N) 的遍历方式更加优秀。 一、经典二分法查找 最常见…

异或运算的应用

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

链表的基本操作——反转与删除

引言 链表相关的问题几乎都是coding问题&#xff0c;以下是两个简单的链表问题。 一、单链表或双链表如何反转 1.1 单链表的反转操作 给定一个 Node 结构&#xff1a; public static class Node {public int value;public Node next;public Node(int data) {this.value d…

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

引言 自己实现简单的队列、栈的逻辑结构。 队列都包含头和尾两个指针&#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 实用技术——Pipeline

引言 Redis是一种基于客户端-服务端模型以及请求/响应协议的TCP服务。 这意味着通常情况下一个请求会遵循以下步骤&#xff1a; 客户端向服务端发送一个查询请求&#xff0c;并监听Socket返回&#xff0c;通常是以阻塞模式&#xff0c;等待服务端响应。服务端处理命令&#x…

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;它的算法思想用到了如递归、荷兰国旗问题等诸多元素&#xff0c;还意外的引入了随机性的概念。 以下将逐步总结三个版本的快速排序&#xff0c;由浅入深总结快速排序的经典实现过程。 荷兰国旗问题参考&#xff1a;…