• Java知识点整理 16 — Spring Bean


    在之前的文章 Java知识点整理 8 — Spring 简介 中介绍了 Spring 的两大核心概念 IoC 和 AOP,但对 Spring Bean 的介绍不全面,本文将补充 Spring 中 Bean 的概念。

    一. 什么是 Spring Bean 

    在 Spring 官方文档中,对 bean 的定义为:构成应用程序主干并由 Spring IoC 容器管理的对象称为 bean。bean 是由 Spring IoC 容器实例化、组装和管理的对象。

    开发者需要告诉 IoC 容器协助管理哪些对象,这个是通过配置元数据来定义的。配置元数据可以是 XML文件(较老)、注解或者 Java 配置类。

    二. 通过例子来理解

    首先有一个 Student 类,里面有两个成员变量 id 和 name,并提供 set、get方法。

    1. public class Student {
    2. private Integer id;
    3. private String name;
    4. public Integer getId() {
    5. return id;
    6. }
    7. public void setId(Integer id) {
    8. this.id = id;
    9. }
    10. public String getName() {
    11. return name;
    12. }
    13. public void setName(String name) {
    14. this.name = name;
    15. }
    16. }

    另一个类是 StudentManager,有一个 Student 的对象,提供了 setStudent 方法初始化 Student 对象,并且它的 show 方法能够打印这个 student 的 id 和 name。

    1. public class StudentManager {
    2. private Student student;
    3. public void setStudent(Student student) {
    4. this.student = student;
    5. }
    6. public void show() {
    7. System.out.println(student.getId());
    8. System.out.println(student.getName());
    9. }
    10. }

    分析以上两段代码发现,两个类之间高度耦合,后者依赖于前者。假如没有及时对 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(对象)

    1. @Component
    2. public class Student {
    3. private Integer id;
    4. private String name;
    5. public Integer getId() {
    6. return id;
    7. }
    8. public void setId(Integer id) {
    9. this.id = id;
    10. }
    11. public String getName() {
    12. return name;
    13. }
    14. public void setName(String name) {
    15. this.name = name;
    16. }
    17. }

    此时"依赖"添加完毕,但还没结束。虽然让 Spring 帮助我们创建了对象,但 StudentManager 怎么知道这个被创建的对象在哪呢?所以接下来要告诉 StudentManager 刚才 Spring 帮助创建的 Bean(对象)在哪,也就是注入 Bean

    注入注解
    声明含义
    @Autowired根据 Bean 的 Class 类型自动装配
    @Inject字面意思注入
    @Resource字面意思资源,根据 Bean 的属性名称(id / name)自动装配

    例如,在 StudentManager 类中声明成员变量 Student 的前面加上 @Autowired,Spring 会自动注入一个 Bean。

    1. @Component
    2. public class StudentManager {
    3. @Autowired
    4. private Student student;
    5. public void show() {
    6. System.out.println(student.getId());
    7. System.out.println(student.getName());
    8. }
    9. }

    三. @Bean 注解的使用

    • @Bean 注解作用在方法上,产生一个 Bean 对象,然后将其交给 Spring 管理。Spring 会将这个 Bean 对象放在自己的 IoC 容器中。
    • @Bean 方法名与返回类名一致,首字母小写。
    • @Component、@Repository、@Controller、@Service 这些注解只局限于自己编写的类,@Bean 注解能把第三方库中的类实例加入 IoC 容器并交给 Spring 管理。
    • @Bean 一般和 @Component 或 @Configuration 一起使用。

    四. @Component 和 @Bean 的区别

    • @Component 注解作用于类,@Bean 作用于方法。
    • @Component 通常是通过类路径扫描来自动侦测以及自动装配到 Spring 容器中,可以使用 @ComponentScan 注解定义要扫描的路径,从中找出标识了需要装配的类,并自动装配到 Spring 的 bean 容器中。@Bean 注解通常是在标有该注解的方法中定义产生这个 bean,它告诉了 Spring 这是某个类的实例,在需要时给我。
    • @Bean 注解比 @Component 注解的自定义性更强,而且很多地方只能通过@Bean 注解来注册 bean,比如第三方库。

    @Bean 注解的使用:

    1. @Configuration //标记该类为配置类,提供配置信息给 Spring 容器。
    2. public class AppConfig {
    3. @Bean
    4. public TransferService transferService() {
    5. return new TransferServiceImpl();
    6. }
    7. }

    五. @Autowired 和 @Resource 的区别

    @Autowired 属于 Spring 内置的注解,默认注入方式为 byType,优先根据接口类型去匹配并注入 Bean(接口的实现类)。

    但如果一个接口存在多个实现类,byType 这种方式可能无法正确注入对象,因为 Spring 找到了多个满足条件的选择,默认情况下不知道选哪一个。这种情况下,注入方式会变为 byName,即根据名称匹配,这个名称通常是类名,如下面的 SmsService。

    1. // smsService 就是上面所说的名称
    2. @Autowired
    3. private SmsService smsService;

    如果 SmsService 接口有两个实现类:SmsServiceImpl1 和 SmsServiceImpl2,且它们都被 Spring 容器管理。

    1. // 报错,byName 和 byType 都无法匹配到 bean
    2. @Autowired
    3. private SmsService smsService;
    4. // 正确注入 SmsServiceImpl1 对象对应的 bean
    5. @Autowired
    6. private SmsService smsServiceImpl1;
    7. // 正确注入 SmsServiceImpl1 对象对应的 bean
    8. // smsServiceImpl1 就是我们上面所说的名称
    9. @Autowired
    10. @Qualifier(value = "smsServiceImpl1")
    11. private SmsService smsService;

    通过 @Qualifier 注解能够显式指定名称,而不是依赖变量名称。

    @Resource 属于JDK提供的注解,默认注入方式为 byName。如果无法通过名称匹配到对应的 Bean,注入方式会变为 byType。

    @Resource 有两个比较重要且日常开发常用的属性:name(名称)和 type(类型)。

    1. // 报错,byName 和 byType 都无法匹配到 bean
    2. @Resource
    3. private SmsService smsService;
    4. // 正确注入 SmsServiceImpl1 对象对应的 bean
    5. @Resource
    6. private SmsService smsServiceImpl1;
    7. // 正确注入 SmsServiceImpl1 对象对应的 bean(比较推荐这种方式)
    8. @Resource(name = "smsServiceImpl1")
    9. private SmsService smsService;

    总结一下:

    • @Autowired 是 Spring 提供的注解,@Resource 是 JDK 提供的注解。
    • @Autowired 默认的注入方式为 byType,@Resource 默认的注入方式为 byName。
    • 当一个接口存在多个实现类时,两个注解都需要通过名称才能正确匹配到对应的 Bean。@Autowired 可以通过 @Qualifier 注解来显示指定名称,@Resource 可以通过 name 属性来显式指定名称。
    • @Autowired 支持在构造函数、方法、字段和参数上使用。@Resource 主要用于字段和方法上的注入,不支持在构造函数或参数上使用。

    六. 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(仅 Web 应用)每个 Web 应用在启动时创建一个 bean(应用 bean),该 bean 仅在当前应用启动时间内有效。
    websocket(仅 Web 应用)每次 WebSocket 会话产生一个新的 bean。

    如何配置:

    1. // 注解方式
    2. @Bean
    3. @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    4. public Person personPrototype() {
    5. return new Person();
    6. }

    七. Bean 的生命周期

    1. 创建 Bean 的实例:Bean 容器首先会找到配置文件中的 Bean 定义,然后使用 Java 反射 API 来创建 Bean 的实例。

    2. Bean 属性赋值/填充:为 Bean 设置相关属性和依赖,例如 @Autowired 等注解注入对象、@Value 注入值、@Resource 注入各种资源等。

    3. Bean 初始化:

    • 如果 Bean 实现了 BeanNameAware 接口,调用 setBeanName() 方法,传入 Bean 的名字。
    • 如果 Bean 实现了 BeanClassLoaderAware 接口,调用 setBeanClassLoader() 方法,传入 ClassLoader 对象的实例。
    • 与上面的类似,如果实现了其他 *.Aware 接口,就调用相应的方法。
    • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessBeforeInitialization() 方法
    • 如果 Bean 实现了 InitializingBean 接口,执afterPropertiesSet() 方法。
    • 如果 Bean 在配置文件中的定义包含 init-method 属性,执行指定的方法。
    • 如果有和加载这个 Bean 的 Spring 容器相关的 BeanPostProcessor 对象,执行postProcessAfterInitialization() 方法。

    4. 销毁 Bean:销毁并不是说立刻把 Bean 销毁掉,而是把 Bean 的销毁方法先记录下来,将来需要销毁 Bean 或者销毁容器的时候,就调用这些方法去释放 Bean 所持有的资源。

    • 如果 Bean 实现了 DisposableBean 接口,执行 destroy() 方法。
    • 如果 Bean 在配置文件中的定义包含 destroy-method 属性,执行指定的 Bean 销毁方法。或者,也可以直接通过 @PreDestroy 注解标记 Bean 销毁之前执行的方法。

    AbstractAutowireCapableBeanFactory 的 doCreateBean() 方法中能看到一次执行了这四个阶段:

    1. protected Object doCreateBean(final String beanName, final RootBeanDefinition mbd, final @Nullable Object[] args)
    2. throws BeanCreationException {
    3. // 1. 创建 Bean 的实例
    4. BeanWrapper instanceWrapper = null;
    5. if (instanceWrapper == null) {
    6. instanceWrapper = createBeanInstance(beanName, mbd, args);
    7. }
    8. Object exposedObject = bean;
    9. try {
    10. // 2. Bean 属性赋值/填充
    11. populateBean(beanName, mbd, instanceWrapper);
    12. // 3. Bean 初始化
    13. exposedObject = initializeBean(beanName, exposedObject, mbd);
    14. }
    15. // 4. 销毁 Bean-注册回调接口
    16. try {
    17. registerDisposableBeanIfNecessary(beanName, bean, mbd);
    18. }
    19. return exposedObject;
    20. }

    Aware 接口能让 Bean 拿到 Spring 容器资源。

    Spring 中提供的 Aware 接口主要有:

    • BeanNameAware:注入当前 Bean 对应 beanName;
    • BeanClassLoaderAware:注入加载当前 bean 的 ClassLoader;
    • BeanFactoryAware:注入当前 BeanFactory 容器的引用。

    BeanPostProcessor 接口时 Spring 为修改 Bean 提供的强大扩展点。

    1. public interface BeanPostProcessor {
    2. // 初始化前置处理
    3. default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
    4. return bean;
    5. }
    6. // 初始化后置处理
    7. default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
    8. return bean;
    9. }
    10. }
    • postProcessBeforeInitialization:Bean 实例化、属性注入完成后,InitializingBean#afterPropertiesSet 方法以及自定义的 init-method 方法之前执行。
    • postProcessAfterInitialization:与上面类似,只是在之后执行。

     八. Bean 是线程安全的吗

    Spring 框架中的 Bean 是否线程安全取决于其作用域和状态。

    通常情况下,Bean 作用域都是默认使用 singleton,常用的还有 prototype。

    prototype 作用域下,每次获取都会创建一个新的 bean 实例,不存在资源竞争问题,所以不存在线程安全问题。singleton 作用域下,IoC容器中有唯一的 bean 实例,可能会存在资源竞争问题,这取决于 bean 是否有状态,即是否包含可变的成员变量的对象。如果是有状态的 bean,则存在线程安全问题。但大部分 bean 实际上都是无状态的,是线程安全的。

    对于有状态单例 bean 的线程安全问题,可以在类中定义一个 ThreadLocal 成员变量,将需要的可变成员变量保存在 ThreadLocal中。

  • 相关阅读:
    Mysql树形表的两种查询方案(递归与自连接)
    「代码当量」指标解读看这一篇就够了
    STM32F103 CAN通讯实操
    Taurus.MVC 微服务框架 入门开发教程:项目部署:2、让Kestrel支持绑定多个域名转发,替代Ngnix使用。
    MySQL8-主从搭建
    线上 hive on spark 作业执行超时问题排查案例分享
    minikube 部署
    odoo17 web.assets_web.min.js 赏析
    【负荷预测】基于自适应灰狼算法(IGWO)改进LSSVM实现负荷预测附Matlab代码
    Elasticsearch 8.9 Bulk批量给索引增加数据源码
  • 原文地址:https://blog.csdn.net/konghaoran1/article/details/140042235