当前位置: 移动技术网 > IT编程>开发语言>Java > @Qualifier高级应用---按类别批量依赖注入【享学Spring】

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

2019年09月17日  | 移动技术网IT编程  | 我要评论

每篇一句

罗斯:选秀状元可能有水货,但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,手动邀请你入群一起飞 ==

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网