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加载
参考文章:
3、说一下SpringBean的生命周期
简述:从实例创建到对象销毁
- Bean 的实例化阶段:创建一个 Bean 对象。
- Bean 实例的属性填充阶段:为 Bean 实例的属性赋值。
- Bean 实例的初始化阶段:对 Bean 实例进行初始化。
- Bean 实例的正常使用阶段。
- 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是如何解决循环依赖的?
答: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为什么是三级缓存?
如果没有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环境才起作用
关于singleton和prototype还存在一个区别就是:
当我们的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;
}