当前位置:   article > 正文

Spring IOC(二)

Spring IOC(二)

1. Bean的定义与获取

1.1 定义Bean

        在Spring 中定义Bean的方式主要有三种:

        1、基于XML配置文件的方式(了解):通常会在配置文件中使用<bean>标签来定义Bean,并设置Bean的属性、依赖关系等信息。

        2、基于注解的方式:在Java代码中使用注解来标识Bean,并指定Bean的属性、依赖关系等信息。常用的注解有@Component、@Controller、@Service等。

  • @Component用于标识一个普通的 Bean 类,没有任何特殊的业务含义
  • @Controller 用于标识一个控制器类、@Service 用于标识一个服务类,通常用于业务处理层、@Repository 注解用于标识一个数据访问类
  • 除了这些区别,它们的使用方式是一样的,本质上都是将被注解的类交由 Spring 容器管理,供其他类去使用

        3、基于Java Config的方式,在Java配置类中使用@Bean注解来定义Bean。当开发者想要使用的Bean是第三方组件时,不能在源码上标注@Component,可以使用@Bean注解的方式。

  • @Bean注解通常放在方法上,用于指示该方法返回的实例应该被注册为Spring容器中的一个Bean
  • 在方法上使用@Bean注解时,Spring框架会根据该方法的返回类型来自动创建并配置该Bean,同时也会根据方法的名称来为该Bean指定一个默认的名称
  • 可以通过为@Bean添加属性值的方式为Bean定义一个新的名称

        总的来说,不同的方式适用于不同的场景。在实际使用过程中,我们可以根据业务需求来选择最适合的方式来定义Bean,也可以兼用多种方式来定义Bean,这些方式也是可以共存的。        

1.2 定义Bean示例

        首先,在cn.obj包下声明BeanDefineConfig类,并在其中使用@Bean注解方式声明Bean:

  1. import org.springframework.context.annotation.Bean;
  2. import org.springframework.context.annotation.ComponentScan;
  3. import org.springframework.context.annotation.Configuration;
  4. import java.util.ArrayList;
  5. @Configuration
  6. @ComponentScan("cn.highedu")
  7. public class BeanDefineConfig {
  8. /**
  9. * 使用@Bean 显示声明Java Bean 组件
  10. * Bean ID 为 names
  11. * @return 创建的JavaBean
  12. */
  13. @Bean
  14. public ArrayList<String> names(){
  15. ArrayList<String> names = new ArrayList<>();
  16. names.add("Tom");
  17. names.add("Jerry");
  18. return names;
  19. }
  20. /**
  21. * 使用 @Bean的属性设置BeanID
  22. * @return 编程语言列表
  23. */
  24. @Bean("languages")
  25. public ArrayList<String> list(){
  26. ArrayList<String> list = new ArrayList<>();
  27. list.add("Java");
  28. list.add("Java Script");
  29. return list;
  30. }
  31. /**
  32. * 使用@Bean 还可以在方法参数中引入其他的Bean
  33. * 本例中引入了名为languages的Bean
  34. * @param languages
  35. * @return
  36. */
  37. @Bean
  38. public ArrayList<String> moreLanguages(ArrayList<String> languages){
  39. ArrayList moreLanguages = new ArrayList(languages);
  40. moreLanguages.add("Python");
  41. moreLanguages.add("SQL");
  42. return moreLanguages;
  43. }
  44. }

        然后,在Application类中测试Bean的创建效果:

  1. public class Application {
  2. public static void main(String[] args) {
  3. ApplicationContext context =
  4. new AnnotationConfigApplicationContext(BeanDefineConfig.class);
  5. // 通过Spring IOC容器对象获取Bean的对象
  6. ArrayList<String> names = (ArrayList<String>) context.getBean("names");
  7. names.forEach(System.out::println);
  8. System.out.println();
  9. // 通过Spring IOC容器对象获取Bean的对象
  10. ArrayList<String> moreCities = (ArrayList<String>) context.getBean("moreCities");
  11. moreCities.forEach(System.out::println);
  12. }
  13. }

1.3 Spring框架中@Bean 和 @Component 的区别

        在Spring框架中,@Bean注解和@Component注解均用于定义Bean,但是添加的位置和应用的场景不同。

        1、@Bean注解主要用于在配置类中显式配置Bean。

  • 可以添加在方法和注解前
  • 将声明集中在一个(或几个)地方,方便统一管理Bean
  • 可用于所有类(包括第三方工具包中的类)

        2、@Component注解主要用于在类前隐式的配置Bean。

  • 主要添加在类前
  • 位置分散,不便于统一管理
  • 适合非常快速的开发

        3、实际应用中,通常是两种方式混合使用。

1.4 Spring IoC获取对象的方式

        在Spring IoC容器中获取Bean可以通过以下几种方式:

        1、context.getBean(Class<T> requiredType):根据类型获取Bean。其中:

  • requiredType为要获取bean的类型
  • 如果容器中有多个实现该类型的Bean,但是没有配置优先使用哪个Bean,则会抛出NoUniqueBeanDefinitionException异常

        2、context.getBean(String name):根据名称获取Bean。其中:

  • name为Bean在Spring IoC容器中的名称
  • 默认情况下,Bean的名称是类名进行了首字母小写的处理后的结果
  • 可以通过在Spring注解中设置名称来设置Bean的名称,如:@Component("myBean")

        3、context.getBean(String name, Class<T> requiredType):根据Bean名称和类型获取Bean。

  • 该方法相当于在getBean(String name)方法的基础上添加了Bean类型的验证
  • 如果获取的Bean对象与requiredType指定的类型不同,将抛出BeanNotOfRequiredTypeException

        4、context.getBeansOfType(Class<T> type):根据类型获取容器中所有实现该类型的Bean。

  • type为要获取bean的类型
  • 该方法返回Map类型,其中key为Bean在容器中的名称,value为Bean实例

1.5 IoC获取对象示例

        在Application类中,测试IoC获取对象的方式:

  1. public class Application {
  2. public static void main(String[] args) {
  3. ApplicationContext context =
  4. new AnnotationConfigApplicationContext(BeanDefineConfig.class);
  5. // IoC容器中存在多个相同类型的Bean时,
  6. // 使用.class获取会抛出异常:NoUniqueBeanDefinitionException
  7. // ArrayList<String> names = (ArrayList<String>) context.getBean(ArrayList.class);
  8. // 使用name+type的方式,不需要再进行强制类型转换
  9. ArrayList<String> moreLanguages = context.getBean("moreLanguages", ArrayList.class);
  10. moreLanguages.forEach(System.out::println);
  11. // getBeansOfType方法返回的类型为Map
  12. context.getBeansOfType(ArrayList.class).forEach((name, bean) -> {
  13. System.out.println(name + " : " + bean);
  14. });
  15. }
  16. }

2. Bean的作用域

2.1 Bean的作用域概述

        Bean的作用域就是指Spring中Java Bean 有效范围,其中最为常用的作用域有2种:

  • 单例作用域(singleton):Bean的默认作用域,任何时候获得的Bean对象都是同一个实例
  • 原型作用域(prototype):每次引到bean时都会创建新的实例

        可以通过@Scope注解来设置Bean的作用域,该注解可以添加在类前(与@Component注解搭配使用),也可以添加在方法前(与@Bean注解搭配使用)。

        Bean的其他类型的作用域如下表所示(了解即可):

2.2 Bean的作用域示例

        在com.obj包下新建SingletonBean:

  1. import org.springframework.stereotype.Component;
  2. @Component
  3. public class SingletonBean {
  4. @Override
  5. public String toString() {
  6. return "SingletonBean";
  7. }
  8. }

        在com.obj.spring包下新建PrototypeBean:

  1. import org.springframework.context.annotation.Scope;
  2. import org.springframework.stereotype.Component;
  3. @Component
  4. @Scope("prototype")
  5. public class PrototypeBean {
  6. @Override
  7. public String toString() {
  8. return "PrototypeBean";
  9. }
  10. }

        在com.obj包下新建ScopeTest类,对Bean的作用域进行测试:

  1. import org.springframework.context.ApplicationContext;
  2. import org.springframework.context.annotation.AnnotationConfigApplicationContext;
  3. public class ScopeTest {
  4. public static void main(String[] args) {
  5. ApplicationContext context =
  6. new AnnotationConfigApplicationContext(ContextConfig.class);
  7. // 测试默认的Scope
  8. SingletonBean bean1 = context.getBean(SingletonBean.class);
  9. SingletonBean bean2 = context.getBean(SingletonBean.class);
  10. System.out.println("bean1 和 bean2 是否是同一个对象:" + (bean1 == bean2));
  11. // 测试prototype
  12. PrototypeBean bean3 = context.getBean(PrototypeBean.class);
  13. PrototypeBean bean4 = context.getBean(PrototypeBean.class);
  14. System.out.println("bean3 和 bean4 是否是同一个对象:" + (bean3 == bean4));
  15. }
  16. }

3. 依赖注入进阶

3.1 Spring支持的依赖注入方式

        Spring框架主要支持三种注入方式:字段注入、构造器注入、setter方法注入。

        1、字段注入(Field Injection):通过直接在类的字段上添加注解来注入依赖。

  • 字段注入在实际使用中比较简便,但也可能降低代码的可测试性和清晰度,因为字段通常是私有的。

        2、构造器注入(Constructor Injection):通过类的构造方法来注入依赖。

  • 在创建对象实例时,容器会通过构造器参数来解析和注入依赖项。
  • 构造器注入强制要求依赖在对象创建时就需要提供,适用于必须的依赖关系。

        3、setter方法注入(Setter Injection):通过类的setter方法来注入依赖。

  • 容器会调用目标类的setter方法,将依赖项注入到目标类的属性中。
  • setter方法注入适用于可选性依赖或需要在对象创建后才能设置的情况。

3.2 构造注入示例

        修改Student类的代码,增加ArrayList类型的属性、带参构造器及toString方法:

  1. @Component
  2. public class Student {
  3. @Autowired
  4. private Computer computer;
  5. private ArrayList<String> languages;
  6. public Student(){
  7. System.out.println("Student无参构造方法被调用");
  8. }
  9. @Autowired
  10. public Student(ArrayList<String> moreLanguages){
  11. System.out.println("Student带参构造方法被调用");
  12. this.languages = moreLanguages;
  13. }
  14. @Override
  15. public String toString() {
  16. return "Student{computer=" + computer + ", languages=" + languages + '}';
  17. }
  18. public void use(){
  19. System.out.println("使用电脑:"+computer);
  20. }
  21. }

        在Application类中获取Student类的对象并输出。注意,本例中的配置类需要使用BeanDefineConfig。

  1. public class Application {
  2. public static void main(String[] args) {
  3. ApplicationContext context =
  4. new AnnotationConfigApplicationContext(BeanDefineConfig.class);
  5. Student student = context.getBean(Student.class);
  6. System.out.println(student);
  7. }
  8. }

        控制台输出如下:

        从输出结果可知,Spring框架调用了Student类的带参构造器来创建Student对象,并且自动从容器中查询到名为moreLanguages的Bean对象,注入到该构造器中。

        接下来做一个实验,将Student类中带参构造器的参数名改为test。

        再次运行Application类的main方法,程序抛出NoUniqueBeanDefinitionException异常。该异常的原因是Spring无法通过test这个名称从容器中找到Bean对象,所以改用ArrayList类型进行查找,找到了不止一个Bean对象,所以抛出异常。

        此处可以看到,采用构造器注入的方式,如果构造器中要求注入的Bean存在问题,则会导致Student对象构造出错。因此,构造器注入适用于必须的依赖关系。

3.3 setter方法注入示例

        修改Student类的代码,增加setLanguages方法,并删除带参构造器前的@Autowired注解:

  1. public Student(ArrayList<String> moreLanguages){
  2. System.out.println("Student带参构造方法被调用");
  3. this.languages = moreLanguages;
  4. }
  5. @Autowired
  6. public void setLanguages(ArrayList<String> moreLanguages){
  7. System.out.println("setLanguages方法被调用");
  8. this.languages = moreLanguages;
  9. }

        运行Application类的main方法,查看控制台输出的内容。

        可以看到,移除带参构造方法前的@Autowired注解后,Spring默认调用无参构造方法来创建Student对象,然后调用set方法注入依赖。因此,set方法的注入时机晚于构造方法。

        在Student类中新增一个String类型的属性,添加对应的set和get方法,并在set方法前添加@Autowired(required=false)注解。

  1. @Component
  2. public class Student {
  3. // 通过Spring框架获取Computer对象
  4. @Autowired
  5. private Computer computer;
  6. private ArrayList<String> languages;
  7. private String name;
  8. @Autowired(required = false)
  9. public void setName(String name) {
  10. this.name = name;
  11. }
  12. public String getName() {
  13. return name;
  14. }
  15. // 省略该类中其他内容

        在Application类中增加对getName方法的调用。

  1. public class Application {
  2. public static void main(String[] args) {
  3. ApplicationContext context =
  4. new AnnotationConfigApplicationContext(BeanDefineConfig.class);
  5. Student student = context.getBean(Student.class);
  6. System.out.println(student);
  7. System.out.println(student.getName());
  8. // 省略该类中其他内容

        执行程序的main方法,可以看到程序正常执行,getName方法返回的结果为null。此处主要演示的是@Autowired(required = false)的执行逻辑,即如果能找到相关符合条件的Bean对象,则执行注入,反之则不执行注入。这种注入方式适用于那些可选的依赖。

3.4 为什么Spring不建议使用字段注入

        1、Spring框架支持三种主要的注入方式:字段注入、构造器注入、setter方法注入。其中,字段注入是指在类的字段上添加注解来注入依赖。

        2、可能导致空指针异常:一个Bean的初始顺序为静态变量或静态代码块 > 实例变量或初始化语句块 > 构造方法 > 字段注入。在静态代码块、初始化语句块、构造方法中使用@Autowired标记的字段,会引起空指针异常。

        3、不利于测试: 在单元测试中,为了隔离被测试类与外部依赖之间的耦合,常常需要模拟依赖对象。字段注入会使得测试类无法直接通过构造器或方法注入模拟对象,从而增加了测试的复杂性。

        4、无法注入 final 字段: 字段注入无法用于注入 final 字段,这会限制一些设计和测试的可能性。

3.5 @Autowired的Bean匹配机制

        @Autowired的Bean匹配机制是指在执行依赖注入之前,Spring容器会根据被注入字段、方法参数的类型来查找匹配的 Bean。

        该机制的执行顺序为:

        1、先根据类型匹配

  • 若没有匹配类型,则注入失败
  • 有匹配类型,对应的实例有1个,则注入

        2、如果匹配类型的实例有多个,则查看优先级注解:

  • 查看是否有@Qualifier注解,有则按其指定规则匹配
  • 查看是否有@Primary注解,有则按其指定规则匹配

        3、如果类型匹配的实例有多个,且无法选出唯一的,则转为根据Bean的名称匹配

  • 如果名称匹配成功就注入,反之注入失败

        在entity包下声明DemoBean类。

  1. @Component
  2. public class DemoBean {
  3. @Autowired
  4. private ArrayList<String> list;
  5. @Override
  6. public String toString() {
  7. return "DemoBean{" +
  8. "list=" + list +
  9. '}';
  10. }
  11. }

        在Application类中获取DemoBean的对象并输出。

  1. public class Application {
  2. public static void main(String[] args) {
  3. ApplicationContext context =
  4. new AnnotationConfigApplicationContext(BeanDefineConfig.class);
  5. DemoBean demoBean = context.getBean(DemoBean.class);
  6. System.out.println(demoBean);
  7. }
  8. }

        此时,程序抛出NoUniqueBeanDefinitionException,表示有多个匹配的实例,但是无法确定该使用哪一个。

        接下来,将list属性的名称修改为moreLanguages。

  1. @Autowired
  2. private ArrayList<String> moreLanguages;
  3. @Override
  4. public String toString() {
  5. return "DemoBean{" + "list=" + moreLanguages + "}";
  6. }

        运行Application类,可以发现,依赖注入成功,根据属性名匹配了名为moreLanguages的Bean。

        然后,在BeanDefineConfig类的names方法前添加@Primary注解。

  1. @Bean
  2. @Primary
  3. public ArrayList<String> names(){
  4. ArrayList<String> names = new ArrayList<>();
  5. names.add("Tom");
  6. names.add("Jerry");
  7. return names;
  8. }

        运行Application类,可以看到,依赖注入成功。从结果可以看出,@Primary注解的优先级高于按属性名匹配。

        接下来,在DemoBean类的list属性前添加@Qualifier("languages")注解,再次运行测试用例,查看效果。

        此处可以发现,@Qualifier("languages")注解的优先级高于@Primary注解。

        综上,如果存在多个匹配的Bean,可以通过三种方式解决:@Qualifier注解,@Primary注解或使用Bean名称匹配。这三种方式的优先级为:@Qualifier > @Primary > Bean名称。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/508462
推荐阅读
相关标签
  

闽ICP备14008679号