从源码深入理解 Spring IoC 注解

全注解下的 Spring IoC

本文基于 Spring Boot,所以并不使用 XML 配置,使用注解描述生成对象
版权声明:本文为博主原创文章,未经博主允许不得转载。

Ioc 容器简介

Spring IoC 容器是一个管理 Bean 的容器,在 Spring 的定义中,它要求所有的 IoC 容器都需要实现接口
BeanFactory,它是一个顶级容器接口。为了增加对它的理解,我们首先阅读其源码,看几个重要的方法。
代码如下

package org.springframework.beans.factory;

import org.springframework.beans.BeansException;
import org.springframework.core.ResolvableType;
import org.springframework.lang.Nullable;

public interface BeanFactory {

    // 前缀
    String FACTORY_BEAN_PREFIX = "&";

    // 多种方式 getBean
    Object getBean(String name) throws BeansException;

    <T> T getBean(String name, @Nullable Class<T> requiredType) throws BeansException;

    Object getBean(String name, Object... args) throws BeansException;

    <T> T getBean(Class<T> requiredType) throws BeansException;

    <T> T getBean(Class<T> requiredType, Object... args) throws BeansException;

    // 是否包含 Bean
    boolean containsBean(String name);

    // Bean 是否单例
    boolean isSingleton(String name) throws NoSuchBeanDefinitionException;

    // Bean 是否原型
    boolean isPrototype(String name) throws NoSuchBeanDefinitionException;

    // 是否类型匹配
    boolean isTypeMatch(String name, ResolvableType typeToMatch) throws NoSuchBeanDefinitionException;

    boolean isTypeMatch(String name, @Nullable Class<?> typeToMatch) throws NoSuchBeanDefinitionException;

    // 获取 Bean 的类型
    @Nullable
    Class<?> getType(String name) throws NoSuchBeanDefinitionException;

    // 获取 Bean 的别名
    String[] getAliases(String name);

}

这段代码中我加入了一些中文注释,通过他们就可以理解这些方法的含义。这里值得注意的是接口中的几个方法。首先我们看到了多个 getBean 方法,这也是 IoC 容器最重要的方法之一,它的意义是从 IoC 容器中获取
Bean。而从多个 getBean 方法中可以看到有按类型获取 Bean按名称获取 Bean,这就意味着在Spring IoC 容器中,允许我们按类型活着名称获取 Bean。

isSingleton 方法则判断 Bean 是否在 Spring IoC 中为单例。这里需要记住的是在 Spring IoC 容器中,默认的情况下,Bean 都是以单例存在的,也就是使用 getBean 方法返回的都是同一个对象。与 isSingleton 方法相反的是 isPrototype,如果它返回的是 true,那么当我们使用 getBean 方法获取 Bean 的时候,Spring IoC 容器就会创建一个新的 Bean 返回给调用者,这些与下一篇要讨论的Bean 的作用域有关。

由于 BeanFactory 的功能还不够强大,因此 Spring 在 BeanFactory 的基础上,还设计了一个更为高级的接口
ApplicationContext。它是 BeanFactory 的子接口之一,在 Spring 的体系中
BeanFactoryApplicationContext 是最为重要的接口设计,在我们使用 Spring IoC 容器时,大部分都是 ApplicationContext 接口的实现类。

在 Spring Boot 当中我们主要是通过注解来装配 Bean 到 Spring IoC 容器中,为了贴近 Spring Boot,这里不再介绍与 XML 相关的 IoC 容器,而主要介绍一个基于注解的 IoC 容器,它就是 AnnotationConfigApplicationContext,从名称就可以看出它是一个基于注解的 IoC 容器。之所以研究它,是因为 Spring Boot 装配和获取 Bean 的方法与它如出一辙。

下面来做一个最简单的例子。首先定义一个 Java 简单对象(Plain Ordinary Java Object,POJO)

User.java

package com.tpsix.spring.pojo;  
  
import lombok.Getter;  
import lombok.Setter;  
  
/**  
 * @author zhangyin  
 * @date 2018/08/27  
 */
@Getter  
@Setter  
public class User {  
  
    private Long id;  
  
    private String name;  
  
    private String desc;  
  
}

然后再定义一个 Java 配置文件 ApplicationConfig.java ,代码如下

package com.tpsix.spring.config;

import org.springframework.context.annotation.Bean;  
import org.springframework.context.annotation.Configuration;  
  
/**  
 * @author zhangyin  
 * @date 2018/08/27  
 */
@Configuration
public class ApplicationConfig {  
      
    @Bean(name = "user")
    public User initUser() {  
        User user = new User();  
        user.setId(1L);  
        user.setName("name_1");  
        user.setDesc("desc_1");  
        return user;  
    }
    
}

这里需要注意的注解, @Configuration 代表这是一个 Java 配置文件,Spring 的容器会根据它来生成 IoC 容器去装配 Bean, @Bean 代表将 initUser 方法返回的POJO 装配到 IoC 容器中,而属性 name 定义这个 Bean 的名称,
如果没有配置它,则将方法名称 initUser 作为 Bean 的名称保存到 IoC 容器中。

做好了这些,就可以使用 AnnotationConfigApplicationContext 来构建自己的 IoC 容器,代码如下

package com.tpsix.spring.config;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.tpsix.spring.pojo.User;

/**
 * @author zhangyin
 * @date 2018/08/27
 */
@Slf4j
public class IocTest {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(
                ApplicationConfig.class
        );
        // 根据类型获取 Bean
        User user = ctx.getBean(User.class);
        log.info(user.getId() + "");
    }
    
}

代码中将 Java 配置文件 ApplicationConfig.class 传递给 AnnotationConfigApplicationContext 的构造方法,这样它就能够读取配置了。然后将配置里面的 Bean 装配到 IoC 容器中,于是可以使用 getBean 方法获取对应的 POJO,你可以看到下面的日志打印:

23:00:21.245 [main] INFO com.tpsix.spring.config.IocTest - 1

显然,配置在配置文件中的名称为 user 的 Bean 已经被装配到 IoC 容器中,并且可以通过 getBean 方法获取对应的 Bean, 并将 Bean 的属性信息输出出来。当然这是很简单的方法,而注解 @Bean 也不是唯一创建 Bean 的方法,还有其他的方法可以让 IoC 容器装配 Bean,而且 Bean 之间还有依赖的关系需要进一步处理。

装配你的 Bean

以下都是基于 Spring Boot 注解方式来装配 Bean

通过扫描装配你的 Bean

如果一个个的 Bean 使用注解 @Bean 注入 Spring IoC 容器中,那将是一件很痛苦的事情。好在 Spring 还允许我们进行扫描装配 Bean 到 IoC 容器中,对于扫描装配而言使用的注解是 @Component@ComponentScan。@Component 是标明哪个类被扫描进入 Spring IoC 容器,而 @ComponentScan 则是标明采用何种模式去扫描装配 Bean。

这里复用之前的 User.java,首先将 User.java 移动到 ApplicationConfig.java 所在包,代码如下 , 加入注解 @Component

package com.tpsix.spring.config;

import java.io.Serializable;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import lombok.Getter;
import lombok.Setter;

/**
 * @author private
 * @date 2018/08/27
 */
@Component("user")
@Setter
@Getter
public class User implements Serializable {

    @Value("1")
    private Long id;
    
    @Value("name_1")
    private String name;
    
    @Value("desc_1")
    private String desc;
    
}

这里的注解 @Component 表明这个类将被 Spring IoC 容器扫描装配,其中配置的 user 则是作为 Bean 的名称,当然你也可以不配置这个字符串,那么 IoC 容器就会把类名第一个字母作为小写,其他不变作为 Bean 名称放入到 IoC 容器中,注解 @Value 则是指定具体的值,使 Spring IoC 给予对应的属性注入对应的值。为了让 Spring Ioc 容器装配这个类,需要改造类 ApplicationConfig.java,代码如下

package com.tpsix.spring.config;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * 
 * @author private
 * @date 2018/08/27
 */
@Configuration
@ComponentScan
public class ApplicationConfig {
}

这里加入了 @ComponentScan,意味着它会进行扫描,但是它只会扫描类 ApplicationConfig.java 所在的当前包和其子包,之前把 User.java 移动到包 com.tpsix.spring.config 就是这个原因。这样就可以删掉之前使用 @Bean 标注的创建对象方法。然后进行测试,代码如下

package com.tpsix.spring.config;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import lombok.extern.slf4j.Slf4j;

/**
 * @author private
 * @date 2018/08/27
 */
@Slf4j
public class IocTest {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(
                ApplicationConfig.class
        );
        
        User user = ctx.getBean(User.class);
        log.info(user.getId() + "");
    }
    
}

这样就可以运行了。然而为了使得 User 类能够被扫描,上面我们把它迁移到了本不该放置它的配置包,这样显然就不太合理了。为了更加合理, @ComponentScan 还允许我们自定义扫描的包,下面看看它的配置项

首先列出 @ComponentScan 源码,代码如下

package org.springframework.context.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Repeatable;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

import org.springframework.beans.factory.support.BeanNameGenerator;
import org.springframework.core.annotation.AliasFor;


@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
// 允许在一个类中可重复定义
@Repeatable(ComponentScans.class)
public @interface ComponentScan {

    // 定义扫描的包
    @AliasFor("basePackages")
    String[] value() default {};

    // 定义扫描的包
    @AliasFor("value")
    String[] basePackages() default {};

    // 定义扫描的类
    Class<?>[] basePackageClasses() default {};

    // Bean name 生成器
    Class<? extends BeanNameGenerator> nameGenerator() default BeanNameGenerator.class;

    // 作用域解析器
    Class<? extends ScopeMetadataResolver> scopeResolver() default AnnotationScopeMetadataResolver.class;

    // 作用域代理模式
    ScopedProxyMode scopedProxy() default ScopedProxyMode.DEFAULT;

    // 资源匹配模式
    String resourcePattern() default ClassPathScanningCandidateComponentProvider.DEFAULT_RESOURCE_PATTERN;

    // 是否启用默认的过滤器
    boolean useDefaultFilters() default true;

    // 当满足过滤器的条件时扫描
    Filter[] includeFilters() default {};

    // 当不满足过滤器的条件时扫描
    Filter[] excludeFilters() default {};

    // 是否延迟初始化
    boolean lazyInit() default false;

    // 定义过滤器
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    @interface Filter {

        // 过滤器类型,可以按注解类型活着正则表达式等过滤
        FilterType type() default FilterType.ANNOTATION;

        // 定义过滤的类
        @AliasFor("classes")
        Class<?>[] value() default {};

        // 定义过滤的类
        @AliasFor("value")
        Class<?>[] classes() default {};

        // 匹配方式
        String[] pattern() default {};

    }

}

这里列出最常用的配置项

  • String[] value() default {}; 定义扫描的包
  • String[] basePackages() default {}; 定义扫描的包
  • Class<?>[] basePackageClasses() default {}; 定义扫描的类
  • Filter[] includeFilters() default {}; 当满足过滤器的条件时扫描
  • Filter[] excludeFilters() default {}; 当不满足过滤器的条件时扫描
  • boolean lazyInit() default false; 是否延迟初始化

首先通过配置项 basePackages 定义扫描的包名,在没有定义的情况下,它只会扫描当前包和其子包的路径,还可以通过 basePackageClasses 定义扫描的类。其中还有 includeFiltersexcludeFiltersincludeFilters 是定义满足过滤器(@Filter)条件的 Bean 才去扫描,excludeFilters 则是排除过滤器条件的 Bean,它们都需要通过一个注解 @Filter 去定义,它有一个 type 类型,这里可以定义为注解或者正则表达式等类型。classes 定义扫描类,pattern 定义正则表达式类。

此时我们再把 User 类放到包 com.tpsix.spring.pojo 中,这样 User 和 ApplicationConfig 就不在同包,那么我们把ApplicationConfig 中的注解修改为:

@ComponentScan("com.tpsix.spring.*")
或
@ComponentScan(basePackages = {"com.tpsix.spring.pojo"})
或
@ComponentScan(basePackageClasses = {User.class})

无论采用何种法昂是都能够使得 IoC 容器去扫描 User 类,而包名可以采用正则式去匹配。但是有时候我们的需求是想扫描一些包,将一些 Bean 装配到 Spring IoC 容器中,而不是想加载这个包里面的某些 Bean。比如说,现在我们有一个 UserService 类,为了标注它为服务类,将类标注 @Service (该注解注入了 @Component,所以在默认的情况下它会被 Spring 扫描装配到 IoC 容器中),这里假设我们采用了策略:

@ComponentScan("com.tpsix.spring.*")

这样对于 com.tpsix.spring.pojo 和 com.tpsix.spring.service,这两个包都会被扫描,此时我们定义的 UserService 如下

package com.tpsix.spring.service;

import org.springframework.stereotype.Service;

import com.tpsix.spring.config.User;

import lombok.extern.slf4j.Slf4j;

/**
 * @author zhangyin
 * @date 2018/08/27
 */
@Slf4j
@Service
public class UserService {

    public void printUser(User user) {
        log.info("编号:", user.getId());
        log.info("名称:", user.getName());
        log.info("备注:", user.getDesc());
    }
    
}

按以上的装配策略,它将会被扫描到 Spring IoC 容器中。为了不被装配,需要把扫描的策略修改为:

@ComponentScan(basePackages = "com.tpsix.spring.*", excludeFilters = {@Filter(classes = {Service.class})})

这样,由于加入了 excludeFilters 的配置,使标注了 @Service 的类将不被 IoC 容器扫描注入,这样就可以把 UserService 类排除到 Spring IoC 容器中了。事实上,我们经常使用的 @SpringBootApplication 也注入了 @ComponentScan,这里列出 @SpringBootApplication 源码:

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.annotation.AliasFor;

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
// 自定义排除的扫描类
@ComponentScan(excludeFilters = {
@Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {

    // 通过类型排除自动配置类
    @AliasFor(annotation = EnableAutoConfiguration.class)
    Class<?>[] exclude() default {};

    // 通过名称排除自动配置类
    @AliasFor(annotation = EnableAutoConfiguration.class)
    String[] excludeName() default {};

    // 定义扫描包
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
    String[] scanBasePackages() default {};

    // 定义被扫描的类
    @AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
    Class<?>[] scanBasePackageClasses() default {};

}

显然,通过它就能定义扫描哪些包。但是这里需要特别注意的是,它提供的 exclude 和 excludeName 两个方法是对于其内部的自动配置类才会生效的。为了能够排除其他类,还可以再加入 @ComponentScan 以达到我们的目的。例如,扫描 User 而不扫描 UserService,可以把启动配置文件写成:

@SpringBootApplication
@ComponentScan(basePackages = {"com.tpsix.spring"}, excludeFilters = {@Filter(classes = Service.class)})

这样就能扫描指定对应的包并排除对应的类了。

自定义第三方 Bean

在项目中往往需要引入许多来自第三方的包,并且很有可能希望把第三方包的类对象也放入到 Spring IoC 容器中,这时 @Bean 注解就可以发挥作用了。

例如,要引入一个 DBCP 数据源, 我们先在 pom.xml 加入项目所需要 DBCP 包和 数据库 MySql 驱动程序的依赖,代码如下

        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-dbcp2</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>

这样 DBCP 和数据库驱动就被加入到了项目中,接着将使用它提供的机制来生成数据源。这时候,可以在 ApplicationConfig.java 中加入如下代码:

    @Bean(name = "dataSource")
    public DataSource getDataSource() {
        Properties props = new Properties();
        props.setProperty("driver", "com.mysql.jdbc.Driver");
        props.setProperty("url", "jdbc:mysql://localhost:3306/spring_demo");
        props.setProperty("username", "root");
        props.setProperty("password", "123");
        DataSource dataSource = null;
        try {
            dataSource = BasicDataSourceFactory.createDataSource(props);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return dataSource;
    }

这里通过 @Bean 定义了其配置项 name 为 "dataSource",那么 Spring 就会把它返回的对象用名称 "dataSource" 保存在 IoC 容器中。当然,你也可以不填写这个名称,那么它就会用你的方法名称作为 Bean 名称保存到 IoC 容器中。通过这样,就可以将第三方包的类装配到 Spring IoC 容器中了。

依赖注入

上文讨论了如何将 Bean 装配到 IoC 容器中,对于如何获取,还有 Bean 之间的依赖没有谈及,在 Spring IoC 的概念中,我们称为依赖注入( Dependency Injection,DI )。

例如,人类( Person ) 有时候利用一些动物( Animal )去完成一些事情,比如说狗( Dog )是用来看门的,猫( Cat )是用来抓老鼠的。做这些事情就依赖与那些动物。

为了更好的理解这个过程,首先来定义两个接口,一个是人类(Person),另外一个是动物(Animal)。人类通过动物去提供一些特殊服务。

代码如下

package com.tpsix.spring.pojo.definition;

/**
 * 人类接口
 * @author private
 */
public interface Person {
    
    // 使用动物服务
    void service();
    
    // 设置动物
    void setAnimal(Animal animal);
    
}
package com.tpsix.spring.pojo.definition;

/**
 * @author private
 */
public interface Animal {

    // 使用
    void use();
    
}

这样我们就拥有了两个接口,接下来我们需要两个实现类。
代码如下

package com.tpsix.spring.pojo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import com.tpsix.spring.pojo.definition.Animal;
import com.tpsix.spring.pojo.definition.Person;

@Component
public class BussinessPerson implements Person {

    @Autowired
    private Animal animal = null;
    
    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }

}
package com.tpsix.spring.pojo;

import org.springframework.stereotype.Component;

import com.tpsix.spring.pojo.definition.Animal;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class Dog implements Animal {

    @Override
    public void use() {
        // getSimpleName() 获取类简称
        log.info("狗【" + Dog.class.getSimpleName() + "】是看门用的。");
    }

}

这里应该注意注解 @Autowired,这也是我们在 Spring 中最常用的注解之一,我们有必要了解它,它会根据属性的类型找到对应的 Bean 进行注入。这里的 Dog 类是动物的一种,所以 Spring IoC 容器会把 Dog 的实例注入 BussinessPerson 中。这样通过 Spring IoC 容器获取 BussinessPerson 实例的时候就能够使用 Dog 实例来提供服务了。下面是测试的代码:

package com.tpsix.spring.config;

import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

import com.tpsix.spring.pojo.BussinessPerson;
import com.tpsix.spring.pojo.definition.Person;

import lombok.extern.slf4j.Slf4j;

/**
 * @author private
 * @date 2018/08/28
 */
@Slf4j
public class IocTest {

    public static void main(String[] args) {
        ApplicationContext ctx = new AnnotationConfigApplicationContext(
                ApplicationConfig.class
        );
        Person person = ctx.getBean(BussinessPerson.class);
        person.service();
    }
    
}

测试一下,就可以得到下面的日志:

23:07:25.372 [main] INFO com.tpsix.spring.pojo.Dog - 狗【Dog】是看门用的。

显然,测试是成功的,这个时候 Spring IoC 容器已经通过注解 @Autowired 成功地将 Dog 注入到了 BussinessPerson 实例中。但是这只是一个简单的例子,下面继续探讨 @Autowired。

注解 @Autowired

@Autowired 是我们使用得最多的注解之一,因此在这里需要进一步探讨他。

它注入的机制最基本的一条是根据类型,我们回顾 IoC 容器的顶级接口 BeanFactory,就可以知道 IoC 容器是通过 getBean 方法获取对应 Bean 的,而 getBean 又支持根据类型或者根据名称获取 Bean。回到上面的例子,我们只是创建了一个动物----狗,实际上动物还可以有猫(Cat),猫可以抓老鼠,下面我们创建一个猫的类,
代码如下:

package com.tpsix.spring.pojo;

import org.springframework.stereotype.Component;

import com.tpsix.spring.pojo.definition.Animal;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
public class Cat implements Animal {

    @Override
    public void use() {
        log.info("猫【" + Cat.class.getSimpleName() + "】是抓老鼠的。");
    }

}

好了,如果我们还是用着 BussinessPerson 类,那么你会发现,因为这个类只是定义了一个动物属性(Animal),而我们却有两个动物,一个狗,一个猫,Spring IoC 如何注入呢?

如果你已经测试,你会看到 IoC 容器抛出异常,如下面日志输出:

Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'bussinessPerson': Unsatisfied dependency expressed through field 'animal'; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'com.tpsix.spring.pojo.definition.Animal' available: expected single matching bean but found 2: cat,dog

从日志可以看出, Spring IoC 容器并不能知道你需要注入什么动物(是狗?是猫?)给 BussinessPerson 类对象,从而引起错误的发生。那么使用 @Autowired 能处理这个问题嘛?肯定可以的塞,假设我们目前需要的是狗提供提供服务,那么可以把属性名称转化为 dog,也就是原来的

    @Autowired
    private Animal animal = null;
    // 修改为
    @Autowired
    private Animal dog    = null;

这里,我们只是将属性的名称从 animal 修改为了 dog,那么我们再测试的时候,你可以看到是采用狗来提供服务的。那是因为 @Autowired 提供这样的规则,首先它会根据类型找到对应的 Bean,如果对应类型的 Bean 不是唯一的,那么它就会根据其属性名称和 Bean 的名称进行匹配。如果匹配上,就会使用该 Bean,如果还无法匹配,就会抛出异常。

还要注意的是 @Autowired 是一个默认必需找到对应 Bean 的注解,如果不能确定其标注属性一定会存在并且允许这个被标注的属性为 null,那么你可以配置 @Autowired 属性 required 为 false。

例如,像下面一样:

@Autowired(required = false)

同样,它除了可以标注属性外,还可以标注方法,如 setAnimal 方法,如下所示:

@Override
@Autowired
public void setAnimal(Animal animal) {
    this.animal = animal;
}

这样它也会使用 setAnimal 方法从 IoC 容器中找到对应的动物进行注入,甚至我们还可以使用在方法的参数上,
后面会谈到

消除歧义性 —— @Primary 和 @Quelifier

在上面我们发现有猫有够的时候,为了使 @Autowired 能够继续使用,将 BussinessPerson 的属性名称从 animal 修改为 dog。显然这样很不合理,好好的一个动物,却被我们定义成了狗。产生注入失败的问题根本是按类型查找,正如动物可以有多种类型,这样会造成 Spring IoC 容器注入的困扰,我们把这个问题称为 歧义性。知道这个原因后,那么这两个注解是从哪个角度去解决这些问题的呢?

首先是一个注解 @Primary,它是一个修改优先权的注解,当我们有猫有狗的时候,假设这次需要使用猫,那么只需要在猫类的定义上加入 @Primary 就可以了,类似下面这样:

@Component
@Primary
public class Cat implements Animal {

    @Override
    public void use() {
        log.info("猫【" + Cat.class.getSimpleName() + "】是抓老鼠的。");
    }

}

这里的 @Primary 的含义告诉 Spring IoC 容器,当发现有多个类型的 Bean 时,请优先使用我进行注入,于是再进行测试时会发现,系统将用猫为你提供服务。因为当 Spring 进行注入的时候,虽然它发现存在多个动物,但因为 Cat 被标注为了 @Primary,所以优先采用 Cat 的实例进行了注入,这样就通过优先级的变换使得 IoC 容器知道注入哪个具体的实例来满足依赖注入。

有时候 @Primary 也可以使用在多个类上,也许无论是猫还是狗都可能带上 @Primary 注解,其结果是 IoC 容器还是无法区分采用哪个 Bean 的实例进行注入,又或者说我们需要更加灵活的机制来实现注入,那么 @Quelifier 可以满足你的愿望。它的配置项 value 需要一个字符串去定义,它将与 @Autowired 组合在一起,通过类型和名称一起到 Bean。我们知道 Bean 名称在 Spring IoC 容器中是唯一的标识,通过这个就可以消除歧义性了。此时你是否想到了 BeanFactory 接口中的这个方法呢?

<T> T getBean(String name, Class<T> requiredType) throws BeansException;

通过它就能够按照名称和类型的结合找到对象了。下面假设猫已经标注了 @Primary,而我们需要的是狗提供服务,因此需要修改 BussinessPerson 属性 animal 的标注以适合我们的需要,如下所示:

@Autowired
@Qualifier("dog")
private Animal animal = null;

一旦这样声明, Spring IoC 将会以类型和名称取寻找对应的 Bean 进行注入。根据类型和名称,显然也只能找到狗为我们服务了。

带有参数的构造方法类的装配

在上面,我们都是基于一个默认的情况,那就是不带参数的构造方法下实现依赖注入。但事实上,有些类只有带有参数的构造方法,于是上述的方法都不能再使用了。为了满足这个功能,我们可以使用 @Autowired 注解对构造方法的参数进行注入,例如,修改类 BussinessPerson 来满足这个功能。
代码如下:

@Component
public class BussinessPerson implements Person {

    private Animal animal = null;
    
    public BussinessPerson(@Autowired @Qualifier("dog") Animal animal) {
        this.animal = animal;
    }
    
    @Override
    public void service() {
        this.animal.use();
    }

    @Override
    public void setAnimal(Animal animal) {
        this.animal = animal;
    }

}

可以看到,代码中取消了 @Autowired 对属性和方法的标注。在参数上加入了 @Autowired 和 @Qualifier 注解,使得它能够注入进来。这里使用 @Qualifier 是为了避免歧义性。当然,如果你的环境中不是有猫有狗,则可以完全不使用 @Qualifier,而单单使用 @Autowired 就可以了。

本文完。如有不同意见的地方,请在下方评论呦 👉

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,948评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,371评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,490评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,521评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,627评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,842评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,997评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,741评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,203评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,534评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,673评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,339评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,955评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,770评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,000评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,394评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,562评论 2 349