Spring三级缓存以及面试题

x33g5p2x  于2022-02-07 转载在 Spring  
字(7.1k)|赞(0)|评价(0)|浏览(568)

Spring的三级缓存

三级缓存的作用:解决循环依赖的问题

循环依赖问题:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用

代码描述:

@Service
public class AServiceImpl implements AService {
    @Autowired
    private BService bService;
    ...
}
@Service
public class BServiceImpl implements BService {
    @Autowired
    private AService aService;
    ...
}

什么是三级缓存?

  • singletonObjects(一级,日常实际获取Bean的地方,里面保存的都是初始化后的Bean);
  • earlySingletonObjects(二级,还没进行属性注入,由三级缓存放进来,经过三级缓存处理可能是原对象或代理对象);
  • singletonFactories(三级,存放一个对象工厂,和lambda表达式,里面保存的都是刚实例化的对象);

本质上这三级缓存就是三个Map :

/** 1级缓存 Cache of singleton objects: bean name to bean instance. */
	private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

	/** 2级缓存 Cache of early singleton objects: bean name to bean instance. */
	private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

	/** 3级缓存 Cache of singleton factories: bean name to ObjectFactory. */
	private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

我们知道了什么是循环依赖,什么是三级缓存,那么我们的Spring是如何通过三级缓存去解决这个问题的呢?

如下图流程:

首先先知道两个概念:

  • 实例化:我们只是将对象创建出来,并没有进行赋值操作
  • 初始化:对我们创建出来的对象也就是实例对象进行属性注入后的对象

接下来我们对这个图进行解释说明:

1、我们创建Aservice对象,将其对应的lambda表达式放入三级缓存,lambda表达式的作用是,判断我们这个实例化对象是否有AOP曹操作,如果有就执行AOP,返回代理后的对象到二级缓存,如果没有,则直接将原对象放入二级缓存 ;

2、然后我们的对Aservice这个实例化对象进行属性注入,填充Bservice对象,首先是去一级缓存中去找,如果没有就去创建Bservice对象

3、初始步骤同样是将Bservice对应的lambda表达式放入我们的三级缓存当中,发现B同样需要注入AService属性

4、就会去一级缓存和二级缓存中找Aservice,发现不存在,那么就去三级缓存当中查找,

5、找到了,那么此时执行三级缓存中Aservice对应的lambda表达式,同步骤1一样,将返回的对象放入二级缓存当中

6、此时,我们的Bservice中有了Aservice但是,Aservice中的Bservice属性尚未注入,对其进行属性注入

7、执行三级缓存中Bservice对应的lambda表达式,得到Bservice对象,并将Bservice对象由二级缓存移入到一级缓存

8、此时Bservice结束

9、继续对Aservice进行属性注入,将一级缓存中的Bservice填充到Aservice,接下来就是初始化Aservice

10、Aservice初始化完毕,将Aservice移入到一级缓存

11、此时Aservice结束

12、循环依赖注入的问题就这样解决了!

Spring相关面试题

如下是Spring的高频面试题,需要掌握!

1、说一下Spring中IOC的构建流程(初始化过程) ?

1、通过BeanFactory 或者 ApplicationContex接口,以及其实现类,读取我们的beans.xml,创建IOC容器

ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");

ClassPathXmlApplicationContext创建容器对象时,构造方法做了如下两件事:

  • ① 调用父容器的构造方法为容器先设置好 Bean 资源加载器。
  • ② 调用父类的 setConfigLocations() 方法设置 Bean 配置信息的定位路径。
  • ③ 调用父类 AbstractApplicationContext 的 refresh() 方法启动整个 IOC 容器对 Bean 的载入,在创建 IOC 容器前如果已有容器存在,需要把已有的容器销毁,保证在 refresh() 方法后使用的是新创建的 IOC 容器。

2、容器创建完成后,通过 loadBeanDefinitions() 方法加载 Bean 配置资源,该方法在加载资源时,首先解析配置文件路径,读取配置文件的内容,然后通过 XML 解析器将 Bean 的配置信息转换成文档对象,之后按照 Spring Bean 的定义规则将文档对象解析为 BeanDefinition 对象。

3、然后将beanName做为Key,BeanDefiniton对象作为Value保存在我们的BeanDefinitonMap中,然后遍历Map集合

4、最后,实例化所有的 Bean 实例(非懒加载):包括实例的创建,实例的属性填充,实例的初始化。

2、说一下Bean加载

参考文章:https://blog.csdn.net/weixin_43591980/article/details/117022413

3、说一下SpringBean的生命周期

简述:从实例创建到对象销毁

  1. Bean 的实例化阶段:创建一个 Bean 对象。
  2. Bean 实例的属性填充阶段:为 Bean 实例的属性赋值。
  3. Bean 实例的初始化阶段:对 Bean 实例进行初始化。
  4. Bean 实例的正常使用阶段。
  5. Bean 实例的销毁阶段:容器关闭后,将 Bean 实例销毁。
    详细流程:

​ 当我们IOC容器创建完成之后,会读取我们bean.xml中的bean标签,然后将其封装成一个BeanDefiniton对象,然后将beanName做为Key,BeanDefiniton对象作为Value保存在我们的BeanDefinitonMap中,然后遍历我们的Map集合,此时对每Bean进行实例化,接着就是对Bean进行属性注入,此时在我们要调用Bean的init方法的时候,会在执行之前调用后置处理器的一个befor方法:postProcessBeforeInitialization(),接下来就是执行Init方法,完成Bean的初始化操作,接着会再次调用后置处理器的一个after方法 :postProcessAfterInitialization(),当after方法执行完毕后,我们就得到了一个可用的Bean对象在IOC容器当中,当我们容器关闭的时候,就会调用我们Bean的Destory方法,将我们的Bean进行销毁处理!

4、什么是Spring的循环依赖问题?

循环依赖:说白是一个或多个对象实例之间存在直接或间接的依赖关系,这种依赖关系构成了构成一个环形调用,如图:

@Service
public class AServiceImpl implements AService {
    @Autowired
    private BService bService;
    ...
}
@Service
public class BServiceImpl implements BService {
    @Autowired
    private AService aService;
    ...
}

注意:目前Spring只支持单例(Singleton)类型的属性循环依赖

5、说一下Spring的三级缓存

所谓的三级缓存其实就是三个Map…首先明确一定,我对这里的三级缓存定义是这样的:

  • singletonObjects(一级,日常实际获取Bean的地方);
  • earlySingletonObjects(二级,还没进行属性注入,由三级缓存放进来);
  • singletonFactories(三级,Value是一个对象工厂);

6、Spring是如何解决循环依赖的?

参考文章:https://www.cnblogs.com/semi-sub/p/13548479.html

答:Spring通过三级缓存解决循环依赖问题!

我们通过A实例依赖B,B实例依赖A的例子来分析具体流程:

1、A对象实例化之后,属性注入之前,其实会把A对象放入三级缓存中,key是BeanName,Value是ObjectFactory

2、等到A对象属性注入时,发现依赖B,又去实例化B时

3、B属性注入需要去获取A对象,这里就是从三级缓存里拿出ObjectFactory,ObjectFactory得到对应的Bean(就是对象A)

4、把三级缓存的A记录给干掉,然后放到二级缓存中

5、显然,二级缓存存储的key是BeanName,value就是Bean(这里的Bean还没做完属性注入相关的工作)

6、等到完全初始化之后,就会把二级缓存给remove掉,塞到一级缓存中

7、我们自己去getBean的时候,实际上拿到的是一级缓存的

大致的过程就是这样

7、Spring为什么是三级缓存?

参考文章:https://www.cnblogs.com/grey-wolf/p/13034371.html

三级缓存的职能:https://blog.csdn.net/m0_43448868/article/details/113578628

如果没有AOP的话确实可以两级缓存就可以解决循环依赖的问题,如果加上AOP,两级缓存是无法解决的,不可能每次执行singleFactory.getObject()方法都给我产生一个新的代理对象,所以还要借助另外一个缓存来保存产生的代理对象

8、BeanFactory 和 FactoryBean 的区别

  • BeanFactory:Spring 容器最核心也是最基础的接口,本质是个工厂类,用于管理 bean 的工厂,最核心的功能是加载 bean
  • FactoryBean:该接口以 bean 样式定义,但是它不是一种普通的 bean,它是个工厂 bean,实现该接口的类可以自己定义要创建的 bean,只需要实现它的 getObject 方法即可

9、BeanFactory 和 ApplicationContext 的区别

Spring提供的2个创建IOC容器的接口,之间的的区别如下:

1、BeanFactory : IOC容器的基本实现,是Spring内部使用的接口,不提供给开发人员使用 ;

  • 加载配置文件的时候,不会创建对象,而是当我们的使用的时候才回去创建对象!

2、ApplicationContext : BeanFactory的子接口提供更多更强大的功能,一般由开发人员进行使用 ; 【推荐】

  • 加载配置文件的时候,会把配置文件中的对象进行创建!

10、说一下SpinrgBean的作用范围?

通过 scope 属性指定 Bean 的作用范围,包括:

  • singleton:单例模式,表示Spring的IOC容器当中只能存在一个Bean实例,默认的。
  • prototype:多实例模式,表示每次从IOC容器当中取一个Bean实例的时候,都是一个新的Bean 。
  • request:每次创建对象,都放在我们的Request域当中(很少用)在一次请求范围内,创建一个实例。
  • session:每次创建对象,都放在我们的session域当中(很少用)在一个会话范围内,创建一个实例。
  • globle-session:在servletContext范围内,创建一个实例

后面三个范围需要在web环境才起作用

关于singletonprototype还存在一个区别就是:

当我们的scop=singleton的时候我们的对象是会在ApplicaitionContex加载xml文件的时候创建的;

当我们的scop=prototype的时候我们的对象不是在加载xml的时候创建的,而是在调用getBean方法的时候创建的!

11、多个AOP的顺序怎么定

我们通过**@Order注解来设置增强类优先级:这个值越小优先级越高**!

@Order(3)
public class UserProxy {}

@Order(1)
public class PersonProxy {}

12、Spring 的 AOP 有哪几种创建代理的方式

Spring 中的 AOP 目前支持 JDK 动态代理和 Cglib 代理。

通常来说:如果被代理对象实现了接口,则使用 JDK 动态代理,否则使用 Cglib 代理。另外,也可以通过指定 proxyTargetClass=true 来

实现强制走 Cglib 代理。

13、JDK 动态代理和 Cglib 代理的区别

  • JDK 动态代理主要是针对类实现了某个接口,AOP 则会使用 JDK 动态代理。他基于反射的机制实现,生成一个实现同样接口的一个代理类,然后通过重写方法的方式,实现对代码的增强。
  • 而如果某个类没有实现接口,AOP 则会使用 CGLIB 代理。他的底层原理是基于 ASM 第三方框架,通过修改字节码生成一个子类,然后重写父类的方法,实现对代码的增强。

14、JDK 动态代理为什么只能对实现了接口的类生成代理

根本原因是通过 JDK 动态代理生成的类已经继承了 Proxy 类,所以无法再使用继承的方式去对类实现代理

15、Spring 事务的实现原理

Spring 事务的底层实现主要使用的技术:AOP(动态代理) + ThreadLocal + try/catch。

动态代理:基本所有要进行逻辑增强的地方都会用到动态代理,AOP 底层也是通过动态代理实现。

ThreadLocal:主要用于线程间的资源隔离,以此实现不同线程可以使用不同的数据源、隔离级别等等。

try/catch:最终是执行 commit 还是 rollback,是根据业务逻辑处理是否抛出异常来决定。

Spring 事务的核心逻辑伪代码如下:

public void invokeWithinTransaction() {
    // 1.事务资源准备
    try {
        // 2.业务逻辑处理,也就是调用被代理的方法
    } catch (Exception e) {
        // 3.出现异常,进行回滚并将异常抛出
    } finally {
        // 现场还原:还原旧的事务信息
    }
    // 4.正常执行,进行事务的提交
    // 返回业务逻辑处理结果
}

16、Spring框架提供哪几种事务传播行为

1、REQUIRED:Spring 默认的事务传播级别,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。

2)REQUIRES_NEW:每次都会新建一个事务,如果上下文中有事务,则将上下文的事务挂起,当新建事务执行完成以后,上下文事务再恢复执行。

3)SUPPORTS:如果上下文存在事务,则加入到事务执行,如果没有事务,则使用非事务的方式执行。

4)MANDATORY:上下文中必须要存在事务,否则就会抛出异常。

5)NOT_SUPPORTED :如果上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。

6)NEVER:上下文中不能存在事务,否则就会抛出异常。

7)NESTED:嵌套事务。如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

17、Spring 的事务隔离级别

Spring 的事务隔离级别底层其实是基于数据库的,Spring 并没有自己的一套隔离级别。

  • READ_UNCOMMITTED:读未提交,最低的隔离级别,会读取到其他事务还未提交的内容,存在脏读。
  • READ_COMMITTED:读已提交,读取到的内容都是已经提交的,可以解决脏读,但是存在不可重复读。
  • REPEATABLE_READ:可重复读,在一个事务中多次读取时看到相同的内容,可以解决不可重复读,但是存在幻读。
  • SERIALIZABLE:串行化,最高的隔离级别,对于同一行记录,写会加写锁,读会加读锁。在这种情况下,只有读读能并发执行,其他并行的读写、写读、写写操作都是冲突的,需要串行执行。可以防止脏读、不可重复度、幻读,没有并发事务问题。

18、Spring中用到的设计模式

代理模式、工厂模式、单例模式、观察者模式、适配器模式

19、@Resource 和 @Autowire 的区别

  • @Resource是Java的原生注解、@Autowired是我们Spring中的注解
  • @Resource默认是按照名字自动装配,@Autowired是按照类型自动装配

20、@Autowire 怎么使用名称来注入

通过搭配@Qualifire指定bean的名称,来完成byName的装配方式

@Component
public class Test {
    @Autowired
    @Qualifier("userService")
    private UserService userService;
}

21、如何让两个Bean按顺序加载

方式1、使用 @DependsOn、depends-on

方式2、让后加载的类依赖先加载的类

@Component
public class A {
    @Autowire
    private B b;
}

相关文章