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:

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import java.util.ArrayList;

@Configuration
@ComponentScan("cn.highedu")
public class BeanDefineConfig {

    /**
     * 使用@Bean 显示声明Java Bean 组件
     * Bean ID 为 names
     * @return 创建的JavaBean
     */
    @Bean
    public ArrayList<String> names(){
        ArrayList<String> names = new ArrayList<>();
        names.add("Tom");
        names.add("Jerry");
        return names;
    }

    /**
     * 使用 @Bean的属性设置BeanID
     * @return 编程语言列表
     */
    @Bean("languages")
    public ArrayList<String> list(){
        ArrayList<String> list = new ArrayList<>();
        list.add("Java");
        list.add("Java Script");
        return list;
    }

    /**
     * 使用@Bean 还可以在方法参数中引入其他的Bean
     * 本例中引入了名为languages的Bean
     * @param languages
     * @return
     */
    @Bean
    public ArrayList<String> moreLanguages(ArrayList<String> languages){
        ArrayList moreLanguages = new ArrayList(languages);
        moreLanguages.add("Python");
        moreLanguages.add("SQL");
        return moreLanguages;
    }
}

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

public class Application {
    public static void main(String[] args) {
        ApplicationContext context =
                new AnnotationConfigApplicationContext(BeanDefineConfig.class);
        // 通过Spring IOC容器对象获取Bean的对象
        ArrayList<String> names = (ArrayList<String>) context.getBean("names");
        names.forEach(System.out::println);
        System.out.println();
        // 通过Spring IOC容器对象获取Bean的对象
        ArrayList<String> moreCities = (ArrayList<String>) context.getBean("moreCities");
        moreCities.forEach(System.out::println);
    }
}

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获取对象的方式:

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

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:

import org.springframework.stereotype.Component;
@Component
public class SingletonBean {
    @Override
    public String toString() {
        return "SingletonBean";
    }
}

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

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;
@Component
@Scope("prototype")
public class PrototypeBean {
    @Override
    public String toString() {
        return "PrototypeBean";
    }
}

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

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class ScopeTest {
    public static void main(String[] args) {
        ApplicationContext context =
                new AnnotationConfigApplicationContext(ContextConfig.class);
        // 测试默认的Scope
        SingletonBean bean1 = context.getBean(SingletonBean.class);
        SingletonBean bean2 = context.getBean(SingletonBean.class);
        System.out.println("bean1 和 bean2 是否是同一个对象:" + (bean1 == bean2));
        // 测试prototype
        PrototypeBean bean3 = context.getBean(PrototypeBean.class);
        PrototypeBean bean4 = context.getBean(PrototypeBean.class);
        System.out.println("bean3 和 bean4 是否是同一个对象:" + (bean3 == bean4));
    }
}

3. 依赖注入进阶

3.1 Spring支持的依赖注入方式

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

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

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

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

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

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

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

3.2 构造注入示例

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

@Component
public class Student {
    @Autowired
    private Computer computer;
    private ArrayList<String> languages;
    public Student(){
        System.out.println("Student无参构造方法被调用");
    }

    @Autowired
    public Student(ArrayList<String> moreLanguages){
        System.out.println("Student带参构造方法被调用");
        this.languages = moreLanguages;
    }

    @Override
    public String toString() {
        return "Student{computer=" + computer + ", languages=" + languages + '}';
    }
    public void use(){
        System.out.println("使用电脑:"+computer);
    }
}

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

public class Application {
    public static void main(String[] args) {
        ApplicationContext context =
                new AnnotationConfigApplicationContext(BeanDefineConfig.class);
        Student student = context.getBean(Student.class);
        System.out.println(student);
    }
}

        控制台输出如下:

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

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

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

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

3.3 setter方法注入示例

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

   public Student(ArrayList<String> moreLanguages){
        System.out.println("Student带参构造方法被调用");
        this.languages = moreLanguages;
    }

    @Autowired
    public void setLanguages(ArrayList<String> moreLanguages){
        System.out.println("setLanguages方法被调用");
        this.languages = moreLanguages;
    }

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

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

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

@Component
public class Student {
    // 通过Spring框架获取Computer对象
    @Autowired
    private Computer computer;
    private ArrayList<String> languages;
    private String name;

    @Autowired(required = false)
    public void setName(String name) {
        this.name = name;
    }
    public String getName() {
        return name;
    }
// 省略该类中其他内容

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

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

        执行程序的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类。

@Component
public class DemoBean {
    @Autowired
    private ArrayList<String> list;

    @Override
    public String toString() {
        return "DemoBean{" +
                "list=" + list +
                '}';
    }
}

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

public class Application {
    public static void main(String[] args) {
        ApplicationContext context =
                new AnnotationConfigApplicationContext(BeanDefineConfig.class);
        DemoBean demoBean = context.getBean(DemoBean.class);
        System.out.println(demoBean);
    }
}

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

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

@Autowired
private ArrayList<String> moreLanguages;

@Override
public String toString() {
    return "DemoBean{" + "list=" + moreLanguages + "}";
}

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

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

    @Bean
    @Primary
    public ArrayList<String> names(){
        ArrayList<String> names = new ArrayList<>();
        names.add("Tom");
        names.add("Jerry");
        return names;
    }

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

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

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

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

相关推荐

  1. SpringIoC原理

    2024-04-29 11:04:01       36 阅读

最近更新

  1. TCP协议是安全的吗?

    2024-04-29 11:04:01       18 阅读
  2. 阿里云服务器执行yum,一直下载docker-ce-stable失败

    2024-04-29 11:04:01       19 阅读
  3. 【Python教程】压缩PDF文件大小

    2024-04-29 11:04:01       18 阅读
  4. 通过文章id递归查询所有评论(xml)

    2024-04-29 11:04:01       20 阅读

热门阅读

  1. NDK 入门(四)—— 静态缓存与 Native 异常

    2024-04-29 11:04:01       12 阅读
  2. css代码的定位及浮动

    2024-04-29 11:04:01       10 阅读
  3. 【c++】【贪心】排队接水

    2024-04-29 11:04:01       13 阅读
  4. 算法:不同的二叉搜索树

    2024-04-29 11:04:01       9 阅读
  5. springboot全局处理sql异常

    2024-04-29 11:04:01       7 阅读
  6. LSTM(Long Short-Term Memory)

    2024-04-29 11:04:01       11 阅读
  7. IO流体系

    2024-04-29 11:04:01       21 阅读
  8. 智商测试:揭秘你的潜在能力与优势

    2024-04-29 11:04:01       13 阅读
  9. 什么是scrum中的3355?

    2024-04-29 11:04:01       11 阅读