Spring面试重点
AOP
- 前置通知(@Before):在⽬标⽅法运行之前运行;
- 后置通知(@After):在⽬标⽅法运行结束之后运行;
- 返回通知(@AfterReturning):在⽬标⽅法正常返回之后运行;
- 异常通知(@AfterThrowing):在⽬标⽅法出现异常是运行;
- 环绕通知(@Around):手动推荐⽬标⽅法运行(proceedingJoinPoint.proceed())。
通用AOP日志切面类
<!-- springboot-aop 技术 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.After;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import java.util.Arrays;
/**
* Title:通用日志切面
* Description:
* @author WZQ
* @version 1.0.0
* @date 2020/12/1
*/
@Aspect
public class LogAspects {
/**
* 公共的切⼊入点表达式
* 1、本类引⽤
* 2、其他的切⾯引⽤
* @author wzq
* @date 2018/11/1
*/
@Pointcut(value = "execution(public int com.wzq.spring.study.service.CalcService.*(..))")
public void pointCut() {
}
/**
* 前置通知
* pointCut切点指定方法
* @param joinPoint joinPoint参数⼀一定要出现在参数列列表第⼀一位,放在后⾯面会报错
*/
@Before(value = "pointCut()")
public void logStart(JoinPoint joinPoint) {
String methodName = joinPoint.getSignature().getName();
Object[] args = joinPoint.getArgs();
System.out.println("@Before-->方法前,前置通知" + ",方法名:" + methodName + ",参数列列表是:" + Arrays.toString(args));
}
/**
* 后置通知
*/
@After(value = "pointCut()")
public void logEnd() {
System.out.println("@After-->方法后,后置通知");
}
/**
* 返回通知
* @param joinPoint
* @param result
*/
@AfterReturning(value = "pointCut()", returning = "result")
public void logReturn(JoinPoint joinPoint, Object result) {
String methodName = joinPoint.getSignature().getName();
System.out.println("@AfterReturning-->返回通知,方法名:" + methodName + ",计算结果:" + result);
}
/**
* 异常通知
* @param joinPoint
* @param exception
*/
@AfterThrowing(value = "pointCut()", throwing = "exception")
public void logException(JoinPoint joinPoint, Exception exception) {
String methodName = joinPoint.getSignature().getName();
System.out.println("@AfterThrowing-->异常通知,方法名:" + methodName + ",异常信息:" + exception);
}
/**
* 环绕通知
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around(value = "pointCut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
Object retValue = null;
System.out.println("@Around-->我是环绕通知之前AAA");
retValue = proceedingJoinPoint.proceed();
System.out.println("@Around-->我是环绕通知之后BBB");
return retValue;
}
}
import com.wzq.spring.study.service.CalcService;
import com.wzq.spring.study.service.impl.CalcServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* aop
* 在程序运⾏期间动态的将某段代码切⼊到指定⽅方法指定位置进⾏运行的编程⽅式
* '@EnableAspectJAutoProxy':开启基于注解的aop动态代理
*
* @author wzq
* @date 2018/11/1
*/
@Configuration
@EnableAspectJAutoProxy
public class ConfigOfAOP {
/**
* 业务逻辑类加⼊入容器中
* @author wzq
* @date 2018/11/1
*/
@Bean
public CalcService calculator() {
return new CalcServiceImpl();
}
/**
* 切面类加⼊入容器中
* @author wzq
* @date 2018/11/1
*/
@Bean
public LogAspects logAspects() {
return new LogAspects();
}
}
//也可以不需要此类,直接在切面类LogAspects加@Component注解注入容器即可,因为配置了切点
aop注解执行顺序
业务逻辑:
public class CalcServiceImpl implements CalcService {
@Override
public int div(int x, int y) {
int result = x / y;
System.out.println("=========>CalcServiceImpl被调用了,我们的计算结果:"+result);
return result;
}
}
测试
@Resource
private CalcService calcService;
@Test
void aopTest1() {
System.out.println("spring版本:"+ SpringVersion.getVersion()+"\t"+"SpringBoot版本:"+ SpringBootVersion.getVersion());
System.out.println();
calcService.div(10,2);
}
spring5.2.8后正常顺序和异常顺序(注意:实际是SpringBoot2.3.3版本之后):
spring版本:5.2.8.RELEASE SpringBoot版本:2.3.3.RELEASE
@Around-->我是环绕通知之前AAA
@Before-->方法前,前置通知,方法名:div,参数列列表是:[10, 2]
=========>CalcServiceImpl被调用了,我们的计算结果:5
@AfterReturning-->返回通知,方法名:div,计算结果:5
@After-->方法后,后置通知
@Around-->我是环绕通知之后BBB
@Around-->我是环绕通知之前AAA
@Before-->方法前,前置通知,方法名:div,参数列列表是:[10, 0]
@AfterThrowing-->异常通知,方法名:div,异常信息:java.lang.ArithmeticException: / by zero
@After-->方法后,后置通知
java.lang.ArithmeticException: / by zero
spring4,spring5前期正常顺序和异常顺序:
spring版本:4.3.13.RELEASE SpringBoot版本:1.5.9.RELEASE
@Around-->我是环绕通知之前AAA
@Before-->方法前,前置通知,方法名:div,参数列列表是:[10, 2]
=========>CalcServiceImpl被调用了,我们的计算结果:5
@Around-->我是环绕通知之后BBB
@After-->方法后,后置通知
@AfterReturning-->返回通知,方法名:div,计算结果:5
spring版本:5.2.2.RELEASE SpringBoot版本:2.2.2.RELEASE
@Around-->我是环绕通知之前AAA
@Before-->方法前,前置通知,方法名:div,参数列列表是:[10, 2]
=========>CalcServiceImpl被调用了,我们的计算结果:5
@Around-->我是环绕通知之后BBB
@After-->方法后,后置通知
@AfterReturning-->返回通知,方法名:div,计算结果:5
spring版本:4.3.13.RELEASE SpringBoot版本:1.5.9.RELEASE
@Around-->我是环绕通知之前AAA
@Before-->方法前,前置通知,方法名:div,参数列列表是:[10, 0]
@After-->方法后,后置通知
@AfterThrowing-->异常通知,方法名:div,异常信息:java.lang.ArithmeticException: / by zero
java.lang.ArithmeticException: / by zero
Spring循环依赖
循环依赖:多个bean之间相互依赖,形成了一个闭环。 比如:A依赖于B、B依赖于c、c依赖于A
通常来说,如果问spring容器内部如何解决循环依赖, 一定是指默认的单例Bean中,属性互相引用的场景
也就是说,Spring的循环依赖,是Spring容器注入时候出现的问题,类似死锁现象
官网说明:https://docs.spring.io/spring-framework/docs/current/spring-framework-reference/core.html#beans
官网可知,解决循环依赖使用setter注入bean,不能使用构造方法注入bean。
我们AB循环依赖问题只要A的注入方式是setter且singleton, 就不会有循环依赖问题
spring容器循环依赖报错演示BeanCurrentlylnCreationException
循环依赖现象在Spring容器中 注入依赖的对象,有2种情况,构造器方式注入依赖和以set方式注入依赖
构造器方式注入
无法解决循环依赖问题,构造器注入没有办法解决循环依赖, 你想让构造器注入支持循环依赖,是不存在的
@Component
public class ServiceA {
private ServiceB serviceB;
public ServiceA(ServiceB serviceB) {
this.serviceB = serviceB;
}
}
@Component
public class ServiceB {
private ServiceA serviceA;
public ServiceB(ServiceA serviceA) {
this.serviceA = serviceA;
}
}
/**
* 通过构造器的方式注入依赖,构造器的方式注入依赖的bean,下面两个bean循环依赖
*
* 测试后发现,构造器循环依赖是无法解决的
*/
public class ClientConstructor {
public static void main(String[] args) {
//A构造函数依赖B注入,B构造函数依赖A注入,一直反复,无法结束
new ServiceA(new ServiceB(new ServiceA(new ServiceB()))); ....
}
}
set方式注入
不会报错
@Component
public class ServiceA {
private ServiceB b;
public ServiceB getB() {
return b;
}
public void setB(ServiceB b) {
this.b = b;
System.out.println("A 里面设置了B");
}
}
@Component
public class ServiceB {
private ServiceA a;
public ServiceA getA() {
return a;
}
public void setA(ServiceA a) {
this.a = a;
System.out.println("B 里面设置了A");
}
}
public class ClientSet {
public static void main(String[] args) {
//创建serviceA
ServiceA serviceA = new ServiceA();
//创建serviceB
ServiceB serviceB = new ServiceB();
//将serviceA注入到serviceB中
serviceB.setServiceA(serviceA);
//将serviceB注入到serviceA中
serviceA.setServiceB(serviceB);
}
}
spring容器
默认的单例(singleton)的set方式注入场景是支持循环依赖的,不报错
原型(Prototype)的场景是不支持循环依赖的,报错,多例下会报循环依赖异常BeanCurrentlylnCreationException
<?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/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/tx">
<!--
1.spring容器默认的单例模式可以解决循环引用,单例默认支持
2.spring容器原型依赖模式scope="prototype"多例模式下不能解决循环引用
-->
<!--depends-on 的意思就是当前这个bean如果要完成,先看depends-on指定的bean是否已经完成了初始化-->
<!--scope="prototype"代表每次都要新建一次对象-->
<bean id="a" class="com.wzq.spring.study.service.impl.ServiceA" scope="singleton">
<property name="b" ref="b"/>
</bean>
<bean id="b" class="com.wzq.spring.study.service.impl.ServiceB" scope="singleton">
<property name="a" ref="a"/>
</bean>
</beans>
/**
* nested exception is org.springframework.beans.factory.BeanCurrentlyInCreationException:
* Error creating bean with name 'a': 578624778
* Requested bean is currently in creation: Is there an unresolvable circular reference?
*
*
* 只有单例的bean会通过三级缓存提前暴露来解决循环依赖的问题,因为单例的时候只有一份,随时复用,那么就放到缓存里面
* 而多例的bean,每次从容器中获取都是一个新的对象,都会重新创建,
* 所以非单例的bean是没有缓存的,不会将其放到三级缓存中。
*/
@Test
public void deTest() {
ApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
ServiceA a = context.getBean("a", ServiceA.class);
ServiceB b = context.getBean("b", ServiceB.class);
System.out.println(a);
System.out.println(b);
}
spring三级缓存
DefaultSingletonBeanRegistry(re zi si g redʒɪstri)
所谓的三级缓存其实就是spring容器内部用来解决循环依赖问题的三个map
- 第一级缓存(也叫单例池)singletonObjects:存放已经经历了完整生命周期的Bean对象
- 第二级缓存: earlySingletonObjects,存放早期暴露出来的Bean对象,Bean的生命周期未结束(属性还未填充完整)
- 第三级缓存: Map<String, ObiectFactory<?>> singletonFactories,存放可以生成Bean的工厂
spring源码分析
实例化/初始化
实例化是堆内存中申请一块内存空间,初始化是属性填充
spring利用单例set注入,解决循环依赖的三级缓存+四大方法
四大方法:
- getSingleton:希望从容器里面获得单例的bean,没有的话
- doCreateBean: 没有就创建bean
- populateBean: 创建完了以后,要填充属性
- addSingleton: 填充完了以后,再添加到容器进行使用
三级缓存:
- 第一层singletonObjects存放的是已经初始化好了的Bean,
- 第二层earlySingletonObjects存放的是实例化了,但是未初始化的Bean
- 第三层singletonFactories存放的是FactoryBean。假如A类实现了FactoryBean,那么依赖注入的时候不是A类,而是A类产生的Bean
三级缓存简单理解,AB单例依赖set注入
- A创建过程中需要B,于是A将自己放到三级缓存里面,去实例化B
- B实例化的时候发现需要A,于是B先查一级缓存,没有,再查二级缓存,还是没有,再查三级缓存,找到了A,然后把三级缓存里面的这个A放到二级缓存里面(实例,但未初始化),并删除三级缓存里面的A
- B顺利初始化完毕,将自己放到一级缓存里面(此时B里面的A依然是创建中状态),然后回来接着创建A,此时B已经创建结束,直接从一级缓存里面拿到B,然后完成创建,并将A自己放到一级缓存里面。
总结spring解决循环依赖
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,走剩下的流程。
源码流程图如下:
debug步骤–》Spring解决循环依赖过程
- 调用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从二级缓存移动到一级缓存中