IOC
IOC(Inversion Of Control,控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,被管理
- 控制:指的是对象创建(实例化、管理)的权力。
- 反转:控制权交给外部环境(Spring 框架中的 IOC 容器)。
IOC 的目的就是解耦,将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。也就是说,IOC 有两个好处:
- 对象之间的耦合性降低。
- 资源变的容易管理,由手动管理变为 Spring IOC 容器进行管理,比如可以很容易的通过 IOC 容器实现单例模式。
Bean
Bean 是指被 IOC 容器管理的 Java 对象。
声明 Bean
作用于类的注解
声明为 Bean 的注解:
- @Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用该注解。
- @Repository:持久化层,用于数据库相关的操作。
- @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
- @Controller:对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层,返回数据给前端。
可以通过 @ComponentScan 注解定义要扫描的路径。
@Bean 注解
@Bean 注解修饰方法,表示在这个方法中定义产生这个 bean。使用示例如下:
1 |
|
@Bean 和 @Component 的区别如下:
- @Component 作用于类,@Bean 作用于方法。
- @Component 修饰类,表示这个类需要自动装配到 Spring 容器中。@Bean 注解修饰方法,表示在这个方法中定义产生这个 bean。
- @Bean 注解比 @Component 的自定义性更强。比如当我们引用第三方库中的类需要装配到 Spring 容器中时,通过 @Bean 实现。
注入 Bean
用于注入 Bean 的注解:
- @Autowired:来自于 org.springframework.bean.factory
- @Resource:来自于 javax.annotation
- @Inject:来自于 javax.inject
@Autowired 和 @Resource 的区别
@Autowired
@Autowired 是 Spring 内置的注解,默认注入方式为 byType(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。
当接口存在多个实现类时,Spring 可以通过被注入的变量名称选择注入的 bean,变量名称等于首字母小写的实现类的名称。可以通过 @Qualifier 注解来显式指定名称而不是依赖变量的名称。
当一个接口存在多个实现类的话,byType 就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。
这种情况下,注入方式变为 byName(根据名称进行匹配),这个名称通常就是类名(首字母小写)。
@Resource
@Resource 是 JDK 提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为 byType。@Resource 有两个属性,name 和 type。
1 | public Resource { |
如果仅指定 name 属性则注入方式为 byName,如果仅指定 type 属性则注入方式为 byType,如果同时指定 name 和 type 属性(不建议这么做)则注入方式为 byType+byName。
Bean 的作用域
Spring 中 Bean 的作用域通常有下面几种:
- singleton : IoC 容器中只有唯一的 bean 实例。Spring 中的 bean 默认都是单例的,是对单例设计模式的应用。
- prototype : 每次获取都会创建一个新的 bean 实例。也就是说,连续 getBean() 两次,得到的是不同的 Bean 实例。
- request (仅 Web 应用可用): 每一次 HTTP 请求都会产生一个新的 bean(请求 bean),该 bean 仅在当前 HTTP request 内有效。
- session (仅 Web 应用可用) : 每一次来自新 session 的 HTTP 请求都会产生一个新的 bean(会话 bean),该 bean 仅在当前 HTTP session 内有效。
- application/global-session (仅 Web 应用可用):每个 Web 应用在启动时创建一个 Bean(应用 Bean),该 bean 仅在当前应用启动时间内有效。
- websocket (仅 Web 应用可用):每一次 WebSocket 会话产生一个新的 bean。
可以通过配置或者 @Scope 注解来声明 Bean 的作用域:@Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
Bean 是线程安全的吗?
取决于其作用域和状态。
- prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。
- singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。无状态则不存在线程安全问题(事实上,大部分 Bean 都是无状态的,如 Dao、Service)。
对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:
- 在 Bean 中尽量避免定义可变的成员变量。
- 使用 ThreadLocal,将有状态的变量保存在 ThreadLocal 中。
Bean 的生命周期
Bean 的声明周期分为四步:
- 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
- Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如 @Autowired 等注解注入的对象、@Value 注入的值、setter 方法或构造函数注入依赖和值、@Resource 注入的各种资源。
- Bean 的初始化:
- 如果实现了
*.Aware
接口,调用相应的方法,如 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 等接口。 - 如果实现了 BeanPostProcessor 接口,执行
postProcessBeforeInitialization()
方法。 - 如果 Bean 实现了 InitializingBean 接口,执行
afterPropertiesSet()
方法。 - 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
- 如果实现了 BeanPostProcessor 接口,执行
postProcessAfterInitialization()
方法。
- 如果实现了
- 销毁 Bean:注册相关销毁回调接口,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
- 如果 Bean 实现了 DisposableBean 接口,执行 destroy 方法。
- 如果 Bean 在配置文件中的定义了 destory-method 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过 @PreDestory 注解标记 Bean 销毁之前执行的方法。
Spring 中的循环依赖
三级缓存
Spring 通过三级缓存来解决循环依赖问题,确保即使在循环依赖的情况下也能正确创建 Bean。Spring 中的三级缓存其实就是三个 Map,如下:
1 | // 一级缓存 |
简单来说,Spring 的三级缓存包括:
- 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池。一般情况获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
- 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中
ObjectFactory
产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用ObjectFactory#getObject()
都是会产生新的代理对象的。 - 三级缓存(singletonFactories):存放
ObjectFactory
,ObjectFactory
的getObject()
方法(最终调用的是getEarlyBeanReference()
方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。
Spring 创建 bean 的过程
Spring 创建 Bean 的流程:
- 先去一级缓存 singletonObjects 中获取,存在就返回;
- 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;
- 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行
ObjectFacotry
的getObject()
就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。
Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 addSingletonFactory
方法,向三级缓存中添加一个 ObjectFactory
对象:
1 | // AbstractAutowireCapableBeanFactory # doCreateBean # |
如何解决循环依赖
如 A 和 B 发生了循环依赖,则会发生如下:
- 当 Spring 创建 A 之后,发现 A 依赖了 B ,又去创建 B,B 依赖了 A ,又去创建 A;
- 在 B 创建 A 的时候,那么此时 A 就发生了循环依赖,由于 A 此时还没有初始化完成,因此在 一二级缓存 中肯定没有 A;
- 那么此时就去三级缓存中调用
getObject()
方法去获取 A 的 前期暴露的对象 ,也就是调用getEarlyBeanReference()
方法,生成一个 A 的 前期暴露对象; - 然后就将这个
ObjectFactory
从三级缓存中移除,并且将前期暴露对象放入到二级缓存中,那么 B 就将这个前期暴露对象注入到依赖,来支持循环依赖。
只有两级缓存够吗
在没有 AOP 的情况下,确实可以只使用一级和三级缓存来解决循环依赖问题。但是,当涉及到 AOP 时,二级缓存就显得非常重要了,因为它确保了即使在 Bean 的创建过程中有多次对早期引用的请求,也始终只返回同一个代理对象,从而避免了同一个 Bean 有多个代理对象的问题。
有 AOP 的流程:
- 生成一个 A 原始对象,A 对象在三级缓存中注册 ObjectFactory(一个回调函数),用于创建 A 的代理对象。
- A 引用 B 对象,B 又引用 A 对象。B 回调 ObjectFactory 创建 A 的代理对象,B 引用 A 的代理对象。
- B 将 A 的 ObjectFactory 移出三级缓存,将 A 的代理对象放入二级缓存。
- A 填充 B 对象,完成属性填充。
- A 在 BeanPostProcessor 接口的 postProcessAfterInitialization 的方法中,A 尝试生成代理对象,首先检查二级缓存,若二级缓存中有代理则不会生成新的代理对象。
@Lazy
@Lazy 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。
Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。
1 | #默认false |
如非必要,尽量不要用全局懒加载。全局懒加载会让 Bean 第一次使用的时候加载会变慢,并且它会延迟应用程序问题的发现(当 Bean 被初始化时,问题才会出现)。
如果一个 Bean 没有被标记为懒加载,那么它会在 Spring IoC 容器启动的过程中被创建和初始化。如果一个 Bean 被标记为懒加载,那么它不会在 Spring IoC 容器启动时立即实例化,而是在第一次被请求时才创建。这可以帮助减少应用启动时的初始化时间,也可以用来解决循环依赖问题。
如何解决循环依赖
如 A 和 B 发生了循环依赖,A 的构造器上添加 @Lazy 注解后(延迟 Bean B 的实例化),加载的流程如下:
首先 Spring 会去创建 A 的 Bean,创建时需要注入 B 的属性;
由于在 A 上标注了 @Lazy 注解,因此 Spring 会去创建一个 B 的代理对象,将这个代理对象注入到 A 中的 B 属性;
之后开始执行 B 的实例化、初始化,在注入 B 中的 A 属性时,此时 A 已经创建完毕了,就可以将 A 给注入进去。
关键点就在于对 A 中的属性 B 进行注入时,注入的是 B 的代理对象,因此不会循环依赖。