目录
一、Spring框架概述
1.1 什么是Spring?
1.2 spring优点有哪些?
二、IOC与DI
2.1 你知道getBean方法的有几种重载方式吗?
2.2 Spring有几种依赖注入方式?
2.3 为什么Spring不建议使用字段注入方式?
2.4 BeanFactory和FactoryBean的关系?
三、Spring创建对象
3.1 Spring创建对象有几种方式?
3.2 你简单说说Bean有哪些作用域?
3.3 Bean的生命周期?
3.4 Bean是线程安全的吗?
四、注解
4.1 你使用过哪些Spring注解?
4.2 那@Controller、@Service、@Repository和@Component有什么区别?
4.3 @Resource与@Autowired的区别?
五、面向切面编程(AOP)
5.1 什么是AOP?
5.2 AOP如何手动实现?
5.3 你在项目中是如何实现的?用过AspectJ吗?
5.4 有哪些通知类型?
5.5 Spring的AOP在什么场景下会失效?
5.6 在Spring中如何使用Spring Event做事件驱动?
六、JdbcTemplate
6.1 用过Spring自带的JDBC API技术JdbcTemplate吗?随便说几个常用的方法
七、事务
7.1 列举spring 支持的事务管理类型
7.2 你在项目中是如何实现事务的?
7.3 @Transactional注解都有哪些属性?
7.4 什么是事务的传播行为?
7.5 有几种传播行为?
7.6 有几种隔离级别?
7.7 你刚才提到@Transactional注解有一个属性timeout,你是怎么理解的?
7.8 那属性readOnly你是如何理解并使用的?
7.9 Spring事务失效可能是哪些原因?
八、SpringBoot相关
8.1 SpringBoot是如何实现自动化配置的?
九、循环依赖问题
9.1 什么是Spring循环依赖?
9.2 如何解决循环依赖问题?
9.3 什么是Spring的三级缓存?
9.4 三级缓存是如何解决循环依赖问题的?
9.5 Spring解决循环依赖一定需要三级缓存吗?
9.6 @Lazy注解能解决循环依赖吗?
十、其它
10.1 Spring6.0和SpringBoot3.0有什么新特性?
10.2 Spring中shutdownhook的作用是什么?
10.3 为什么不建议直接使用@Async?
一、Spring框架概述
1.1 什么是Spring?
答:spring的核心实现思想IOC,由spring来负责控制对象的生命周期和对象间的关系。注意以前对于一个类型(类),手动的使用new来实例化这个对象变量,现在不用你new了,交给spring控制。
在传统的开发中,在一个类定义另一个类的实例,这样的话,互相依赖,耦合在一起。spring的思想是两个类是独立的,用的时候在往里放,即轻松解耦。
1.2 spring优点有哪些?
答:可以从四个方面来答,解耦、切面、方便集成各种优秀框架、一站式。
一站式:在IOC和AOP的基础上可以整合各种企业应用的开源框架和优秀的第三方类库(实际上spring自身也提供了展现层的springmvc和持久层的spring JDBC),也就是说完全可以用一个spring技术就可以搭建一个web项目。
二、IOC与DI
2.1 你知道getBean方法的有几种重载方式吗?
答:三种
//第一种getBean(id):通过id去spring容器中找对象
Person person01 = (Person) ac.getBean("personOne");
//第二种getBean(类型):通过类型去spring容器中找对象
Person person02 = ac.getBean(Person.class);
//第三种getBean(id,类型):通过id + 类型去spring容器中找对象
Person person03 = ac.getBean("personTwo",Person.class);
2.2 Spring有几种依赖注入方式?
答:三种
分别是setter方式、构造器方式和字段方式。
2.3 为什么Spring不建议使用字段注入方式?
答:有这四个方面可以回答。
单一职责问题
我们都知道,根据SOLID设计原则来讲,一个类的设计应该符合单一职责原则,就是一个类只能做一件功能,当我们便用基于字段注入的时候,随看业务的暴增,字段越来越多,我们是很难发现我们已经默默中违背了单一职责原则的。
但是如果我们便用基于构造器注入的方式,因为构造器注入的写法比较雕肿,所以它就在间接提醒我们,违背了单一职责原则,该做重构了。
可能产生NPE
对于一个bean来说,它的初始化顺序为:
静态变量或静态语句块->实例变量或初始化语句块->构造方法->@Autowired
所以,在静态语句块,初始化语句块,构造方法中便用Autowired表明的字段,都会引起NPE问题。
@Component
class Test {
@Autowired
private Bean bean;
private final String beanName;
public Test() {
//此时bean尚未被初始化,会抛出NPE
this.beanName = bean.getName();
}
}
相反,用构造器的DI,就会实例化对象在使用的过程中,字段一定不为空。
隐藏依赖
对于一个正常使用依赖注入的Bean来说,它应该“显式”的通知容器,自己需要哪些Bean,可以通过构造器通知,public的setter方法通知,这些设计都是没问题的。
外部容器不应该感知到Bean内部私有字段(如上例中的private bean)的存在,私有字段对外部应该是不可见的。由于私有字段不可见,所以在设计层面,我们不应该通过字段注入的方式将依赖注入到私有字段中。这样会破坏封装性。
所以,当我们对字段做注入的时候,Spring就需要关心一个本来被我们封装到一个bean中的私有成员变量,这就和他的封装性违背了。因为我们应该通过setter或者构造函数来修改一个字段的值。
不利于测试
很明显,使用了Autowired注解,说明这个类依赖了Spring容器,这让我们在进行UT的时候必须要启动一个Spring容器才可以测试这个类,显然太麻烦,这种测试方式非常重,对于大型项目来说,往往后启动一个容器就要好几分钟,这样非常误时间。
不过,如果使用构造器的依赖注入就不会有这种问题,或者,我们可以使用Resource注解也可以解决上述问题。
2.4 BeanFactory和FactoryBean的关系?
答:两个都是接口。
BeanFactory比较常用,名字也比较容易理解,就是Bean工厂,他是整个Spring IoC容器的一部分,负责管理Bean的创建和生命周期。
FactoryBean是一个接口,用于定义一个工厂Bean,它可以产生某种类型的对象。当在Spring配置文件中定义一个Bean时,如果这个Bean实现了FactoryBean接口,那么Spring容器不直接返回这个Bean实例,而是返回FactoryBean#getObject方法所返回的对象。
所以,FactoryBean通常用于创建很复杂的对象,比如需要通过某种特定的创建过程才能得到的对象。例如,创建与JNDI资源的连接或与代理对象的创建。就如我们的Dubbo中的ReferenceBean。
三、Spring创建对象
3.1 Spring创建对象有几种方式?
答:三种
分别是无参构造、静态工厂、实例工厂方式。
3.2 你简单说说Bean有哪些作用域?
答:Bean有四种作用域,分别是Singleton(默认的)、Prototype、Request、Session。
3.3 Bean的生命周期?
答:
(1)实例化Bean
- Spring容器首先创建Bean实例。
- 在
AbstractAutowireCapableBeanFactory
类中的createBeanInstance
方法中实现。
(2)设置属性值
- Spring容器注入必要的属性到Bean中。
- 在
AbstractAutowireCapableBeanFactory
的populateBean
方法中处理。
(3)检查Aware
- 如果Bean实现了
BeanNameAware
,BeanClassLoaderAware
等这些Aware接口,Spring容器会调用它们。 - 在
AbstractAutowireCapableBeanFactory
的initializeBean
方法中调用。
(4)调用BeanPostProcessor的前置处理方法
- 在Bean初始化之前,允许自定义的BeanPostProcessor对Bean实例进行处理,如修改Bean的状态。BeanPostProcessor的
postProcessBeforeInitialization
方法会在此时被调用。 - 由
AbstractAutowireCapableBeanFactory
的applyBeanPostProcessorsBeforeInitialization
方法执行。
(5)调用InitializingBean的afterPropertiesSet
方法
- 提供一个机会,在所有Bean属性设置完成后进行初始化操作。如果Bean实现了
InitializingBean
接口,afterPropertiesSet
方法会被调用。 - 在
AbstractAutowireCapableBeanFactory
的invokeInitMethods
方法中调用。
(6)调用自定义init-method方法
- 提供一种配置方式,在XML配置中指定Bean的初始化方法。如果Bean在配置文件中定义了初始化方法,那么该方法会被调用。
- 在
AbstractAutowireCapableBeanFactory
的invokeInitMethods
方法中调用。
(7)调用BeanPostProcessor的后置处理方法
- 在Bean初始化之后,再次允许BeanPostProcessor对Bean进行处理。BeanPostProcessor的
postProcessAfterInitialization
方法会在此时被调用。 - 由
AbstractAutowireCapableBeanFactory
的applyBeanPostProcessorsAfterInitialization
方法执行。
(8)注册Destruction回调
- 如果Bean实现了DisposableBean接口或在Bean定义中指定了自定义的销毁方法,Spring容器会为这些 Bean注册一个销毁回调,确保在容器关闭时能够正确地清理资源。
- 在AbstractAutowireCapableBeanFactory类中的registerDisposableBeanIfNecessary方法中实现
(9)Bean准备就绪
- 此时,Bean已完全初始化,可以开始处理应用程序的请求了。
(10)调用DisposableBean的destroy方法
- 当容器关闭时,如果Bean实现了DisposableBean接口,destroy方法会被调用。
- 在DisposableBeanAdapter的destroy方法中实现
(11)调用自定义的destroy-method
- 如果Bean在配置文件中定义了销毁方法,那么该方法会被调用。
- 在DisposableBeanAdapter的destroy方法中实现
可以看到,整个Bean的创建的过程都依赖于AbstractAutowireCapableBeanFactory这个类,而销毁主要依赖DisposableBeanAdapter这个类。
3.4 Bean是线程安全的吗?
答:"Spring的Bean是否线程安全,这个要取决于他的作用域。Spring的Bean有多种作用域,其中用的比较多的就是Singleton和Prototype。
默认情况下,SpringBean是单例的(Singleton)。这意味着在整个Spring容器中只存在一个Bean实例。如果将Bean的作用域设置为原型的(Prototype),那么每次从容器中获取Bean时都会创建一个新的实例。
对于Prototype这种作用域的Bean,他的Bean实例不会被多个线程共享,所以不存在线程安全的问题。
但是对于Singleton的Bean,就可能存在线程安全问题了,但是也绝不绝对,要看这个Bean中是否有共享变量。"
四、注解
4.1 你使用过哪些Spring注解?
答:
- @Controller:一般用在控制层
- @Service:一般用在业务层
- @Repository:一般用在数据层
- @Component:可以用在各种类上
- @Resource:根据名称注入类,但注意此类是JDK的注解,并不是Spring的,只不过我们通常拿它和@Autowired一起比较
- @Autowired:用于在 spring bean 中自动装配,是根据类型注入类。
- @Scope:用于配置 spring bean 的作用域。
- @Aspect,@Before,@After,@Around,@Pointcut 用于切面编程(AOP)。
4.2 那@Controller、@Service、@Repository和@Component有什么区别?
答:虽然都用在各自的层(控制层、业务层、数据层)上,但其实本质没什么区别,哪怕你@Controller用在业务层,@Service用在数据层也不会出错,Spring是不会校验这个的,只不过我们这么写是为了让我们自己知道这个类是干什么的,哪个层,真正的意图是什么。
4.3 @Resource与@Autowired的区别?
答:
(1)来源不同:@Autowired 和 @Resource 来自不同的“父类”,其中 @Autowired 是 Spring 定义的注解,而 @Resource 是 Java 定义的注解,它来自于 JSR-250(Java 250 规范提案)。
(2)查找顺序不同:@Autowired 是先根据类型(byType)查找,如果存在多个 Bean 再根据名称(byName)进行查找。而@Resource 是先根据名称查找,如果(根据名称)查找不到,再根据类型进行查找。
五、面向切面编程(AOP)
5.1 什么是AOP?
答:AOP把重复的功能拿出来放到类中,让应用对象只关注于自己所针对的业务领域问题,而其他方面的问题由其他应用对象来处理。
举例:我们公司想要请明星做代言,明星跟我们洽谈工资、合同、以及租场地、演出,这些都是明星自己一个人干,那么这就不是切面编程。切面编程是让明星只关注于自己的业务领域(演出),其它的活可以交给经纪人(代理对象)去做。
5.2 AOP如何手动实现?
答:有两种方式,JDK动态代理和CGLIB,JDK动态代理必须是接口,而CGLIB没有接口,只有实现类。采用字节码增强框架cglib,在运行时创建目标类的子类,从而对目标类进行增强。
5.3 你在项目中是如何实现的?用过AspectJ吗?
答:用过
AspectJ是一个基于Java语言的AOP框架。
spring2.0以后新增了对AspectJ切点表达式的支持。
@AspectJ是AspectJ1.5新增功能,通过JDK5注解技术,允许直接在Bean类中定义切面。
新版本spring框架,建议使用AspectJ方式来开发AOP。
主要用途:自定义开发。
5.4 有哪些通知类型?
答:我知道有5种通知类型,分别是:
before:前置通知,应用于各种校验,在方法执行前执行,如果通知抛出异常,阻止方法运行。
afterReturning:后置通知,应用于常规数据处理,方法正常返回后执行,如果方法中抛出异常,通知无法执行。必须在方法执行后才执行,所以可以获得方法的返回值。
around:环绕通知,十分强大,可以做任何事情,方法执行前后分别执行,可以阻止方法的执行。必须手动执行目标方法。
afterThrowing:抛出异常通知,一般应用包装异常信息,方法抛出异常后执行,如果方法没有抛出异常,无法执行。
after:最终通知,一般用于清理现场环节,方法执行完毕后执行,无论方法中是否出现异常。相当于在finally代码块中。
5.5 Spring的AOP在什么场景下会失效?
答:
1、私有方法调用
2、静态方法调用
3、final方法调用
4、 类内部自调用
5、内部类方法调用
5.6 在Spring中如何使用Spring Event做事件驱动?
答:
SpringEvent是Spring框架中的一种事件机制,它允许不同组件之间通过事件的方式进行通信。Spring框架中的事件机制建立在观察者模式的基础上,允许应用程序中的组件注册监听器来监听特定类型的事件,并在事件发生时执行相应的操作。
SpringEvent的使用需要定义以下三个内容:
- 事件(Event):事件是一个普通的Java对象,用于封装关于事件发生的信息。通常,事件类会包含一些数据字段,以便监听器能够获取事件的相关信息。
- 事件发布者(EventPublisher):事件发布者是负责触发事件并通知所有注册的监听器的组件。
- 事件监听器(EventListener):事件监听器是负责响应特定类型事件的组件。它们实现了一个接口或者使用注解来标识自己是一个事件监听器,并定义了在事件发生时需要执行的逻辑。
六、JdbcTemplate
6.1 用过Spring自带的JDBC API技术JdbcTemplate吗?随便说几个常用的方法
答:当然用过,常用的方法有这几种:
增 update方法
删 update方法
改 update方法
批量增删改 batchupdate方法
查询实体 Bean queryForObject方法
查询 List query方法
查询 Map queryForMap方法
查询 List<Map<String,Object> queryForList方法
七、事务
7.1 列举spring 支持的事务管理类型
答:支持编程式事务管理和声明式事务管理。
使用原生的JDBC API进行事务管理类似于这样:
① 获取数据库连接Connection对象。
② 取消事务的自动提交。
③ 执行操作。
④ 正常完成操作时手动提交事务。
⑤ 执行失败时回滚事务。
⑥ 关闭相关资源。
使用原生的JDBC API实现事务管理是所有事务管理方式的基石,同时也是最典型的编程式事务管理。编程式事务管理需要将事务管理代码嵌入到业务方法中来控制事务的提交和回滚,在使用编程的方式管理事务时,必须在每个事务操作中包含额外的事务管理代码。相对于核心业务而言,事务管理的代码显然属于非核心业务,如果多个模块都使用同样模式的代码进行事务管理,显然会造成较大程度的代码冗余。
7.2 你在项目中是如何实现事务的?
答:使用@Transactional注解实现事务,可以作用在类和方法上。
7.3 @Transactional注解都有哪些属性?
答:常用的共有四种,分别是propagation(传播行为)、isolation(隔离级别)、timeout(超时操作)、readOnly(是否只读)。
7.4 什么是事务的传播行为?
答:A方法和B方法都有事务,当A在调用B时,会将A中的事务传播给B方法,B方法对于事务的处理方式就是事务的传播行为。
7.5 有几种传播行为?
答:七种,分别是required(默认)、supports、mandatory、requires_new、not_supported、never、nested。
7.6 有几种隔离级别?
答:读未提交、读已提交、可重复读、串行化。
7.7 你刚才提到@Transactional注解有一个属性timeout,你是怎么理解的?
答:情景:假如说京东秒杀活动,有1万个请求同时执行,这时有1000个请求在某个步骤上卡住了,那么你如果没有设置超时时间,请求会一直卡在那。因为我们知道一个连接对象要么提交要么回滚才算一个事务执行完毕。如果设置了超时时间假如说是5秒,那么就会等待5秒后还没有提交事务的话就直接强制回滚。这样的话也就释放连接了。
7.8 那属性readOnly你是如何理解并使用的?
答:指定当前事务中的一系列的操作是否为只读。若设置为只读,mysql就会在请求访问数据的时候不加锁,提高性能。
问题:如果一个事务内既有读也有修改操作,那么可以设置readOnly="true"吗?
可以设置,但是没什么用,反而变麻烦了,假如你设置了true,并且事务中有了修改操作。那么就会让所有的操作都不加锁,这样的话会造成脏读、幻读、不可重复读都来了。所以只有你这个事务内全部都是读的操作,才能加上readOnly="true"。
7.9 Spring事务失效可能是哪些原因?
答:
(1)代理失效的情况
Spring中比较容易失效的就是通过@Transactional定义的声明式事务,它在以下几个场景中会导致事务失效,首先,就是Spring的@Transactional是基于Spring的AOP机制实现的,而AOP机制又是基于动态代理实现的。那么如果代理失效了,事务也就会失效。
(2)@Transactional用的不对
@Transactional注解属性propagation设置错误
@Service
public class ExampleService {
@Autowired
private ExampleRepository repository;
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void methodA() {
repository.someDatabaseOperation();
}
public void methodB() {
repository.anotherDatabaseOperation();
}
}
@Service
public class SomeService {
@Autowired
private ExampleService exampleService;
@Transactional
public void doSomething() {
//这里执行一些业务逻辑
exampleService.methodA(); //这个方法由于NOT_SUPPORTED属性,会在非事务下执行
exampleService.methodB();
//...
}
}
以上,如果事务发生回滚,则methodA并不会回滚。因为他的propagation是不支持事务,那么他就不会一起回滚。
(3)异常被捕获
异常被catch捕获导致@Transactional失效。
public class MyService {
@Transactional
public void doSomething() {
try {
doInternal();
} catch(Exception e) {
logger.error(e);
}
}
}
因为异常被捕获,所以就没办法基于异常进行rollback了,所以事务会失效。
(4)事务中用了多线程
@Transactional的事务管理使用的是ThreadLocal机制来存储事务上下文,而ThreadLocal变量是线程隔离的,即每个线程都有自己的事务上下文副本。因此,在多线程环境下,Spring的声明式事务会失效,即新线程中的操作不会被包含在原有的事务中。
(5)数据库引擎不支持事务
这个好理解,如myisam,不支持的肯定就不行了。
八、SpringBoot相关
8.1 SpringBoot是如何实现自动化配置的?
答:SpringBoot会根据类路径中的jar包、类,为jar包里的类自动配置,这样可以极大的减少配置的数量。简单点说就是它会根据定义在classpath下的类,自动的给你生成一些Bean,并加载到Spring的Context中。
SpringBoot通过Spring的条件配置决定哪些bean可以被配置,将这些条件定义成具体的Configuration,然后将这些Configuration配置到spring.factories文件中(这种方式Springboot2.7.0版本已不建议使用,最新的方式是便用/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports)
作为key:org.springframework.boot.autoconfigure.EnableAutoConfiguration的值
这时候,容器在启动的时候,由于使用了EnableAutoConfiguration注解,该注解Import的
EnableAutoConfigurationlmportSelector会去扫描classpath下的所有spring.factories文件,然后进行bean的自动化配置。
九、循环依赖问题
9.1 什么是Spring循环依赖?
答:在Spring框架中,循环依赖是指两个或多个bean之间相互依赖,形成了一个循环引用的情况。如果不加以处理,这种情况会导致应用程序启动失败。
@Service
public class ServiceA {
@Autowired
private ServiceB serviceB;
// ... 其他代码
}
@Service
public class ServiceB {
@Autowired
private ServiceA serviceA;
// ... 其他代码
}
9.2 如何解决循环依赖问题?
答:构造器注入的循环依赖,可以通过一定的手段解决。
1、重新设计,彻底消除循环依赖
循环依赖,一般都是设计不合理导致的,可以从根本上做一些重构,来彻底解决,
2、改成非构造器注入
可以改成setter注入或者字段注入。
3、使用@Lazy解决
9.3 什么是Spring的三级缓存?
答:在Spring的BeanFactory体系中,BeanFactory是Spring IoC容器的基础接口,其DefaultSingletonBeanRegistry类实现了BeanFactory接口,并且维护了三级缓存:
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
// 一级缓存,保存完全创建好的单例bean对象
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);
// 三级缓存,保存单例bean的创建工厂
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);
// 二级缓存,存储“半成品”的bean对象
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap<>(16);
}
singletonObjects 是一级缓存,存储的是完全创建好的单例bean对象。在创建一个单例bean时,会先从singletonObjects中尝试获取该bean的实例,如果能获取到,则直接返回该实例,否则继续创建该bean。
earlySingletonObjects 是二级缓存,存储的是尚未完全创建好的单例bean对象。在创建单例bean时,如果发现该bean存在循环依赖,则会把创建该bean的“半成品”对象,存储于earlySingletonObjects中。当循环依赖的bean创建完成后,Spring会将完整的bean实例分别缓存到singletonObjects中,并将earlySingletonObjects中存储的代理对象替换为完整的bean实例对象。这样可以保证单例bean的创建过程不会出现循环依赖问题。
singletonFactories 是三级缓存,存储的是单例bean的创建工厂。当一个单例bean被创建时,Spring会先将该bean的创建工厂存储到singletonFactories中,然后再执行创建工厂的getObject()方法,生成该bean的实例对象。在bean被创建引用时,Spring会从singletonFactories中获取该bean的创建工厂,创建出该bean的实例对象,并将该bean的实例对象存储到singletonObjects中。
9.4 三级缓存是如何解决循环依赖问题的?
答:
Spring中Bean的创建过程其实可以分成两步,第一步叫做实例化,第二步叫做初始化。
实例化的过程只需要调用构造函数把对象创建出来并给他分配内存空间,而初始化则是给对象的属性进行赋值。
而Spring之所以可以解决循环依赖就是因为对象的初始化是可以延后的,也就是说,当我创建一个BeanServiceA的时候,会先把这个对象实例化出来,然后再初始化其中的ServiceB属性。
为何需要三级缓存而不是二级缓存呢?二级缓存虽然可以在bean完成属性填充后将其放入,但此时bean还未完成初始化,如果其他bean此时引用了这个bean,可能会导致不可预期的行为。而三级缓存则可以在bean实例化后立即放入一个工厂引用,使得其他bean可以引用到这个“早期对象”,但又不会影响到当前bean的后续初始化过程。
实例解释
假设有两个bean,A和B,它们相互依赖。当Spring创建A时,发现A依赖于B,于是开始创建B。在B的创建过程中,又发现B依赖于A。这时,Spring会先通过三级缓存中的工厂引用创建A的早期对象,并放入二级缓存。然后,B可以通过二级缓存引用到这个早期对象A,完成依赖注入。接着,B继续完成其初始化过程,并放入一级缓存。最后,A完成其初始化过程,并从二级缓存移动到一级缓存。
9.5 Spring解决循环依赖一定需要三级缓存吗?
答:其实,使用二级缓存也能解决循环依赖的问题,但是如果完全依靠二级缓存来解决循环依赖,意味着当我们依赖了一个代理类的时候,就需要在Bean实例化之后完成AOP代理。正如Spring的设计中,为了解释Bean的初始化代理处理,是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理的。
但是,在Spring的初始化过程中,他是不知道哪些Bean可能有循环依赖的,那么,这时候Spring面临两个选择:
- 不管有没有循环依赖,都提前把代理对象创建出来,将代理对象缓存起来,出现循环依赖时,其他对象直接就可以取到代理对象并注入。
- 不提前创建代理对象,在出现循环依赖时,再生成代理对象。这样在没有循环依赖的情况下,Bean就可以按照Spring设计原则的步骤来创建。
第一个方案看上去比较简单,只需要二级缓存就可以了。但是他也意味着,Spring需要在所有的bean的创建过程中就要先把代理对象准备好;那么这就和Spring的aop的设计原则(前文提到:在Spring的设计中,为了解释Bean的初始化代理处理,是通过AnnotationAwareAspectJAutoProxyCreator这个后置处理器来在Bean生命周期的最后一步来完成AOP代理)是相悖的。
而Spring为了不破坏AOP的设计原则,则引入三级缓存,在三级缓存中保存对象工厂,因为通过对象工厂我们可以在想要创建对象的时候直接获取对象。有了它,在bean发生循环依赖时,如果依赖的是被AOP代理,那么通过这个工厂获取到的就是代理后的AOP代理;如果依赖的是原始的bean,那么通过这个工厂获取到的就是真实的实例对象。
9.6 @Lazy注解能解决循环依赖吗?
答:能,Spring的三级缓存是没法解决构造器注入这种循环依赖的,但可以使用@Lazy注解,当我们使用@Lazy注解时,Spring容器会在需要该bean的时候才创建它,而不是在启动时。这意味着如果两个bean互相依赖,可以通过延迟其中一个bean的初始化来打破依赖循环。
十、其它
10.1 Spring6.0和SpringBoot3.0有什么新特性?
答:
(1)AOT编译
Ahead-Of-Time,即预先编译,这是相对于我们熟知的Just-In-Time(JIT,即时编译)来说的。
相比于JIT编译,AOT指的是在程序运行前编译,这样就可以避免在运行时的编译性能消耗和内存消耗,可以在程序运行初期就达到最高性能、也可以显著的加快程序的启动。
AOT的引入,意味着Spring生态正式引入了提前编译技术,相比于JIT编译,AOT有助于优化Spring框架启动慢、占用内存多、以及垃圾无法被回收等问题。
(2)Spring Native
在Spring的新版本中引入了Spring Native。
有了Spring Native,Spring可以不再依赖Java虚拟机,而是基于GraalVM将Spring应用程序编译成原生镜像(native image),提供了一种新的方式来部署Spring应用。这种部署Spring的方式是云原生友好的。
Spring Native的优点是编译出来的原生Spring应用可以作为一个独立的可执行文件进行部署,而不需要安装VM,而且启动时间非常短、并且有更少的资源消耗。他的缺点就是构建时长要比VM更长一些。
10.2 Spring中shutdownhook的作用是什么?
答:在Spring框架中,ShutdownHook(关闭钩子)是一种机制,用于在应用程序关闭时执行一些清理操作。
Spring会向JVM注册一个shutdownhook,在接收到关闭通知的时候,进行bean的销毁,容器的销毁处理等操作。
在Spring框架中,可以使用AbstractApplicationContext类或其子类来注册ShutdownHook。这些类提供了一个registerShutdownHook()方法,用于将ShutdownHook与应用程序上下文关联起来。
很多中间件的优雅上下线的功能(优雅停机),都是基于Spring的shutdownhook的机制实现的,比如Dubbo的优雅下线。
10.3 为什么不建议直接使用@Async?
答:当我们没自定义线程池的情况下,@Async就会用SimpleAsyncTaskExecutor这个默认线程池。
simpleAsyncTaskExecutor这玩意坑很大,其实他并不是真的线程池,它是不会重用线程的,每次调用都会创建一个新的线程,也没有最大线程数设置。并发大的时候会产生严重的性能问题。所以,我们们应该自定义线程池来配合@Async使用,而不是直接就用默认的。