2021-12-15
19、什么是bean装配?什么是bean的自动装配?
装配 = 注入
自动装配 = 自动注入
bean的装配:
spring会帮我们创建一个个的对象,但是如果没有装配,那么这些创建好的对象之间是没有任何关系的。如果想让这些创建好的对象之间有关系,肯定是需要进行装配的。
1、装配的方式可以采用手动装配的方式:
在xml里面写bean标签的时候,有property属性。也可以引用外部bean
2、自动装配的方式:
使用Autowired,设置自动装配的方式。 只要符合自动装配的原则就会自动寻找bean。自动装配的方式有很多。暂时先不讲。装配和自动装配都是为了让Spring之间的对象互相依赖起来。
20、自动注入有什么限制吗?【了解】
自动注入的实现方式是使用Autowired。而且指定byName或者byType。在xml的bean标签里面设置autowired属性,有default/no/byName/byType/constructor
使用这个自动装配有什么限制呢?
1、自动注入的属性一定要声明set方法。
2、自动注入之后,仍然可以在bean标签里面使用constructor-arg或者property属性来覆盖自动注入的属性值。
3、基本数据类型是无法进行自动注入的。
手动注入还是可以注入基本数据类型的。
手动注入:property name=“userName” value=“张三”
或者使用@Value注解。
自动注入无法注入基本数据类型【包括字符串】
4、模糊特性:
使用自动注入的时候,有可能匹配到多个值。所以就会出现可能匹配到的不是我们期待的。
所以这边更推荐使用@Autowired手动装配的方式进行装配。这样首先会根据类型去匹配,再根据属性名去匹配。
21、自动装配的方式有几种?【经典面试题】
考核初级阶段的Autowired自动装配的方式掌握的好不好。
我们在写xml的bean标签的时候,写了一个属性autowired。
这个属性有五个值可以取。这五个值就是本面试题的答案。
自动装配的方式:
1、default = no
2、【默认值】no:不进行自动装配,通过手动设置ref属性装配bean或者使用直接@Autowired手动指定需要指定注入的属性。使用注解@Autowired会更加的灵活。因为先按照类型再按照名字。很人性化
3、constructor:按照构造函数进行自动装配。
【注意】:这个属性不能是基本数据类型。且需要该属性单独的构造函数。
比如Person类里面有个Cat类类型。
那么就需要有这个构造函数才行。
class Person{
? public Person(Cat cat){
? this.cat = cat;
? }
}
先按照type再按照名字。永不报错。用null代替。
4、byName:根据名字进行自动装配。
【注意】:这里的按照名字不是说按照你bean类里面定义的属性名字去匹配,而是按照set方法名。去掉set,然后把首字母改为小写。如果使用的lombok默认就是属性名。
5、byType:根据参数类型。同理也是根据set方法里面的参数类型去Spring容器里面找。找到了之后,调用set方法进行自动装配。
22、Bean有哪些生命周期的回调方法?有哪几种实现方式?
之前在的第11道面试题里面说IOC的加载过程中涉及到哪些扩展点的时候,最后就说了在bean的初始化的时候,会调用很多Aware接口和很多生命周期的回调接口。
关于生命周期的回调方法,分为两种:
1、初始化的时候调用的。
2、销毁的时候调用的。
不管是初始化还是销毁都有三种实现方式。
【方式1:通过注解】
在初始化init方法头上面加上注解@PostConstruct
在销毁destroy方法头上面加上注解@PreDestroy
例如:
@Configuration
public class MainConfig{
@Bean
public UserService userService(){
return new UserService();
}
}
class UserService{
@PostConstruct
public void init(){
sout("初始化");
}
@PreDestroy
public void destroy(){
sout("销毁");
}
}
【注意】:
执行这个destroy方法的时机:是ApplicationContext对象调用了close()方法。
测试方法:
psvm(){
context = new ApplicationContext();
UserService ss = context.getBean(UserService.class);
context.close();
}
【方式2:通过接口】
初始化:IntializingBean接口
销毁:DisposableBean
class UserService implements IntializingBean,DisposableBean{
public void afterProperties(){
sout("初始化");
}
public void destroy(){
sout("销毁 ");
}
}
【方式3:通过xml指定属性或者javaConfig+@Bean】
指定属性:initMethod=初始化方法名。destroyMethod=销毁方法名
如果是xml就是init-method和destroy-method
@Configuration
public class MainConfig{
@Bean(initMethod = "init" , destroyMethod = "destroy")
public UserService userService(){
return new UserService();
}
}
class UserService{
public void init(){
sout("初始化");
}
public void destroy(){
sout("销毁");
}
}
23、Spring在加载过程中Bean有哪几种形态?
通过这个面试题就可以知道你对SpringIOC的加载过程是否清楚。
1、概念态:配置的bean,此时没有执行new ApplicationContext()
2、定义态:已经new了一个IOC容器,将概念态的定义信息存储到BeanDefinition对象里面去。
3、纯净态:拿到BeanDefinition定义信息之后,检查是否具备生产条件,刚实例化出来的bean就是纯净态。
4、成熟态:对纯净态的bean进行属性注入。得到成熟态的bean,将所有的成熟态的bean加到单例池【一级缓存】里面。最终使用的bean就是成熟态的bean。
24、解释Spring框架中bean的生命周期【经典面试题】
什么是bean的生命周期?
是指bean从创建到销毁的这个过程。【从生到死】
IOC的加载过程就是bean的创建过程
大致分为四大步:
1、实例化:
实例化就是通过反射去推断构造函数进行实例化。但是实例化的方式有很多,不仅仅是反射。比如还有实例工厂,静态工厂,实现FactoryBean接口…
2、属性赋值:
解析自动装配,DI的体现。看是哪种方式【byName,byType,no,constructor,default】和注解@Autowired。
解析DI的时候,会出现bean的循环依赖。
什么是循环依赖呢?
A:
@Component
class Person{
@Autowired
private Pet pet;
}
B:
@Component
class Pet{
@Autowired
private Person person;
}
出现死循环。但是Spring目前已经解决了这个问题。
3、初始化:
初始化会回调很多xxxAware接口。
稍微记住几个:BeanNameAware,BeanClassLoaderAware,BeanFactoryAware
然后会调用初始化生命周期的回调:有三种方式,xml,实现接口和注解。
如果bean实现了AOP,还会初始化的时候去创建动态代理。
4、销毁:
在Spring容器进行关闭的时候进行调用。
【123步是在IOC加载过程中进行调用的,但是4销毁需要容器关闭的时候才会调用。 】
25、Spring是如何解决Bean的循环依赖的?【经典+难度】
夺命连环问。
Spring采用三学习三个map
Spring解决循环依赖的底层原理:
创建BeanA
1、调用getBean(A)来进行创建
2、调用doGetBean(A)
3、调用getSingleton(A,boolean) 这个时候会去一级缓存中找一下A对象有没有找到。如果找到了,就直接返回。
【一级缓存的作用】:存储完整的bean
首次缓存中肯定是没有A对象的。
4、调用getSingleton(A,objectFactory)
5、调用createBean(A,boolean)
调用doCreateBean(A,....)
开始进行创建,进入bean的生命周期
6、实例化,实例化之后加入到三级缓存中。
【三级缓存的作用】:
三级缓存保存的是 函数式接口【函数式接口不会立即调用】,通过使用lambda表达式,把方法传进去。其实就是把bean的实例和bean的名字传进去了。后续可能会进行AOP的创建,但是不会立即调用。
【为什么函数式接口不会立即调用呢?】
正常:实例化之后将函数式接口保存到三级缓存,而函数式接口不会立即调用。
为什么没有在实例化后面直接创建动态代理,直接存储到三级缓存,这样就不需要二级缓存了?
因为:如果在实例化之后立即调用函数式接口的话,所有的AOP,不管bean是否循环依赖了,都会在实例化后创建动态代理。如果在实例化之后不存储函数式接口,直接调用方法,创建动态代理的话,会出现:所有使用了AOP的bean都会在这一步创建动态代理。但是:正常的bean是在初始化的时候创建动态代理,这才是bean的正常的生命周期。正常的bean(也就是没有发生循环依赖问题的bean),Spring还是希望它能够遵循自己的生命周期,在初始化的时候创建动态代理。
我们出现循环依赖问题的bean(非正常的bean),为了解决这个循环依赖问题,只能在属性赋值的时候创建动态代理,这个是没有办法的。迫于解决循环依赖的问题。如果你不在属性赋值的时候创建动态代理的话,那么BeanB里面的属性BeanA赋值的就不是动态代理了,而是赋的实例。为了不立即创建动态代理,所以将其存入函数式接口中。
为了避免多次调用,所以出现二级缓存。
7、属性赋值:
属性赋值的时候,发现BeanA依赖BeanB
所以会:
getBean(B)
doGetBean(B)
getSingleton(B,boolean):去一级缓存里面找。
createBean(B,boolean)
doCreateBean(B,…)
实例化BeanB:并将BeanB加入到三级缓存。 同样保存函数式接口。
属性赋值:发现BeanB依赖BeanA
getBean(A)
doGetBean(A)
getSingleton(A,boolean):去一级缓存里面找。一级缓存里面存储的是最终创建完整的bean。也就是整个流程完了之后才会有。所以一级缓存里面没有BeanA,二级缓存也没有,因为目前还没有涉及到二级缓存的存储。但是三级缓存里面有实例化之后的BeanA。所以去三级缓存里面拿BeanA。如果这个时候BeanA使用了AOP的话,他就会帮你创建动态代理,如果没有使用AOP,依然返回之前三级缓存里面保存的实例化的BeanA。前提是:A使用了动态代理。然后将返回的BeanA对象放到二级缓存里面。
为什么要放到二级缓存呢?
为了避免重复创建。当A依赖B,B依赖A;A依赖C,C依赖A的场景。如果A没有存储到二级缓存的话,那么A就会在这种多重依赖的情况下重复创建两次。
【二级缓存的作用】:为了避免在多重依赖的情况下重复创建。
现在就把A的动态代理放到二级缓存中去了。且把创建好的动态代理return了。
所以:创建BeanB的属性赋值:依赖的BeanA已经赋值成功。
所以BeanB的循环依赖有了出口了。
继续走BeanB:
BeanB的属性赋值成功之后,就进入到BeanB的初始化了。
将BeanB添加到一级缓存【这个是完整的BeanB】,且将二三级缓存的BeanB移除掉。且返回完整对象BeanB。
8、BeanA的属性赋值就成功了。
9、BeanA初始化,将BeanA添加到一级缓存【这个是完整的BeanA】,且将二三级缓存的BeanA移除掉,返回完整对象BeanA。
【三个缓存的作用】:
【一级缓存的作用】:存储完整的bean;
【二级缓存的作用】:是在初始化之后创建的对象,此时已经创建了代理对象。为了避免在多重依赖的情况下重复创建代理对象;
【三级缓存的作用】:实例化之后的对象存放在三级缓存里面,此时还没有创建动态代理。三级缓存保存的是 函数式接口【函数式接口不会立即调用】,通过使用lambda表达式,把方法传进去。其实就是把bean的实例和bean的名字传进去了。后续可能会进行AOP的创建,但是不会立即调用。
【二级缓存能不能解决循环依赖?】
如果只是死循环的依赖的问题,一级缓存就可以解决。
因为当你实例化BeanA的时候直接放到一级缓存里面,然后第二次创建BeanB的时候可以去一级缓存里面拿BeanA,给BeanB赋属性。这也解决了出口的问题。只不过,如果出现了动态代理,会出现一个问题:无法避免在并发情况下获取到的bean不完整。【这个问题之后的面试题来解释为什么】
只使用二级缓存依然可以解决循环依赖的问题。
只不过出现的问题是:如果BeanA被多次依赖【当A依赖B,B依赖A;A依赖C,C依赖A的场景】,那么BeanA会出现多次创建动态代理对象的情况。
【Spring有没有解决多例Bean的循环依赖?】
什么是多例Bean的循环依赖呢?
就是A依赖B,B依赖A。且A和B都是多实例的。(scope=prototype)
@Component
@Scope("prototype")
class BeanA{
@Autowired
private BeanB b;
}
@Component
@Scope("prototype")
class BeanB{
@Autowired
private BeanA a;
}
循环依赖:circular reference。
【多例Bean不能解决循环依赖。】
为什么呢?
因为多例Bean自始至终就不会使用缓存【1,2,3级】,前面也讲了,你要解决循环依赖,你至少要有一个缓存,作为循环出口。但是多例bean不会使用缓存进行存储。因为多例Bean每次创建对象都需要重新创建。不使用缓存,就没办法解决循环依赖。【解决循环依赖最关键的是利用缓存保存bean早期对象,作为循环的出口】。
【Spring有没有解决构造参数Bean的循环依赖呢?】
使用构造函数的形式去注入:
@Component
class BeanA{
private BeanB b;
public BeanA(BeanB b){
this.b = b;
}
}
@Component
class BeanB{
private BeanA a;
public BeanB(BeanA a){
this.a = a;
}
}
构造函数的Bean的循环依赖也会报错的,Spring默认没有解决。
可以通过延迟加载@Lazy来解决。不会立即创建依赖的BeanB,而是等到用到的时候才通过动态代理进行创建。
@Component
class BeanA{
private BeanB b;
@Lazy
public BeanA(BeanB b){
this.b = b;
}
}
@Component
class BeanB{
private BeanA a;
@Lazy
public BeanB(BeanA a){
this.a = a;
}
}
A实例->构造->为B创建cglib a=null
? B代理<-
B实例->构造->为A创建cglib b=null
? A代理<-
调用:
ioc.get(A)
A实例
a.get(B)
获取B代理
a.getB().xxx
B代理.intercept()
ioc.get(B).xxx()此时就调用beanB的方法。
使用注解@Lazy,就不会立即创建依赖的bean了,而是等用到之后才通过动态代理进行创建。这就可以避免循环依赖问题。
|