spring面试题3( 2021-12-15)

spring面试题3( 2021-12-15),第1张

spring面试题3( 2021-12-15) 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{
    //重写接口IntializingBean方法
    public void afterProperties(){
        sout("初始化");
    }
    
    //重写接口DisposableBean的方法
    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了,而是等用到之后才通过动态代理进行创建。这就可以避免循环依赖问题。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5671486.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存