上一篇 :10. 面试问题简析
文章目录
- 1. Spring AOP
- 1.1. Aop 常用注解
- 1.2 测试前的准备工作
- 1.2.1 业务类
- 1.2.2 切面类
- 1.3 Spring4 下的测试
- 1.3.1 POM 文件
- 1.3.2 创建测试类
- 1.3.3 Aop 测试结果
- 1.4 Spring 5 下的测试
- 1.4.1 POM 文件
- 1.4.2 创建测试类
- 1.4.3 Aop 测试结果
- 1.5 Aop 执行顺序总结
- 2. Spring 循环依赖
- 2.1 大厂面试题
- 2.2 什么是循环依赖?
- 2.3 两种注入方式对循环依赖的影响
- 2.4 Spring容器循环依赖及异常情况演示
- 2.4.1 构造器方式注入依赖
- 2.4.2 Setter 方式注入
- 2.4.3 Spring 容器演示循环依赖
- 2.4.4 Spring 内部使用三级缓存解决循环依赖
- 2.5 源码 Deug 前置知识
- 2.5.1 实例化 & 初始化
- 2.5.2 三级缓存 + 四大方法
- 2.5.3 对象在三级缓存中的迁移
- 2.6 详细 Debug 流程
- 2.7、循环依赖总结
- 2.7.1 三级缓存总结
- 2.7.2 Debug 步骤总结
1. Spring AOP
1.1. Aop 常用注解
- @Before 前置通知: 目标方法之前执行
- @After 后置通知: 目标方法之后执行(始终执行)
- @AfterReturning 返回后通知: 执行方法结束前执行(异常不执行)
- @AfterThrowing 异常通知: 出现异常时候执行
- @Around 环绕通知: 环绕目标方法执行
1.2 测试前的准备工作
1.2.1 业务类
-
创建业务接口类:CalcService
public interface CalcService {public int div(int x, int y); }
-
创建业务接口的实现类:CalcServiceImpl
@Service public class CalcServiceImpl implements CalcService {@Overridepublic int div(int x, int y) {int result = x / y;System.out.println("=========>CalcServiceImpl被调用了,我们的计算结果:" + result);return result;} }
1.2.2 切面类
-
想在除法方法前后各种通知,引入切面编程
@Aspect:指定一个类为切面类
@Component:纳入 Spring 容器管理 -
创建切面类 MyAspect
@Aspect @Component public class MyAspect {@Before("execution(public int com.heygo.spring.aop.CalcServiceImpl.*(..))")public void beforeNotify() {System.out.println("******** @Before我是前置通知MyAspect");}@After("execution(public int com.heygo.spring.aop.CalcServiceImpl.*(..))")public void afterNotify() {System.out.println("******** @After我是后置通知");}@AfterReturning("execution(public int com.heygo.spring.aop.CalcServiceImpl.*(..))")public void afterReturningNotify() {System.out.println("********@AfterReturning我是返回后通知");}@AfterThrowing("execution(public int com.heygo.spring.aop.CalcServiceImpl.*(..))")public void afterThrowingNotify() {System.out.println("********@AfterThrowing我是异常通知");}@Around("execution(public int com.heygo.spring.aop.CalcServiceImpl.*(..))")public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {Object retValue = null;System.out.println("我是环绕通知之前AAA");retValue = proceedingJoinPoint.proceed();System.out.println("我是环绕通知之后BBB");return retValue;} }
1.3 Spring4 下的测试
1.3.1 POM 文件
-
在 POM 文件中导入 SpringBoot 1.5.9.RELEASE 版本
-
SpringBoot 1.5.9.RELEASE 版本的对应的 Spring 版本为 4.3.13 Release
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><!-- <version>2.3.3.RELEASE</version> --><version>1.5.9.RELEASE</version><relativePath/></parent><modelVersion>4.0.0</modelVersion>…… </project>
1.3.2 创建测试类
-
注意:SpringBoot 1.5.9 版本在测试类上需要加上 @RunWith(SpringRunner.class) 注解,单元测试需要导入的包名为 import org.junit.Test;
@SpringBootTest @RunWith(SpringRunner.class) //1.5.9 public class AopTest {@Autowiredprivate CalcService calcService;@Testpublic void testAop4() {System.out.println("spring版本:" + SpringVersion.getVersion() + "\t" + "SpringBoot版本:" + SpringBootVersion.getVersion());System.out.println();calcService.div(10, 2);// calcService.div(10, 0);} }
1.3.3 Aop 测试结果
-
正常执行的结果
环绕通知将前置通知与目标方法包裹住,执行完 @After 才执行 @AfterReturning
-
异常执行的结果
由于抛出了异常,因此环绕通知后半部分没有执行,执行完 @After 才执行 @AfterThrowing
注:Spring4 默认用的是 JDK 的动态代理
1.4 Spring 5 下的测试
1.4.1 POM 文件
-
在 POM 文件中导入 SpringBoot 2.3.3…RELEASE 版本
-
SpringBoot 2.3.3.RELEASE 版本的对应的 Spring 版本为 5.2.8 Release
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>2.3.3.RELEASE</version><!-- <version>1.5.9.RELEASE</version> --><relativePath/></parent><modelVersion>4.0.0</modelVersion>…… </project>
1.4.2 创建测试类
-
在 Spring4 的测试类下修改代码
-
注意:SpringBoot 2.3.3 版本下,不需要在测试类上面添加 @RunWith(SpringRunner.class) 直接,单元测试需要导入的包名为 import org.junit.jupiter.api.Test;,不再使用 import org.junit.Test;
@SpringBootTest public class AopTest {@Autowiredprivate CalcService calcService;@Testpublic void testAop4() {System.out.println("spring版本:" + SpringVersion.getVersion() + "\t" + "SpringBoot版本:" + SpringBootVersion.getVersion());System.out.println();calcService.div(10, 0);}@Testpublic void testAop5() {System.out.println("spring版本:" + SpringVersion.getVersion() + "\t" + "SpringBoot版本:" + SpringBootVersion.getVersion());System.out.println();calcService.div(10, 5);} }
1.4.3 Aop 测试结果
- 正常执行的结果
感觉 Spring5 的环绕通知才是真正意义上的华绕通知,它将其他通知和方法都包裹起来了,而且 @AfterReturning 和 @After 之前,合乎逻辑!
- 异常执行的结果
由于方法抛出了异常,因此环绕通知后半部分没有执行,并且 @AfterThrowing 和 @After 之前
1.5 Aop 执行顺序总结
2. Spring 循环依赖
2.1 大厂面试题
- 你解释下spring中的三级缓存?
- 三级缓存分别是什么?三个Map有什么异同?
- 什么是循环依赖?请你谈谈?看过 Spring源码吗?一般我们说的 Spring容器是什么?
- 如何检测是否存在循环依赖?实际开发中见过循环依赖的异常吗?
- 多例的情况下,循环依赖问题为什么无法解决?
……
2.2 什么是循环依赖?
-
多个 bean 之间相互依赖,形成了一个闭环
-
比如:A 依赖于 B、B 依赖于 C、C 依赖于 A
-
通常来说,如果问 Spring 容器内部如何解决循环依赖, 一定是指默认的单例 Bean 中,属性互相引用的场景。也就是说,Spring 的循环依赖,是 Spring 容器注入时候出现的问题
2.3 两种注入方式对循环依赖的影响
-
构造器注入:容易造成无法解决的循环依赖,不推荐使用(If you use predominantly constructor injection, it is possible to create an unresolvable circular dependency scenario.)
-
Setter 注入:推荐使用 setter 方式注入单例 bean
结论:我们 AB 循环依赖问题只要 A 的注入方式是 setter 且 singleton,就不会有循环依赖问题
2.4 Spring容器循环依赖及异常情况演示
2.4.1 构造器方式注入依赖
-
ServiceA
@Component public class ServiceA {private ServiceB serviceB;public ServiceA(ServiceB serviceB) {this.serviceB = serviceB;} }
-
ServiceB
@Component public class ServiceB {private ServiceA serviceA;public ServiceB(ServiceA serviceA) {this.serviceA = serviceA;} }
-
ClientConstructor
/*** 通过构造器的方式注入依赖,构造器的方式注入依赖的bean,下面两个bean循环依赖** 测试后发现,构造器循环依赖是无法解决的*/ public class ClientConstructor {public static void main(String[] args) {new ServiceA(new ServiceB(new ServiceA(new ServiceB()))); ....} }
-
结论:
构造器注入没有办法解决循环依赖, 你想让构造器注入支持循环依赖,是不存在的。如果构造器能够解决循环依赖问题,那么我就可以无限套娃~
2.4.2 Setter 方式注入
-
ServiceA
@Component public class ServiceA {private ServiceB serviceB;public void setServiceB(ServiceB serviceB) {this.serviceB = serviceB;System.out.println("A 里面设置了B");} }
-
ServiceB
略 -
ClientConstructor
public class ClientSet {public static void main(String[] args) {//创建serviceAServiceA serviceA = new ServiceA();//创建serviceBServiceB serviceB = new ServiceB();//将serviceA注入到serviceB中serviceB.setServiceA(serviceA);//将serviceB注入到serviceA中serviceA.setServiceB(serviceB);} }
-
结论:
setter 方式可以解决循环依赖问题
2.4.3 Spring 容器演示循环依赖
-
类 A
public class A {private B b;public B getB() {return b;}public void setB(B b) {this.b = b;}public A() {System.out.println("---A created success");} }
-
类 B
同 A -
ClientSpringContainer 类
/*** 只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,因为单例的时候只有一份,随时复用,那么就放到缓存里面* 而多例的bean,每次从容器中荻取都是—个新的对象,都会重B新创建,所以非单例的bean是没有缓存的,不会将其放到三级缓存中。*/ public class ClientSpringContainer {public static void main(String[] args) {ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");A a = context.getBean("a", A.class);B b = context.getBean("b", B.class);} }
-
在 resources 文件夹下创建 applicationContext.xml 文件,对 bean 中的属性进行注入
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/contexthttp://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/aophttp://www.springframework.org/schema/aop/spring-aop.xsdhttp://www.springframework.org/schema/txhttp://www.springframework.org/schema/tx/spring-tx.xsd"><!--1.spring容器默认的单例模式可以解决循环引用,单例默认支持2.spring容器原型依赖模式scope="prototype"多例模式下不能解决循环引用--><!--depends-on 的意思就是当前这个bean如果要完成,先看depends-on指定的bean是否已经完成了初始化--><!--scope="prototype"代表每次都要新建一次对象--><bean id="a" class="com.heygo.spring.circulardependency.A"><property name="b" ref="b"/></bean><bean id="b" class="com.heygo.spring.circulardependency.B"><property name="a" ref="a"/></bean> </beans>
-
scope = “singleton”,默认的单例(Singleton)的场景是支持循环依赖的,不报错
-
beanA 和 beanB 都创建成功了,程序没有抛异常
-
scope = “prototype”,原型(Prototype)的场景是不支持循环依赖的,报错
-
将 bean 的生命周期改为 prototype
-
此时就会报错 : BeanCurrentlyInCreationException: Error creating bean with name ‘a’: Requested bean is currently in creation: Is there an unresolvable circular reference
2.4.4 Spring 内部使用三级缓存解决循环依赖
-
所谓的三级缓存其实就是 Spring 容器内部用来解决循环依赖问题的三个 Map,这三个 Map 在
DefaultSingletonBeanRegistry
类中
-
第一级缓存:Map<String, Object> singletonObjects,我愿称之为成品单例池,常说的 Spring 容器就是指它,我们获取单例 bean 就是在这里面获取的,存放已经经历了完整生命周期的Bean对象
-
第二级缓存:Map<String, Object> earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完整,可以认为是半成品的 bean)
-
第三级缓存:Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂,用于生产(创建)对象
-
-
结论
只有单例的 Bean 会通过三级缓存提前暴露来解决循环依赖问题,而非单例的 Bean,每次从容器中获取都是一个新的对象,都会重新创建,所以非单例的 Bean 是没有缓存的,不会将其放入三级缓存中
2.5 源码 Deug 前置知识
2.5.1 实例化 & 初始化
- 实例化和初始化的区别
- 实例化:堆内存中申请一块内存空间
- 初始化:完成属性的填充
- 实例化:堆内存中申请一块内存空间
2.5.2 三级缓存 + 四大方法
-
三级缓存
-
第一级缓存:存放的是已经初始化好了的Bean,bean名称与bean实例相对应,即所谓的单例池。表示已经经历了完整生命周期的Bean对象
-
第一级缓存:存放的是实例化了,但是未初始化的Bean,bean名称与bean实例相对应。表示Bean的生命周期还没走完(Bean的属性还未填充)就把这个Bean存入该缓存中。也就是实例化但未初始化的bean放入该缓存里
-
第三级缓存:表示存放生成bean的工厂,存放的是FactoryBean,bean名称与bean工厂对应。假如A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Bean
-
-
四大方法
- getSingleton():从容器里面获得单例的bean,没有的话则会创建 bean
- doCreateBean():执行创建 bean 的操作(在 Spring 中以 do 开头的方法都是干实事的方法)
- populateBean():创建完 bean 之后,对 bean 的属性进行填充
- addSingleton():bean 初始化完成之后,添加到单例容器池中,也即添加到一级缓存中,下次执行 getSingleton() 方法时就能获取到
-
注:关于三级缓存 Map<String, ObjectFactory<?>> singletonFactories的说明,singletonFactories 的 value 为 ObjectFactory 接口实现类的实例。ObjectFactory 为函数式接口,在该接口中定义了一个 getObject() 方法用于获取 bean,这也正是工厂思想的体现(工厂设计模式)
2.5.3 对象在三级缓存中的迁移
- A/B 两对象在三级缓存中的迁移说明
-
A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B
-
B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A,然后把三级缓存里面的这个A放到二级缓存里面,并删除三级缓存里面的A
-
B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
2.6 详细 Debug 流程
-
先打断点
-
进入
new ClassPathXmlApplicationContext("applicationContext.xml")
- 先步入,进入一个静态代码块,不需要关注
- 接着跳出,回到原来行
- 再步入,进入以下构造方法,但是再该构造方法中又调用了一个重载的构造方法
-
步入重载的构造方法
-
步入
refresh()
-
步入
finishBeanFactoryInitialization(beanFactory)
-
步入
preInstantiateSingletons()
-
步入
getBean(beanName)
-
步入
doGetBean
-
步入
getSingleton(beanName)
-
开始返回,一直返回到
doGetBean
方法中Object sharedInstance = getSingleton(beanName);
行 -
继续向下执行
-
进入
getSingleton()
-
步入
createBean(beanName, mbd, args)
-
步入
doCreateBean(beanName, mbdToUse, args)
-
步入
populateBean(beanName, mbd, instanceWrapper)
-
步入
applyPropertyValues(beanName, mbd, bw, (PropertyValues)pvs)
-
步入
resolveValueIfNecessary(pv, originalValue)
-
步入
resolveReference(argName, ref)
-
步入
getBean(resolvedName)
,又回到了这个熟悉的方法
-
再次步入
doGetBean
,下面又是和上文中相同的路径,只不过这次是从一级缓存中获取 BeanB
-
再次步入
createBean
-
再次步入
populateBean
-
再次步入
applyPropertyValues
,……,沿着刚刚的路径一直走,直到再再一次进入doGetBean
,再再一次进入getSingleton(beanName)
-
getSingleton 方法返回,回到
doGetBean
-
开始返回, 直到 applyPropertyValues ->
resolveValueIfNecessary
处 -
开始返回 ,返回到 doCreateBean 的
populateBean
处
-
开始返回,返回到 getSingleton 的
singletonFactory.getObject()
-
步入
addSingleton
-
一直返回,回退到
applyPropertyValues
,并且沿着前面一样的路径继续执行 -
继续执行,几次返回之后,再次按照前面的路径一直走,步入
getSingleton
- 继续执行,直到再次步入
addSingleton
2.7、循环依赖总结
2.7.1 三级缓存总结
-
Spring创建bean主要分为两个步骤,创建原始bean对象,接着去填充对象属性和初始化
- 每次创建bean之前,我们都会从缓存中查下有没有该bean,因为是单例,只能有一个。
- 当我们创建 beanA的原始对象后,并把它放到三级缓存中,接下来就该填充对象属性了,这时候发现依赖了beanB,接着就又去创建beanB,同样的流程,创建完 beanB填充属性时又发现它依赖了beanA又是同样的流程。
- 不同的是:这时候可以在三级缓存中查到刚放进去的原始对象beanA,所以不需要继续创建,用它注入beanB,完成beanB的创建既然 beanB创建好了,所以beanA就可以完成填充属性的步骤了,接着执行剩下的逻辑,闭环完成
- Spring解决循环依赖依靠的是Bean的“中间态"这个概念,而这个中间态指的是已经实例化但还没初始化的状态,也即半成品。
- 实例化的过程又是通过构造器创建的,如果A还没创建好出来怎么可能提前曝光,所以构造器的循环依赖无法解决。
-
Spring为了解决单例的循环依赖问题,使用了三级缓存:
- 其中一级缓存为单例池〈 singletonObjects)
- 二级缓存为提前曝光对象( earlySingletonObjects)
- 三级缓存为提前曝光对象工厂( singletonFactories)
-
假设A、B循环引用,实例化A的时候就将其放入三级缓存中,接着填充属性的时候,发现依赖了B,同样的流程也是实例化后放入三级缓存,接着去填充属性时又发现自己依赖A,这时候从缓存中查找到早期暴露的A,没有AOP代理的话,直接将A的原始对象注入B,完成B的初始化后,进行属性填充和初始化,这时候B完成后,就去完成剩下的A的步骤,如果有AOP代理,就进行AOP处理获取代理后的对象A,注入B,走剩下的流程。
2.7.2 Debug 步骤总结
- 调用doGetBean()方法,想要获取beanA,于是调用getSingleton()方法从缓存中查找beanA
- 在getSingleton()方法中,从一级缓存中查找,没有,返回null
- doGetBean()方法中获取到的beanA为null,于是走对应的处理逻辑,调用getSingleton()的重载方法(参数为ObjectFactory的)
- 在getSingleton()方法中,先将beanA_name添加到一个集合中,用于标记该bean正在创建中。然后回调匿名内部类的creatBean()方法
- 进入AbstractAutowireCapableBeanFactory#doCreateBean(),先反射调用构造器创建出beanA的实例,然后判断是否为单例、是否允许提前暴露引用(对于单例一般为true)、是否正在创建中〈即是否在第四步的集合中)。判断为true则将beanA添加到【三级缓存】中
- 对beanA进行属性填充,此时检测到beanA依赖于beanB,于是开始查找beanB
- 调用doGetBean()方法,和上面beanA的过程一样,到缓存中查找beanB,没有则创建,然后给beanB填充属性
- 此时beanB依赖于beanA,调用getsingleton()获取beanA,依次从一级、二级、三级缓存中找,此时从三级缓存中获取到beanA的创建工厂,通过创建工厂获取到singletonObject,此时这个singletonObject指向的就是上面在doCreateBean()方法中实例化的beanA
- 这样beanB就获取到了beanA的依赖,于是beanB顺利完成实例化,并将beanA从三级缓存移动到二级缓存中
- 随后beanA继续他的属性填充工作,此时也获取到了beanB,beanA也随之完成了创建,回到getsingleton()方法中继续向下执行,将beanA从二级缓存移动到一级缓存中
流程图链接:https://www.processon.com/view/link/644926c0e6ec87493a22d446