【Spring】Bean/IoC/AOP/启动流程
什么是 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,而不是一个工厂。
那为什么名字如此奇怪,它其实是一个接口,并且有以下几个方法:
如果一个对象实现了这接口,那它就成为一种特殊的 Bean,注册到 IOC 容器之后,如果调用 getBean 获取得到的其实是 FactoryBean.getObject() 方法返回的结果。
为什么要这样做?
假设你依赖一个第三方的类 A,而我们又不能修改第三方的类,并且这个对象创建比较复杂,那么你就可以创建一个 bean 来封装它:
public class AFactoryBean implements FactoryBean<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 的生命周期,我用一幅图先总结一下:
大致了解生命周期之后,我们再来看详细的操作,可以看到有好多扩展点:
@PostConstruct 和 @PreDestroy 注解的作用是什么?
Spring 通过它们来管理 Bean 的生命周期。
**
@PostConstruct
**:用于标记方法,当依赖注入完成后,在 Bean 初始化完成后调用,这个方法会自动被调用。常用于进行初始化逻辑,例如设置默认值、检查依赖等。使用场景:
- 依赖注入后做额外的初始化工作:例如,某个服务需要在依赖注入后连接外部系统。
- 进行状态检查:在 Bean 初始化后验证某些关键属性是否正确配置。
执行时机:
@PostConstruct
方法在依赖注入完成后立即执行,但在 Bean 可以被其他对象使用之前调用(即在 Bean 完成初始化前调用,Bean 处于准备状态)。
**
@PreDestroy
**:用于标记方法,在 Bean 即将被销毁时调用。常用于进行资源的释放、清理工作。使用场景:
资源清理:例如关闭数据库连接、文件句柄、线程池等。
会话管理:例如在 Web 应用中,清理用户会话或缓存。
执行时机:
@PreDestroy
方法在 Bean 即将被销毁时调用,一般是在 Spring 容器关闭时执行。对于单例(singleton
)作用域的 Bean,会在容器关闭时调用;对于原型(prototype
)作用域的 Bean,不会调用销毁方法,因为容器不管理其生命周期。
Spring 的启动过程?
Bean的一生从总体上来说可以分为两个阶段:
- 容器启动阶段
- Bean实例化阶段
加载配置文件,初始化容器:Spring 启动时首先会读取配置文件(如 XML 配置文件、Java Config 类等),包括配置数据库连接、事务管理、AOP 配置等。
实例化容器:Spring 根据配置文件中的信息创建容器 ApplicationContext,在容器启动阶段实例化 BeanFactory,会扫描所有的 Bean 定义。对于基于注解的配置,Spring 会扫描指定包下的所有带有
@Component
、@Service
、@Controller
等注解的类,并加载它们的定义到容器中。对于 XML 配置,Spring 会解析<bean>
元素并加载定义,得到 BeanDefinitions。解析 BeanDefinitions:Spring 容器会解析配置文件中的 BeanDefinitions,即声明的 Bean 元数据,包括 Bean 的作用域、依赖关系等信息。
实例化 Bean:Spring 根据 BeanDefinitions 实例化 Bean 对象,将其放入容器管理,但不会立即注入依赖。这个过程类似于类的构造函数调用。
注入依赖:Spring 进行依赖注入,将 Bean 之间的依赖关系进行注入,包括构造函数注入、属性注入等。
构造函数注入:通过构造函数来注入依赖。
Setter 注入:通过 Setter 方法注入依赖。
字段注入:通过
@Autowired
注解直接注入字段。
处理 Bean 生命周期初始化方法:
Spring 调用 Bean 初始化方法(如果有定义的话),对 Bean 进行初始化。
如果 Bean 实现了
InitializingBean
接口,Spring 会调用其afterPropertiesSet
方法。
处理 BeanPostProcessors:容器定义了很多 BeanPostProcessor,处理其中的自定义逻辑,例如 postProcessBeforeInitialization 会在 Bean 初始化前调用, postProcessAfterInitialization 则在之后调用。
Spring AOP 代理也在这个阶段生成。
发布事件:Spring 可能会在启动过程中发布一些事件,比如容器启动事件。
完成启动:当所有 Bean 初始化完毕、依赖注入完成、AOP 配置生效等都准备就绪时,Spring 容器启动完成。
需要指出,容器启动阶段与Bean实例化阶段存在多少时间差。
如果我们选择懒加载的方式,那么直到我们伸手向Spring要依赖对象实例之前,其都是以BeanDefinationRegistry中的一个个的BeanDefination的形式存在,也就是Spring只有在我们需要依赖对象的时候才开启相应对象的实例化阶段。
而如果我们不是选择懒加载的方式,容器启动阶段完成之后,将立即启动Bean实例化阶段,通过隐式的调用所有依赖对象的getBean方法来实例化所有配置的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 中,只有同时满足以下两点才能解决循环依赖的问题:
- 依赖的 Bean 必须都是单例
- 依赖注入的方式,必须不全是构造器注入,且 beanName 字母序在前的不能是构造器注入
为什么必须都是单例
因为原型模式都需要创建新的对象,不能用以前的对象。如果是单例的话,创建 A 需要创建 B,而创建的 B 需要的是之前的 A, 不然就不叫单例了,对吧?
也是基于这点, Spring 就能操作操作了。
具体做法就是:先创建 A,此时的 A 是不完整的(没有注入 B),用个 map 保存这个不完整的 A,再创建 B ,B 需要 A,所以从那个 map 得到“不完整”的 A,此时的 B 就完整了,然后 A 就可以注入 B,然后 A 就完整了,B 也完整了,且它们是相互依赖的。
为什么不能全是构造器注入
在 Spring 中创建 Bean 分三步:
- 实例化,createBeanInstance,就是 new 了个对象
- 属性注入,populateBean, 就是 set 一些属性值
- 初始化,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:
- 一级缓存,singletonObjects,存储所有已创建完毕的单例 Bean (完整的 Bean)
- 二级缓存,earlySingletonObjects,存储所有仅完成实例化,但还未进行属性注入和初始化的 Bean
- 三级缓存,singletonFactories,存储能建立这个 Bean 的一个工厂,通过工厂能获取这个 Bean,延迟化 Bean 的生成,工厂生成的 Bean 会塞入二级缓存
这三个 map 是如何配合的呢?
- 首先,获取单例 Bean 的时候会通过 BeanName 先去 singletonObjects(一级缓存) 查找完整的 Bean,如果找到则直接返回,否则进行步骤 2。
- 看对应的 Bean 是否在创建中,如果不在直接返回找不到,如果是,则会去 earlySingletonObjects (二级缓存)查找 Bean,如果找到则返回,否则进行步骤 3
- 去 singletonFactories (三级缓存)通过 BeanName 查找到对应的工厂,如果存着工厂则通过工厂创建 Bean ,并且放置到 earlySingletonObjects 中。
- 如果三个缓存都没找到,则返回 null。
从上面的步骤我们可以得知,如果查询发现 Bean 还未创建,到第二步就直接返回 null,不会继续查二级和三级缓存。
返回 null 之后,说明这个 Bean 还未创建,这个时候会标记这个 Bean 正在创建中,然后再调用 createBean 来创建 Bean,而实际创建是调用方法 doCreateBean。
doCreateBean 这个方法就会执行上面我们说的三步骤:
- 实例化
- 属性注入
- 初始化
在实例化 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 的注入
像字段注入其实官方是不推荐的使用的,因为依赖注解,后没有控制注入顺序且无法注入静态字段。