在之前的文章 Java知识点整理 8 — Spring 简介 中介绍了 Spring 的两大核心概念 IoC 和 AOP,但对 Spring Bean 的介绍不全面,本文将补充 Spring 中 Bean 的概念。
在 Spring 官方文档中,对 bean 的定义为:构成应用程序主干并由 Spring IoC 容器管理的对象称为 bean。bean 是由 Spring IoC 容器实例化、组装和管理的对象。
开发者需要告诉 IoC 容器协助管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML文件(较老)、注解或者 Java 配置类。
首先有一个 Student 类,里面有两个成员变量 id 和 name,并提供 set、get方法。
- public class Student {
- private Integer id;
- private String name;
-
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
- }
另一个类是 StudentManager,有一个 Student 的对象,提供了 setStudent 方法初始化 Student 对象,并且它的 show 方法能够打印这个 student 的 id 和 name。
- public class StudentManager {
- private Student student;
-
- public void setStudent(Student student) {
- this.student = student;
- }
-
- public void show() {
- System.out.println(student.getId());
- System.out.println(student.getName());
- }
- }
分析以上两段代码发现,两个类之间高度耦合,后者依赖于前者。假如没有及时对 StudentManager 的 student 绑定对象,却调用了 show 方法,那么程序就会报空指针异常的错误。因此 Spring 提供了 IoC(控制反转)和 DI(依赖注入)进行解耦。
在 Spring 中不需要自己创建对象,只需要告诉 Spring,哪些类需要创建,然后在启动项目时 Spring 就会自动帮助创建对应的对象,并且只存在一个类的实例。这个类的实例也就是 Bean,而这种模式通常称为单例模式,即一个类只有一个实例。
继续思考,开发者该如何告诉 Spring 哪些类需要创建对象呢?
最简单最常用的方式就是 Java 注解配置。也就是将一个类声明为 Bean 所需要的注解。
声明 | 含义 |
@Component | 通用注解,可标注任意类为 Spring 组件。如果一个 Bean 不知道属于哪个层,可以使用该注解标注 |
@Repository | 当前类在持久层(Dao层),主要用于数据库相关操作。 |
@Service | 当前类在业务逻辑层,主要涉及复杂的逻辑。 |
@Controller | 当前类在控制层,主要用于接受用户请求并调用 Service 层返回数据给前端页面。 |
其实以上四种声明方式效果完全一致,使用不同的关键词是让开发者快速了解该类属于哪一层。
例如,在刚才的 Student 类前加上 @Component 注解,就告诉 Spring:你要在项目创建运行时帮我创建 Student 类的 Bean(对象)。
- @Component
- public class Student {
- private Integer id;
- private String name;
-
- public Integer getId() {
- return id;
- }
-
- public void setId(Integer id) {
- this.id = id;
- }
-
- public String getName() {
- return name;
- }
-
- public void setName(String name) {
- this.name = name;
- }
- }
此时"依赖"添加完毕,但还没结束。虽然让 Spring 帮助我们创建了对象,但 StudentManager 怎么知道这个被创建的对象在哪呢?所以接下来要告诉 StudentManager 刚才 Spring 帮助创建的 Bean(对象)在哪,也就是注入 Bean。
注入注解 | |
声明 | 含义 |
@Autowired | 根据 Bean 的 Class 类型自动装配 |
@Inject | 字面意思注入 |
@Resource | 字面意思资源,根据 Bean 的属性名称(id / name)自动装配 |
例如,在 StudentManager 类中声明成员变量 Student 的前面加上 @Autowired,Spring 会自动注入一个 Bean。
- @Component
- public class StudentManager {
- @Autowired
- private Student student;
-
- public void show() {
- System.out.println(student.getId());
- System.out.println(student.getName());
- }
- }
@Bean 注解的使用:
- @Configuration //标记该类为配置类,提供配置信息给 Spring 容器。
- public class AppConfig {
- @Bean
- public TransferService transferService() {
- return new TransferServiceImpl();
- }
- }
@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;
通过 @Qualifier 注解能够显式指定名称,而不是依赖变量名称。
@Resource 属于JDK提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean,注入方式会变为 byType。
@Resource 有两个比较重要且日常开发常用的属性:name(名称)和 type(类型)。
- // 报错,byName 和 byType 都无法匹配到 bean
- @Resource
- private SmsService smsService;
- // 正确注入 SmsServiceImpl1 对象对应的 bean
- @Resource
- private SmsService smsServiceImpl1;
- // 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
- @Resource(name = "smsServiceImpl1")
- private SmsService smsService;
总结一下:
通常有以下几种:
作用域 | 含义 |
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(仅 Web 应用) | 每个 Web 应用在启动时创建一个 bean(应用 bean),该 bean 仅在当前应用启动时间内有效。 |
websocket(仅 Web 应用) | 每次 WebSocket 会话产生一个新的 bean。 |
如何配置:
- // 注解方式
- @Bean
- @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
- public Person personPrototype() {
- return new Person();
- }
1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。
2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如 @Autowired 等注解注入对象、@Value 注入值、@Resource 注入各种资源等。
3. Bean 初始化:
4. 销毁 Bean:销毁并不是说立刻把 Bean 销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。
AbstractAutowireCapableBeanFactory 的 doCreateBean() 方法中能看到一次执行了这四个阶段:
- protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
- throws BeanCreationException {
-
- // 1. 创建 Bean 的实例
- BeanWrapper instanceWrapper = null;
- if (instanceWrapper == null) {
- instanceWrapper = createBeanInstance(beanName, mbd, args);
- }
-
- Object exposedObject = bean;
- try {
- // 2. Bean 属性赋值/填充
- populateBean(beanName, mbd, instanceWrapper);
- // 3. Bean 初始化
- exposedObject = initializeBean(beanName, exposedObject, mbd);
- }
-
- // 4. 销毁 Bean-注册回调接口
- try {
- registerDisposableBeanIfNecessary(beanName, bean, mbd);
- }
-
- return exposedObject;
- }
Aware 接口能让 Bean 拿到 Spring 容器资源。
Spring 中提供的 Aware 接口主要有:
BeanPostProcessor 接口时 Spring 为修改 Bean 提供的强大扩展点。
- public interface BeanPostProcessor {
-
- // 初始化前置处理
- default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
- return bean;
- }
-
- // 初始化后置处理
- default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
- return bean;
- }
-
- }
Spring 框架中的 Bean 是否线程安全取决于其作用域和状态。
通常情况下,Bean 作用域都是默认使用 singleton,常用的还有 prototype。
prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC容器中有唯一的 bean 实例,可能会存在资源竞争问题,这取决于 bean 是否有状态,即是否包含可变的成员变量的对象。如果是有状态的 bean,则存在线程安全问题。但大部分 bean 实际上都是无状态的,是线程安全的。
对于有状态单例 bean 的线程安全问题,可以在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal中。