Java知识点整理 16 — Spring Bean

07-04 1790阅读

在之前的文章 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方法。

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 注解的使用

  • @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 注解的使用:

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

      五. @Autowired 和 @Resource 的区别

      @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;
      

      总结一下:

      • @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。

        如何配置:

        // 注解方式
        @Bean
        @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE)
        public Person personPrototype() {
            return new Person();
        }
        

        七. 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() 方法中能看到一次执行了这四个阶段:

            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 接口主要有:

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

              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;
              	}
              }
              • postProcessBeforeInitialization:Bean 实例化、属性注入完成后,InitializingBean#afterPropertiesSet 方法以及自定义的 init-method 方法之前执行。
              • postProcessAfterInitialization:与上面类似,只是在之后执行。

                Java知识点整理 16 — Spring Bean

                 八. Bean 是线程安全的吗

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

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

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

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

VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]