從源碼深入理解 Spring IoC 注解

全注解下的 Spring IoC

本文基于 Spring Boot,所以并不使用 XML 配置,使用注解描述生成對象
版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。

Ioc 容器簡介

Spring IoC 容器是一個管理 Bean 的容器,在 Spring 的定義中,它要求所有的 IoC 容器都需要實(shí)現(xiàn)接口
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 容器中,默認(rèn)的情況下,Bean 都是以單例存在的,也就是使用 getBean 方法返回的都是同一個對象。與 isSingleton 方法相反的是 isPrototype,如果它返回的是 true,那么當(dāng)我們使用 getBean 方法獲取 Bean 的時候,Spring IoC 容器就會創(chuàng)建一個新的 Bean 返回給調(diào)用者,這些與下一篇要討論的Bean 的作用域有關(guān)。

由于 BeanFactory 的功能還不夠強(qiáng)大,因此 Spring 在 BeanFactory 的基礎(chǔ)上,還設(shè)計了一個更為高級的接口
ApplicationContext。它是 BeanFactory 的子接口之一,在 Spring 的體系中
BeanFactoryApplicationContext 是最為重要的接口設(shè)計,在我們使用 Spring IoC 容器時,大部分都是 ApplicationContext 接口的實(shí)現(xiàn)類。

在 Spring Boot 當(dāng)中我們主要是通過注解來裝配 Bean 到 Spring IoC 容器中,為了貼近 Spring Boot,這里不再介紹與 XML 相關(guān)的 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 的容器會根據(jù)它來生成 IoC 容器去裝配 Bean, @Bean 代表將 initUser 方法返回的POJO 裝配到 IoC 容器中,而屬性 name 定義這個 Bean 的名稱,
如果沒有配置它,則將方法名稱 initUser 作為 Bean 的名稱保存到 IoC 容器中。

做好了這些,就可以使用 AnnotationConfigApplicationContext 來構(gòu)建自己的 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
        );
        // 根據(jù)類型獲取 Bean
        User user = ctx.getBean(User.class);
        log.info(user.getId() + "");
    }
    
}

代碼中將 Java 配置文件 ApplicationConfig.class 傳遞給 AnnotationConfigApplicationContext 的構(gòu)造方法,這樣它就能夠讀取配置了。然后將配置里面的 Bean 裝配到 IoC 容器中,于是可以使用 getBean 方法獲取對應(yīng)的 POJO,你可以看到下面的日志打印:

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

顯然,配置在配置文件中的名稱為 user 的 Bean 已經(jīng)被裝配到 IoC 容器中,并且可以通過 getBean 方法獲取對應(yīng)的 Bean, 并將 Bean 的屬性信息輸出出來。當(dāng)然這是很簡單的方法,而注解 @Bean 也不是唯一創(chuàng)建 Bean 的方法,還有其他的方法可以讓 IoC 容器裝配 Bean,而且 Bean 之間還有依賴的關(guān)系需要進(jìn)一步處理。

裝配你的 Bean

以下都是基于 Spring Boot 注解方式來裝配 Bean

通過掃描裝配你的 Bean

如果一個個的 Bean 使用注解 @Bean 注入 Spring IoC 容器中,那將是一件很痛苦的事情。好在 Spring 還允許我們進(jìn)行掃描裝配 Bean 到 IoC 容器中,對于掃描裝配而言使用的注解是 @Component@ComponentScan。@Component 是標(biāo)明哪個類被掃描進(jìn)入 Spring IoC 容器,而 @ComponentScan 則是標(biāo)明采用何種模式去掃描裝配 Bean。

這里復(fù)用之前的 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 的名稱,當(dāng)然你也可以不配置這個字符串,那么 IoC 容器就會把類名第一個字母作為小寫,其他不變作為 Bean 名稱放入到 IoC 容器中,注解 @Value 則是指定具體的值,使 Spring IoC 給予對應(yīng)的屬性注入對應(yīng)的值。為了讓 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,意味著它會進(jìn)行掃描,但是它只會掃描類 ApplicationConfig.java 所在的當(dāng)前包和其子包,之前把 User.java 移動到包 com.tpsix.spring.config 就是這個原因。這樣就可以刪掉之前使用 @Bean 標(biāo)注的創(chuàng)建對象方法。然后進(jìn)行測試,代碼如下

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() + "");
    }
    
}

這樣就可以運(yùn)行了。然而為了使得 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
// 允許在一個類中可重復(fù)定義
@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;

    // 是否啟用默認(rèn)的過濾器
    boolean useDefaultFilters() default true;

    // 當(dāng)滿足過濾器的條件時掃描
    Filter[] includeFilters() default {};

    // 當(dāng)不滿足過濾器的條件時掃描
    Filter[] excludeFilters() default {};

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

    // 定義過濾器
    @Retention(RetentionPolicy.RUNTIME)
    @Target({})
    @interface Filter {

        // 過濾器類型,可以按注解類型活著正則表達(dá)式等過濾
        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 {}; 當(dāng)滿足過濾器的條件時掃描
  • Filter[] excludeFilters() default {}; 當(dāng)不滿足過濾器的條件時掃描
  • boolean lazyInit() default false; 是否延遲初始化

首先通過配置項 basePackages 定義掃描的包名,在沒有定義的情況下,它只會掃描當(dāng)前包和其子包的路徑,還可以通過 basePackageClasses 定義掃描的類。其中還有 includeFiltersexcludeFilters,includeFilters 是定義滿足過濾器(@Filter)條件的 Bean 才去掃描,excludeFilters 則是排除過濾器條件的 Bean,它們都需要通過一個注解 @Filter 去定義,它有一個 type 類型,這里可以定義為注解或者正則表達(dá)式等類型。classes 定義掃描類,pattern 定義正則表達(dá)式類。

此時我們再把 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。比如說,現(xiàn)在我們有一個 UserService 類,為了標(biāo)注它為服務(wù)類,將類標(biāo)注 @Service (該注解注入了 @Component,所以在默認(rèn)的情況下它會被 Spring 掃描裝配到 IoC 容器中),這里假設(shè)我們采用了策略:

@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 的配置,使標(biāo)注了 @Service 的類將不被 IoC 容器掃描注入,這樣就可以把 UserService 類排除到 Spring IoC 容器中了。事實(shí)上,我們經(jīng)常使用的 @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 兩個方法是對于其內(nèi)部的自動配置類才會生效的。為了能夠排除其他類,還可以再加入 @ComponentScan 以達(dá)到我們的目的。例如,掃描 User 而不掃描 UserService,可以把啟動配置文件寫成:

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

這樣就能掃描指定對應(yīng)的包并排除對應(yīng)的類了。

自定義第三方 Bean

在項目中往往需要引入許多來自第三方的包,并且很有可能希望把第三方包的類對象也放入到 Spring IoC 容器中,這時 @Bean 注解就可以發(fā)揮作用了。

例如,要引入一個 DBCP 數(shù)據(jù)源, 我們先在 pom.xml 加入項目所需要 DBCP 包和 數(shù)據(jù)庫 MySql 驅(qū)動程序的依賴,代碼如下

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

這樣 DBCP 和數(shù)據(jù)庫驅(qū)動就被加入到了項目中,接著將使用它提供的機(jī)制來生成數(shù)據(jù)源。這時候,可以在 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 容器中。當(dāng)然,你也可以不填寫這個名稱,那么它就會用你的方法名稱作為 Bean 名稱保存到 IoC 容器中。通過這樣,就可以將第三方包的類裝配到 Spring IoC 容器中了。

依賴注入

上文討論了如何將 Bean 裝配到 IoC 容器中,對于如何獲取,還有 Bean 之間的依賴沒有談及,在 Spring IoC 的概念中,我們稱為依賴注入( Dependency Injection,DI )。

例如,人類( Person ) 有時候利用一些動物( Animal )去完成一些事情,比如說狗( Dog )是用來看門的,貓( Cat )是用來抓老鼠的。做這些事情就依賴與那些動物。

為了更好的理解這個過程,首先來定義兩個接口,一個是人類(Person),另外一個是動物(Animal)。人類通過動物去提供一些特殊服務(wù)。

代碼如下

package com.tpsix.spring.pojo.definition;

/**
 * 人類接口
 * @author private
 */
public interface Person {
    
    // 使用動物服務(wù)
    void service();
    
    // 設(shè)置動物
    void setAnimal(Animal animal);
    
}
package com.tpsix.spring.pojo.definition;

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

    // 使用
    void use();
    
}

這樣我們就擁有了兩個接口,接下來我們需要兩個實(shí)現(xiàn)類。
代碼如下

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() + "】是看門用的。");
    }

}

這里應(yīng)該注意注解 @Autowired,這也是我們在 Spring 中最常用的注解之一,我們有必要了解它,它會根據(jù)屬性的類型找到對應(yīng)的 Bean 進(jìn)行注入。這里的 Dog 類是動物的一種,所以 Spring IoC 容器會把 Dog 的實(shí)例注入 BussinessPerson 中。這樣通過 Spring IoC 容器獲取 BussinessPerson 實(shí)例的時候就能夠使用 Dog 實(shí)例來提供服務(wù)了。下面是測試的代碼:

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 容器已經(jīng)通過注解 @Autowired 成功地將 Dog 注入到了 BussinessPerson 實(shí)例中。但是這只是一個簡單的例子,下面繼續(xù)探討 @Autowired。

注解 @Autowired

@Autowired 是我們使用得最多的注解之一,因此在這里需要進(jìn)一步探討他。

它注入的機(jī)制最基本的一條是根據(jù)類型,我們回顧 IoC 容器的頂級接口 BeanFactory,就可以知道 IoC 容器是通過 getBean 方法獲取對應(yīng) Bean 的,而 getBean 又支持根據(jù)類型或者根據(jù)名稱獲取 Bean。回到上面的例子,我們只是創(chuàng)建了一個動物----狗,實(shí)際上動物還可以有貓(Cat),貓可以抓老鼠,下面我們創(chuàng)建一個貓的類,
代碼如下:

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 類,那么你會發(fā)現(xiàn),因為這個類只是定義了一個動物屬性(Animal),而我們卻有兩個動物,一個狗,一個貓,Spring IoC 如何注入呢?

如果你已經(jīng)測試,你會看到 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 類對象,從而引起錯誤的發(fā)生。那么使用 @Autowired 能處理這個問題嘛?肯定可以的塞,假設(shè)我們目前需要的是狗提供提供服務(wù),那么可以把屬性名稱轉(zhuǎn)化為 dog,也就是原來的

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

這里,我們只是將屬性的名稱從 animal 修改為了 dog,那么我們再測試的時候,你可以看到是采用狗來提供服務(wù)的。那是因為 @Autowired 提供這樣的規(guī)則,首先它會根據(jù)類型找到對應(yīng)的 Bean,如果對應(yīng)類型的 Bean 不是唯一的,那么它就會根據(jù)其屬性名稱和 Bean 的名稱進(jìn)行匹配。如果匹配上,就會使用該 Bean,如果還無法匹配,就會拋出異常。

還要注意的是 @Autowired 是一個默認(rèn)必需找到對應(yīng) Bean 的注解,如果不能確定其標(biāo)注屬性一定會存在并且允許這個被標(biāo)注的屬性為 null,那么你可以配置 @Autowired 屬性 required 為 false。

例如,像下面一樣:

@Autowired(required = false)

同樣,它除了可以標(biāo)注屬性外,還可以標(biāo)注方法,如 setAnimal 方法,如下所示:

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

這樣它也會使用 setAnimal 方法從 IoC 容器中找到對應(yīng)的動物進(jìn)行注入,甚至我們還可以使用在方法的參數(shù)上,
后面會談到

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

在上面我們發(fā)現(xiàn)有貓有夠的時候,為了使 @Autowired 能夠繼續(xù)使用,將 BussinessPerson 的屬性名稱從 animal 修改為 dog。顯然這樣很不合理,好好的一個動物,卻被我們定義成了狗。產(chǎn)生注入失敗的問題根本是按類型查找,正如動物可以有多種類型,這樣會造成 Spring IoC 容器注入的困擾,我們把這個問題稱為 歧義性。知道這個原因后,那么這兩個注解是從哪個角度去解決這些問題的呢?

首先是一個注解 @Primary,它是一個修改優(yōu)先權(quán)的注解,當(dāng)我們有貓有狗的時候,假設(shè)這次需要使用貓,那么只需要在貓類的定義上加入 @Primary 就可以了,類似下面這樣:

@Component
@Primary
public class Cat implements Animal {

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

}

這里的 @Primary 的含義告訴 Spring IoC 容器,當(dāng)發(fā)現(xiàn)有多個類型的 Bean 時,請優(yōu)先使用我進(jìn)行注入,于是再進(jìn)行測試時會發(fā)現(xiàn),系統(tǒng)將用貓為你提供服務(wù)。因為當(dāng) Spring 進(jìn)行注入的時候,雖然它發(fā)現(xiàn)存在多個動物,但因為 Cat 被標(biāo)注為了 @Primary,所以優(yōu)先采用 Cat 的實(shí)例進(jìn)行了注入,這樣就通過優(yōu)先級的變換使得 IoC 容器知道注入哪個具體的實(shí)例來滿足依賴注入。

有時候 @Primary 也可以使用在多個類上,也許無論是貓還是狗都可能帶上 @Primary 注解,其結(jié)果是 IoC 容器還是無法區(qū)分采用哪個 Bean 的實(shí)例進(jìn)行注入,又或者說我們需要更加靈活的機(jī)制來實(shí)現(xiàn)注入,那么 @Quelifier 可以滿足你的愿望。它的配置項 value 需要一個字符串去定義,它將與 @Autowired 組合在一起,通過類型和名稱一起到 Bean。我們知道 Bean 名稱在 Spring IoC 容器中是唯一的標(biāo)識,通過這個就可以消除歧義性了。此時你是否想到了 BeanFactory 接口中的這個方法呢?

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

通過它就能夠按照名稱和類型的結(jié)合找到對象了。下面假設(shè)貓已經(jīng)標(biāo)注了 @Primary,而我們需要的是狗提供服務(wù),因此需要修改 BussinessPerson 屬性 animal 的標(biāo)注以適合我們的需要,如下所示:

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

一旦這樣聲明, Spring IoC 將會以類型和名稱取尋找對應(yīng)的 Bean 進(jìn)行注入。根據(jù)類型和名稱,顯然也只能找到狗為我們服務(wù)了。

帶有參數(shù)的構(gòu)造方法類的裝配

在上面,我們都是基于一個默認(rèn)的情況,那就是不帶參數(shù)的構(gòu)造方法下實(shí)現(xiàn)依賴注入。但事實(shí)上,有些類只有帶有參數(shù)的構(gòu)造方法,于是上述的方法都不能再使用了。為了滿足這個功能,我們可以使用 @Autowired 注解對構(gòu)造方法的參數(shù)進(jìn)行注入,例如,修改類 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 對屬性和方法的標(biāo)注。在參數(shù)上加入了 @Autowired 和 @Qualifier 注解,使得它能夠注入進(jìn)來。這里使用 @Qualifier 是為了避免歧義性。當(dāng)然,如果你的環(huán)境中不是有貓有狗,則可以完全不使用 @Qualifier,而單單使用 @Autowired 就可以了。

本文完。如有不同意見的地方,請在下方評論呦 ??

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容