Dawn's Blogs

分享技术 记录成长

0%

SSM学习之Spring (2) IOC

IOC

IOC(Inversion Of Control,控制反转)是一种设计思想,就是将原本在程序中手动创建对象的控制权,交由 Spring 框架来管理,被管理

  • 控制:指的是对象创建(实例化、管理)的权力。
  • 反转:控制权交给外部环境(Spring 框架中的 IOC 容器)。

IOC 的目的就是解耦,将对象之间的相互依赖关系交给 IoC 容器来管理,并由 IoC 容器完成对象的注入。也就是说,IOC 有两个好处:

  1. 对象之间的耦合性降低。
  2. 资源变的容易管理,由手动管理变为 Spring IOC 容器进行管理,比如可以很容易的通过 IOC 容器实现单例模式。

Bean

Bean 是指被 IOC 容器管理的 Java 对象。

声明 Bean

作用于类的注解

声明为 Bean 的注解:

  • @Component:通用的注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用该注解。
  • @Repository:持久化层,用于数据库相关的操作。
  • @Service:对应服务层,主要涉及一些复杂的逻辑,需要用到 Dao 层。
  • @Controller:对应 Spring MVC 控制层,主要用于接受用户请求并调用 Service 层,返回数据给前端。

可以通过 @ComponentScan 注解定义要扫描的路径。

@Bean 注解

@Bean 注解修饰方法,表示在这个方法中定义产生这个 bean。使用示例如下:

1
2
3
4
5
6
7
8
@Configuration
public class AppConfig {
@Bean
public TransferService transferService() {
return new TransferServiceImpl();
}

}

@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
2
3
4
public @interface Resource {
String name() default "";
Class<?> type() default Object.class;
}

如果仅指定 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 的线程安全问题,常见的有两种解决办法:

  1. 在 Bean 中尽量避免定义可变的成员变量。
  2. 使用 ThreadLocal,将有状态的变量保存在 ThreadLocal 中。

Bean 的生命周期

Bean 的声明周期分为四步:

  1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
  2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如 @Autowired 等注解注入的对象、@Value 注入的值、setter 方法或构造函数注入依赖和值、@Resource 注入的各种资源。
  3. Bean 的初始化
    • 如果实现了 *.Aware 接口,调用相应的方法,如 BeanNameAware、BeanClassLoaderAware、BeanFactoryAware 等接口。
    • 如果实现了 BeanPostProcessor 接口,执行 postProcessBeforeInitialization() 方法。
    • 如果 Bean 实现了 InitializingBean 接口,执行 afterPropertiesSet() 方法。
    • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
    • 如果实现了 BeanPostProcessor 接口,执行 postProcessAfterInitialization() 方法。
  4. 销毁 Bean:注册相关销毁回调接口,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
    • 如果 Bean 实现了 DisposableBean 接口,执行 destroy 方法。
    • 如果 Bean 在配置文件中的定义了 destory-method 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过 @PreDestory 注解标记 Bean 销毁之前执行的方法。

img

Spring 中的循环依赖

三级缓存

Spring 通过三级缓存来解决循环依赖问题,确保即使在循环依赖的情况下也能正确创建 Bean。Spring 中的三级缓存其实就是三个 Map,如下:

1
2
3
4
5
6
7
8
9
10
11
// 一级缓存
/** Cache of singleton objects: bean name to bean instance. */
private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

// 二级缓存
/** Cache of early singleton objects: bean name to bean instance. */
private final Map<String, Object> earlySingletonObjects = new HashMap<>(16);

// 三级缓存
/** Cache of singleton factories: bean name to ObjectFactory. */
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap<>(16);

简单来说,Spring 的三级缓存包括:

  1. 一级缓存(singletonObjects):存放最终形态的 Bean(已经实例化、属性填充、初始化),单例池。一般情况获取 Bean 都是从这里获取的,但是并不是所有的 Bean 都在单例池里面,例如原型 Bean 就不在里面。
  2. 二级缓存(earlySingletonObjects):存放过渡 Bean(半成品,尚未属性填充),也就是三级缓存中 ObjectFactory 产生的对象,与三级缓存配合使用的,可以防止 AOP 的情况下,每次调用 ObjectFactory#getObject() 都是会产生新的代理对象的。
  3. 三级缓存(singletonFactories):存放 ObjectFactoryObjectFactorygetObject() 方法(最终调用的是 getEarlyBeanReference() 方法)可以生成原始 Bean 对象或者代理对象(如果 Bean 被 AOP 切面代理)。三级缓存只会对单例 Bean 生效。

Spring 创建 bean 的过程

Spring 创建 Bean 的流程:

  1. 先去一级缓存 singletonObjects 中获取,存在就返回;
  2. 如果不存在或者对象正在创建中,于是去 二级缓存 earlySingletonObjects 中获取;
  3. 如果还没有获取到,就去 三级缓存 singletonFactories 中获取,通过执行 ObjectFacotrygetObject() 就可以获取该对象,获取成功之后,从三级缓存移除,并将该对象加入到二级缓存中。

Spring 在创建 Bean 的时候,如果允许循环依赖的话,Spring 就会将刚刚实例化完成,但是属性还没有初始化完的 Bean 对象给提前暴露出去,这里通过 addSingletonFactory 方法,向三级缓存中添加一个 ObjectFactory 对象:

1
2
3
4
5
6
7
8
9
// AbstractAutowireCapableBeanFactory # doCreateBean #
public abstract class AbstractAutowireCapableBeanFactory ... {
protected Object doCreateBean(...) {
//..

// 支撑循环依赖:将 ()->getEarlyBeanReference 作为一个 ObjectFactory 对象的 getObject() 方法加入到三级缓存中
addSingletonFactory(beanName, () -> getEarlyBeanReference(beanName, mbd, bean));
}
}

如何解决循环依赖

如 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 的流程:

  1. 生成一个 A 原始对象,A 对象在三级缓存中注册 ObjectFactory(一个回调函数),用于创建 A 的代理对象。
  2. A 引用 B 对象,B 又引用 A 对象。B 回调 ObjectFactory 创建 A 的代理对象,B 引用 A 的代理对象。
  3. B 将 A 的 ObjectFactory 移出三级缓存,将 A 的代理对象放入二级缓存
  4. A 填充 B 对象,完成属性填充。
  5. A 在 BeanPostProcessor 接口的 postProcessAfterInitialization 的方法中,A 尝试生成代理对象,首先检查二级缓存,若二级缓存中有代理则不会生成新的代理对象。

@Lazy

@Lazy 用来标识类是否需要懒加载/延迟加载,可以作用在类上、方法上、构造器上、方法参数上、成员变量中。

Spring Boot 2.2 新增了全局懒加载属性,开启后全局 bean 被设置为懒加载,需要时再去创建。

1
2
#默认false
spring.main.lazy-initialization=true

如非必要,尽量不要用全局懒加载。全局懒加载会让 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 的代理对象,因此不会循环依赖。