乐趣区

Qualifier高级应用按类别批量依赖注入享学Spring

每篇一句

罗斯:选秀状元可能有水货,但 MVP 绝对没有

前言

在上篇文章 (讲解 @LoadBalanced 负载均衡) 的末尾,我抛出了一个很重要的问题,建议小伙伴自己深入思考一番;本文主要针对此问题,作出一个统一的答复和讲解。
由于本人觉得这块知识点它属于 Spring Framework 的核心内容之一,非常的重要,因此单拎出来作专文讲述,希望对你有所帮助。

背景案例

说到 @Qualifier 这个注解大家并不陌生:它用于“精确匹配”Bean,一般用于同一类型的 Bean 有多个不同实例的 case 下,可通过此注解来做鉴别和匹配。
本以为 @Qualifier 注解使用在属性上、类上用于鉴别就够了,直到我看到 LoadBalancerAutoConfiguration 里有这么应用:

@LoadBalanced
@Autowired(required = false)
private List<RestTemplate> restTemplates = Collections.emptyList();    

它能把容器内所有 RestTemplate 类型并且标注有 @LoadBalanced 注解的 Bean 全注入进来
这个用法让我非常的惊喜,它给我提供额外一条思路,让我的框架多了一种玩法。为了融汇贯通它,使用起来尽量避免不采坑,那就只能揭开它,从底层原理处理解它的用法了。


QualifierAnnotationAutowireCandidateResolver详解

它是依赖注入候选处理器接口 AutowireCandidateResolver 的实现类,继承自 GenericTypeAwareAutowireCandidateResolver,所以此类是功能最全、最为强大的一个处理器(ContextAnnotationAutowireCandidateResolver 除外~),Spring内默认使用它进行候选处理。

它几乎可以被称为 @Qualifier 注解的 ” 实现类 ”,专门用于解析此注解。
带着上面的疑问进行原理分析如下:

// @since 2.5
public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwareAutowireCandidateResolver {
    // 是个 List,可以知道它不仅仅只支持 org.springframework.beans.factory.annotation.Qualifier
    private final Set<Class<? extends Annotation>> qualifierTypes = new LinkedHashSet<>(2);
    private Class<? extends Annotation> valueAnnotationType = Value.class;


    // 空构造:默认支持的是 @Qualifier 以及 JSR330 标准的 @Qualifier
    public QualifierAnnotationAutowireCandidateResolver() {this.qualifierTypes.add(Qualifier.class);
        try {this.qualifierTypes.add((Class<? extends Annotation>) ClassUtils.forName("javax.inject.Qualifier", QualifierAnnotationAutowireCandidateResolver.class.getClassLoader()));
        } catch (ClassNotFoundException ex) {// JSR-330 API not available - simply skip.}
    }
    
    // 非空构造:可自己额外指定注解类型
    // 注意:如果通过构造函数指定 qualifierType,上面两种就不支持了,因此不建议使用
    // 而建议使用它提供的 addQualifierType() 来添加~~~
    public QualifierAnnotationAutowireCandidateResolver(Class<? extends Annotation> qualifierType) {
    ... // 省略 add/set 方法    

    // 这是个最重要的接口方法~~~  判断所提供的 Bean-->BeanDefinitionHolder 是否是候选的
    //(返回 true 表示此 Bean 符合条件)@Override
    public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
        // 1、先看父类:ean 定义是否允许依赖注入、泛型类型是否匹配
        boolean match = super.isAutowireCandidate(bdHolder, descriptor);
        // 2、若都满足就继续判断 @Qualifier 注解~~~~
        if (match) {
            // 3、看看标注的 @Qualifier 注解和候选 Bean 是否匹配~~~(本处的核心逻辑)// descriptor 一般封装的是属性写方法的参数,即方法参数上的注解
            match = checkQualifiers(bdHolder, descriptor.getAnnotations());
            // 4、若 Field/ 方法参数匹配,会继续去看看参数所在的方法 Method 的情况
            // 若是构造函数 / 返回 void。进一步校验标注在构造函数 / 方法上的 @Qualifier 限定符是否匹配
        if (match) {MethodParameter methodParam = descriptor.getMethodParameter();
                // 若是 Field,methodParam 就是 null  所以这里是需要判空的
                if (methodParam != null) {Method method = methodParam.getMethod();
                    // method == null 表示构造函数 void.class 表示方法返回 void
                    if (method == null || void.class == method.getReturnType()) {// 注意 methodParam.getMethodAnnotations()方法是可能返回空的
                        // 毕竟构造方法 / 普通方法上不一定会标注 @Qualifier 等注解呀~~~~
                        // 同时警示我们:方法上的 @Qualifier 注解可不要乱标
                        match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations());
                    }
                }
            }
        }
        return match;
    }
    ...
}

在源码注释的地方,我按照步骤标出了它进行匹配的一个执行步骤逻辑。需要注意如下几点:

  • qualifierTypes是支持调用者自己指定的(默认只支持 @Qualifier 类型)
  • 只有类型匹配、Bean 定义匹配、泛型匹配等全部 Ok 了,才会使用 @Qualifier 去更加精确的匹配
  • descriptor.getAnnotations()的逻辑是:

        -  如果 `DependencyDescriptor` 描述的是字段(`Field`),那就去字段里拿注解们
        -  若描述的是方法参数(`MethodParameter`),那就返回的是方法参数的注解
  • 步骤 3 的 match = true 表示 Field/ 方法参数上 的限定符是匹配的~

说明:能走到 isAutowireCandidate() 方法里来,那它肯定是标注了 @Autowired 注解的(才能被 AutowiredAnnotationBeanPostProcessor 后置处理),所以 descriptor.getAnnotations() 返回的数组长度至少为 1

checkQualifiers()方法:
QualifierAnnotationAutowireCandidateResolver:// 将给定的限定符注释与候选 bean 定义匹配。命名中你发现:这里是负数形式,表示多个注解一起匹配
    // 此处指的限定符,显然默认情况下只有 @Qualifier 注解
    protected boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) {
        // 很多人疑问为何没标注注解返回的还是 true?// 请参照上面我的解释:methodParam.getMethodAnnotations()方法是可能返回空的,so... 可以理解了吧
        if (ObjectUtils.isEmpty(annotationsToSearch)) {return true;}
        SimpleTypeConverter typeConverter = new SimpleTypeConverter();

        // 遍历每个注解(一般有 @Autowired+@Qualifier 两个注解)// 本文示例的两个注解:@Autowired+@LoadBalanced 两个注解~~~(@LoadBalanced 上标注有 @Qualifier)for (Annotation annotation : annotationsToSearch) {Class<? extends Annotation> type = annotation.annotationType();
            boolean checkMeta = true; // 是否去检查元注解
            boolean fallbackToMeta = false;

            // isQualifier 方法逻辑见下面:是否是限定注解(默认的 / 开发自己指定的)// 本文的 org.springframework.cloud.client.loadbalancer.LoadBalanced 是返回 true 的
            if (isQualifier(type)) {
                // checkQualifier:检查当前的注解限定符是否匹配
                if (!checkQualifier(bdHolder, annotation, typeConverter)) {fallbackToMeta = true; // 没匹配上。那就 fallback 到 Meta 去吧} else {checkMeta = false; // 匹配上了,就没必要校验元数据了喽~~~}
            }

            // 开始检查元数据(如果上面匹配上了,就不需要检查元数据了)// 比如说 @Autowired 注解 / 其它自定义的注解(反正就是未匹配上的),就会进来一个个检查元数据
            // 什么时候会到 checkMeta 里来:如 @A 上标注有 @Qualifier。@B 上标注有 @A。这个时候限定符是 @B 的话会 fallback 过来
            if (checkMeta) {
                boolean foundMeta = false;
                // type.getAnnotations()结果为元注解们:@Documented、@Retention、@Target 等等
                for (Annotation metaAnn : type.getAnnotations()) {Class<? extends Annotation> metaType = metaAnn.annotationType();
                    if (isQualifier(metaType)) {
                        foundMeta = true; // 只要进来了 就标注找到了,标记为 true 表示从元注解中找到了
                        // Only accept fallback match if @Qualifier annotation has a value...
                        // Otherwise it is just a marker for a custom qualifier annotation.
                        // fallback=true(是限定符但是没匹配上才为 true)但没有 valeu 值
                        // 或者根本就没有匹配上,那不好意思,直接 return false~
                        if ((fallbackToMeta && StringUtils.isEmpty(AnnotationUtils.getValue(metaAnn))) || !checkQualifier(bdHolder, metaAnn, typeConverter)) {return false;}
                    }
                }
                // fallbackToMeta =true 你都没有找到匹配的,就返回 false 的
                if (fallbackToMeta && !foundMeta) {return false;}
            }
        }
        // 相当于:只有所有的注解都木有返回 false,才会认为这个 Bean 是合法的~~~
        return true;
    }

    // 判断一个类型是否是限定注解   qualifierTypes:表示我所有支持的限定符
    // 本文的关键在于下面这个判断语句:类型就是限定符的类型 or @Qualifier 标注在了此注解上(isAnnotationPresent)protected boolean isQualifier(Class<? extends Annotation> annotationType) {for (Class<? extends Annotation> qualifierType : this.qualifierTypes) {
            // 类型就是限定符的类型 or @Qualifier 标注在了此注解上(isAnnotationPresent)if (annotationType.equals(qualifierType) || annotationType.isAnnotationPresent(qualifierType)) {return true;}
        }
        return false;
    }

checkQualifiers()方法它会检查标注的所有的注解(循环遍历一个个检查),规则如下:

  • 若是限定符注解(自己就是 @Qualifier 或者isAnnotationPresent),匹配上了,就继续看下一个注解

        - 也就说 `@Qualifier` 所标注的注解也算是限定符(`isQualifier() = true`)
  • 若是限定符注解但是没匹配上,那就 fallback。继续看看标注在它身上的限定符注解(如果有)能否匹配上,若匹配上了也成
  • 若不是限定符注解,也是走 fallback 逻辑
  • 总之:若不是限定符注解直接忽略。若有多个限定符注解都生效,必须全部匹配上了,才算做最终匹配上。

Tips:限定符不生效 的效果不一定是注入失败,而是如果是单个的话还是注入成功的。只是若出现多个 Bean 它就无法起到区分的效果了,所以才会注入失败了~

它的 fallback 策略最多只能再向上再找一个层级 (多了就不行了)。例如上例子中使用 @B 标注也是能起到@Qualifier 效果的,但是若再加一个 @C 层级,限定符就不生效了。

注意:Class.isAnnotationPresent(Class<? extends Annotation> annotationClass)表示 annotationClass 是否标注在此类型上(此类型可以是任意 Class 类型)。
此方法不具有传递性:比如注解 A 上标注有 @Qualifier,注解 B 上标注有 @A 注解,那么你用此方法判断 @B 上是否有 @Qualifier它是返回 false 的 (即使都写了@Inherited 注解,因为和它没关系)

到这其实还是不能解释本文中为何 @LoadBalanced 参与了依赖注入,还得继续看精髓中的精髓 checkQualifier() 方法(方法名是单数,表示精确检查某一个单独的注解):

QualifierAnnotationAutowireCandidateResolver:// 检查某一个注解限定符,是否匹配当前的 Bean
    protected boolean checkQualifier(BeanDefinitionHolder bdHolder, Annotation annotation, TypeConverter typeConverter) {
        // type:注解类型 bd:当前 Bean 的 RootBeanDefinition 
        Class<? extends Annotation> type = annotation.annotationType();        
        RootBeanDefinition bd = (RootBeanDefinition) bdHolder.getBeanDefinition();
    
        // ======== 下面是匹配的关键步骤 =========
        // 1、Bean 定义信息的 qualifiers 字段一般都无值了(XML 时代的配置除外)// 长名称不行再拿短名称去试了一把。显然此处 qualifier 还是为 null 的
        AutowireCandidateQualifier qualifier = bd.getQualifier(type.getName());
        if (qualifier == null) {qualifier = bd.getQualifier(ClassUtils.getShortName(type));
        }
        
        // 这里才是真真有料的地方~~~ 请认真看步骤
        if (qualifier == null) {
            // First, check annotation on qualified element, if any
            // 1、词方法是从 bd 标签里拿这个类型的注解声明,非 XML 配置时代此处 targetAnnotation 为 null
            Annotation targetAnnotation = getQualifiedElementAnnotation(bd, type);
            // Then, check annotation on factory method, if applicable
            // 2、若为 null。去工厂方法里拿这个类型的注解。这方法里标注了两个注解 @Bean 和 @LoadBalanced,所以此时 targetAnnotation 就不再为 null 了~~
            if (targetAnnotation == null) {targetAnnotation = getFactoryMethodAnnotation(bd, type);
            }

            // 若本类木有,还会去父类去找一趟
            if (targetAnnotation == null) {RootBeanDefinition dbd = getResolvedDecoratedDefinition(bd);
                if (dbd != null) {targetAnnotation = getFactoryMethodAnnotation(dbd, type);
                }
            }


            // 若 xml、工厂方法、父里都还没找到此方法。那好家伙,回退到还去类本身上去看
            // 也就是说,如果 @LoadBalanced 标注在 RestTemplate 上,也是阔仪的
            if (targetAnnotation == null) {
                // Look for matching annotation on the target class
                ...
            }
        
            // 找到了,并且当且仅当就是这个注解的时候,就 return true 了~
            // Tips:这里使用的是 equals,所以即使目标的和 Bean 都标注了 @Qualifier 属性,value 值相同才行哟~~~~
            // 简单的说:只有 value 值相同,才会被选中的。否则这个 Bean 就是不符合条件的
            if (targetAnnotation != null && targetAnnotation.equals(annotation)) {return true;}
        }

        // 赞。若 targetAnnotation 还没找到,也就是还没匹配上。仍旧还不放弃,拿到当前这个注解的所有注解属性继续尝试匹配
        Map<String, Object> attributes = AnnotationUtils.getAnnotationAttributes(annotation);
        if (attributes.isEmpty() && qualifier == null) {return false;}
        ... // 详情不描述了。这就是为什么我们吧 @Qualifier 标注在某个类上面都能生效的原因 就是这里做了非常强大的兼容性~
    }

// ================= 它最重要的两个判断 =================
if (targetAnnotation != null && targetAnnotation.equals(annotation));

// Fall back on bean name (or alias) match
if (actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) &&
                    expectedValue instanceof String && bdHolder.matchesName((String) expectedValue));

checkQualifier()方法的实现,足以看到 Spring 作为一个优秀框架它对 case 的全面性,兼容性、灵活性的考虑还是很到位的。正因为 Spring 提供的强大的支持和灵活扩展,才给与了 SpringBoot、SpringCloud 在框架层面设计上更多可能性~




@Qualifier高级使用

@Autowired是根据类型进行自动装配的,当 Spring 容器内同一类型的 Bean 不止一个的时候,就需要借助 @Qualifier 来一起使用了。

示例一:
@Configuration
public class WebMvcConfiguration {@Qualifier("person1")
    @Autowired
    public Person person;

    @Bean
    public Person person1() {return new Person("fsx01", 16);
    }
    @Bean
    public Person person2() {return new Person("fsx02", 18);
    }
}

单测代码如下(下同):

public static void main(String[] args) {ApplicationContext context = new AnnotationConfigApplicationContext(WebMvcConfiguration.class);
    WebMvcConfiguration bean = context.getBean(WebMvcConfiguration.class);
    // 打印字段的值
    System.out.println(bean.person);

}

运行后打印 Person(name=fsx01, age=16),完全符合预期。这也是我们对@Qualifier 注解最常规、最简单的使用。

示例二:

若你细心的话你可能注意到了 @Qualifier 注解它允许继承(@Inherited)、能标注在字段上、方法上、方法参数、类上、注解上
因此它还能这么用:

@Configuration
public class WebMvcConfiguration {

    @MyAnno // 会把所有标注有此注解的 Bean 都收入囊中,请 List 装(因为会有多个)@Autowired
    public List<Person> person;
    
    @MyAnno
    @Bean
    public Person person1() {return new Person("fsx01", 16);
    }
    @MyAnno
    @Bean
    public Person person2() {return new Person("fsx02", 18);
    }

    // 自定义注解:上面标注有 @Qualifier 注解
    @Target({FIELD, METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Qualifier
    @interface MyAnno {}}

运行单测,打印[Person(name=fsx01, age=16), Person(name=fsx02, age=18)],符合预期。

实例三:

若你不想自定义注解,直接使用 @Qualifier 注解分类注入也是可以的,如下案例:

@Configuration
public class WebMvcConfiguration {@Qualifier("person2")
    @Autowired
    public List<Person> person;

    @Qualifier("person2")
    @Bean
    public Person person1() {return new Person("fsx01", 16);
    }

    @Qualifier
    @Bean
    public Person person2() {return new Person("fsx02", 18);
    }
    @Qualifier
    @Bean
    public Person person3() {return new Person("fsx03", 20);
    }
}

运行的最终结果是:

[Person(name=fsx01, age=16), Person(name=fsx02, age=18)]

它把 @Qualifier 指定的 value 值相同的 或者 beanName(或者别名)相同的都注入进来了。这部分匹配代码为:

checkQualifier 方法:1、头上标注的注解完全 equals(类型和 value 值都一样,算作匹配成功)targetAnnotation != null && targetAnnotation.equals(annotation)
    
2、Fall back on bean name (or alias) match。若 @Qualifier 没匹配上,回退到 BeanName 的匹配,规则为:取头上注解的 `value` 属性(必须有此属性),如果 beanName/alias 能匹配上次名称,也算最终匹配成功了
   
    actualValue == null && attributeName.equals(AutowireCandidateQualifier.VALUE_KEY) &&
    expectedValue instanceof String && bdHolder.matchesName((String) expectedValue)

备注:使用在类上、入参上的使用比较简单,此处就不做示范了。
@Qualifier 设计的细节可以看到,注解的 value 属性并不是必须的,所以它可以很好的使用在联合注解的场景。

关于依赖注入和 @Qualifier 的使用亦需注意如下细节:

  1. @Autowired可不要写在 Object 类型的字段上去注入,因为容器内可以找到 N 多个会报错的。但是 List<Object> 是可以的(相当于把所有 Bean 都拿过来~)
  2. 可以利用 @Qualifier 这个高级特性,实现按需、按类别(不是类型)进行依赖注入,这种能力非常赞,给了框架二次开发设计者提供了更多的可能性

如果说指定 value 是按照 key 进行限定 / 匹配,那么类似 @LoadBalanced 这种注解匹配可以理解成就是按照莫一类进行归类限定了,并且自由度也更高了。

推荐阅读

为何一个 @LoadBalanced 注解就能让 RestTemplate 拥有负载均衡的能力?【享学 Spring Cloud】

总结

本文介绍 @Qualifier 高级应用场景和案例,通过结合 @LoadBalanced 对此注解的使用,应该说是能给你打开了一个新的视角去看待 @Qualifier,甚至看待Spring 的依赖注入,这对后续的理解、自定义扩展 / 使用还是蛮有意义的。

== 若对 Spring、SpringBoot、MyBatis 等源码分析感兴趣,可加我 wx:fsx641385712,手动邀请你入群一起飞 ==
== 若对 Spring、SpringBoot、MyBatis 等源码分析感兴趣,可加我 wx:fsx641385712,手动邀请你入群一起飞 ==

退出移动版