什么是 Spring Bean?

在 Spring 中,构成应用程序主干并由 Spring IoC 容器管理的对象称为 bean。bean 是由Spring IoC 容器实例化、组装和管理的对象。

什么是 Spring IoC?

IoC,即 Inversion of Control,控制反转。控制反转通过依赖注入(DI)方式实现对象之间的松耦合关系

首先要明确 IOC 是一种思想,而不是一个具体的技术,其次 IOC 这个思想也不是 Spring 创造的。

然后我们要理解到底控制的是什么,其实就是控制对象的创建,IOC 容器根据配置文件来创建对象,在对象的生命周期内,在不同时期根据不同配置进行对象的创建和改造。

那什么被反转了?其实就是关于创建对象且注入依赖对象的这个动作,本来这个动作是由我们程序员在代码里面指定的,例如对象 A 依赖对象 B,在创建对象 A 代码里,我们需要写好如何创建对象 B,这样才能构造出一个完整的 A。

而反转之后,这个动作就由 IOC 容器触发,IOC 容器在创建对象 A 的时候,发现依赖对象 B ,根据配置文件,它会创建 B,并将对象 B 注入 A 中。

这里要注意,注入的不一定非得是一个对象,也可以注入配置文件里面的一个值给对象 A 等等。

在 Spring 中,类的实例化、依赖的实例化、依赖的传入都交由 Spring Bean 容器控制,而不是用 new 方式实例化对象、通过非构造函数方法传入依赖等常规方式。实质的控制权已经交由程序管理,而不是程序员管理,所以叫控制反转。

Spring 中的 DI 是什么?

DI,Dependency Injection,依赖注入。

普遍的答案是 DI 是 IOC 的一种实现。

其实它跟 IOC 的概念一致,只是从不同角度来描述罢了。

大致理解为容器在运行的时候,可以找到被依赖的对象,然后将其注入,通过这样的方式,使得各对象之间的关系可由运行期决定,而不用在编码时候明确。

Spring IOC 有什么好处?

对象的创建都由 IOC 容器来控制之后,对象之间就不会有很明确的依赖关系,使得非常容易设计出松耦合的程序。

例如,对象 A 需要依赖一个实现 B,但是对象都由 IOC 控制之后,我们不需要明确地在对象 A 的代码里写死依赖的实现 B,只需要写明依赖一个接口,这样我们的代码就能顺序的编写下去。

然后,我们可以在配置文件里定义 A 依赖的具体的实现 B,根据配置文件,在创建 A 的时候,IOC 容器就知晓 A 依赖的 B,这时候注入这个依赖即可。

如果之后你有新的实现需要替换,那 A 的代码不需要任何改动,你只需要将配置文件 A 依赖 B 改成 B1,这样重启之后,IOC 容器会为 A 注入 B1。

并且也因为创建对象由 IOC 全权把控,那么我们就能很方便的让 IOC 基于扩展点来“加工”对象,例如我们要代理一个对象,IOC 在对象创建完毕,直接判断下这个对象是否需要代理,如果要代理,则直接包装返回代理对象。

这等于我们只要告诉 IOC 我们要什么,IOC 就能基于我们提供的配置文件,创建符合我们需求的对象。

Spring 中的 FactoryBean 是什么?

从命名角度来看,我们可以得知它就是一个 Bean,而不是一个工厂。

那为什么名字如此奇怪,它其实是一个接口,并且有以下几个方法:

img

如果一个对象实现了这接口,那它就成为一种特殊的 Bean,注册到 IOC 容器之后,如果调用 getBean 获取得到的其实是 FactoryBean.getObject() 方法返回的结果。

为什么要这样做?

假设你依赖一个第三方的类 A,而我们又不能修改第三方的类,并且这个对象创建比较复杂,那么你就可以创建一个 bean 来封装它:

public class AFactoryBean implements FactoryBean<A> {
public A getObject() throws Exception {
A a = new A();
a.setXXX
....
...
return A
}
....省略一些实现
}

这样,我们 getBean(“A”) 会得到 AFactoryBean.getObject 的结果,如果单纯只想要 AFactoryBean, 那么加个 “&” 即可,即 getBean(“&A”)

Spring Bean 一共有几种作用域?

从官网,我们很容易可以得知,最新版本一共有六种作用域:

  • singleton:默认是单例,含义不用解释了吧,一个 IOC 容器内部仅此一个
  • prototype:原型,多实例
  • request:每个请求都会新建一个属于自己的 Bean 实例,这种作用域仅存在 Spring Web 应用中
  • session:一个 http session 中有一个 bean 的实例,这种作用域仅存在 Spring Web 应用中
  • application:整个 ServletContext 生命周期里,只有一个 bean,这种作用域仅存在 Spring Web 应用中
  • websocket:一个 WebSocket 生命周期内一个 bean 实例,这种作用域仅存在 Spring Web 应用中

说下 Spring Bean 的生命周期?

在说具体的生命周期前,我们需要先知晓之所以 Bean 容易被添加一些属性,或者能在运行时被改造就是因为在生成 Bean 的时候,Spring对外暴露出很多扩展点。

基于这些点我们可以设置一些逻辑,Spring 会在 Bean 创建的某些阶段根据这些扩展点,基于此进行 Bean 的改造。

有了上面的认识,我们再来看 Spring Bean 的生命周期,我用一幅图先总结一下:

大致了解生命周期之后,我们再来看详细的操作,可以看到有好多扩展点:

Spring Bean 注册到容器有哪些方式?

Spring Bean 注册到容器的方式主要包括以下几种:

1)基于 XML 的配置

使用 XML 文件配置 Bean,并定义 Bean 的依赖关系。

2)基于 @Component 注解及其衍生注解

使用注解如 @Component@Service@Controller@Repository 等进行配置。

声明 含义
@Component 当前类是组件,没有明确的意思,可以用于任何类,标记该类为 Spring 管理的 Bean。
@Service 当前类在业务逻辑层使用,用于标记服务类,以便于识别和管理业务逻辑组件。
@Repository 当前类在数据访问层
@Controller 当前类在展示层(MVC)使用,用于处理 HTTP 请求和响应。

以上四种声明方式效果完全一致,使用不同的关键词是为了给阅读的人能够快速了解该类属于哪一层。

3)基于 @Configuration@Bean 注解

使用 @Configuration 注解声明配置类,并使用 @Bean 注解定义 Bean。

4)基于 @Import 注解

@Import 可以将普通类导入到 Spring 容器中,这些类会自动被注册为 Bean。

@Bean 和 @Component有什么区别?

@Bean 和 @Component 都是用于定义 Spring 容器中的 Bean 的注解,但它们的使用场景和方式有所不同:

  • @Bean 注解通常用于 Java 配置类的方法上,以声明一个 Bean 并将其添加到 Spring 容器中。
  • @Component 注解用于类级别,将该类标记为 Spring 容器中的一个组件,自动检测并注册为 Bean(需要扫对应的包)。

Spring 的单例 Bean 是否有并发安全问题?

Spring 的单例 Bean 默认是非线程安全的,但是只要我们避免在多个 Bean 之间共享一些数据,就不用害怕并发问题。

原因分析

1)单例 Bean 的生命周期:Spring 容器在初始化时会创建并管理单例 Bean,这些 Bean 在整个应用程序生命周期内只会被创建一次,并且被多个线程共享使用。

2)多线程访问:如果单例 Bean 中包含共享的可变状态(如实例变量),多个线程同时访问并修改这些共享状态时,可能会导致并发安全问题,如数据不一致、脏读、死锁等。

Spring Bean如何保证并发安全?

大致可以分三种方式:

1)无状态 Bean:如果单例 Bean 不包含可变状态,或者仅包含只读状态,通常不会有并发安全问题。例如,所有方法都是无状态的纯函数,或者使用的是本地变量。

2)线程安全设计:确保单例 Bean 中的方法是线程安全的。可以通过同步方法、使用线程安全的数据结构(如 ConcurrentHashMap)、或使用 java.util.concurrent 包中的其他并发工具类。

3)@Scope(“prototype”):对于需要状态的 Bean,可以将其作用域设置为原型(prototype),这样每次请求该 Bean 时,Spring 容器都会返回一个新的实例。

什么是循环依赖(常问)?

例如 A 要依赖 B,发现 B 还没创建,于是开始创建 B ,创建的过程发现 B 要依赖 A, 而 A 还没创建好呀,因为它要等 B 创建好,就这样它们俩就搁这卡 bug 了

Spring 如何解决循环依赖?

关键就是提前暴露未完全创建完毕的 Bean

在 Spring 中,只有同时满足以下两点才能解决循环依赖的问题:

  1. 依赖的 Bean 必须都是单例
  2. 依赖注入的方式,必须不全是构造器注入,且 beanName 字母序在前的不能是构造器注入

为什么必须都是单例

因为原型模式都需要创建新的对象,不能用以前的对象。如果是单例的话,创建 A 需要创建 B,而创建的 B 需要的是之前的 A, 不然就不叫单例了,对吧?

也是基于这点, Spring 就能操作操作了。

具体做法就是:先创建 A,此时的 A 是不完整的(没有注入 B),用个 map 保存这个不完整的 A,再创建 B ,B 需要 A,所以从那个 map 得到“不完整”的 A,此时的 B 就完整了,然后 A 就可以注入 B,然后 A 就完整了,B 也完整了,且它们是相互依赖的。

为什么不能全是构造器注入

在 Spring 中创建 Bean 分三步:

  1. 实例化,createBeanInstance,就是 new 了个对象
  2. 属性注入,populateBean, 就是 set 一些属性值
  3. 初始化,initializeBean,执行一些 aware 接口中的方法,initMethod,AOP代理等

明确了上面这三点,再结合我上面说的“不完整的”,我们来理一下。

如果全是构造器注入,比如A(B b),那表明在 new 的时候,就需要得到 B,此时需要 new B ,但是 B 也是要在构造的时候注入 A ,即B(A a),这时候 B 需要在一个 map 中找到不完整的 A ,发现找不到。

为什么找不到?因为 A 还没 new 完呢,所以找不到完整的 A,因此如果全是构造器注入的话,那么 Spring 无法处理循环依赖

如果循环依赖不完全是构造器注入,则可能成功,可能失败,具体跟BeanName的字母序有关系。

Spring 解决循环依赖全流程

明确了 Spring 创建 Bean 的三步骤之后,我们再来看看它为单例搞的三个 map:

  1. 一级缓存,singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
  2. 二级缓存,earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean
  3. 三级缓存,singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存

这三个 map 是如何配合的呢?

  1. 首先,获取单例 Bean 的时候会通过 BeanName 先去 singletonObjects(一级缓存) 查找完整的 Bean,如果找到则直接返回,否则进行步骤 2。
  2. 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects (二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3
  3. 去 singletonFactories (三级缓存)通过 BeanName 查找到对应的工厂,如果存着工厂则通过工厂创建 Bean ,并且放置到 earlySingletonObjects 中。
  4. 如果三个缓存都没找到,则返回 null。

从上面的步骤我们可以得知,如果查询发现 Bean 还未创建,到第二步就直接返回 null,不会继续查二级和三级缓存。

返回 null 之后,说明这个 Bean 还未创建,这个时候会标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean。

doCreateBean 这个方法就会执行上面我们说的三步骤:

  1. 实例化
  2. 属性注入
  3. 初始化

在实例化 Bean 之后,会往 singletonFactories 塞入一个工厂,而调用这个工厂的 getObject 方法,就能得到这个 Bean

为什么 Spring 循环依赖需要三级缓存,二级不够吗?

我们思考下,解决循环依赖需要三级缓存吗?

很明显,如果仅仅只是为了破解循环依赖,二级缓存够了,压根就不必要三级。

你思考一下,在实例化 Bean A 之后,我在二级 map 里面塞入这个 A,然后继续属性注入,发现 A 依赖 B 所以要创建 Bean B,这时候 B 就能从二级 map 得到 A ,完成 B 的建立之后, A 自然而然能完成。

所以为什么要搞个三级缓存,且里面存的是创建 Bean 的工厂呢

问题就出在时机,这跟 Bean 的生命周期有关系。

正常代理对象的生成是基于后置处理器,是在被代理的对象初始化后期调用生成的所以如果你提早代理了其实是违背了 Bean 定义的生命周期

所以 Spring 先在一个三级缓存放置一个工厂,如果产生循环依赖,那么就调用这个工厂提早得到代理对象,如果没产生依赖,这个工厂根本不会被调用,所以 Bean 的生命周期就是对的。

什么是 AOP?

AOP,Aspect Oriented Programming,面向切面编程。

将一些通用的逻辑集中实现,然后通过 AOP 进行逻辑的切入,减少了零散的碎片化代码,提高了系统的可维护性。

面向切面编程是一种通过将横切关注点(如日志记录、事务管理、安全检查等)与业务逻辑分离的技术。在传统的编程中,这些横切关注点通常会分散在各个业务逻辑模块中,导致代码的重复和难以维护。而在 Spring 中,通过 AOP 可以将这些横切关注点封装成切面,然后在需要的时候将切面织入到业务逻辑中,从而提高代码的可维护性和可扩展性。

实现上代理大体上可以分为:动态代理静态代理

  • 动态代理,即在运行时将切面的逻辑进去,按照上面的逻辑就是你实现 A 类,然后定义要代理的切入点和切面的实现,程序会自动在运行时生成类似上面的代理类。
  • 静态代理,在编译时或者类加载时进行切面的织入,典型的 AspectJ 就是静态代理。

Spring AOP 相关术语都有哪些?

1)切面(Aspect):其实就是定义了一个 java 类,里面包含了通知(advice)和切点(pointcut),定义了在何处以及何时执行通知,将切面的一些东西模块化了。

2)通知(Advice)

  • 前置通知(Before advice):在目标方法执行前执行。
  • 后置通知(After returning advice):在目标方法成功执行后执行。
  • 后置异常通知(After throwing advice):在目标方法抛出异常后执行。
  • 后置最终通知(After (finally) advice):无论目标方法如何结束(正常返回或抛出异常),都会执行。
  • 环绕通知(Around advice):在目标方法执行前后都执行,并且可以控制目标方法的执行过程,环绕通知可以用作日志打印或者权限校验。

3)切点(Pointcut):切点是一个表达式,用于定义在哪些连接点上执行通知,简单理解就是通过这个表达式可以找到想要织入的哪些方法。

4)连接点(Join point):连接点是程序执行过程中可以应用切面的点,例如方法的调用、方法的执行、异常的抛出等,可以拿到切入方法名等诸多属性。

Spring 一共有几种注入方式?

  • 构造器注入,Spring 倡导构造函数注入,因为构造器注入返回给客户端使用的时候一定是完整的。
  • setter 注入,可选的注入方式,好处是在有变更的情况下,可以重新注入。
  • 字段注入,就是平日我们用 @Autowired 标记字段
  • 方法注入,就是平日我们用 @Autowired 标记方法
  • 接口回调注入,就是实现 Spring 定义的一些内建接口,例如 BeanFactoryAware,会进行 BeanFactory 的注入

像字段注入其实官方是不推荐的使用的,因为依赖注解,后没有控制注入顺序且无法注入静态字段。