Spring Boot从入门到精通
上QQ阅读APP看书,第一时间看更新

3.2 Bean的注册和获取

Bean指的是由Spring Boot容器管理的对象。Bean是根据Spring Boot配置文件中的数据信息予以创建的。如果把Spring Boot容器看作是一个大工厂,那么Bean就相当于这个工厂生成的产品。本节要讲解的内容是在Spring Boot中实现Bean的注册和获取。

3.2.1 Bean与依赖注入

在明确了什么是Bean后,还需要明确另外一个非常重要的概念:依赖注入。

依赖注入(denpendency injection,简写为DI)是Martin Fowler于2004年在对“控制反转”进行解释时提出的。Martin Fowler认为“控制反转”一词很晦涩,无法让人很直接地理解“到底是哪里反转了”,因此他建议使用“依赖注入”来代替“控制反转”。

在面向对象程序设计中,对象和对象之间存在一种叫作“依赖”的关系。简单来说,依赖关系就是在一个对象中需要用到另外一个对象,即对象中存在一个属性,该属性是另外一个类的对象。

例如,在一个B类对象中,有一个A类的对象a,那么就可以说B类的对象依赖于对象a。而依赖注入就是基于这种“依赖关系”而产生的。

Spring Boot在创建一个对象的过程中,会根据“依赖关系”,把这个对象依赖的对象注入其中,这就是所谓的“依赖注入”。

掌握了Bean和依赖注入的概念后,再结合图3.5理解两个过程:一个是Bean的注册;另一个是Bean的注入。其中,Spring Boot能够自动寻找程序开发人员已经创建好的Bean,并将其保存在Spring Boot容器中,这个过程被称作Bean的注册。把Spring Boot容器中的Bean赋值给某个尚未被赋值的成员变量,这个过程被称作Bean的注入。

图3.5 Bean的注册和Bean的注入

当Spring Boot项目被启动时,Spring Boot首先会自动扫描所有的组件,然后注册所有的Bean,最后把这些Bean注入各自的使用场景当中。下面通过一个实例,演示注册Bean和注入Bean的这两个过程。

【例3.1】将用户名注册成Bean并交由Spring Boot注入(实例位置:资源包\TM\sl\3\1)

(1)创建一个名为MySpringBootDemo的Spring Boot项目,项目的源码文件结构如图3.6所示。

图3.6 项目中源码文件结构

 com.mr.component包下的BeanComponent是用于注册Bean的组件类,代码如下:

BeanComponent类被@Component标注,表示这个类是一个组件类。类中只有一个name()方法,并且被@Bean标注,表示这个方法返回的对象被注册成了Bean,并将其放到Spring Boot容器中。

 com.mr.controller包下的BeanTestController是控制器类,代码如下:

BeanTestController类被@RestController标注,表示这个类是一个负责页面跳转的控制器,会直接返字符串结果。类中的name属性被@Autowired标注,表示这个属性的值由Spring Boot注入。showName()方法被@RequestMapping("/bean")标注,表示该方法映射“/bean”地址,并将name的值展示在页面中。

(2)启动项目,打开浏览器,访问http://127.0.0.1:8080/bean地址,可以看到如图3.7所示的结果。

图3.7 网页展示的结果

页面中显示“David”,但BeanTestController类中没有出现任何“David”的字样,这个值是哪来的呢?“David”这个值出现在BeanComponent类的name()方法的返回值中。当Spring Boot项目被启动时,扫描器发现了BeanComponent类,并在该类下发现了被@Bean标注的方法,于是把该方法返回的对象注册成Bean,再放到Spring Boot容器中。与此同时,扫描器也发现了BeanTestController类,发现这个类有一个name属性需要被注入值,Spring Boot便在Spring Boot容器中查找有没有类型相同、名称匹配的Bean,于是就找到了name()方法返回的字符串“David”,便将“David”赋给了name属性。当前端发来请求时,showName()方法便将name的值(也就是“David”)展示在了网页中。这就是一个注册Bean和注入Bean的例子。

3.2.2 注册Bean

注册Bean需要用到@Bean注解,该注解用于标注方法,表示方法的返回值是一个即将进入SpringBoot容器中的Bean。下面介绍@Bean注解的具体用法。

1.让Spring Boot发现@Bean

如果想让@Bean注解生效,那么被标注的方法所在的类必须能够被Spring Boot的组件扫描器扫描到。以下几个注解都可以让被标注的方法所在的类被扫描到:

 @Configuration:声明配置类。

 @Controller:声明控制器类。

 @Service:声明服务接口或类。

 @Repository:声明数据仓库。

 @Component:如果不知道类属于什么模块,就用这个注解将类声明成组件。推荐使用此注解。

如果不使用上面这5个注解,那么也可以用@Import注解将@Bean所在类的主动注册给Spring Boot。例如,修改例3.1,删除BeanComponent类上的@Component注解,代码如下:

在Spring Boot项目的启动类中,通过使用@Import({com.mr.component.BeanComponent.class})声明启动类,让项目启动时自动导入BeanComponent类,代码如下:

这样,当Spring Boot项目被启动时,BeanComponent类中的@Bean也可以被注册到Spring Boot容器中。如果想要导入多个指定的类,@Import的语法如下(注意圆括号和大括号的位置):

    @Import({A.class, B.class, C.class})
2.@Bean的使用方法

@Bean注解有很多属性,其核心源码如下:

    public @interface Bean {
        @AliasFor("name")
        String[] value() default {};
        @AliasFor("value")
        String[] name() default {};
        boolean autowireCandidate() default true;
        String initMethod() default "";
        String destroyMethod() default AbstractBeanDefinition.INFER_METHOD;
    }

下面分别介绍这几个属性的用法:

(1)value和name这两个属性的作用是一样的,就是给Bean起别名,让Spring Boot可以区分多个类型相同的Bean。给Bean起别名的语法如下:

    @Bean("goudan")
    @Bean(value = "goudan")
    @Bean(name = "goudan")
    @Bean(name = {"goudan", "GouDan", "Golden"})  //同时给一个Bean起多个别名

如果没有给Bean起任何别名的话,那么@Bean注解会默认将方法名作为别名,例如:

    @Bean
    public String getName() {
        return "David";
    }

上面代码等同于:

    @Bean(name = "getName")
    public String getName() {
        return "David";
    }

(2)autowireCandidate属性表示Bean是否采用默认的自动匹配机制,默认值为true,如果将其赋值为false,这个Bean就不会被默认的自动匹配机制匹配到,只能通过使用别名的方式匹配到。

【例3.2】Leon的名字必须通过别名注入(实例位置:资源包\TM\sl\3\2)

创建一个名为MySpringBootDemo的Spring Boot项目,项目的源码文件结构如图3.8所示。

图3.8 项目中源码文件结构

com.mr.component包下的BeanComponent是用于注册Bean的组件类,代码如下:

类中创建了两个方法,返回值类型均为String。name2()方法定义了别名,并且autowireCandidate的值为false,表示Spring在匹配Bean时会自动忽略name2()方法的返回值。下面再来看一下注入的代码。

com.mr.controller包下的BeanController类是控制器类,代码如下:

BeanController类定义了一个name属性,该属性使用@Resource标注,表示这个属性由Spring自动匹配并注入值。启动项目,打开浏览器访问http://127.0.0.1:8080/bean地址,看一下name被注入的值是什么。用户看到的结果如图3.9所示。

图3.9 网页展示的结果

name的值为“David”,即使Spring容器中有两个String类型的Bean,但值为“Leon”的Bean拒绝自动匹配机制,所以name只能得到“David”这个值。

如果想要得到“Leon”这个Bean,就需要在注入时指定Bean的别名。例如使用@Resource注解读取别名为“ln”的Bean,关键代码如下:

    @Resource(name = "ln")
    private String name;

这样,name取的值就是别名为“ln”的Bean的值。重启Spring Boot项目,并且重新访问地址http://127.0.0.1:8080/bean后,就可以看到网页上显示的值变成了“Leon”,效果如图3.10所示。

图3.10 再次访问同一地址看到的结果

(3)initMethod属性用于指定Bean初始化时会调用什么方法,destroyMethod属性用于指定Bean被Spring销毁时会调用什么方法,两个属性的值均为Bean对象的方法名。

3.2.3 获取Bean

获取Bean就是在类中创建一个属性(可以是private属性),通过为属性添加注解,让Spring Boot为这个属性注入Bean。可以获取Bean的注解有3个:@Autowired、@Resouce和@Value。这3个注解只能在可以被扫描到的类中使用。下面分别介绍这3个注解的用法。

1.@Autowired

@Autowired注解可以自动到Spring容器中寻找名称相同或类型相同的Bean。例如,注册Bean的方法为:

    @Bean
    public String dave() {
        return "David";
    }

获取这个Bean的代码可以写成:

    @Autowired
    String dave;

@Autowired可以自动匹配与属性同名(即别名为“dave”)的Bean。如果匹配不到同名的Bean,@Autowired可以自动匹配类型相同的Bean。例如,注册方法不变,获取Bean的代码为:

    @Autowired
    String name;

即使这么写,name也可以获得“David”这个值,因为两者数据类型是相同的。

但要注意,当Spring Boot容器中仅有一个该类型的Bean时,@Autowired才能匹配成功。如果存在多个该类型的Bean,Spring就不知道应该匹配哪个Bean了,项目就会抛出异常。例如,注册Bean的方法如下:

    @Bean
    public String dave() {
        return "David";
    }

    @Bean
    public String ln() {
        return "Leon";
    }

现在容器中有两个String类型的Bean,然后获取Bean的代码为:

    @Autowired
    String name;

启动项目后会抛出如下异常日志:

    Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'java.lang.
    String' available: expected single matching bean but found 2: dave,ln

这个异常日志的意思是:程序需要自动匹配一个独立的Bean,却找到了两个符合条件的Bean。其中,一个Bean的别名叫“dave”,另一个Bean的别名叫“ln”。程序因为不知道哪个Bean是当前需要的,所以就停止了。

在这种因同时存在多个Bean而无法自动匹配的情况下,就需要指定Bean的别名以获取Bean。指定别名有两种方式:

 将类属性名改成Bean的别名。如果Bean的别名叫“dave”,@Autowired标注的属性名也叫“dave”。

 使用@Qualifier注解。@Qualifier注解有一个value属性,用于指定要获取的Bean的别名。可以与@Autowired配套使用。例如,获取别名为“ln”的Bean,代码如下:

    @Autowired
    @Qualifier("ln")
    String name;
2.@Resource

@Resource注解的功能与@Autowired类似:@Resouce注解自带name属性,可直接指定Bean的别名。其中,name属性的默认值为空字符串,表示自动将被标注的属性名作为Bean的别名。例如,获取别名为“dave”的Bean,可以有3种写法,第一种写法:

    @Resource(name="dave")
    String name;

第二种写法:

    @Resource
    String dave;  //属性名就叫david

第三种写法虽然可以执行,实际与第一种写法是一样的,不推荐这样写:

    @Resource(name="")
    String dave;

注意

如果使用@Autowired注入Object类型的Bean时,抛出了org.springframework.beans.factory. NoUniqueBeanDefinitionException:No qualifying bean of type 'java.lang.Object' available异常,就将@Autowired换成@Resource。

3.@Value

@Value注解可以动态地向属性注入值。@Value有3种语法,分别是:

 注入常量值。下面的语法会让name的值等于“dave”这个字符串,例如:

    @Value("dave")
    String name;

 注入Bean。使用“#{Bean别名}”格式可以注入指定别名的Bean,其效果类似于@Resource (name="Bean别名"),例如:

    @Value("#{dave}")
    String name;

 注入配置文件中配置信息的值。使用“${配置项}”格式可以注入application.properties文件中指定名称的配置信息的值,例如:

    @Value("${dave}")
    String name;

注意

如果配置文件中没有该项则会抛出BeanCreationException异常。