1.Spring Bean的生命周期
- 整体上可以简单分为四步:实例化 —> 属性赋值 —> 初始化 —> 销毁。
- 初始化这一步涉及到的步骤比较多,包含
Aware
接口的依赖注入、BeanPostProcessor
在初始化前后的处理以及InitializingBean
和init-method
的初始化操作。 - 销毁这一步会注册相关销毁回调接口,最后通过
DisposableBean
和destory-method
进行销毁。
2.Bean是线程安全的吗?
Spring 框架中的 Bean 是否线程安全,取决于其作用域和状态。
我们这里以最常用的两种作用域 prototype 和 singleton 为例介绍。几乎所有场景的 Bean 作用域都是使用默认的 singleton ,重点关注 singleton 作用域即可。
- prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。
- singleton 作用域下,IoC 容器中只有唯一的 bean 实例,可能会存在资源竞争问题(取决于 Bean 是否有状态)。如果这个 bean 是有状态的话,那就存在线程安全问题(有状态 Bean 是指包含可变的成员变量的对象)。
不过,大部分 Bean 实际都是无状态(没有定义可变的成员变量)的(比如 Dao、Service),这种情况下, Bean 是线程安全的。
对于有状态单例 Bean 的线程安全问题,常见的有两种解决办法:
- 在 Bean 中尽量避免定义可变的成员变量。
- 在类中定义一个
ThreadLocal
成员变量,将需要的可变成员变量保存在ThreadLocal
中(推荐的一种方式)。
3.Spring 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。
4.使用Spring容器多次创建实例
默认情况下,当一个bean被定义在Spring容器中时,Spring会为这个bean创建一个单例实例。这意味着在整个Spring容器中,无论我们在哪里引用这个bean,都是引用的同一个实例。
然而,我们也可以配置Spring容器,使其为每个bean的请求创建一个新的实例,即多例模式。要实现这个目标,我们需要在bean的定义中使用scope
属性,并将其值设置为prototype
。
下面是一个如何在Spring中实现多例bean的例子:
public class MyBean { private String name; public MyBean(String name) { this.name = name; } public String getName() { return name; } @Override public String toString() { return "MyPrototypeBean{" + "name='" + name + '\'' + '}'; }
}
1.在配置类中使用注解,将scope
属性设置为prototype
@Configuration
public class AppConfig { @Bean @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public MyBean myBean(String name) { return new MyBean(name); }
}
2.在需要时从Spring容器中请求这个bean。因为这是原型bean,所以每次请求都会得到一个新的实例。
public class MainApp { public static void main(String[] args) { // 进行Bean管理ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class); MyBean bean1 = context.getBean("myBean", MyBean.class); bean1.setName("Bean 1"); System.out.println(bean1); MyBean bean2 = context.getBean("myBean", MyBean.class); bean2.setName("Bean 2"); System.out.println(bean2); // 因为它们是原型bean,所以它们不是同一个实例 System.out.println(bean1 == bean2); // 输出:false System.out.println(bean1.getName() + " " + bean2.getName()); // 输出:Bean 1 Bean 2 }
}
由于原型bean的生命周期是由Spring容器管理的,因此Spring容器会在每次请求时创建一个新的bean实例,并在不再需要时销毁它。
这意味着,如果你在你的代码中持有一个对原型bean的引用,并且这个引用不再被使用,那么这个bean实例可能会被垃圾收集器回收。因此,你应该始终从Spring容器中请求你需要的原型bean,而不是持有对它们的长期引用。
5.@Autowired和@Resource的区别是什么?
Autowired
Autowired
属于 Spring 内置的注解,默认的注入方式为byType
(根据类型进行匹配),也就是说会优先根据接口类型去匹配并注入 Bean (接口的实现类)。
这会有什么问题呢? 当一个接口存在多个实现类的话,byType
这种方式就无法正确注入对象了,因为这个时候 Spring 会同时找到多个满足条件的选择,默认情况下它自己不知道选择哪一个。
这种情况下,注入方式会变为 byName
(根据名称进行匹配),这个名称通常就是类名(首字母小写)。就比如说下面代码中的 smsService
就是我这里所说的名称,这样应该比较好理解了吧。
// smsService 就是我们上面所说的名称
@Autowired
private SmsService smsService;
举个例子,SmsService
接口有两个实现类: SmsServiceImpl1
和 SmsServiceImpl2
,且它们都已经被 Spring 容器所管理。
// 报错,byName 和 byType 都无法匹配到 bean
@Autowired
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Autowired
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean
// smsServiceImpl1 就是我们上面所说的名称
@Autowired
@Qualifier(value = "smsServiceImpl1")
private SmsService smsService;
Resource
@Resource
属于 JDK 提供的注解,默认注入方式为 byName
。如果无法通过名称匹配到对应的 Bean 的话,注入方式会变为byType
。
@Resource
有两个比较重要且日常开发常用的属性:name
(名称)、type
(类型)。
如果仅指定 name
属性则注入方式为byName
,如果仅指定type
属性则注入方式为byType
,如果同时指定name
和type
属性(不建议这么做)则注入方式为byType
+byName
。
// 报错,byName 和 byType 都无法匹配到 bean
@Resource
private SmsService smsService;
// 正确注入 SmsServiceImpl1 对象对应的 bean
@Resource
private SmsService smsServiceImpl1;
// 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
@Resource(name = "smsServiceImpl1")
private SmsService smsService;
简单总结一下:
@Autowired
是 Spring 提供的注解,@Resource
是 JDK 提供的注解。Autowired
默认的注入方式为byType
(根据类型进行匹配),@Resource
默认注入方式为byName
(根据名称进行匹配)。- 当一个接口存在多个实现类的情况下,
@Autowired
和@Resource
都需要通过名称才能正确匹配到对应的 Bean。Autowired
可以通过@Qualifier
注解来显式指定名称,@Resource
可以通过name
属性来显式指定名称。 @Autowired
支持在构造函数、方法、字段和参数上使用。@Resource
主要用于字段和方法上的注入,不支持在构造函数或参数上使用。
6.为什么更推荐构造函数注入
注入实现
final
关键字:修饰的字段必须指定初值,要么直接赋值,要么指定构造函数。这里指定
final
关键字后,可以通过@RequiredArgsConstructor
构造器指定final关键字进行赋值(防止构造非final修饰的字段)。
对比字段注入
依赖关系:
-
构造函数注入:依赖关系在类的构造函数中显式声明。强制要求在类的实例化时提供所有必需的依赖,使得依赖关系明确。
-
字段注入:依赖关系通过注解隐式注入。依赖关系在类的内部声明,可能在代码阅读时不容易一目了然。
更容易测试:
-
构造函数注入:在单元测试中,可以直接通过构造函数注入模拟对象。不依赖容器来注入依赖,可以更容易地进行单元测试。
-
字段注入:需要利用反射实现模拟对象的注入。测试时需要额外配置来注入依赖,增加了复杂性。
性能问题:
-
构造函数注入:基于构造函数,性能更好。
-
字段注入:需要利用反射注入,性能较差。
-
构造函数注入:在单元测试中,可以直接通过构造函数注入模拟对象。不依赖容器来注入依赖,可以更容易地进行单元测试。
-
字段注入:需要利用反射实现模拟对象的注入。测试时需要额外配置来注入依赖,增加了复杂性。
性能问题:
-
构造函数注入:基于构造函数,性能更好。
-
字段注入:需要利用反射注入,性能较差。