Bean的作用域
5.1 singleton
默认情况下,Spring的IoC容器创建的Bean对象是单例的。来测试一下:
执行结果:
通过测试得知:Spring的IoC容器中,默认情况下,Bean对象是单例的。
这个对象在什么时候创建的呢?可以为SpringBean提供一个无参数构造方法,测试一下,如下:
将测试程序中getBean()所在行代码注释掉:
执行测试程序:
通过测试得知,默认情况下,Bean对象的创建是在初始化Spring上下文的时候就完成的。
5.2 prototype
如果想让Spring的Bean对象以多例的形式存在,可以在bean标签中指定scope属性的值为:prototype,这样Spring会在每一次执行getBean()方法的时候创建Bean对象,调用几次则创建几次。
执行结果:
我们可以把测试代码中的getBean()方法所在行代码注释掉:
执行结果:
可以看到这一次在初始化Spring上下文的时候,并没有创建Bean对象。
那你可能会问:scope如果没有配置,它的默认值是什么呢?默认值是singleton,单例的。
执行结果:
通过测试得知,没有指定scope属性时,默认是singleton单例的。
5.3 其它scope
scope属性的值不止两个,它一共包括8个选项:
●singleton:默认的,单例。
●prototype:原型。每调用一次getBean()方法则获取一个新的Bean对象。或每次注入的时候都是新对象。
●request:一个请求对应一个Bean。仅限于在WEB应用中使用。
●session:一个会话对应一个Bean。仅限于在WEB应用中使用。
●global session:portlet应用中专用的。如果在Servlet的WEB应用中使用global session的话,和session一个效果。(portlet和servlet都是规范。servlet运行在servlet容器中,例如Tomcat。portlet运行在portlet容器中。)
●application:一个应用对应一个Bean。仅限于在WEB应用中使用。
●websocket:一个websocket生命周期对应一个Bean。仅限于在WEB应用中使用。
●自定义scope:很少使用。
接下来咱们自定义一个Scope,线程级别的Scope,在同一个线程中,获取的Bean都是同一个。跨线程则是不同的对象:(以下内容作为了解)
●第一步:自定义Scope。(实现Scope接口)
○spring内置了线程范围的类:org.springframework.context.support.SimpleThreadScope,可以直接用。
●第二步:将自定义的Scope注册到Spring容器中。
spring-scope.xml
XML
复制代码
<beanclass="org.springframework.beans.factory.config.CustomScopeConfigurer">
<propertyname="scopes">
<map>
<entrykey="myThread">
<beanclass="org.springframework.context.support.SimpleThreadScope"/>
entry>
map>
property>
bean>
●第三步:使用Scope。
spring-scope.xml
XML
复制代码
1
<beanid="sb"class="com.powernode.spring6.beans.SpringBean"scope="myThread"/>
编写测试程序:
Java
复制代码
@Test
publicvoidtestCustomScope(){
ApplicationContextapplicationContext=newClassPathXmlApplicationContext("spring-scope.xml");
SpringBeansb1=applicationContext.getBean("sb",SpringBean.class);
SpringBeansb2=applicationContext.getBean("sb",SpringBean.class);
System.out.println(sb1);
System.out.println(sb2);
// 启动线程
newThread(newRunnable(){
@Override
publicvoidrun(){
SpringBeana=applicationContext.getBean("sb",SpringBean.class);
SpringBeanb=applicationContext.getBean("sb",SpringBean.class);
System.out.println(a);
System.out.println(b);
}
}).start();
}
执行结果:
六、GoF之工厂模式
●设计模式:一种可以被重复利用的解决方案。
●GoF(Gang of Four),中文名——四人组。
●《Design Patterns: Elements of Reusable Object-Oriented Software》(即《设计模式》一书),1995年由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合著。这几位作者常被称为"四人组(Gang of Four)"。
●该书中描述了23种设计模式。我们平常所说的设计模式就是指这23种设计模式。
●不过除了GoF23种设计模式之外,还有其它的设计模式,比如:JavaEE的设计模式(DAO模式、MVC模式等)。
●GoF23种设计模式可分为三大类:
○创建型(5个):解决对象创建问题。
■单例模式
■工厂方法模式
■抽象工厂模式
■建造者模式
■原型模式
○结构型(7个):一些类或对象组合在一起的经典结构。
■代理模式
■装饰模式
■适配器模式
■组合模式
■享元模式
■外观模式
■桥接模式
○行为型(11个):解决类或对象之间的交互问题。
■策略模式
■模板方法模式
■责任链模式
■观察者模式
■迭代子模式
■命令模式
■备忘录模式
■状态模式
■访问者模式
■中介者模式
■解释器模式
●工厂模式是解决对象创建问题的,所以工厂模式属于创建型设计模式。这里为什么学习工厂模式呢?这是因为Spring框架底层使用了大量的工厂模式。
6.1 工厂模式的三种形态
工厂模式通常有三种形态:
●第一种:简单工厂模式(Simple Factory):不属于23种设计模式之一。简单工厂模式又叫做:静态 工厂方法模式。简单工厂模式是工厂方法模式的一种特殊实现。
●第二种:工厂方法模式(Factory Method):是23种设计模式之一。
●第三种:抽象工厂模式(Abstract Factory):是23种设计模式之一。
6.2 简单工厂模式
简单工厂模式的角色包括三个:
●抽象产品 角色
●具体产品 角色
●工厂类 角色
简单工厂模式的代码如下:
抽象产品角色:
具体产品角色:
工厂类角色:
测试程序(客户端程序):
执行结果:
简单工厂模式的优点:
●客户端程序不需要关心对象的创建细节,需要哪个对象时,只需要向工厂索要即可,初步实现了责任的分离。客户端只负责“消费”,工厂负责“生产”。生产和消费分离。
简单工厂模式的缺点:
●缺点1:工厂类集中了所有产品的创造逻辑,形成一个无所不知的全能类,有人把它叫做上帝类。显然工厂类非常关键,不能出问题,一旦出问题,整个系统瘫痪。
●缺点2:不符合OCP开闭原则,在进行系统扩展时,需要修改工厂类。
Spring中的BeanFactory就使用了简单工厂模式。
6.3 工厂方法模式
工厂方法模式既保留了简单工厂模式的优点,同时又解决了简单工厂模式的缺点。
工厂方法模式的角色包括:
●抽象工厂角色
●具体工厂角色
●抽象产品角色
●具体产品角色
代码如下:
客户端程序:
执行客户端程序:
如果想扩展一个新的产品,只要新增一个产品类,再新增一个该产品对应的工厂即可,例如新增:匕首
客户端程序:
执行结果如下:
我们可以看到在进行功能扩展的时候,不需要修改之前的源代码,显然工厂方法模式符合OCP原则。
工厂方法模式的优点:
●一个调用者想创建一个对象,只要知道其名称就可以了。
●扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。
●屏蔽产品的具体实现,调用者只关心产品的接口。
工厂方法模式的缺点:
●每次增加一个产品时,都需要增加一个具体类和对象实现工厂,使得系统中类的个数成倍增加,在一定程度上增加了系统的复杂度,同时也增加了系统具体类的依赖。这并不是什么好事。
6.4 抽象工厂模式(了解)
抽象工厂模式相对于工厂方法模式来说,就是工厂方法模式是针对一个产品系列的,而抽象工厂模式是针对多个产品系列的,即工厂方法模式是一个产品系列一个工厂类,而抽象工厂模式是多个产品系列一个工厂类。
抽象工厂模式特点:抽象工厂模式是所有形态的工厂模式中最为抽象和最具一般性的一种形态。抽象工厂模式是指当有多个抽象角色时,使用的一种工厂模式。抽象工厂模式可以向客户端提供一个接口,使客户端在不必指定产品的具体的情况下,创建多个产品族中的产品对象。它有多个抽象产品类,每个抽象产品类可以派生出多个具体产品类,一个抽象工厂类,可以派生出多个具体工厂类,每个具体工厂类可以创建多个具体产品类的实例。每一个模式都是针对一定问题的解决方案,工厂方法模式针对的是一个产品等级结构;而抽象工厂模式针对的是多个产品等级结果。
抽象工厂中包含4个角色:
●抽象工厂角色
●具体工厂角色
●抽象产品角色
●具体产品角色
抽象工厂模式的类图如下:
抽象工厂模式代码如下:
第一部分:武器产品族
第二部分:水果产品族
第三部分:抽象工厂类
第四部分:具体工厂类
第五部分:客户端程序
执行结果:
抽象工厂模式的优缺点:
●优点:当一个产品族中的多个对象被设计成一起工作时,它能保证客户端始终只使用同一个产品族中的对象。
●缺点:产品族扩展非常困难,要增加一个系列的某一产品,既要在AbstractFactory里加代码,又要在具体的里面加代码。
七、Bean的实例化方式
Spring为Bean提供了多种实例化方式,通常包括4种方式。(也就是说在Spring中为Bean对象的创建准备了多种方案,目的是:更加灵活)
●第一种:通过构造方法实例化
●第二种:通过简单工厂模式实例化
●第三种:通过factory-bean实例化
●第四种:通过FactoryBean接口实例化
7.1 通过构造方法实例化
我们之前一直使用的就是这种方式。默认情况下,会调用Bean的无参数构造方法。
执行结果:
7.2 通过简单工厂模式实例化
第一步:定义一个Bean
第二步:编写简单工厂模式当中的工厂类
第三步:在Spring配置文件中指定创建该Bean的方法(使用factory-method属性指定)
第四步:编写测试程序
执行结果:
7.3 通过factory-bean实例化
这种方式本质上是:通过工厂方法模式进行实例化。
第一步:定义一个Bean
第二步:定义具体工厂类,工厂类中定义实例方法
第三步:在Spring配置文件中指定factory-bean以及factory-method
第四步:编写测试程序
执行结果:
7.4 通过FactoryBean接口实例化
以上的第三种方式中,factory-bean是我们自定义的,factory-method也是我们自己定义的。
在Spring中,当你编写的类直接实现FactoryBean接口之后,factory-bean不需要指定了,factory-method也不需要指定了。
factory-bean会自动指向实现FactoryBean接口的类,factory-method会自动指向getObject()方法。
第一步:定义一个Bean
第二步:编写一个类实现FactoryBean接口
第三步:在Spring配置文件中配置FactoryBean
测试程序:
执行结果:
FactoryBean在Spring中是一个接口。被称为“工厂Bean”。“工厂Bean”是一种特殊的Bean。所有的“工厂Bean”都是用来协助Spring框架来创建其他Bean对象的。
7.5 BeanFactory和FactoryBean的区别
7.5.1 BeanFactory
Spring IoC容器的顶级对象,BeanFactory被翻译为“Bean工厂”,在Spring的IoC容器中,“Bean工厂”负责创建Bean对象。
BeanFactory是工厂。
7.5.2 FactoryBean
FactoryBean:它是一个Bean,是一个能够辅助Spring实例化其它Bean对象的一个Bean。
在Spring中,Bean可以分为两类:
●第一类:普通Bean
●第二类:工厂Bean(记住:工厂Bean也是一种Bean,只不过这种Bean比较特殊,它可以辅助Spring实例化其它Bean对象。)
7.6 注入自定义Date
我们前面说过,java.util.Date在Spring中被当做简单类型,简单类型在注入的时候可以直接使用value属性或value标签来完成。但我们之前已经测试过了,对于Date类型来说,采用value属性或value标签赋值的时候,对日期字符串的格式要求非常严格,必须是这种格式的:Mon Oct 10 14:30:26 CST 2022。其他格式是不会被识别的。如以下代码:
执行结果:
如果把日期格式修改一下:
执行结果:
这种情况下,我们就可以使用FactoryBean来完成这个骚操作。
编写DateFactoryBean实现FactoryBean接口:
编写spring配置文件:
执行测试程序:
八、Bean的生命周期
8.1 什么是Bean的生命周期
Spring其实就是一个管理Bean对象的工厂。它负责对象的创建,对象的销毁等。
所谓的生命周期就是:对象从创建开始到最终销毁的整个过程。
什么时候创建Bean对象?
创建Bean对象的前后会调用什么方法?
Bean对象什么时候销毁?
Bean对象的销毁前后调用什么方法?
8.2 为什么要知道Bean的生命周期
其实生命周期的本质是:在哪个时间节点上调用了哪个类的哪个方法。
我们需要充分的了解在这个生命线上,都有哪些特殊的时间节点。
只有我们知道了特殊的时间节点都在哪,到时我们才可以确定代码写到哪。
我们可能需要在某个特殊的时间点上执行一段特定的代码,这段代码就可以放到这个节点上。当生命线走到这里的时候,自然会被调用。
8.3 Bean的生命周期之5步
Bean生命周期的管理,可以参考Spring的源码:AbstractAutowireCapableBeanFactory类的doCreateBean()方法。
Bean生命周期可以粗略的划分为五大步:
●第一步:实例化Bean
●第二步:Bean属性赋值
●第三步:初始化Bean
●第四步:使用Bean
●第五步:销毁Bean
编写测试程序:
定义一个Bean
执行结果:
需要注意的:
●第一:只有正常关闭spring容器,bean的销毁方法才会被调用。
●第二:ClassPathXmlApplicationContext类才有close()方法。
●第三:配置文件中的init-method指定初始化方法。destroy-method指定销毁方法。
8.4 Bean生命周期之7步
在以上的5步中,第3步是初始化Bean,如果你还想在初始化前和初始化后添加代码,可以加入“Bean后处理器”。
编写一个类实现BeanPostProcessor类,并且重写before和after方法:
在spring.xml文件中配置“Bean后处理器”:
一定要注意:在spring.xml文件中配置的Bean后处理器将作用于当前配置文件中所有的Bean。
执行测试程序:
如果加上Bean后处理器的话,Bean的生命周期就是7步了:
8.5 Bean生命周期之10步
如果根据源码跟踪,可以划分更细粒度的步骤,10步:
上图中检查Bean是否实现了Aware的相关接口是什么意思?
Aware相关的接口包括:BeanNameAware、BeanClassLoaderAware、BeanFactoryAware
●当Bean实现了BeanNameAware,Spring会将Bean的名字传递给Bean。
●当Bean实现了BeanClassLoaderAware,Spring会将加载该Bean的类加载器传递给Bean。
●当Bean实现了BeanFactoryAware,Spring会将Bean工厂对象传递给Bean。
测试以上10步,可以让User类实现5个接口,并实现所有方法:
●BeanNameAware
●BeanClassLoaderAware
●BeanFactoryAware
●InitializingBean
●DisposableBean
代码如下:
执行结果:
通过测试可以看出来:
●InitializingBean的方法早于init-method的执行。
●DisposableBean的方法早于destroy-method的执行。
对于SpringBean的生命周期,掌握之前的7步即可。够用。
8.6 Bean的作用域不同,管理方式不同
Spring 根据Bean的作用域来选择管理方式。
●对于singleton作用域的Bean,Spring 能够精确地知道该Bean何时被创建,何时初始化完成,以及何时被销毁;
●而对于 prototype 作用域的 Bean,Spring 只负责创建,当容器创建了 Bean 的实例后,Bean 的实例就交给客户端代码管理,Spring 容器将不再跟踪其生命周期。
我们把之前User类的spring.xml文件中的配置scope设置为prototype:
执行测试程序:
通过测试一目了然。只执行了前8步,第9和10都没有执行。
8.7 自己new的对象如何让Spring管理
有些时候可能会遇到这样的需求,某个java对象是我们自己new的,然后我们希望这个对象被Spring容器管理,怎么实现?
执行结果:
TESTS PASSED:1 OF 1 TEST - 66MS
C:|DEV/JAVALJDK-17.0.4\BINLJAVA.EXE
COM.POWERNODE.SPRING6.BEAN.USERQ3C5A99DA
COM.POWERNODE.SPRING6.BEAN.USER@3C5A99DA
九、Bean的循环依赖问题
9.1 什么是Bean的循环依赖
A对象中有B属性。B对象中有A属性。这就是循环依赖。我依赖你,你也依赖我。
比如:丈夫类Husband,妻子类Wife。Husband中有Wife的引用。Wife中有Husband的引用。
9.2 singleton下的set注入产生的循环依赖
我们来编写程序,测试一下在singleton+setter的模式下产生的循环依赖,Spring是否能够解决?
执行结果:
通过测试得知:在singleton + set注入的情况下,循环依赖是没有问题的。Spring可以解决这个问题。
9.3 prototype下的set注入产生的循环依赖
我们再来测试一下:prototype+set注入的方式下,循环依赖会不会出现问题?
执行测试程序:发生了异常,异常信息如下:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'husbandBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:265)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
... 44 more
翻译为:创建名为“husbandBean”的bean时出错:请求的bean当前正在创建中:是否存在无法解析的循环引用?
通过测试得知,当循环依赖的所有Bean的scope="prototype"的时候,产生的循环依赖,Spring是无法解决的,会出现BeanCurrentlyInCreationException异常。
大家可以测试一下,以上两个Bean,如果其中一个是singleton,另一个是prototype,是没有问题的。
为什么两个Bean都是prototype时会出错呢?
9.4 singleton下的构造注入产生的循环依赖
我们再来测试一下singleton + 构造注入的方式下,spring是否能够解决这种循环依赖。
执行结果:发生了异常,信息如下:
Caused by: org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'hBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.beforeSingletonCreation(DefaultSingletonBeanRegistry.java:355)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:227)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:199)
at org.springframework.beans.factory.support.BeanDefinitionValueResolver.resolveReference(BeanDefinitionValueResolver.java:325)
... 56 more
和上一个测试结果相同,都是提示产生了循环依赖,并且Spring是无法解决这种循环依赖的。
为什么呢?
主要原因是因为通过构造方法注入导致的:因为构造方法注入会导致实例化对象的过程和对象属性赋值的过程没有分离开,必须在一起完成导致的。
9.5 Spring解决循环依赖的机理
Spring为什么可以解决set + singleton模式下循环依赖?
根本的原因在于:这种方式可以做到将“实例化Bean”和“给Bean属性赋值”这两个动作分开去完成。
实例化Bean的时候:调用无参数构造方法来完成。此时可以先不给属性赋值,可以提前将该Bean对象“曝光”给外界。
给Bean属性赋值的时候:调用setter方法来完成。
两个步骤是完全可以分离开去完成的,并且这两步不要求在同一个时间点上完成。
也就是说,Bean都是单例的,我们可以先把所有的单例Bean实例化出来,放到一个集合当中(我们可以称之为缓存),所有的单例Bean全部实例化完成之后,以后我们再慢慢的调用setter方法给属性赋值。这样就解决了循环依赖的问题。
那么在Spring框架底层源码级别上是如何实现的呢?请看:
在以上类中包含三个重要的属性:
Cache of singleton objects: bean name to bean instance. 单例对象的缓存:key存储bean名称,value存储Bean对象【一级缓存】
Cache of early singleton objects: bean name to bean instance. 早期单例对象的缓存:key存储bean名称,value存储早期的Bean对象【二级缓存】
Cache of singleton factories: bean name to ObjectFactory. 单例工厂缓存:key存储bean名称,value存储该Bean对应的ObjectFactory对象【三级缓存】
这三个缓存其实本质上是三个Map集合。
我们再来看,在该类中有这样一个方法addSingletonFactory(),这个方法的作用是:将创建Bean对象的ObjectFactory对象提前曝光。
再分析下面的源码:
从源码中可以看到,spring会先从一级缓存中获取Bean,如果获取不到,则从二级缓存中获取Bean,如果二级缓存还是获取不到,则从三级缓存中获取之前曝光的ObjectFactory对象,通过ObjectFactory对象获取Bean实例,这样就解决了循环依赖的问题。
总结:
Spring只能解决setter方法注入的单例bean之间的循环依赖。ClassA依赖ClassB,ClassB又依赖ClassA,形成依赖闭环。Spring在创建ClassA对象后,不需要等给属性赋值,直接将其曝光到bean缓存当中。在解析ClassA的属性时,又发现依赖于ClassB,再次去获取ClassB,当解析ClassB的属性时,又发现需要ClassA的属性,但此时的ClassA已经被提前曝光加入了正在创建的bean的缓存中,则无需创建新的的ClassA的实例,直接从缓存中获取即可。从而解决循环依赖问题。